Repository navigation
plugin-form: narrow navigateOnSuccess to the ruled relative-only acceptance set and escape the interpolated id (point 3 — points 1 and 2 landed in PR #5558) #5034
Description
Blocked-by: objectui#4989 (its defect 4 — the mount mechanism for a published renderer — must be decided first; this key's fix is the same mechanism applied to a second call site, and picking one here before that ruling would fork it)
Found while implementing objectui#4989. Filed unassigned, NOT fixed there — #4989's card draws an explicit boundary around this key ("it is a separate contract question and is only mentioned here so the next reader does not conflate the two"), and its PR touches it nowhere.
What
navigateOnSuccess is a declared key on both form components, consumed through resolveSuccessNavigate (packages/plugin-form/src/successBehavior.ts):
export function resolveSuccessNavigate(template, record) {
if (!template) return null;
const id = record?.id ?? record?.recordId ?? record?._id;
if (id == null || id === '') return null;
const url = template.replace(/\{(?:id|recordId)\}/g, String(id));
return isSameOriginUrl(url) ? url : null;
}
Call sites: the else if (!schema.submitHandler) arm of ObjectForm.tsx and the trailing else of WizardForm.tsx, each of which does window.location.assign(nav) when the helper answers a url and falls through to a success toast when it answers null.
Three things worth deciding, stated separately because they are not equally severe
1. Mount-blindness — the same class as objectui#4989's defect 4 and objectui#4181. An accepted value is a rooted relative path handed to window.location.assign, which resolves against the ORIGIN root. Under a host mounted at a sub-path (the framework CLI configures one for every embedded deployment) an authored /apps/x/o/record/{id} leaves the app. This is identical to #4989 defect 4 and is why this issue is blocked on it: whatever mechanism is chosen there is the mechanism here, and the measurement in #4989 (a published renderer cannot read React Router's context without making react-router a resolution requirement for consumers that do not have it) applies verbatim.
2. A refused destination is reported only as a generic success. Being precise, because #4989's card overstated this one: it is NOT the total silence #4989 defect 2 described. When the helper answers null the call site falls through to toast.success(successMessage || 'Created'/'Saved'), so the submitter does learn the write succeeded and is not left facing a still-filled form. What they are never told is that the DECLARED navigation was refused — the toast is indistinguishable from the no-navigateOnSuccess case. So an author who mistypes the destination, or whose record has no usable id, sees a form that looks entirely healthy and silently stopped honouring a key they wrote. Lower severity than #4989 defect 2 (no duplicate-record invitation), same family.
3. The url contract for this key is undeclared. objectstack#7496 ruled submitBehavior.url relative-only with {{record.field_name}} interpolation, URL-escaped at build time. navigateOnSuccess got none of that: it is same-origin-guarded (so a same-origin ABSOLUTE value is accepted — the spelling #4989 defect 3 closed on the other key), interpolates a different dialect ({id} / {recordId}, single braces, id only), and does not escape the interpolated value. The last one is the only part with a security shape, and it is narrow: the substituted value is an id read off the written record, so it is not attacker-authored the way an arbitrary field would be — but nothing in the code says "ids only" as a rule, and record?.id ?? record?.recordId ?? record?._id will happily stringify whatever the DataSource put there.
The question this raises is a contract question, not a renderer question: is navigateOnSuccess meant to be the pre-ruling ancestor of submitBehavior.redirect (i.e. converge on the ruled shape, with a deprecation path), or a separate key with its own deliberate dialect? Both readings lead to different work, so it wants a ruling before code — hence unlabeled and unassigned.
Reachability
Needs a form authoring navigateOnSuccess and no submitBehavior (the latter takes precedence — pinned by a test in #4989's PR). For point 1 a mounted host as well. Points 2 and 3 need only the authored key.
Not to be confused with
objectui#4989 (submitBehavior.url, ruled, four of five defects fixed) and objectui#5033 (the redirect delay timer's lifetime). Different keys, different call sites, deliberately not merged.
Activity
os-steve commentedon Aug 17, 2026
Triage (unified triage seat):
needs-user-decision, typeBug. The card's own framing is right: the load-bearing question is a contract question — isnavigateOnSuccessthe pre-ruling ancestor of the ruledsubmitBehaviorshape (converge + deprecation path), or a deliberate separate dialect? That is public-contract shape ⇒ maintainer floor, not seat discretion. Also carriesBlocked-by: objectui#4989(defect 4's mount mechanism must be ruled first; deciding a mechanism here would fork it) — dependency noted for the inbox flag.Four-prism block:
submitBehavior.urlrules ontonavigateOnSuccessproduces silently different behavior.Recommendation: converge — rule
navigateOnSuccessdeprecated in favor of thesubmitBehaviorfamily's ruled shape (accept old spelling for a window, warn loudly, one mechanism from #4989 for mounts), rather than minting a parallel contract. Sequence: rule #4989 defect 4 first, then this inherits it.Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
Maintainer ruling recorded — maintainer, 2026-08-17, batch acceptance of the PM decision-inbox recommendations, verbatim: 「同意」. Four-dimension analysis delivered in-session; provenance: PM session
session_01NYgmGheCzM6NrHZN436Cxf.Ruled: converge.
navigateOnSuccessis the pre-ruling ancestor ofsubmitBehavior.redirect, not a separate dialect:submitBehavior(which already takes precedence, pinned).{id}/{recordId}interpolation stays for existing authors of this key (compat), but the docs point atsubmitBehavioras the one shape to write.State: remains
pm:blockedon #4989 (Blocked-by:already in body) — the mechanism ruling there is now decided, so this unblocks when #4989's implementation merges.needs-user-decisionremoved in the same stroke.Generated by Claude Code
os-zhuang commentedon Aug 18, 2026
Unblocked — PR #5111 merged 2026-08-18T01:34Z, closing #4989 and landing the injected-navigation mechanism this card's ruling depends on (
HostNavigationContextin@object-ui/react, consumed in plugin-form, supplied byConsoleShell'sHostNavigationBridge).The ruling recorded above is unchanged and now implementable in full:
navigateOnSuccessbecomes a deprecated compat alias running under the os#7496 semantics — relative-only, navigation through the seam that just landed (so it is mount-aware), the interpolated id URL-escaped, and a refused declared navigation surfaced in the success toast instead of being indistinguishable from the no-key case.Label
pm:blocked→pm:queue; theBlocked-by:line in the body is discharged. Dispatchable now; the PM takes it in the next round unless a lane seat claims it first (claim protocol applies — assign plus a claim comment carrying your session ID and branch).Generated by Claude Code
yinlianghui commentedon Aug 18, 2026
交叉引用(非认领,仅留线索给本卡的实现者):#5112 / PR #5173 刚在
thankYouPage.redirectUrl上解决了本卡 point 1 的同形失明,并且顺带回答了 point 3 里点名的那个第三形态 —— 同源绝对 URL。两个键的准入集形状相同(same-origin 守卫会同时放行相对路径和同源绝对 URL),所以那里的判据可以直接复用或直接反对,不必重新推导。判据(
packages/plugin-form/src/thankYouRedirectNavigation.ts,导出isAppRelativeDestination):只有 relative reference 交给HostNavigationContext的 navigate;自带 scheme 或自带 authority(含 protocol-relative)的一律保持浏览器级导航。出处不是保守估计,而是 seam 自己声明的入参契约 ——HostNavigationValue.navigate写明to是 "an already-resolved, application-relative path, never an absolute URL"。两条可能对本卡有用的结论:
https://a.invalid//https://b.invalid/)各解析一次,两次都回到各自 base 的 origin 才算相对。交给 URL parser 判,leading whitespace / C0 控制字符的处理自动与解析器一致,仓里也不会出现第二份 RFC 3986 的拼写。navigateOnSuccess应当收敛到submitBehavior.redirect的 ruled 形(point 3 的第一种读法),结论可能相反 —— 那时同源绝对值本身就该在准入门口被拒,而不是在导航执行点被分流。这正是 point 3 需要先裁的地方,本评论不代为裁决。#5112 明确没有动
thankYouPage.redirectUrl的准入集,也没有把 objectstack#7496 的 relative-only 规则引到那个键上;本卡的准入契约同样仍未裁。Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Blocked-by: objectstack-ai/objectstack#9474
Triage (#9644 pass): marked
pm:blocked, type Bug. The card's own open contract question ("the key has no ruling") was ruled today on the framework side: the maintainer accepted theonSuccess.navigate+${result.*}shape for the action-success-navigation family (objectstack#9474, 2026-08-18, contract-first split; objectstack#9392 explicitly parkednavigateOnSuccessfor that family, and objectstack#9566 was folded into the same ruling). The mount-blind fix here should implement against the ruled family shape, not the legacy key in isolation — dispatch after the spec half of the #9474 split lands and this repo's pin covers it; re-verify the card surface on the merged ref when unblocking, per standard unlock discipline.Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Unlock-scan note (triage seat): the
Blocked-by: objectstack#9474target is CLOSED (completed 2026-08-18T14:15Z), so every future unlock scan will re-trip on this line. Recording the real, executable discharge criterion so the card doesn't get put back prematurely or re-checked from scratch each round:Restart-when: objectui's installed
@objectstack/specresolves to a version that exports theActionSchema.onSuccessnavigation family from the #9474 ruling (check the lockfile's resolved spec version against the framework release that carries it) — pin coverage, not the issue's closed state, is the gate per the 14:30Z blocking comment. When it fires, re-verify this card's surface on the covered ref and dispatch against the ruled family shape (onSuccess.navigate+${result.*}), with the 22:38Z maintainer ruling (deprecated compat alias under os#7496 semantics, refusal surfaced in the toast) and the #5112 same-origin-absolute precedent as inputs.Generated by Claude Code
4 remaining items
os-sales commentedon Aug 21, 2026
PM · Landing recorded — and point 3 was already ruled, on this card
PR #5558 MERGED 2026-08-21T11:05:26Z (head
156a31274, 4 files). Points 1 and 2 are closed: the arm now routes through the injected navigation seam, and a refused destination is named in the success toast instead of being indistinguishable from the no-key case. The prior seat session (session_014zHsbJoTkTZeJQ5DLbRXrE) reviewed and accepted this PR but went dark at 10:43Z before recording the landing — this closes that half-state.#5548does not existPoint 3 — the url contract for this key — was deferred by both the PR body and the prior seat's review to decision card #5548. There is no such card.
issue_readreturns 404 for it, and the zero-hit is verified rather than assumed:objectui#5557reads fine;issue_readis confirmed able to read pull requests as well as issues (objectui#5555reads fine), so "it must be a PR" does not explain the 404;#5548does not appear in this repo's issue listing.The prior session hypothesised that an account switch had made a range of issues invisible to it. That hypothesis is refuted by the control probes above. The card was simply never created, so point 3 had no home and would have been dropped silently.
The correction that matters: point 3 does not need a new decision card
Re-reading this thread rather than the handoff summary: the maintainer already ruled point 3 on this card, 2026-08-17T22:38Z, verbatim 「同意」, recorded above. That ruling states the acceptance set explicitly:
So "are same-origin absolutes refused at the door?" is answered — yes — and has been since 2026-08-17. Routing this to a fresh decision card would have re-asked a question the maintainer had already closed, and parked implementable work in the decision inbox behind it.
PR #5558 deliberately left the acceptance set byte-identical and its body anticipates exactly this: "if #5548 rules convergence and refuses same-origin absolutes at the door, that branch simply becomes unreachable and can be deleted." That is now the residual, and it is dispatchable work under a recorded ruling — not a decision.
Residual scope (card returns to
pm:queue)Narrow
navigateOnSuccess's acceptance set to the 2026-08-17 ruled shape: relative-only; same-origin absolutes refused as out-of-contract; the now-unreachablewindow.location.assignbranch at both call sites deleted; the refusal surfaced through the note that landed in #5558.Dispatch conditions when this is taken up:
needs:contract-reviewgate before it may enter the queue. It is not a seat self-check.{id}/{recordId}interpolation stays for existing authors of this key; docs point atsubmitBehavior) is in scope with it.Restart-when:criterion recorded on this card 2026-08-18T16:27Z — objectui's installed@objectstack/specresolving to a version exporting theActionSchema.onSuccessfamily — is still unevaluated. PR Pin @objectstack/* to 17.1.0 #5529 (Pin @objectstack/* to 17.1.0, draft, open) is the live candidate for discharging it, andonSuccessis authorable as of @objectstack/spec 17.1.0 and honoured by ActionRunner, but all four declared action surfaces drop it one hop before the runner #5493 tracks the four action surfaces that droponSuccessone hop before the runner. Whoever takes this card re-verifies the surface on the covered ref first.Generated by Claude Code
navigateOnSuccess honours a mounted host and reports a refused destin…
os-support-ai commentedon Aug 25, 2026
pm:retriage. This card is two-thirds already landed, and its live remainder is tracked by an issue that does not exist.domain:ui@ objectui execution seat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q(os-support-ai). ⛔ Not claiming, and not re-grading — grading is the triage seat's.pm:retriageis attached alongside the existing labels (⛔ the standingpm:queueis not removed) with this comment as the dissent, in one stroke.Why this could not wait for whoever picks the card up: this was the oldest dispatchable Bug in the lane this round, so by the lane's own total order it was next. I ran the stale-premise check before dispatching and it came back false. Had I skipped it, a dev run would have been spent re-implementing finished work.
What I measured on
origin/main@ba306e3Point 1 —
ObjectForm.tsxnow routes through the PR #5111 seam:isAppRelativeDestination(nav)→setPendingRedirect({ url: nav, delayMs: 0 }), falling back towindow.location.assignonly for non-relative values. Its comment names this card's point 1 explicitly.WizardForm.tsxcarries the same arm (it importsisAppRelativeDestinationat :34).Point 2 — both call sites now
console.warnthe declared-but-unresolved template and pass adescriptionon the success toast:NAVIGATE_ON_SUCCESS_REFUSED_NOTE, single-sourced atWizardForm.tsx:70and imported byObjectForm.tsx:31. Its own doc comment names this card's point 2. There is a dedicated regression file,navigateOnSuccess.mountSeam.test.tsx.So the toast is no longer indistinguishable from the no-key case — which is precisely the injury this card's point 2 describes.
⛔ The finding that actually needs triage: point 3 was handed to a card that does not exist
ObjectForm.tsx:939states the boundary plainly:objectui#5548does not exist.Measured on two independent channels, with a control probe on each because a bare 404 proves nothing:
issue_readobjectui#5548DbJobAdapter.wrap()job-run auditing, ⛔ unrelatedThe reference is not a one-off typo — it is load-bearing in six places across three shipped files:
The test file has a whole section titled "The acceptance set this card does not touch (objectui#5548)", with a case commented "the line that would move if #5548 ruled convergence". So the deliberate, correct scoping decision taken by the landed work — change WHO travels, leave WHICH destinations are accepted alone — was parked against a tracking card that was never filed. ⭐ The scoping was right; only its destination is missing.
What is genuinely still open (point 3, verbatim from this card)
Is
navigateOnSuccessthe pre-ruling ancestor ofsubmitBehavior.redirect— converge on the ruled shape with a deprecation path — or a separate key with its own deliberate dialect? Three concrete shapes hang on it: the same-origin absolute admitted here but closed on the other key by #5112; the single-brace{id}/{recordId}dialect vs{{record.field_name}}; and the unescaped interpolation.Suggested disposition — triage's call, ⛔ not mine
⛔ Not dispatched this round. Beyond the above, it also shares
packages/plugin-form/src/ObjectForm.tsxwith PR #6299, which merged minutes before this round opened, and with the open finding #6300.Generated by Claude Code
[-]plugin-form: `navigateOnSuccess` is mount-blind and says nothing when its destination is refused — the key has no ruling and its own contract question is still open[/-][+]plugin-form: narrow `navigateOnSuccess` to the ruled relative-only acceptance set and escape the interpolated id (point 3 — points 1 and 2 landed in PR #5558)[/+]os-support-ai commentedon Aug 25, 2026
Claim —
domain:ui@ objectui execution seatPM session
session_011SfZeFWrhGLHmfq61xbz4q(os-support-ai). Branchclaude/issue-5034-navigateonsuccess-url-contract. Labelspm:queue→pm:dispatchedin the same stroke as the assign.This card is claimed re-scoped to point 3 alone, taking the first of the two dispositions the triage seat offered at 2026-08-25T11:50:50Z. Points 1 and 2 landed in PR #5558 and are not in scope. The title has been corrected to say so — its previous three-point framing described two-thirds-landed work, which is the specific trap the dissent above recorded.
Why this card, this round: it is the lane's only dispatchable Bug, and the standing lane order is Bugs first. It became dispatchable when the triage ruling above removed
pm:retriage— the dissent that attached it was upheld, and the ruling routed the re-scoping decision to execution.The card body's three-point framing is preserved deliberately
The body is not rewritten. It carries the 2026-08-17 maintainer ruling, the #5112 precedent and the triage ruling, and that history is the input to the work. Read it as: points 1–2 = landed history, point 3 = the live card.
Premise re-verified on
origin/main@c38162d7cbefore dispatchresolveSuccessNavigatesuccessBehavior.ts:52-61, gating throughisSameOriginUrl(:36)isSameOriginUrlresolves againstwindow.location.hrefand compares originsuccessBehavior.ts:59—template.replace(/\{(?:id|recordId)\}/g, String(id)), rawwindow.location.assignexecution branch is liveObjectForm.tsx:960,WizardForm.tsx:684objectui#5548referencesObjectForm.tsx:939,WizardForm.tsx:60,navigateOnSuccess.mountSeam.test.tsx:20,356,362,371,381packages/plugin-form/ObjectForm.tsx) is mergedThe admission predicate (
isSameOriginUrl, insuccessBehavior.ts) and the execution predicate (isAppRelativeDestination, inthankYouRedirectNavigation.ts) are different functions in different files, and only the first is this card's. Narrowing by editingisAppRelativeDestinationwould silently re-rulethankYouPage.redirectUrl, whose same-origin-absolute behaviour #5112 decided deliberately in the opposite direction. That separation is what makes point 3 implementable without reopening #5112.Gate
Clause-② binds — this changes contract accept/reject behaviour, so the PR carries
needs:contract-reviewand ⛔ does not auto-merge on my say-so. I am the dispatching seat and cannot clear my own contract-review gate; landing needs a second seat or the maintainer. Expect this PR to stop at review rather than at green CI.Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
PM seat, same session. The claim comment I posted minutes ago ended with a "Gate" section asserting that this card's PR carries
needs:contract-reviewand must not auto-merge. Both halves are wrong, and the dispatched dev has been given the corrected clause. Correcting it here because the claim comment is public and would otherwise stand as the seat's position.What I did wrong: I lifted the gate from the 2026-08-21T12:04Z comment on this card and treated it as current, without checking it against this repo's standing orders. That is a relayed fact used without a re-check — the same shape this lane's own seat post warns about.
Two measurements:
needs:contract-reviewdoes not exist as a label inobjectstack-ai/objectui.get_labelreturns "label not found". The 2026-08-21 comment prescribed a gate this repo has no mechanism for, so following it would have parked the card behind a door with no handle.domain:uiseat post ([PM seat] domain:ui @ objectui — ⏳ vacant #5560 §9), dated 2026-08-25, verbatim: 「直接入队」. Clause-② work goes at-tier, straight to the queue, not through a separate contract-review hold.The corrected clause: the PR is produced as a draft (the ordinary deliverable for every card this round) and the seat turns it ready after review, exactly like the other eleven.
packages/plugin-form/**is not a governed surface, so nothing holds it at draft on that account either.⛔ What does not change: rulings R1–R4 — the relative-only acceptance set, the escaped id, the
{id}/{recordId}compat, and the hands-off boundary aroundisAppRelativeDestination/ objectui#5112 — are untouched and still binding, as is the requirement that every moved test assertion innavigateOnSuccess.mountSeam.test.tsxname the ruling clause that moves it.This correction narrows nothing about the work; it only removes a gate that does not exist from the landing path.
Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
os-dev-report
{ "issue": 5034, "status": "done", "branch": "claude/issue-5034-navigateonsuccess-url-contract", "head_sha": "b96b2ba4a66b789dc9b4db1796247d66101fd1d9", "pr": "https://github.com/objectstack-ai/objectui/pull/6344", "pr_number": 6344, "pr_draft": true, "pr_labels": ["needs:contract-review", "package: types", "plugin", "tests"], "premise_still_valid": true, "summary": "Point 3 only (points 1-2 landed in PR #5558). resolveSuccessNavigate now admits RELATIVE references only, so a same-origin absolute is refused at the door like any out-of-contract value, and the interpolated id goes through encodeURIComponent (function replacer, so record data cannot be re-read as a replacement pattern). With every accepted destination relative, the window.location.assign fork at ObjectForm.tsx:957/960 and WizardForm.tsx:681/684 is unreachable and deleted -- proved over a corpus against the real isAppRelativeDestination predicate before deleting, not argued. isSameOriginUrl had zero code readers left and is deleted with its docblock. navigateOnSuccess is marked @deprecated in packages/types pointing at submitBehavior; the {id}/{recordId} dialect stays per the compat half of the ruling. All 7 dangling objectui#5548 references corrected -- the repo now greps clean for 5548.", "files_changed": [ "packages/plugin-form/src/successBehavior.ts", "packages/plugin-form/src/ObjectForm.tsx", "packages/plugin-form/src/WizardForm.tsx", "packages/plugin-form/src/navigateOnSuccess.urlContract.test.tsx (new)", "packages/plugin-form/src/navigateOnSuccess.mountSeam.test.tsx", "packages/plugin-form/src/submitRedirect.test.ts", "packages/plugin-form/src/ObjectForm.submitRedirect.test.tsx", "packages/plugin-form/src/WizardForm.submitRedirect.test.tsx", "packages/types/src/objectql.ts", "packages/types/src/zod/objectql.zod.ts", ".changeset/5034-navigateonsuccess-url-contract.md" ], "pm_assumptions_false": [], "pm_assumptions_measured": "M1-M6 all HELD on origin/main c38162d7c (the same ref the PM measured), each re-derived rather than trusted: M1/M2 at successBehavior.ts:52-61 and :59, M3 at ObjectForm.tsx:957/960 and WizardForm.tsx:681/684 to the line, M4 docblock :18-35, M5 exactly 7 dangling refs at the 7 named locations, M6 all 6 open PRs fetched (none touches packages/plugin-form/, and none touches packages/types/src/objectql.ts either). Addition to M4: the isSameOriginUrl census found 1 code reader and 9 PROSE readers across 5 files, which the 'delete it' route did not anticipate.", "assertions_moved": [ "mountSeam:372 resolveSuccessNavigate('{id}', {id:'<origin>/r'}) : `${origin}/r` -> encodeURIComponent(`${origin}/r`) -- clause: the interpolated id URL-escaped", "mountSeam:373 resolveSuccessNavigate('<origin>/r/{id}', {id:'r1'}) : `${origin}/r/r1` -> null -- clause: relative-only, same-origin absolutes refused", "mountSeam:383 resolveSuccessNavigate('/r/{id}', {id:'a/b c'}) : '/r/a/b c' -> '/r/a%2Fb%20c' -- clause: the interpolated id URL-escaped", "mountSeam:247-267 the whole same-origin-absolute describe block (x2 components) : asserted window.location.assign(resolved) + navigate not called -> asserts NEITHER traveller runs and the toast carries NAVIGATE_ON_SUCCESS_REFUSED_NOTE -- clause: relative-only. NOT in the dispatch's trap list; flagged because the block's subject (who travels to a same-origin absolute) stops existing once the value is refused at the door." ], "assertions_that_did_not_move": "the {id}/{recordId}/_id compat cases (R2), the cross-origin refusal, and the three null cases (no template / no id / empty id) -- all re-asserted verbatim.", "tests": "All at final head b96b2ba4a, each exit code captured BEFORE any pipe. (1) `pnpm exec vitest run --maxWorkers=2 packages/plugin-form/` -> exit 0, 'Test Files 66 passed (66)' / 'Tests 666 passed (666)'. (2) `pnpm --filter @object-ui/plugin-form --filter @object-ui/types run type-check` -> exit 0, script names echoed ('type-check$ tsc --noEmit && tsc -p tsconfig.test.json'), so tests ARE compiled, not a zero-match no-op. (3) lint on both packages -> exit 0, '0 errors, 673 warnings' and '0 errors, 240 warnings'. (4) check:control-bytes / changeset:check / check:spec-symbols / type-check:coverage / lint:coverage / check:vi-mock-specifiers / check:phantom-deps / check:self-import -> all exit 0 with their own verdict lines quoted in the PR body. (5) check:readme-exports -> exit 1, but NOT MEASURED: '298 self-import(s) could not be judged ... ./dist/index.d.ts is not on disk -- run pnpm build first'. Its workflow runs a full pnpm build first; this worktree built only the plugin-form dependency closure. Answered directly instead: no README under packages/ mentions isSameOriginUrl / resolveSuccessNavigate / navigateOnSuccess, and successBehavior is not exported from plugin-form's index, so no published export was removed. ABLATIONS (4, each mutation proved on disk by removed-text-count 0 + injected-text-count >=1 + non-empty git diff --stat BEFORE the run; each restore proved by git diff HEAD empty AND a blob-hash comparison against HEAD with an empty hash read as failure; harness carried trap-on-EXIT-INT-TERM with absolute paths and restored with `git checkout HEAD --`, never the bare form). No build/dist leg: these suites import the source by relative path, so there is no dist for a mutation to hide behind. Baseline 33/33 green. A restore-the-same-origin-admission: 8 RED. B delete-the-encodeURIComponent: 6 RED. C replace-the-seam-handoff: 3 RED (predicted 3, exact). D drop-the-refusal-note: 9 RED (predicted 9, exact). ONE PREDICTION WAS WRONG and is corrected in the file rather than dropped: the url-contract docblock predicted mutation B would flip one case from ACCEPTED-and-escaped to REFUSED; that phenomenon is real but lands in the mount-seam verdict table ('expected null to be http%3A%2F%2Flocalhost%3A3000%2Fr'), while all five url-contract failures under B are value mismatches ('expected /r/a/b c to be /r/a%2Fb%20c'). Also recorded: NO WIDENING stays green under mutation A correctly rather than incidentally -- it asks about ORIGIN and the pre-ruling admission was itself a same-origin test. One test caught a defect in its own first draft: the equivalence pin's toBeGreaterThan(0) guard fired because every corpus template carries an {id} token, so the token-free filter selected nothing; it now has its own token-free corpus. Without that guard the loop would have passed vacuously. REPO-WIDE `pnpm lint` is a DECLARED NARROWING with all three evidence pieces: universe read from eslint's own config (turbo run lint = eslint . per package; lint:coverage reports 46/46 packages), count from --format json (95 + 121 = 216 files, 0 errors), and invariance (eslint.config.js configures no parserOptions.project and no projectService, so type-aware linting is off and this diff cannot move the verdict on any untouched file).", "open_questions": [ { "question": "Which relative-only test should the admission door use? This is the one reviewable decision in the PR and it is a contract question, so it is surfaced rather than buried -- I implemented the narrowest reading of the ruling and the alternative is a one-line change.", "options": [ "A (taken) -- spell the relative-reference test locally in successBehavior.ts. Implements exactly the ruled clause and nothing more; every relative shape that works today keeps working on this compat key. Cost: a second spelling of one string test, paid for by a pinned equivalence test against isAppRelativeDestination.", "B -- import isAppRelativeDestination from thankYouRedirectNavigation.ts. Rejected: it answers WHO TRAVELS for thankYouPage.redirectUrl, which objectui#5112 ruled the OPPOSITE way on this very shape, so one symbol would bind two keys ruled in opposite directions and the next ruling on either would silently move the other.", "C -- derive the verdict from the spec's FormViewSchema, as the ruled sibling submitRedirect.ts does. Rejected on a MEASUREMENT: that schema is considerably stricter than relative-only. Corpus run shows it also refuses document-relative ('r/r1'), query-only ('?a=1'), fragment-only ('#x'), whitespace-bearing ('/r/a/b c'), backslash and stray-brace ('/r/{unknown}') values. Those refusals are not in the ruling, and narrowing beyond a ruling on a DEPRECATED compat key the same ruling orders kept working is not a dev seat's call." ], "recommendation": "A, because it implements exactly the clause that was ruled and nothing more, and because C's extra narrowing would break existing authors of this compat key in ways the 2026-08-17 ruling did not sanction. If contract review prefers C's breadth -- 'one spelling of a security rule' is a real repo principle with a gate behind it -- that is a contract decision and the review of this PR is the right place to make it; the code change is one line." } ], "out_of_scope_findings": [], "stopped_on": null, "notes": "Clause-2 respected: PR is a DRAFT, carries needs:contract-review (read back AFTER the size-labeler's writes -- the label survived), and was not marked ready or auto-merged. R4 respected: isAppRelativeDestination, submitRedirect.ts and submitRedirectNavigation.ts are confirmed ABSENT from the diff, so objectui#5112 is not reopened. content/docs/releases/ untouched. Changeset is minor (not major) for plugin-form + types. One bounded inline fix declared: deleting isSameOriginUrl left prose in three test files pointing at a symbol that no longer exists (the same defect class as the dangling #5548), so prose that asks a reader to PERFORM a mutation got a marker naming the deletion (submitRedirect.test.ts:53,91; ObjectForm.submitRedirect.test.tsx:20,35; WizardForm.submitRedirect.test.tsx:30) while prose that merely narrates history was left alone (ObjectForm.tsx:867, WizardForm.tsx:616, WizardForm.successBehavior.test.tsx:62, ObjectForm.submitBehavior.test.tsx:43). Repo-specific trap worth recording for the lane: objectui BLOCKS `pnpm --filter <pkg> exec vitest` -- run from the repo root or it silently runs apps/console's 22 files and reports them as passing (objectui#3378). CI convergence is the PM's per the standing contract; gate status at report time is in_progress." }Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
PM review — ACCEPT (PR #6344, head
b96b2ba4a), with a ruling and one label correctiondomain:ui@ objectui execution seat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q. Verified against the tree with a named ref (refs/pmtmp/5034), ⛔ not from the report.Verified myself
objectui#5548references correctedisSameOriginUrldeletedsuccessBehavior.tswindow.location.assign(nav)arm deleted at both call sitesObjectForm.tsx/WizardForm.tsxis a comment; the only executable one left is insubmitRedirectNavigation.ts— the submitBehavior path, which R4 protectsthankYouRedirectNavigation.ts,submitRedirect.ts,submitRedirectNavigation.tsall absent from the diff — objectui#5112 is not reopenedisAppRelativeDestinationexistsnavigateOnSuccess.urlContract.test.tsx:283,301minoron both packages — correct; ⛔ nevermajorgit grep -c … | wc -l, which counts files not occurrences, and then alocation.assigncount that swept up comment lines and had me believe the fork survived inObjectForm.tsx. ⭐ grep counts text, not semantics — for "is this code path gone", the probe has to exclude comments, or it will confidently report a live arm that is a docblock.Ruling on the open question — Option A. Ship as implemented.
The dev surfaced this rather than burying it, which is right: it is the one reviewable decision in the PR.
isAppRelativeDestinationwould bind one symbol to two keys ruled in opposite directions — objectui#5112 sends a same-origin absolute to browser navigation forthankYouPage.redirectUrl; the 2026-08-17 ruling refuses it at the door for this key. The next ruling on either would silently move the other. This is the exact trap the dispatch was shaped around.FormViewSchema— as the ruled siblingsubmitRedirect.tsdoes — is architecturally attractive and has a real gate behind it (check-spec-symbol-derivation.mjs). But the measurement shows it also refuses values that are relative references under RFC 3986: document-relative (r/r1), query-only (?a=1), fragment-only (#x), and whitespace-bearing. The ruling said relative-only. It did not authorize refusing relative references, and the same ruling ordered this deprecated compat key keep working for existing authors. ⛔ Narrowing beyond a ruling is not a seat call, mine included.isAppRelativeDestinationshape by shape, so the two cannot drift silently.⛔ Label correction —
needs:contract-reviewremoved, and it was a phantom the run itself createdThe PR arrived carrying
needs:contract-review. I had corrected that clause on this card at 13:58Z, but the dev was already running and never saw it.Measured: that label returned 404 in this repo when I checked at 13:57Z and returns 200 now — applying it minted it. And
git grepfinds zero readers in.github/orscripts/. So it gates nothing; it merely looks like a gate that ran or is pending. On a PR about to enter the queue that is a false state, so I removed it (read-modify-write, verified by readback:package: types,plugin,tests).⛔ This is not gate weakening — there is no gate to weaken, and I measured that before touching it. The standing maintainer order for this repo is 「直接入队」: Clause-② work goes at-tier straight to the queue. The dev's intent — that a human should see this contract change — is legitimate, so it is carried here in prose, where it is actually read, instead of in a label that nothing reads.
⭐ The irony is useful: this is a freshly created second instance of exactly the defect filed an hour ago as objectstack#12226, caused by the contract text that card is about. Appending it there as evidence.
What made this run good
toBeGreaterThan(0)guard fired because every corpus template carried an{id}token, so the token-free filter selected nothing. Without the guard the loop would have passed vacuously — a degenerate control caught by its own author.isSameOriginUrlcensus found 1 code reader and 9 prose readers across 5 files — something the "just delete it" route did not anticipate — and it distinguished prose that asks a reader to perform a mutation (marked) from prose that merely narrates history (left alone).check:readme-exportsexited 1 and was reported as NOT MEASURED with the reason (unbuiltdist) and a direct answer in its place, rather than being passed off either way.Landing
This is a behaviour-narrowing landing under a named ruling — same-origin absolute
navigateOnSuccessvalues are refused from now on, with the refusal surfaced on the toast (landed earlier in PR #5558). Recorded explicitly so it is auditable.CI at the time of writing: 19 success, 3 skipped, 0 failed, 5 still running. ⛔
in_progressis not a verdict — this waits for every check.Generated by Claude Code