Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(app-shell): the flow end node's form offers no typed control for config.message, which outcome: 'refused' requires and outcome: 'completed' forbids #9336

Closed

Description

@os-tesla

Measured on origin/main 0f3d15314 against installed @objectstack/spec 17.4.0, from the objectui#9278 execution seat. Filed unassigned — recording, not claiming.

The observation, and the half neither the card nor triage had measured

objectui#9278 established that EndConfigSchema.outcome is a closed enum of completed | refused. Running that card's probe at this seat reproduced it — and surfaced a second rule on the same two keys that neither the card nor triage saw, because both stopped at completed:

{}                                  ACCEPTED  {"outcome":"completed"}
{outcome:'completed'}               ACCEPTED  {"outcome":"completed"}
{outcome:'refused'}                 REJECTED: `outcome: 'refused'` requires a `message` — a refusal with no
                                              text is the shape this contract exists to replace.
{outcome:'refused', message:'why'}  ACCEPTED  {"outcome":"refused","message":"why"}
{outcome:'completed', message:'x'}  REJECTED: `message` is only rendered when `outcome` is 'refused' — on a
                                              completed end nothing shows it, so the key would be a silent no-op.

So the pair is cross-validated in both directions by a superRefine on EndConfigSchema: refused requires message, completed refuses it. message is z.string().min(1).optional(), documented as a {token} template interpolated at run time exactly like a screen description.

Reproduce:

node --input-type=module -e "
import * as A from '@objectstack/spec/automation';
const t = (c) => { const r = A.FlowNodeSchema.safeParse({ id: 'e', type: 'end', label: 'E', config: c });
  console.log(JSON.stringify(c), r.success ? 'ACCEPTED' : 'REJECTED: ' + r.error.issues[0].message); };
t({ outcome: 'completed' });
t({ outcome: 'refused' });
t({ outcome: 'refused', message: 'Refused: {record.name} is a confirmed duplicate' });
"

The gap

packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts's end group has no field for message. The key is therefore authorable only through the Advanced (JSON) block — reachable, since FlowNodeInspector reveals that block behind a + even when a node carries no extra keys, but untyped, unlabelled and undiscoverable from the control that needs it.

⚠️ objectui#9278's repair makes this newly reachable in one click. Before it, the Outcome key was a free-text box whose placeholder advertised two words the loader refuses, so refused was not on offer at all. After it, refused is one of exactly two dropdown options — and picking it, with nothing else done, produces a flow that fails to load. The failure is loud and the spec's own message is instructive, so this is strictly better than the state it replaced; it is not finished.

Why it was not repaired inside objectui#9278

Triage's acceptance on that card is a closed list about the outcome field: control kind, option list, declared default, placeholder deletion. Adding a second field is outside it on the letter, and on the mechanics it is not free either — a showWhen-gated field on end moves three exact-count census assertions in flow-node-config.inactiveRetained.test.ts (gatedByType.size 8, totalGated 33, ungatedTypes.length 24, all read on 0f3d15314), which is a second verification surface rather than the same one. The executing seat therefore did the bounded half that was ruled and named this, with the measurement above, rather than widening under its own authority. What it did do inside the fence: the Outcome field's help now states the message requirement in words, so picking refused is not a silent route to a failed load.

The shape a repair would take, for whoever rules it

A cfg('message', …, 'text', { showWhen: { field: 'outcome', equals: ['refused'] } }) in the end group. The mechanism is well-trodden in this table (about twenty showWhen descriptors, including ones gated on a select controller such as boundaryConfig.eventType), and the gating works out correctly with objectui#9278's declared default: controllerAdmits resolves an unset outcome through defaultValue: 'completed', so the field stays off screen until the author actually chooses refused, while a stored value keeps it visible — which is what lets an author clear a stale message after switching back to completed, the case the spec's other direction refuses.

Open questions a seat should answer rather than assume: whether the control is text or textarea; whether the label follows the spec's own wording ("Why the run was refused"); and whether the census movement above should be re-measured at landing time rather than copied from here, the way triage ruled for the declaration-count pin that objectui#9277 and objectui#9278 share.

Refs: objectui#9278 (the outcome line; its repair is what makes refused offerable) · objectui#9335 (the same group's third fault, outputVariable) · objectui#7707 (rendering a run that ended refused, which is the same vocabulary at the other end). Neither of those is addressed by this card.

Generated by Claude Code in session session_01UzHd6hDYatoDn17BuwKxnZ, out of objectui#9278's fence.

Activity

  1. self-assigned this
    on Sep 13, 2026
  2. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    Claim — domain:ui seat (os-tesla)

    Dispatching to an os-dev subagent at the default judgment tier.

    Clause-②: no. packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts is not exported from packages/app-shell/src/index.ts (checked with a live control — the index is 470 lines, so the zero is a reading and not a dead grep), and message is a key EndConfigSchema already declares and requires. Teaching the form to author a declared key is pulling back to the declared contract, which the clause exempts — ⛔ it is not a widening. If your actual diff says otherwise, the diff is the fact and this declaration is only a prediction: re-declare and carry needs:contract-review on both carriers.

    ⚠️⚠️ SIBLING DEFECT ON THE SAME FILE AND THE SAME GROUP — ⛔ do not ride it

    objectui#9335 is unclaimed and lives in the end group you are about to edit: the same form writes config.outputVariable, a key EndConfigSchema refuses by name, so anything typed in that box makes the save fail.

    ⛔ Do not fix it in this PR. It is a separate card and widening this PR to cover it is the seat's call, not yours.

    ⭐ But do answer one question in your report: are the two genuinely separable — can message get its typed control without touching or breaking the outputVariable row? If they are inseparable in practice, say so plainly with the evidence and ⛔ stop rather than deciding it yourself; I will route them together next round. A half-corrected end group that nobody flagged is the worst outcome here.

    The defect

    EndConfigSchema cross-validates outcome and message in both directions via superRefine: refused requires message, completed forbids it. The form offers no typed control for message at all ⇒ ⭐ an author who picks refused in the UI cannot produce a valid flow, because the one key that outcome requires is unauthorable. That is a blocked authoring path, ⛔ not merely a missing nicety.

    The card measured this on origin/main 0f3d15314 against installed @objectstack/spec 17.4.0, with accepted rows printed alongside the refusals so the refusals are a reading and not a dead probe. ⭐ Re-run that probe at your head and quote your own output — the spec pin may have moved, and an inherited number is not a measurement.

    Scope

    packages/app-shell/**. The card notes this half was found only because its seat ran objectui#9278's probe past completed, where both the card and triage had stopped — so ⛔ do not assume the card's enumeration is complete either. Probe both enum values and both directions yourself.


    Generated by Claude Code

  3. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    Clause-②-correction: 5656680863
    Clause-②: no
    Session: session_011QreXiyMEqKLN4U5daMPVa

    Contract-text: EndConfigSchema already declares message — z.string().min(1).optional() — and its superRefine requires it when outcome: 'refused'. The key is on the published contract today; only the inspector form cannot author it.

    ⭐ The declaration is unchanged — this corrects only its SPELLING, the same seat error corrected on objectui#9342 and objectui#9414 in this round.

    The claim composed **Clause-②: no.** with prose trailing on the same line instead of copying the template's literal Clause-②: yes | no. Teaching a form to author a key the published contract already declares and requires moves no accept set; it is 拉回已声明契约. ⛔ No second Claim:; the claim comment is not edited.


    Generated by Claude Code

  4. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 9336,
      "status": "done",
      "branch": "claude/issue-9336-end-message-control",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9444",
      "premise_still_valid": true,
      "summary": "The card's premise holds and was re-verified at my own head (e2feb13, commit 1d1ca96) against installed @objectstack/spec 17.4.0. The flow `end` group now offers a typed control for `config.message` — cfg('message', 'Why the run was refused', 'textarea', { showWhen: { field: 'outcome', equals: ['refused'] } }) — so picking `refused` in the UI is no longer a blocked authoring path. Kind, label, placeholder and help are DERIVED from the installed spec (its describe says `message` interpolates 'exactly like a screen description', and that field is a textarea in this same table, as is notify's sibling `message` key); no defaultValue, because the key is .optional() with no .default(). The gate keeps BOTH directions of the superRefine authorable: an unset outcome resolves through the declared `completed` default so the field stays hidden, while a STORED message re-shows it, which is the only way to clear a stale message after switching back to `completed`. The Outcome help (English + zh-CN overlay) no longer claims the message is set in Advanced, since this change makes that false. Census in flow-node-config.inactiveRetained.test.ts re-measured at landing, not copied: 8->9 buckets, 33->34 gated fields, 24->23 ungated types. assignee was `os-tesla` (the seat) on arrival; I never wrote it.",
      "probe_at_my_head": "spec 17.4.0, run from packages/app-shell. Accepted rows printed beside refusals: {} ACCEPTED {outcome:completed} | {outcome:'completed'} ACCEPTED | {outcome:'refused'} REJECTED [config.message] requires a message | {outcome:'refused',message:'why'} ACCEPTED | {outcome:'completed',message:'x'} REJECTED [config.message] only rendered when refused | {outcome:'success'} REJECTED [config.outcome] Invalid option: expected one of completed|refused. THREE ROWS THE CARD DID NOT HAVE, each of which changed the repair: (1) {message:'x'} with NO outcome at all is REJECTED — the completed-side rule bites on the EFFECTIVE outcome via the declared default, which is why the control is gated rather than always-on; (2) message:'' is REJECTED under BOTH outcomes (min(1) on one side, the no-op rule on the other), so a control committing '' on clear would ship an unloadable flow either way — setAtPath deletes the leaf instead, and that is now pinned; (3) {outcome:'refused',outputVariable:'result'} reports ONLY unrecognized_keys — an unrecognized key masks the superRefine entirely. EndConfigSchema shape keys: ['outcome','message'].",
      "separability_objectui_9335": "SEPARABLE — yes, and the work is done without touching that row. Evidence: (a) structurally a pure insertion; the `outputVariable` descriptor is byte-identical to origin/main and appears in the diff only as unchanged context. (b) No shared mechanism: `message` is gated by showWhen on `outcome`, `outputVariable` is ungated and reads no controller; no shared descriptor, controller or code path. (c) At the schema the asymmetry runs the SAFE way: an unrecognized key masks the superRefine, so objectui#9335 hides THIS card's defect and never the reverse — repairing `message` cannot make objectui#9335 harder to see or to repair. (d) Pinned deliberately: the new suite asserts outputVariable is still present, still `text`, still ungated, and that the three-key config is still refused, so a future repair of objectui#9335 must come past that assertion on purpose. No half-corrected group left unflagged.",
      "clause_2_declaration": "Clause-②: no — re-declared against the ACTUAL diff, which agrees with the seat's prediction. flow-node-config.ts is not exported from packages/app-shell/src/index.ts; its only occurrence there is inside a comment, and the zero is a reading, not a dead grep (that file is 470 lines with 92 `export` occurrences and 84 relative-import occurrences as lit controls). The package publishes one subpath ('.'). The diff adds NO exported symbol and moves NO signature: one element appended to an internal descriptor array, one key added to an internal zh overlay, plus tests and a changeset. check-changeset-presence confirms mechanically: '0 of them a manifest whose published contract moved'. And `message` is a key the published contract already declares AND requires, so this is pulling back to the declared contract. needs:contract-review NOT applied to either carrier.",
      "ablation_prediction_vs_outcome": "PREDICTION WRITTEN BEFORE THE RUN (recorded to scratchpad ABLATION-PREDICTION.txt before any ablation): direction TURN-RED (not diagnostics-grow, not inversion); 9 failed tests across 2 files — 7 of the new pin's 10 plus the 2 census tests; the 3 predicted to stay GREEN were NAMED in advance (the two rows reading only the installed spec, and the outputVariable separability row). OUTCOME: 'Tests 9 failed | 15 passed (24)' — exactly the predicted 7+2 split, and the three named survivors survived. PREDICTION MATCHED EXACTLY.",
      "tests": "All runs from the REPO ROOT with path filters (AGENTS.md: package-level invocations reach a different conclusion), and every heavy run through the shared lock (OS_VERIFY_LOCK_SLOT=dev-objectui-9336); verdicts read from the printed `VERDICT command-exit` line, never a bare $?. (1) BASELINE at e2feb13 before any edit — 3 files / 86 tests passed. (2) NEW PIN + census after the fix — 'Test Files 2 passed (2), Tests 24 passed (24)', VERDICT command-exit 0. (3) WHOLE metadata-admin TREE — 'Test Files 259 passed (259), Tests 2733 passed | 1 skipped (2734)', VERDICT command-exit 0. (4) type-check `pnpm --filter @object-ui/app-shell type-check` — VERDICT command-exit 0; it runs `tsc --noEmit && tsc -p tsconfig.test.json`, and `--listFiles` on the test project returns 1 hit for the new suite, so the green demonstrably covers it. FIRST ATTEMPT reported unbuilt workspace siblings (TS2307 Cannot find module '@object-ui/*') — recorded as NOT MEASURED, not red; fixed by building the closure `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build` (note: the flag must precede --filter; placing `--concurrency=2` after it was forwarded to tsup and failed the build). (5) lint `pnpm --filter @object-ui/app-shell lint` — VERDICT command-exit 0, 0 errors, 2999 pre-existing warnings; WHOLE PACKAGE, so no narrowing evidence is owed. (6) gates: check-changeset-presence PASS (1 changeset declared for 3 published source files), check-changeset-no-major PASS, check-control-bytes PASS (7605 tracked text files), check-governed-queue-guard --test => 'NOT GOVERNED, 5 paths checked, none matched'. Self-scan for control bytes over the changed files found none. ABLATION: mutation deleted ONLY the 4-line message descriptor; proven on disk BEFORE the run — HEAD blob 4ea15bf4f6cd51087ef442e63da6ea4a2acfc3c1 vs mutated blob 2f832f385ae62ba5575a7f689d637c8347ebb8b0, marker grep count 1 -> 0, with a hard non-zero exit had either check failed. Script carried a trap on EXIT INT TERM calling a RESTORE function (placeholder spelled as a word, never as an angle-bracket shape, because the GitHub body sanitizer eats tag-shaped fragments even inside backticks) with an ABSOLUTE path from git rev-parse --show-toplevel. RESTORE PROVEN BY STATE, never by re-running the test: `git checkout HEAD -- PATH` (never a bare `git checkout --`, which restores the mutation out of the index), then disk blob == HEAD blob 4ea15bf4f6cd51087ef442e63da6ea4a2acfc3c1 and `git diff HEAD` empty, working tree clean. Repo-wide `pnpm lint` and full `pnpm test` are CI's runs and are NOT claimed here.",
      "mcp_calls": "0 — channel declared: the repo-scoped REST read probe returned HTTP 200 at this seat, so every GitHub read AND write (card, claim comment, PR creation, PR body read-back, this report comment) went through REST. No MCP GitHub tool was invoked. No dedup search was needed because no new card is filed.",
      "open_questions": [],
      "out_of_scope_findings": [
        "already filed as objectui#9335: the same `end` group's `outputVariable` row writes a key EndConfigSchema refuses by name. Deliberately untouched and left open; separability answered above (SEPARABLE), and the new suite pins the row's current shape so the next repair has to pass that assertion on purpose.",
        "noted, not filed: an unrecognized key on an end config MASKS the outcome/message superRefine entirely — {outcome:'refused', outputVariable:'result'} reports only unrecognized_keys, never the missing-message refusal. This is spec-side Zod strict-object ordering, not a defect, but it means objectui#9335 currently hides this card's defect on any node using the Output variable box. Successor: whoever repairs objectui#9335 — same file, same group.",
        "noted, not filed: FLOW_FIELD_ZH (packages/app-shell/src/views/metadata-admin/i18n.ts) has no mechanical parity gate asserting that every flow field descriptor has a zh entry; a new descriptor silently falls back to English. Deliberate by design per its own doc comment (plugin nodes must stay readable), so this is an observation rather than a defect. Successor: whoever repairs objectui#9335 — it will add or move a descriptor in this same group and will meet the same overlay."
      ]
    }

    Generated by Claude Code

  5. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    Claim: PM loop round R34
    Session: session_011QreXiyMEqKLN4U5daMPVa
    Branch: claude/issue-9336-end-message-control
    Worktree: objectui-issue-9336
    Domain: domain:ui
    File surface: packages/app-shell/src/views/metadata-admin/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier
    Clause-②: no
    Thread-read: 5657022747
    Serial constraints cleared: objectui#9335 is the unclaimed sibling defect in the SAME file and the SAME end group and was deliberately NOT dispatched alongside this card; objectui#9340 is a third card on the same file, also held back. The dev was required to answer whether the two are separable and answered SEPARABLE with evidence

    Contract-text: EndConfigSchema already declares message as z.string().min(1).optional(), and its superRefine requires it when outcome: 'refused'. The key is on the published contract today; only the inspector form could not author it.


    ⚠️ This is the FIRST Claim: comment on this card, not a second one. Posted under the maintainer's ruling of 2026-09-13, recorded at objectui#9445. The seat's original claim (comment 5656680863) was composed as prose rather than copied from the template.

    ⛔ Ownership unchanged and never ambiguous; sole assignee throughout. Correction comment 5657022747 is superseded by the Clause-②: no line above.


    Generated by Claude Code

  6. removed their assignment
    on Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions