Validate RPC bootstrap artifacts and pin reference regression failures

Record main-only settings bug and leave the acceptance consolidation census for step 0.5.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-09 20:48:59 -04:00
parent cf67d13683
commit 0a1eadd2c8
13 changed files with 1196 additions and 273 deletions
+415
View File
@@ -0,0 +1,415 @@
# Step 0.5 acceptance consolidation remains separate
Keep all current behavior, including malformed-result exceptions; classify the following predicates into named policy families before replacing any caller. The census includes if/conditional/return expressions using `.ok`, `.success`, or `isSuccess`, with exact source locations; it deliberately retains transport and non-RPC matches for review.
Three existing definitions to remove only after partition fixtures exist:
- mobile/src/transport/mobile-relay-direct-upgrade.ts:166 requireSuccess
- mobile/src/transport/mobile-relay-pairing-recovery.ts:297 requireSuccess
- mobile/src/transport/pre-profile-pairing-coordinator.ts:286 requireSuccess
Inline predicate census:
- mobile/src/agent-history/MobileAgentSessionHistoryPanel.tsx:85 — `worktreeResponse.ok`
- mobile/src/agent-history/MobileAgentSessionHistoryPanel.tsx:394 — `!repoResponse.ok`
- mobile/src/agent-history/MobileAgentSessionHistoryPanel.tsx:399 — `folderWorkspaceResponse?.ok === true`
- mobile/src/agent-history/MobileAgentSessionHistoryPanel.tsx:405 — `projectGroupResponse?.ok === true`
- mobile/src/agent-history/MobileAgentSessionHistoryPanel.tsx:409 — `settingsResponse?.ok === true`
- mobile/src/agent-history/MobileAgentSessionHistoryPanel.tsx:413 — `worktreeResponse?.ok === true`
- mobile/src/agent-history/use-mobile-agent-history-state.ts:95 — `!statusResponse.ok`
- mobile/src/agent-history/use-mobile-agent-history-state.ts:125 — `!response.ok`
- mobile/src/browser/mobile-browser-frame-state.ts:111 — `!response.ok`
- mobile/src/browser/use-mobile-browser-request.ts:46 — `!response.ok`
- mobile/src/components/accounts-snapshot.ts:232 — `!result.success`
- mobile/src/components/codex-reset-credit-capability.ts:15 — `!response.ok || !response.result || typeof response.result !== 'object'`
- mobile/src/components/codex-reset-credit.ts:139 — `parsed.success ? parsed.data : null`
- mobile/src/components/codex-reset-credit.ts:139 — `parsed.success`
- mobile/src/components/codex-reset-credit.ts:164 — `!scope.success || !scopesEqual(scope.data, expectedScope)`
- mobile/src/components/codex-reset-credit.ts:236 — `!response.ok`
- mobile/src/components/pr-sidebar/MobileLinkPrForm.tsx:34 — `outcome.ok`
- mobile/src/components/pr-sidebar/PRActionsSection.tsx:62 — `outcome.ok`
- mobile/src/components/pr-sidebar/PRChecksSection.tsx:65 — `outcome.ok`
- mobile/src/components/pr-sidebar/PrSidebarCreateEmptyState.tsx:115 — `!outcome.ok`
- mobile/src/components/pr-sidebar/ReviewerPickerDrawer.tsx:57 — `outcome.ok`
- mobile/src/components/use-new-workspace-create-submit.ts:92 — `settingsResponse.ok`
- mobile/src/components/use-new-workspace-execution-target.ts:57 — `!response.ok`
- mobile/src/components/use-new-workspace-execution-target.ts:92 — `response.ok`
- mobile/src/components/use-new-workspace-execution-target.ts:118 — `!response.ok`
- mobile/src/components/use-new-workspace-repositories.ts:56 — `stale || !response.ok`
- mobile/src/components/use-new-workspace-runtime-context.ts:13 — `entry.status === 'fulfilled' && entry.value.ok ? (entry.value as RpcSuccess) : null`
- mobile/src/components/use-new-workspace-runtime-context.ts:13 — `entry.status === 'fulfilled' && entry.value.ok`
- mobile/src/components/use-new-workspace-setup-script.ts:45 — `stale || !response.ok`
- mobile/src/diagnostics/connection-diagnostics-submission.ts:37 — `response.ok ? { ok: true } : { ok: false, error: \`status ${response.status}\` }`
- mobile/src/diagnostics/connection-diagnostics-submission.ts:37 — `response.ok`
- mobile/src/dictation/mobile-dictation-setup.ts:36 — `!response.ok`
- mobile/src/dictation/mobile-dictation-setup.ts:63 — `!response.ok`
- mobile/src/dictation/mobile-dictation-setup.ts:73 — `!response.ok`
- mobile/src/dictation/mobile-dictation-setup.ts:84 — `!response.ok`
- mobile/src/files/MobileFileExplorerPanel.tsx:114 — `!response.ok`
- mobile/src/files/MobileFileExplorerPanel.tsx:125 — `legacy.ok`
- mobile/src/files/MobileFilePreviewScreen.tsx:36 — `route.ok`
- mobile/src/files/MobileFilePreviewScreen.tsx:40 — `route.ok`
- mobile/src/files/MobileFilePreviewScreen.tsx:91 — `route.ok`
- mobile/src/files/mobile-file-mutation-ownership.ts:77 — `!response.ok`
- mobile/src/files/mobile-file-preview-request.ts:95 — `!response.ok && typeof source !== 'string' && source.source === 'terminalArtifact'`
- mobile/src/files/mobile-file-preview-request.ts:139 — `response.ok`
- mobile/src/files/mobile-file-preview-request.ts:168 — `!response.ok`
- mobile/src/files/mobile-file-preview-request.ts:189 — `!response.ok`
- mobile/src/files/mobile-file-preview-request.ts:209 — `!response.ok`
- mobile/src/files/mobile-file-preview-response.ts:44 — `!response.ok`
- mobile/src/files/mobile-file-tab-doc.ts:38 — `!response.ok`
- mobile/src/files/mobile-file-tab-doc.ts:60 — `!preview.ok`
- mobile/src/files/mobile-file-tab-doc.ts:76 — `!response.ok`
- mobile/src/files/mobile-terminal-artifact-grant-refresh.ts:31 — `response.ok || !isTerminalArtifactGrantFailure(response, options)`
- mobile/src/files/mobile-terminal-artifact-grant-refresh.ts:41 — `!refreshed.ok`
- mobile/src/home/mobile-home-host-requests.ts:43 — `!disposed() && response.ok`
- mobile/src/home/mobile-home-host-requests.ts:61 — `!disposed() && response.ok`
- mobile/src/home/mobile-home-host-requests.ts:84 — `settingsResponse.ok`
- mobile/src/home/mobile-home-host-requests.ts:88 — `preflightResponse.ok`
- mobile/src/home/mobile-home-host-requests.ts:91 — `linearResponse.ok`
- mobile/src/hooks/mobile-dictation-audio-chunk.ts:40 — `!response.ok`
- mobile/src/hooks/mobile-dictation-desktop-start.ts:69 — `!response.ok`
- mobile/src/hooks/use-mobile-dictation.ts:218 — `!response.ok`
- mobile/src/host-screen/use-host-repo-metadata.ts:21 — `response.ok ? (response as RpcSuccess).result : null`
- mobile/src/host-screen/use-host-repo-metadata.ts:21 — `response.ok`
- mobile/src/host-screen/use-host-repo-metadata.ts:94 — `clientRef.current !== requestClient || hostId !== requestHostId || !repoResponse.ok`
- mobile/src/host-screen/use-host-view-settings.ts:98 — `clientRef.current !== requestClient || hostId !== requestHostId || !response.ok`
- mobile/src/host-screen/use-host-worktree-actions.ts:130 — `!response.ok`
- mobile/src/notifications/mobile-notifications.ts:158 — `!response.ok`
- mobile/src/session/ai-vault-resume-launch.ts:173 — `!created.ok`
- mobile/src/session/ai-vault-resume-launch.ts:189 — `!sent.ok`
- mobile/src/session/ai-vault-resume-preparation.ts:39 — `!response.ok`
- mobile/src/session/github-pr-mutations.ts:24 — `!response.ok`
- mobile/src/session/github-pr-mutations.ts:59 — `!response.ok`
- mobile/src/session/github-pr-mutations.ts:65 — `r.ok === true`
- mobile/src/session/github-pr-mutations.ts:109 — `!response.ok`
- mobile/src/session/github-pr-mutations.ts:267 — `!response.ok`
- mobile/src/session/github-pr-rpc.ts:96 — `!response.ok`
- mobile/src/session/mobile-clipboard-image.ts:98 — `!response.ok`
- mobile/src/session/mobile-clipboard-image.ts:136 — `!startResponse.ok`
- mobile/src/session/mobile-diff-review-loaders.ts:52 — `!response.ok`
- mobile/src/session/mobile-diff-review-loaders.ts:72 — `!statusResponse.ok`
- mobile/src/session/mobile-diff-review-loaders.ts:87 — `!worktreeResponse.ok`
- mobile/src/session/mobile-diff-review-loaders.ts:132 — `!response.ok`
- mobile/src/session/mobile-file-tap-open.ts:92 — `!response.ok`
- mobile/src/session/mobile-file-tap-open.ts:180 — `!openResponse.ok`
- mobile/src/session/mobile-new-tab-agent-loader.ts:29 — `!settingsResponse.ok`
- mobile/src/session/mobile-new-tab-agent-loader.ts:32 — `!detectedResponse.ok`
- mobile/src/session/mobile-new-tab-agent-loader.ts:48 — `!repoResponse.ok`
- mobile/src/session/mobile-pr-sidebar-state.ts:91 — `hintOutcome.ok && hintOutcome.result?.provider === 'github'`
- mobile/src/session/mobile-pr-sidebar-state.ts:98 — `!prOutcome.ok`
- mobile/src/session/mobile-pr-sidebar-state.ts:115 — `!checksOutcome.ok`
- mobile/src/session/mobile-pr-sidebar-state.ts:137 — `outcome.ok ? outcome.result : null`
- mobile/src/session/mobile-pr-sidebar-state.ts:137 — `outcome.ok`
- mobile/src/session/mobile-session-tab-activation.ts:36 — `response.ok`
- mobile/src/session/mobile-session-tab-activation.ts:60 — `response.ok`
- mobile/src/session/mobile-session-tabs-stream-health.ts:264 — `!response.ok`
- mobile/src/session/mobile-structured-agent-session-launch.ts:108 — `!supportResponse || typeof supportResponse !== 'object' || typeof supportResponse.ok !== 'boolean' || !supportResponse.ok`
- mobile/src/session/mobile-structured-agent-session-launch.ts:140 — `!response || typeof response !== 'object' || typeof response.ok !== 'boolean'`
- mobile/src/session/mobile-structured-agent-session-launch.ts:143 — `!response.ok`
- mobile/src/session/mobile-structured-agent-session-launch.ts:154 — `!result || typeof result !== 'object' || typeof result.ok !== 'boolean'`
- mobile/src/session/mobile-structured-agent-session-launch.ts:157 — `!result.ok`
- mobile/src/session/mobile-structured-agent-session-rpc.ts:46 — `!response.ok`
- mobile/src/session/mobile-structured-agent-session-rpc.ts:141 — `!result.ok && method === 'agentSession.conversationCommand' && result.refusal.code === 'agent_session_operation_unknown'`
- mobile/src/session/mobile-structured-agent-session-rpc.ts:147 — `result.ok ? { status: 'accepted', value: result.value } : { status: 'refused', message: result.refusal.message }`
- mobile/src/session/mobile-structured-agent-session-rpc.ts:147 — `result.ok`
- mobile/src/session/pr-actions-engine.ts:162 — `outcome.ok`
- mobile/src/session/pr-ai-triage-launch.ts:24 — `!created.ok`
- mobile/src/session/pr-ai-triage-launch.ts:36 — `!sent.ok`
- mobile/src/session/use-live-worktree-name.ts:113 — `!response.ok`
- mobile/src/session/use-mobile-diff-review-comment-actions.ts:74 — `!response.ok`
- mobile/src/session/use-mobile-diff-review-git-actions.ts:36 — `!response.ok`
- mobile/src/session/use-mobile-diff-review-git-actions.ts:71 — `response.ok`
- mobile/src/session/use-mobile-diff-review-interactions.ts:135 — `{ clearSentNotes, closeComposer, copyNotes, createTerminalAndSend, deleteComment, jumpHunk: (direction: 'next' | 'previous') => { if (diffState.kind !== 'ready') { return } const currentLineIndex = activeHunkIndex === null ? -1 : (diffState.hunks[activeHunkIndex]?.startIndex ?? -1) const nextIndex = direction === 'next' ? findNextMobileDiffHunkIndex(diffState.hunks, currentLineIndex) : findPreviousMobileDiffHunkIndex(diffState.hunks, currentLineIndex) const target = nextIndex === null ? null : diffState.hunks[nextIndex] if (!target || nextIndex === null) { return } setActiveHunkIndex(nextIndex) listRef.current?.scrollToIndex({ index: target.startIndex, animated: true, viewPosition: 0.16 }) triggerSelection() }, markReviewed, markUnreviewed, moveFile: (direction: 'next' | 'previous') => { if (filteredQueue.length === 0) { return } setCurrentIndex((index) => direction === 'next' ? index + 1 >= filteredQueue.length ? 0 : index + 1 : index - 1 < 0 ? filteredQueue.length - 1 : index - 1 ) }, openComposer, openEditComposer, openInSession: async () => { if (!client || !currentItem || currentItem.scope === 'branch') { return } const response = await client.sendRequest('files.openDiff', { worktree: \`id:${worktreeId}\`, relativePath: currentItem.filePath, staged: currentItem.scope === 'staged' }) if (!response.ok) { setActionError(response.error?.message || 'Unable to open in session') return } onOpenSession() }, openSendSheet, retryAction: () => { if (connState !== 'connected' && hostId) { void onReconnect(hostId) return } void loadReviewData() }, runGitMutation, saveComposer, selectFilter: (nextFilter: MobileDiffReviewQueueFilter) => { setFilter(nextFilter) setCurrentIndex(0) }, sendPromptToTerminal, stageReviewedFiles }`
- mobile/src/session/use-mobile-diff-review-interactions.ts:190 — `!response.ok`
- mobile/src/session/use-mobile-diff-review-send-actions.ts:88 — `!response.ok`
- mobile/src/session/use-mobile-diff-review-send-actions.ts:113 — `!response.ok`
- mobile/src/session/use-mobile-diff-review-send-actions.ts:135 — `!response.ok`
- mobile/src/session/use-mobile-native-chat-file-search.ts:94 — `!response.ok || generationRef.current !== generation`
- mobile/src/session/use-mobile-native-chat-file-search.ts:130 — `response.ok`
- mobile/src/session/use-mobile-native-chat-readability.ts:36 — `response.ok`
- mobile/src/session/use-mobile-native-chat-session.ts:249 — `!response.ok`
- mobile/src/session/use-mobile-pr-branch-context.ts:156 — `{ isGithubRepo: slugOutcome.ok && slugOutcome.result !== null }`
- mobile/src/session/use-mobile-pr-branch-context.ts:177 — `response.ok ? readMobileGitStatusResult(response.result) : null`
- mobile/src/session/use-mobile-pr-branch-context.ts:177 — `response.ok`
- mobile/src/session/use-mobile-pr-branch-context.ts:194 — `response.ok ? readMobileBranchCompareResult(response.result) : null`
- mobile/src/session/use-mobile-pr-branch-context.ts:194 — `response.ok`
- mobile/src/session/use-mobile-pr-comment-actions.ts:139 — `outcome.ok`
- mobile/src/session/use-mobile-pr-title-action.ts:75 — `outcome.ok`
- mobile/src/session/use-mobile-session-accessory-selection.ts:195 — `!repoResponse.ok`
- mobile/src/session/use-mobile-session-bulk-close.ts:76 — `!response.ok`
- mobile/src/session/use-mobile-session-close-actions.ts:46 — `response.ok`
- mobile/src/session/use-mobile-session-close-actions.ts:72 — `response.ok`
- mobile/src/session/use-mobile-session-close-actions.ts:107 — `response.ok`
- mobile/src/session/use-mobile-session-content-create-actions.ts:45 — `!createResponse.ok`
- mobile/src/session/use-mobile-session-content-create-actions.ts:58 — `!openResponse.ok`
- mobile/src/session/use-mobile-session-content-create-actions.ts:104 — `!response.ok`
- mobile/src/session/use-mobile-session-content-create-actions.ts:145 — `!response.ok`
- mobile/src/session/use-mobile-session-diff-comments.ts:36 — `!response.ok`
- mobile/src/session/use-mobile-session-diff-comments.ts:54 — `!response.ok`
- mobile/src/session/use-mobile-session-document-readers.ts:24 — `response.ok`
- mobile/src/session/use-mobile-session-document-readers.ts:54 — `!fallback.ok`
- mobile/src/session/use-mobile-session-markdown-actions.ts:147 — `!response.ok`
- mobile/src/session/use-mobile-session-startup.ts:132 — `response.ok`
- mobile/src/session/use-mobile-session-startup.ts:161 — `activationResponse?.ok`
- mobile/src/session/use-mobile-session-terminal-create-actions.ts:110 — `response.ok`
- mobile/src/session/use-mobile-session-terminal-create-actions.ts:167 — `!sendResponse.ok`
- mobile/src/session/use-mobile-session-terminal-list.ts:53 — `terminalInventoryRequest.run( allowEmptyLoaded, async (allowsEmpty, isCurrent) => { try { const response = await client.sendRequest('terminal.list', { worktree: \`id:${worktreeId}\`, includeVisualLayouts: false }) if (!isCurrent() || !response.ok) { return false } const result = (response as RpcSuccess).result as { terminals: Terminal[] } if (result.terminals.length === 0 && !allowsEmpty()) { return true } // Why: require two consecutive empties before trusting 0, so transient empty responses don't flash the UI empty. if (result.terminals.length === 0 && lastKnownTerminalCountRef.current > 0) { lastKnownTerminalCountRef.current = 0 return true } const liveHandles = new Set(result.terminals.map((terminal) => terminal.handle)) const pruneContext = { liveHandles, showNativeChat: showNativeChatRef.current, activeHandle: activeHandleRef.current } // Why: terminal.list is the lifetime signal; lagging tab snapshots must not erase a user's buffered-mode opt-out. // Sweep against the retained set, not the raw list: a chat-covered handle // keeps its subscription across a graph reload, so erasing its live-input // preference on the same refresh is the erasure this guard exists to stop. const retainedHandles = resolveRetainedTerminalHandles(pruneContext) pruneTerminalHandlesFromLiveInput(retainedHandles) bufferedTerminalDraftState.pruneDrafts(retainedHandles) defaultTerminalHandlesToLiveInput([...liveHandles]) const shouldPrune = createTerminalPrunePredicate(pruneContext) for (const handle of Array.from(terminalUnsubsRef.current.keys())) { if (!shouldPrune(handle)) { continue } unsubscribeTerminal(handle) terminalRefs.current.delete(handle) initializedHandlesRef.current.delete(handle) viewportResubscribeBudgetRef.current.forget(handle) clearTerminalLiveInputDefault(handle) } setTerminalKeyboardMetrics((prev) => pruneTerminalKeyboardMetrics(prev, shouldPrune)) // Why: a chat-covered handle the host reports again refills its rearm budget, // so an exhausted rearm can't lock the composer until leave-chat. nativeChatStream.notifyListedHandles(liveHandles) // Why: same absence-gated refill for the viewport-fit budget — a handle that // left the list and returned may converge now, so it earns fresh attempts. viewportResubscribeBudgetRef.current.notifyListedHandles(liveHandles) lastKnownTerminalCountRef.current = result.terminals.length // Why: dedupe duplicate handles (rename/split race) to avoid a React duplicate-key throw; keep first for tab-strip order. const seen = new Set<string>() const deduped = result.terminals.filter((t) => { if (seen.has(t.handle)) { return false } seen.add(t.handle) return true }) const mergedTerminals = mergeTerminalListWithKnownRecords( deduped, terminalsRef.current, sessionTabsRef.current ) setTerminals((prev) => terminalRecordsEqual(prev, mergedTerminals) ? prev : mergedTerminals ) terminalsRef.current = mergedTerminals // Session tabs are the UI authority; terminal.list only refreshes per-handle metadata for existing terminal surfaces. return true } catch { // Failed to list terminals return false } }, opts.onPhysicalRequestStarted )`
- mobile/src/session/use-mobile-session-terminal-list.ts:61 — `!isCurrent() || !response.ok`
- mobile/src/session/use-mobile-structured-agent-state.ts:172 — `result.ok && sessionKeyRef.current === requestSessionKey && streamGenerationRef.current === requestGeneration`
- mobile/src/session/use-pr-bot-author-overrides.ts:38 — `stale || !response.ok`
- mobile/src/session/use-quick-commands.ts:132 — `!response.ok`
- mobile/src/session/use-quick-commands.ts:195 — `!response.ok`
- mobile/src/source-control/MobileGitHistoryList.tsx:111 — `response.ok`
- mobile/src/source-control/mobile-branch-base-ref.ts:69 — `worktreeResponse?.ok`
- mobile/src/source-control/mobile-branch-base-ref.ts:76 — `repoResponse?.ok`
- mobile/src/source-control/mobile-branch-base-ref.ts:85 — `!defaultResponse.ok`
- mobile/src/source-control/mobile-commit-message-ai.ts:20 — `!response.ok`
- mobile/src/source-control/mobile-commit-message-ai.ts:27 — `result.success === true && typeof result.message === 'string' && result.message.length > 0`
- mobile/src/source-control/mobile-commit-message-ai.ts:33 — `result.success === false && typeof result.error === 'string' && result.error.length > 0`
- mobile/src/source-control/mobile-commit-message-ai.ts:36 — `{ success: false, error: hostError, ...(result.success === false && result.canceled ? { canceled: true } : {}) }`
- mobile/src/source-control/mobile-commit-message-ai.ts:39 — `result.success === false && result.canceled`
- mobile/src/source-control/mobile-git-history.ts:68 — `!response.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent-runner.ts:46 — `!outcome.ok && progress === 'committing' && outcome.committed === false`
- mobile/src/source-control/mobile-hosted-review-create-intent-runner.ts:55 — `!prepared.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent-runner.ts:79 — `!created.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:117 — `!staged.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:121 — `!stagedStatus.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:154 — `!generated.success`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:167 — `!committed.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:171 — `!committedStatus.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:197 — `initialStatus.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:199 — `{ ok: false, error: initialStatus.ok ? 'Branch changed while preparing the pull request.' : initialStatus.error, status: currentStatus }`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:201 — `initialStatus.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:209 — `!committed.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:226 — `!remote.ok`
- mobile/src/source-control/mobile-hosted-review-create-intent.ts:233 — `!refreshedStatus.ok`
- mobile/src/source-control/mobile-hosted-review-git-preparation.ts:15 — `!response.ok`
- mobile/src/source-control/mobile-hosted-review-git-preparation.ts:37 — `!response.ok`
- mobile/src/source-control/mobile-hosted-review-git-preparation.ts:56 — `!response.ok`
- mobile/src/source-control/mobile-hosted-review-git-preparation.ts:60 — `result?.success !== true`
- mobile/src/source-control/mobile-hosted-review-remote-prerequisite.ts:27 — `result.ok ? { ok: true, ran: true } : result`
- mobile/src/source-control/mobile-hosted-review-remote-prerequisite.ts:27 — `result.ok`
- mobile/src/source-control/mobile-hosted-review-remote-prerequisite.ts:37 — `result.ok ? { ok: true, ran: true } : result`
- mobile/src/source-control/mobile-hosted-review-remote-prerequisite.ts:37 — `result.ok`
- mobile/src/source-control/mobile-hosted-review-remote-prerequisite.ts:50 — `result.ok ? { ok: true, ran: true } : result`
- mobile/src/source-control/mobile-hosted-review-remote-prerequisite.ts:50 — `result.ok`
- mobile/src/source-control/mobile-hosted-review-service.ts:52 — `!response.ok`
- mobile/src/source-control/mobile-hosted-review-service.ts:181 — `!response.ok`
- mobile/src/source-control/mobile-hosted-review-service.ts:195 — `result.ok`
- mobile/src/source-control/mobile-hosted-review-service.ts:218 — `{ ok: true, url: result.url, number: result.number, ...(existing ? { existing: true } : {}), ...(linked.ok ? {} : { linkError: linked.error }) }`
- mobile/src/source-control/mobile-hosted-review-service.ts:223 — `linked.ok`
- mobile/src/source-control/mobile-hosted-review-service.ts:236 — `!push.ok`
- mobile/src/source-control/mobile-hosted-review-service.ts:245 — `!response.ok`
- mobile/src/source-control/mobile-hosted-review-service.ts:249 — `result.ok`
- mobile/src/source-control/mobile-pr-link.ts:57 — `!response.ok`
- mobile/src/source-control/mobile-pr-link.ts:92 — `!response.ok`
- mobile/src/source-control/mobile-pr-link.ts:122 — `!response.ok`
- mobile/src/source-control/reveal-mobile-source-control-session-diff.ts:67 — `!response.ok`
- mobile/src/source-control/reveal-mobile-source-control-session-diff.ts:109 — `response.ok && readActiveTabId(response.result) === tabId`
- mobile/src/source-control/use-mobile-commit-message-generation.ts:44 — `result.success`
- mobile/src/source-control/use-mobile-create-pr-runner.ts:59 — `useCallback( async (pushFirst: boolean) => { setShowActionSheet(false) const branch = status?.branch if (!client || !branch) { triggerError() setActionError('Check out a branch before creating a pull request.') return } const created: { current: MobileHostedReviewCreateIntentRunOutcome | null } = { current: null } let progress: MobileHostedReviewCreateIntentProgress | null = null const ran = await runGitWorkflow(pushFirst ? 'push-create-pr' : 'create-pr', async () => { created.current = await runMobileHostedReviewCreateIntent(client, worktreeId, { branch, title: branchLabel, status, commitMessage, onProgress: (nextProgress: MobileHostedReviewCreateIntentProgress) => { progress = nextProgress setActionError(mobileHostedReviewCreateIntentProgressMessage(nextProgress)) } }) if (!created.current.ok) { throw new Error(created.current.error) } }) const outcome = created.current if (outcome?.committed && mountedRef.current) { setCommitMessage('') } if (!ran && outcome?.status !== undefined && mountedRef.current) { await loadStatus({ preserveReadyOnFailure: true, clearActionErrorOnSuccess: false, force: true }) } if (!ran || !mountedRef.current || !outcome || !outcome.ok) { if (!ran && outcome && isMobileHostedReviewCommitFailure(outcome, progress)) { const outcomeStagedEntries = getMobileCommitFailureStagedEntries(outcome.status?.entries) recordCommitFailure({ error: outcome.error, commitMessage: outcome.commitMessage ?? commitMessage.trim(), stagedEntries: outcomeStagedEntries.length > 0 ? outcomeStagedEntries : stagedEntries }) } return } setActionError(null) setCreatedPrUrl(outcome.url) setCreatedPrWarning(outcome.warning ?? null) }, [ branchLabel, client, commitMessage, loadStatus, mountedRef, recordCommitFailure, runGitWorkflow, setActionError, setCommitMessage, setCreatedPrUrl, setCreatedPrWarning, setShowActionSheet, stagedEntries, status, worktreeId ] )`
- mobile/src/source-control/use-mobile-create-pr-runner.ts:83 — `!created.current.ok`
- mobile/src/source-control/use-mobile-create-pr-runner.ts:98 — `!ran || !mountedRef.current || !outcome || !outcome.ok`
- mobile/src/source-control/use-mobile-git-requests.ts:33 — `!response.ok`
- mobile/src/source-control/use-mobile-git-requests.ts:48 — `!result || result.success !== true`
- mobile/src/source-control/use-mobile-source-control-loaders.ts:127 — `!response.ok`
- mobile/src/source-control/use-mobile-source-control-loaders.ts:204 — `response.ok`
- mobile/src/source-control/use-mobile-source-control-openers.ts:120 — `!response.ok && isMobileGitUnavailable(response.error?.code, response.error?.message)`
- mobile/src/source-control/use-mobile-source-control-openers.ts:127 — `!response.ok`
- mobile/src/source-control/use-mobile-source-control-openers.ts:245 — `!response.ok`
- mobile/src/storage/codex-reset-attempt-journal.ts:105 — `!result.success || result.data.hostId !== identity.hostId || !stableAccountScopesEqual(result.data.expectedScope, identity.expectedScope)`
- mobile/src/storage/codex-reset-attempt-journal.ts:138 — `withScopeMutation(identity, async () => { const key = storageKey(identity) const raw = await AsyncStorage.getItem(key) if (raw !== null) { return parseAttempt(raw, identity) } const idempotencyKey = identity.createIdempotencyKey() if (!IdempotencyKeySchema.safeParse(idempotencyKey).success) { throw new Error('Codex reset attempt idempotency key is invalid') } const attempt = CodexResetAttemptSchema.parse({ v: 1, hostId: identity.hostId, expectedScope: identity.expectedScope, idempotencyKey }) // Why: the key must survive a committed provider mutation whose response is // lost; no reset RPC may start until this write has completed successfully. await AsyncStorage.setItem(key, JSON.stringify(attempt)) return attempt })`
- mobile/src/storage/codex-reset-attempt-journal.ts:146 — `!IdempotencyKeySchema.safeParse(idempotencyKey).success`
- mobile/src/tasks/composer-source-base-resolve.ts:37 — `!response.ok`
- mobile/src/tasks/composer-source-base-resolve.ts:68 — `!response.ok`
- mobile/src/tasks/mobile-tasks-item-mapping.ts:23 — `Boolean(response && typeof response === 'object' && (response as RpcSuccess).ok)`
- mobile/src/tasks/setup-hook-trust.ts:49 — `!response.ok`
- mobile/src/tasks/smart-source-paste-intent.ts:112 — `!response.ok && response.error.code === 'method_not_found'`
- mobile/src/tasks/smart-source-paste-intent.ts:118 — `response.ok`
- mobile/src/tasks/smart-source-paste-intent.ts:137 — `!response.ok`
- mobile/src/tasks/smart-source-paste-intent.ts:159 — `!response.ok`
- mobile/src/tasks/smart-source-paste-intent.ts:178 — `!response.ok`
- mobile/src/tasks/smart-source-search-requests.ts:34 — `!response.ok`
- mobile/src/tasks/smart-source-search-requests.ts:56 — `!response.ok`
- mobile/src/tasks/smart-source-search-requests.ts:88 — `!response.ok`
- mobile/src/tasks/smart-source-search-requests.ts:106 — `!response.ok`
- mobile/src/tasks/use-mobile-tasks-client-settings-actions.tsx:208 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-github-check-file-actions.tsx:47 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-github-check-file-actions.tsx:51 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-github-check-file-actions.tsx:90 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-github-check-file-actions.tsx:138 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-github-check-file-actions.tsx:204 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-github-check-file-actions.tsx:253 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-github-check-file-actions.tsx:261 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-github-reply-merge-actions.tsx:73 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-github-reply-merge-actions.tsx:81 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-github-reply-merge-actions.tsx:154 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-github-reply-merge-actions.tsx:158 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-github-reply-merge-actions.tsx:189 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-gitlab-github-status-actions.tsx:44 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-gitlab-github-status-actions.tsx:48 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-gitlab-github-status-actions.tsx:89 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-gitlab-github-status-actions.tsx:93 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-hosted-comment-review-actions.tsx:77 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-hosted-comment-review-actions.tsx:85 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-hosted-comment-review-actions.tsx:152 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-hosted-comment-review-actions.tsx:156 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-hosted-comment-review-actions.tsx:234 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-hosted-metadata-actions.tsx:48 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-hosted-metadata-actions.tsx:52 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-hosted-metadata-actions.tsx:131 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-hosted-metadata-actions.tsx:135 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-item-detail-loading.tsx:55 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-item-detail-loading.tsx:116 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-item-detail-loading.tsx:207 — `!isSuccess(issueResponse)`
- mobile/src/tasks/use-mobile-tasks-item-detail-loading.tsx:211 — `isSuccess(commentsResponse)`
- mobile/src/tasks/use-mobile-tasks-item-detail-metadata-effects.tsx:55 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-item-detail-metadata-effects.tsx:89 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-linear-item-actions.tsx:46 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-linear-item-actions.tsx:50 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-linear-item-actions.tsx:87 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-linear-item-actions.tsx:127 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-linear-item-actions.tsx:138 — `result.ok === false || !result.id || !result.identifier`
- mobile/src/tasks/use-mobile-tasks-list-and-detail-effects.tsx:200 — `isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-list-and-detail-effects.tsx:241 — `isSuccess(statesResponse)`
- mobile/src/tasks/use-mobile-tasks-project-detail-loading.tsx:105 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-detail-loading.tsx:137 — `!result.ok`
- mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.tsx:79 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.tsx:141 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.tsx:149 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.tsx:216 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.tsx:220 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.tsx:274 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.tsx:278 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-loading-actions.tsx:54 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-loading-actions.tsx:64 — `!result.ok`
- mobile/src/tasks/use-mobile-tasks-project-loading-actions.tsx:82 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-loading-actions.tsx:88 — `!result.ok`
- mobile/src/tasks/use-mobile-tasks-project-loading-actions.tsx:124 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-loading-actions.tsx:130 — `!result.ok`
- mobile/src/tasks/use-mobile-tasks-project-loading-actions.tsx:269 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-loading-actions.tsx:283 — `!result.ok`
- mobile/src/tasks/use-mobile-tasks-project-metadata-actions.tsx:57 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-metadata-actions.tsx:61 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-metadata-actions.tsx:168 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-metadata-actions.tsx:172 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-metadata-actions.tsx:234 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-metadata-actions.tsx:238 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-metadata-loading.tsx:51 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-metadata-loading.tsx:57 — `!result.ok`
- mobile/src/tasks/use-mobile-tasks-project-metadata-loading.tsx:106 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-metadata-loading.tsx:112 — `!result.ok`
- mobile/src/tasks/use-mobile-tasks-project-metadata-loading.tsx:164 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-metadata-loading.tsx:170 — `!result.ok`
- mobile/src/tasks/use-mobile-tasks-project-repository-resolution.tsx:67 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-review-check-actions.tsx:50 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-review-check-actions.tsx:54 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-review-check-actions.tsx:129 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-review-check-actions.tsx:174 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-review-check-actions.tsx:178 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-review-check-actions.tsx:216 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-thread-reply-actions.tsx:54 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-thread-reply-actions.tsx:61 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-thread-reply-actions.tsx:115 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-thread-reply-actions.tsx:189 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-thread-reply-actions.tsx:197 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-workspace-comment-actions.tsx:117 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-workspace-comment-actions.tsx:121 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-project-workspace-comment-actions.tsx:199 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-workspace-comment-actions.tsx:205 — `!result.ok`
- mobile/src/tasks/use-mobile-tasks-project-workspace-comment-actions.tsx:251 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-project-workspace-comment-actions.tsx:258 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-provider-load-actions.tsx:49 — `!isSuccess(statusResponse)`
- mobile/src/tasks/use-mobile-tasks-provider-load-actions.tsx:70 — `!isSuccess(teamsResponse)`
- mobile/src/tasks/use-mobile-tasks-provider-load-actions.tsx:117 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-provider-load-actions.tsx:194 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-route-and-item-state.tsx:64 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx:198 — `!isSuccess(statusResponse)`
- mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx:262 — `isSuccess(settingsResponse)`
- mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx:267 — `isSuccess(uiResponse)`
- mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx:282 — `isSuccess(preflightResponse)`
- mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx:285 — `isSuccess(linearStatusResponse)`
- mobile/src/tasks/use-mobile-tasks-task-create-actions.tsx:61 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-task-create-actions.tsx:70 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-task-create-actions.tsx:118 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-task-create-actions.tsx:129 — `result.ok === false || !result.id || !result.identifier`
- mobile/src/tasks/use-mobile-tasks-task-create-actions.tsx:188 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-task-list-loading.tsx:146 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-task-list-loading.tsx:171 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-task-list-loading.tsx:223 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-task-pagination-actions.tsx:57 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-task-pagination-actions.tsx:61 — `result.ok === false`
- mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx:76 — `isSuccess(settingsResponse)`
- mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx:178 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx:223 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx:264 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-source-effects.tsx:54 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-source-effects.tsx:125 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx:91 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx:139 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx:55 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx:91 — `!isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx:129 — `isSuccess(response)`
- mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx:194 — `!isSuccess(response)`
- mobile/src/tasks/worktree-create-capability.ts:40 — `!response.ok`
- mobile/src/tasks/worktree-create-retry.ts:84 — `response.ok`
- mobile/src/terminal/terminal-send-rpc-response.ts:8 — `!response.ok`
- mobile/src/terminal/terminal-viewport-refit-state.ts:17 — `!response.ok || typeof response.result !== 'object' || response.result == null`
- mobile/src/terminal/terminal-viewport-refit-state.ts:24 — `!response.ok || typeof response.result !== 'object' || response.result == null`
- mobile/src/terminal/terminal-viewport-refit-state.ts:33 — `response.ok`
- mobile/src/terminal/worker-terminal-takeover-report.ts:45 — `!response.ok`
- mobile/src/transport/direct-rpc-client.ts:252 — `!response.ok && response.error.code === 'unauthorized'`
- mobile/src/transport/host-credential-cleanup.ts:87 — `!result.ok`
- mobile/src/transport/host-credential-cleanup.ts:99 — `!current.ok`
- mobile/src/transport/host-endpoint-edit.ts:27 — `!normalizedInput.ok`
- mobile/src/transport/host-endpoint-edit.ts:33 — `sameEndpointAuthority(normalizedInput.endpoint, storedEndpoint) || (normalizedDisplay.ok && sameEndpointAuthority(normalizedInput.endpoint, normalizedDisplay.endpoint))`
- mobile/src/transport/host-metadata-store.ts:40 — `parsed.flatMap((item) => { // Why: pre-v0.0.3 records embedded secrets; users re-pair instead of migrating them. if (item && typeof item === 'object' && 'deviceToken' in item) { return [] } const result = StoredHostProfileSchema.safeParse(item) return result.success ? [result.data] : [] })`
- mobile/src/transport/host-metadata-store.ts:46 — `result.success ? [result.data] : []`
- mobile/src/transport/host-metadata-store.ts:46 — `result.success`
- mobile/src/transport/host-status-gates.ts:54 — `!response.ok`
- mobile/src/transport/mobile-endpoint-supervisor-support.ts:55 — `first.ok || first.error instanceof RelayDialAbortedError || !isDirectorResolutionFailure(first.error) || !relay`
- mobile/src/transport/mobile-relay-credential-bundle.ts:84 — `result.success && result.data.hostId === hostId ? result.data : null`
- mobile/src/transport/mobile-relay-credential-bundle.ts:84 — `result.success && result.data.hostId === hostId`
- mobile/src/transport/mobile-relay-credential-rotation.ts:56 — `!response.ok`
- mobile/src/transport/mobile-relay-credential-rotation.ts:167 — `!response.ok`
- mobile/src/transport/mobile-relay-direct-upgrade-journal.ts:53 — `parsed.success && parsed.data.hostId === hostId ? parsed.data : null`
- mobile/src/transport/mobile-relay-direct-upgrade-journal.ts:53 — `parsed.success && parsed.data.hostId === hostId`
- mobile/src/transport/mobile-relay-direct-upgrade.ts:167 — `!response.ok`
- mobile/src/transport/mobile-relay-direct-upgrade.ts:174 — `!response.ok && response.error.code === 'method_not_found'`
- mobile/src/transport/mobile-relay-e2ee-link.ts:162 — `!parsed.success`
- mobile/src/transport/mobile-relay-e2ee-link.ts:165 — `!parsed.data.ok`
- mobile/src/transport/mobile-relay-host-overlay-store.ts:19 — `value.flatMap((item) => { const result = MobileRelayHostOverlaySchema.safeParse(item) return result.success ? [result.data] : [] })`
- mobile/src/transport/mobile-relay-host-overlay-store.ts:21 — `result.success ? [result.data] : []`
- mobile/src/transport/mobile-relay-host-overlay-store.ts:21 — `result.success`
- mobile/src/transport/mobile-relay-invite-director.ts:33 — `new Promise((resolve, reject) => { let settled = false const timeout = setTimeout( () => finish(new Error('relay director resolution timed out')), args.timeoutMs ?? 5_000 ) socket.onopen = () => { socket.send( JSON.stringify({ type: 'relay-auth', v: 1, mode: 'connect', credential: args.relay.inviteToken }) ) } socket.onmessage = (event) => { if (typeof event.data !== 'string') { finish(new Error('invalid relay director move')) return } let value: unknown try { value = JSON.parse(event.data) } catch { finish(new Error('invalid relay director move')) return } const moved = RelayMovedSchema.safeParse(value) if (!moved.success) { finish(new Error('invalid relay director move')) return } if (moved.data.assignmentEpoch <= args.relay.assignmentEpoch) { finish( new RelayDirectorMoveNotNewerError({ cellUrl: moved.data.cellUrl, assignmentEpoch: moved.data.assignmentEpoch, currentCellUrl: args.relay.cellUrl, currentAssignmentEpoch: args.relay.assignmentEpoch }) ) return } settled = true clearTimeout(timeout) socket.close() resolve({ ...args.relay, cellUrl: moved.data.cellUrl, assignmentEpoch: moved.data.assignmentEpoch }) } socket.onerror = () => finish(new Error('relay director transport error')) socket.onclose = (event) => { if (!settled) { finish(new Error(\`relay director closed before move: ${event.code || 1006}\`)) } } function finish(error: Error): void { if (settled) { return } settled = true clearTimeout(timeout) socket.close() reject(error) } })`
- mobile/src/transport/mobile-relay-invite-director.ts:62 — `!moved.success`
- mobile/src/transport/mobile-relay-pairing-journal-store.ts:128 — `result.success ? result.data : null`
- mobile/src/transport/mobile-relay-pairing-journal-store.ts:128 — `result.success`
- mobile/src/transport/mobile-relay-pairing-journal-store.ts:137 — `result.success ? result.data : null`
- mobile/src/transport/mobile-relay-pairing-journal-store.ts:137 — `result.success`
- mobile/src/transport/mobile-relay-pairing-recovery.ts:298 — `!response.ok`
- mobile/src/transport/mobile-relay-physical-client.ts:144 — `!parsed.success`
- mobile/src/transport/mobile-relay-physical-client.ts:147 — `!parsed.data.ok`
- mobile/src/transport/mobile-relay-resume-director.ts:55 — `!response.ok`
- mobile/src/transport/mobile-relay-rpc-session.ts:182 — `!response.ok`
- mobile/src/transport/mobile-relay-rpc-streams.ts:105 — `!response.ok`
- mobile/src/transport/mobile-relay-rpc-streams.ts:122 — `response.ok && response.streaming !== true`
- mobile/src/transport/mobile-relay-rpc-streams.ts:131 — `!response.ok`
- mobile/src/transport/mobile-relay-session-establisher.ts:61 — `result.ok`
- mobile/src/transport/mobile-runtime-capability-negotiation.ts:32 — `!response.ok`
- mobile/src/transport/pairing-candidate-race.ts:13 — `new Promise((resolve, reject) => { const successes: PairingCandidate[] = [] let failures = 0 let settled = false let selectionQueued = false for (const candidate of candidates) { void candidate.client.sendRequest('status.get').then( (response) => { if (!response.ok) { failures++ rejectIfFinished() return } successes.push(candidate) if (selectionQueued) { return } selectionQueued = true // Why: defer one microtask so simultaneous successes are visible and // direct deterministically wins the exact tie regardless of callback order. queueMicrotask(() => { if (settled) { return } settled = true const winner = successes.find(({ path }) => path === 'direct') ?? successes[0]! for (const loser of candidates) { if (loser !== winner) { loser.client.close() } } resolve(winner) }) }, () => { failures++ rejectIfFinished() } ) } function rejectIfFinished(): void { if (!settled && failures === candidates.length && successes.length === 0) { settled = true reject(new Error('direct and relay pairing paths both failed')) } } })`
- mobile/src/transport/pairing-candidate-race.ts:21 — `!response.ok`
- mobile/src/transport/pre-profile-pairing-coordinator.ts:287 — `!response.ok`
- mobile/src/transport/pre-profile-pairing-coordinator.ts:294 — `!response.ok && response.error.code === 'method_not_found'`
- mobile/src/transport/rpc-client-socket-session.ts:235 — `message.type === 'e2ee_error' || (message.ok === false && message.error?.code === 'unauthorized')`
- mobile/src/transport/rpc-client-stream-registry.ts:115 — `response.ok && response.streaming === true`
- mobile/src/transport/rpc-client-stream-registry.ts:120 — `response.ok`
- mobile/src/transport/rpc-client-stream-registry.ts:139 — `response.ok`
- mobile/src/transport/rpc-client-stream-registry.ts:140 — `response.ok`
- mobile/src/transport/rpc-response-shape.ts:15 — `response.ok === true`
- mobile/src/transport/rpc-response-shape.ts:18 — `( response.ok === false && !!response.error && typeof response.error.code === 'string' && typeof response.error.message === 'string' )`
- mobile/src/transport/runtime-capability-probe.ts:26 — `!response.ok`
- mobile/src/worktree/home-host-worktree-fetch.ts:44 — `!response.ok`
- mobile/src/worktree/worktree-catalog-snapshot-client.ts:78 — `!response.ok`
- mobile/src/worktree/worktree-show-resolution.ts:26 — `response.ok`
+176 -104
View File
@@ -1,12 +1,7 @@
{
"schemaVersion": 1,
"scope": [
"mobile/src",
"mobile/app"
],
"excludes": [
"*.generated.*"
],
"scope": ["mobile/src", "mobile/app"],
"excludes": [],
"entries": [
{
"file": "mobile/app/connection-log.tsx",
@@ -316,12 +311,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"browser.back",
"browser.forward",
"browser.goto",
"browser.reload"
]
"methods": ["browser.back", "browser.forward", "browser.goto", "browser.reload"]
}
},
{
@@ -1813,11 +1803,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"ssh.getState",
"status.get",
"worktree.show"
]
"methods": ["ssh.getState", "status.get", "worktree.show"]
}
},
{
@@ -2941,11 +2927,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"host.platform",
"settings.get",
"ssh.listTargetSummaries"
]
"methods": ["host.platform", "settings.get", "ssh.listTargetSummaries"]
}
},
{
@@ -4283,10 +4265,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"github.resolveReviewThread",
"github.updatePRTitle"
]
"methods": ["github.resolveReviewThread", "github.updatePRTitle"]
}
},
{
@@ -7406,7 +7385,10 @@
"agentSession.hold",
"agentSession.options",
"agentSession.release",
"agentSession.send"
"agentSession.respondToApproval",
"agentSession.respondToQuestion",
"agentSession.send",
"agentSession.setOption"
]
}
},
@@ -8415,11 +8397,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"git.discard",
"git.stage",
"git.unstage"
]
"methods": ["git.discard", "git.stage", "git.unstage"]
}
},
{
@@ -10684,6 +10662,62 @@
"methods": []
}
},
{
"file": "mobile/src/session/use-mobile-native-chat-file-search.test.ts",
"line": 149,
"column": 63,
"symbol": "'sendRequest'",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
"file": "mobile/src/session/use-mobile-native-chat-file-search.test.ts",
"line": 150,
"column": 11,
"symbol": "sendRequest",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
"file": "mobile/src/session/use-mobile-native-chat-file-search.test.ts",
"line": 158,
"column": 52,
"symbol": "'sendRequest'",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
"file": "mobile/src/session/use-mobile-native-chat-file-search.test.ts",
"line": 162,
"column": 22,
"symbol": "sendRequest",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
"file": "mobile/src/session/use-mobile-native-chat-file-search.ts",
"line": 91,
@@ -13028,11 +13062,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"browser.back",
"browser.forward",
"browser.reload"
]
"methods": ["browser.back", "browser.forward", "browser.reload"]
}
},
{
@@ -14628,10 +14658,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"git.bulkStage",
"git.push"
]
"methods": ["git.bulkStage", "git.push"]
}
},
{
@@ -16385,10 +16412,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"gitlab.addIssueComment",
"gitlab.addMRComment"
]
"methods": ["gitlab.addIssueComment", "gitlab.addMRComment"]
}
},
{
@@ -16432,10 +16456,49 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"gitlab.updateIssue",
"gitlab.updateMR"
]
"methods": ["gitlab.updateIssue", "gitlab.updateMR"]
}
},
{
"file": "mobile/src/tasks/use-mobile-tasks-item-detail-loading.test.ts",
"line": 19,
"column": 11,
"symbol": "sendRequest",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
"file": "mobile/src/tasks/use-mobile-tasks-item-detail-loading.test.ts",
"line": 24,
"column": 17,
"symbol": "sendRequest",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
"file": "mobile/src/tasks/use-mobile-tasks-item-detail-loading.test.ts",
"line": 46,
"column": 12,
"symbol": "sendRequest",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
@@ -16558,6 +16621,48 @@
"policy": "unclassified",
"role": "call"
},
{
"file": "mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.test.ts",
"line": 12,
"column": 11,
"symbol": "sendRequest",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
"file": "mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.test.ts",
"line": 18,
"column": 17,
"symbol": "sendRequest",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
"file": "mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.test.ts",
"line": 33,
"column": 12,
"symbol": "sendRequest",
"method": "dynamic",
"kind": "request",
"policy": "unclassified",
"role": "reference",
"descriptor": {
"kind": "access-reference",
"methods": []
}
},
{
"file": "mobile/src/tasks/use-mobile-tasks-project-file-merge-actions.tsx",
"line": 65,
@@ -16599,10 +16704,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"github.updateIssue",
"github.updatePRState"
]
"methods": ["github.updateIssue", "github.updatePRState"]
}
},
{
@@ -16666,10 +16768,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"github.project.clearItemField",
"github.project.updateItemField"
]
"methods": ["github.project.clearItemField", "github.project.updateItemField"]
}
},
{
@@ -16813,10 +16912,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"github.project.updateIssueBySlug",
"github.project.updatePullRequestBySlug"
]
"methods": ["github.project.updateIssueBySlug", "github.project.updatePullRequestBySlug"]
}
},
{
@@ -16960,10 +17056,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"github.createIssue",
"gitlab.createIssue"
]
"methods": ["github.createIssue", "gitlab.createIssue"]
}
},
{
@@ -17887,7 +17980,10 @@
"agentSession.hold",
"agentSession.options",
"agentSession.release",
"agentSession.respondToApproval",
"agentSession.respondToQuestion",
"agentSession.send",
"agentSession.setOption",
"aiVault.listSessions",
"aiVault.prepareSessionResume",
"browser.back",
@@ -18068,11 +18164,7 @@
],
"owner": "Jinwoo",
"policyId": "transport-forwarding",
"scenarioIds": [
"transport:fulfilled",
"transport:refused",
"transport:rejection"
],
"scenarioIds": ["transport:fulfilled", "transport:refused", "transport:rejection"],
"reason": "transport port, below the operation boundary"
}
},
@@ -18116,11 +18208,7 @@
],
"owner": "Jinwoo",
"policyId": "transport-forwarding",
"scenarioIds": [
"transport:fulfilled",
"transport:refused",
"transport:rejection"
],
"scenarioIds": ["transport:fulfilled", "transport:refused", "transport:rejection"],
"reason": "transport port, below the operation boundary"
}
},
@@ -18944,11 +19032,7 @@
],
"owner": "Jinwoo",
"policyId": "transport-forwarding",
"scenarioIds": [
"transport:fulfilled",
"transport:refused",
"transport:rejection"
],
"scenarioIds": ["transport:fulfilled", "transport:refused", "transport:rejection"],
"reason": "transport port, below the operation boundary"
}
},
@@ -18963,10 +19047,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"browser.screencast",
"runtime.clientEvents.subscribe"
]
"methods": ["browser.screencast", "runtime.clientEvents.subscribe"]
}
},
{
@@ -18980,10 +19061,7 @@
"role": "call",
"descriptor": {
"kind": "caller-resolved",
"methods": [
"browser.screencast",
"runtime.clientEvents.subscribe"
]
"methods": ["browser.screencast", "runtime.clientEvents.subscribe"]
}
},
{
@@ -19485,7 +19563,10 @@
"agentSession.hold",
"agentSession.options",
"agentSession.release",
"agentSession.respondToApproval",
"agentSession.respondToQuestion",
"agentSession.send",
"agentSession.setOption",
"aiVault.listSessions",
"aiVault.prepareSessionResume",
"browser.back",
@@ -19666,11 +19747,7 @@
],
"owner": "Jinwoo",
"policyId": "transport-forwarding",
"scenarioIds": [
"transport:fulfilled",
"transport:refused",
"transport:rejection"
],
"scenarioIds": ["transport:fulfilled", "transport:refused", "transport:rejection"],
"reason": "transport port, below the operation boundary"
}
},
@@ -21243,7 +21320,10 @@
"agentSession.hold",
"agentSession.options",
"agentSession.release",
"agentSession.respondToApproval",
"agentSession.respondToQuestion",
"agentSession.send",
"agentSession.setOption",
"aiVault.listSessions",
"aiVault.prepareSessionResume",
"browser.back",
@@ -21424,11 +21504,7 @@
],
"owner": "Jinwoo",
"policyId": "transport-forwarding",
"scenarioIds": [
"transport:fulfilled",
"transport:refused",
"transport:rejection"
],
"scenarioIds": ["transport:fulfilled", "transport:refused", "transport:rejection"],
"reason": "transport port, below the operation boundary"
}
},
@@ -21486,11 +21562,7 @@
],
"owner": "Jinwoo",
"policyId": "transport-forwarding",
"scenarioIds": [
"transport:fulfilled",
"transport:refused",
"transport:rejection"
],
"scenarioIds": ["transport:fulfilled", "transport:refused", "transport:rejection"],
"reason": "transport port, below the operation boundary"
}
},
+10
View File
@@ -106,6 +106,16 @@
"file": "mobile/src/tasks/use-mobile-tasks-item-detail-loading.tsx",
"line": 189
}
},
{
"id": "new-workspace-runtime-context-null-settings-typeerror",
"status": "ticket",
"description": "useNewWorkspaceRuntimeContext throws an unhandled TypeError when settings.get fulfills with a null/absent result: .settings is read off a cast with no guard and the void async effect has no catch",
"main": {
"sha": "aac38d698f",
"file": "mobile/src/components/use-new-workspace-runtime-context.ts",
"line": 53
}
}
]
}
@@ -0,0 +1,40 @@
{
"revision": "bcba08b3e4675fe0c687fe6346655f8ce66cd138",
"sources": [
{
"file": "mobile/src/session/native-host-session-native-chat-operations.ts",
"sha256": "2b8bf516424f730617826e3e6e180a5c3e7b3b626d0aba759c695677968d3000"
},
{
"file": "mobile/src/session/github-pr-mutations.ts",
"sha256": "9d07404d2936a7eca4a0b97aad495ae0adab01b18a15d37d9796d762edc0739a"
},
{
"file": "mobile/src/tasks/native-host-task-project-mutation-operations.ts",
"sha256": "360e0609c30ce099c9bba014b3bb8b7458f0af453351e0614cc1a12b5608a594"
},
{
"file": "mobile/src/tasks/native-host-task-detail-operations.ts",
"sha256": "92df0e2f312caeb7d6f8af20132ce6553d3343904ca2422c4a89c45344cec66b"
}
],
"supportingFunctions": ["rankSuggestions", "buildGithubPrParams", "githubPrRepoSlugParam"],
"supportingSource": "current main; ranking and sender parameter construction only, never reply acceptance or lifecycle",
"results": [
{
"id": "b1",
"killed": true,
"observed": "[\"stale/app.ts\"]"
},
{
"id": "b2",
"killed": true,
"observed": "null result accepted"
},
{
"id": "b3",
"killed": true,
"observed": "issue refused"
}
]
}
+56
View File
@@ -0,0 +1,56 @@
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import ts from 'typescript'
import { files, root } from './rpc-artifact-io.mts'
const predicates: { file: string; line: number; expression: string }[] = []
for (const file of files(join(root, 'mobile/src')).filter(
(file) => /\.tsx?$/.test(file) && !/\.test\.|test-support|\.generated\./.test(file)
)) {
const sf = ts.createSourceFile(
file,
readFileSync(join(root, file), 'utf8'),
ts.ScriptTarget.Latest,
true
)
const visit = (node: ts.Node): void => {
const expression = ts.isIfStatement(node)
? node.expression
: ts.isConditionalExpression(node)
? node.condition
: ts.isReturnStatement(node)
? node.expression
: undefined
if (expression && /\.ok\b|\.success\b|\bisSuccess\(/.test(expression.getText(sf))) {
predicates.push({
file,
line: sf.getLineAndCharacterOfPosition(expression.getStart(sf)).line + 1,
expression: expression.getText(sf).replace(/\s+/g, ' ')
})
}
ts.forEachChild(node, visit)
}
visit(sf)
}
const helpers = [
'mobile/src/transport/mobile-relay-direct-upgrade.ts:166 requireSuccess',
'mobile/src/transport/mobile-relay-pairing-recovery.ts:297 requireSuccess',
'mobile/src/transport/pre-profile-pairing-coordinator.ts:286 requireSuccess'
]
const text =
'# Step 0.5 acceptance consolidation remains separate\n\nKeep all current behavior, including malformed-result exceptions; classify the following predicates into named policy families before replacing any caller. The census includes if/conditional/return expressions using `.ok`, `.success`, or `isSuccess`, with exact source locations; it deliberately retains transport and non-RPC matches for review.\n\nThree existing definitions to remove only after partition fixtures exist:\n\n' +
helpers.map((value) => `- ${value}`).join('\n') +
'\n\nInline predicate census:\n\n' +
predicates
.map((row) => `- ${row.file}:${row.line} — \`${row.expression.replace(/`/g, '\\`')}\``)
.join('\n') +
'\n'
const path = join(root, 'mobile/rpc-foundation/STEP0_5-TODO.md')
if (process.argv.includes('--check')) {
if (readFileSync(path, 'utf8') !== text) {
throw new Error('Stale acceptance census')
}
} else {
writeFileSync(path, text)
}
console.log(`acceptance census: 3 definitions, ${predicates.length} predicate expressions`)
+35 -127
View File
@@ -1,5 +1,6 @@
import { join } from 'node:path'
import ts from 'typescript'
import { createRpcAccessResolver } from './rpc-access-resolution.mts'
import { emit, files, root } from './rpc-artifact-io.mts'
export type AccessEntry = {
@@ -21,7 +22,7 @@ export type AccessEntry = {
}
}
const paths = [...files(join(root, 'mobile/src')), ...files(join(root, 'mobile/app'))].filter(
(file) => /\.[jt]sx?$/.test(file) && !/\.generated\./.test(file)
(file) => /\.[jt]sx?$/.test(file)
)
const config = ts.readConfigFile(join(root, 'mobile/tsconfig.json'), ts.sys.readFile)
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, join(root, 'mobile'))
@@ -31,123 +32,18 @@ const program = ts.createProgram(
)
const checker = program.getTypeChecker()
const entries: AccessEntry[] = []
function entryKind(name: string | undefined): AccessEntry['kind'] | undefined {
return name === 'sendRequest' ? 'request' : name === 'subscribe' ? 'subscribe' : undefined
}
function resolveKind(node: ts.Node, seen = new Set<ts.Node>()): AccessEntry['kind'] | undefined {
if (seen.has(node)) return
seen.add(node)
if (ts.isPropertyAccessExpression(node)) return entryKind(node.name.text)
if (
ts.isElementAccessExpression(node) &&
node.argumentExpression &&
ts.isStringLiteral(node.argumentExpression)
)
return entryKind(node.argumentExpression.text)
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'bind'
)
return resolveKind(node.expression.expression, seen)
if (!ts.isIdentifier(node)) return
const direct = entryKind(node.text)
if (direct) return direct
let symbol = checker.getSymbolAtLocation(node)
if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol)
for (const declaration of symbol?.declarations ?? []) {
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
const kind = resolveKind(declaration.initializer, seen)
if (kind) return kind
}
if (ts.isBindingElement(declaration))
return entryKind(
(declaration.propertyName ?? declaration.name).getText().replace(/['"]/g, '')
)
}
}
const calls: ts.CallExpression[] = []
for (const file of paths) {
const walk = (node: ts.Node): void => {
if (ts.isCallExpression(node)) calls.push(node)
ts.forEachChild(node, walk)
}
walk(program.getSourceFile(join(root, file))!)
}
function methods(node: ts.Expression | undefined, seen = new Set<ts.Node>()): string[] {
if (!node) return []
if (ts.isStringLiteralLike(node)) return [node.text]
if (seen.has(node)) return []
seen = new Set(seen).add(node)
const type = checker.getTypeAtLocation(node)
const types = type.isUnion() ? type.types : [type]
if (types.every((item) => item.isStringLiteral()))
return types.map((item) => (item as ts.StringLiteralType).value).sort()
const symbol = checker.getSymbolAtLocation(node)
for (const declaration of symbol?.declarations ?? []) {
if (ts.isVariableDeclaration(declaration) && declaration.initializer)
return methods(declaration.initializer, seen)
if (ts.isBindingElement(declaration) && ts.isObjectBindingPattern(declaration.parent)) {
const binding = declaration.parent.parent
if (ts.isVariableDeclaration(binding) && binding.initializer) {
const key = (declaration.propertyName ?? declaration.name).getText()
const target = checker.getSymbolAtLocation(binding.initializer)
for (const param of target?.declarations ?? []) {
if (!ts.isParameter(param) || !ts.isFunctionLike(param.parent)) continue
const index = param.parent.parameters.indexOf(param)
const values = calls
.filter((call) => checker.getResolvedSignature(call)?.declaration === param.parent)
.map((call) => call.arguments[index])
.flatMap((arg) => {
if (!arg || !ts.isObjectLiteralExpression(arg)) return []
return arg.properties
.filter((property) => property.name?.getText() === key)
.flatMap((property) =>
ts.isPropertyAssignment(property)
? methods(property.initializer, seen)
: ts.isShorthandPropertyAssignment(property)
? methods(property.name, seen)
: []
)
})
if (values.length) return [...new Set(values)].sort()
}
}
}
if (ts.isShorthandPropertyAssignment(declaration)) {
const value = checker.getShorthandAssignmentValueSymbol(declaration)
for (const item of value?.declarations ?? [])
if (ts.isParameter(item)) {
const owner = item.parent
if (ts.isFunctionLike(owner)) {
const index = owner.parameters.indexOf(item)
const values = calls
.filter((call) => checker.getResolvedSignature(call)?.declaration === owner)
.flatMap((call) => methods(call.arguments[index], seen))
if (values.length) return [...new Set(values)].sort()
}
}
}
if (ts.isParameter(declaration)) {
const owner = declaration.parent
if (!ts.isFunctionLike(owner)) continue
const index = owner.parameters.indexOf(declaration)
const callerArgs = calls
.filter((call) => checker.getResolvedSignature(call)?.declaration === owner)
.map((call) => call.arguments[index])
const resolved = callerArgs.map((arg) => methods(arg, seen))
if (resolved.length && resolved.every((values) => values.length))
return [...new Set(resolved.flat())].sort()
}
}
return []
}
const { entryKind, resolveKind, methods, calls } = createRpcAccessResolver(
program,
paths.map((file) => join(root, file))
)
for (const file of paths) {
const sf = program.getSourceFile(join(root, file))!
const positions = new Set<number>()
function add(node: ts.Node, kind: AccessEntry['kind'], call?: ts.CallExpression): void {
const start = node.getStart(sf)
if (positions.has(start)) return
if (positions.has(start)) {
return
}
positions.add(start)
const location = sf.getLineAndCharacterOfPosition(start)
const literals = methods(call?.arguments[0])
@@ -161,8 +57,11 @@ for (const file of paths) {
policy: 'unclassified',
role: call ? 'call' : 'reference'
}
if (!call) base.descriptor = { kind: 'access-reference', methods: [] }
else if (literals.length > 1) base.descriptor = { kind: 'caller-resolved', methods: literals }
if (!call) {
base.descriptor = { kind: 'access-reference', methods: [] }
} else if (literals.length > 1) {
base.descriptor = { kind: 'caller-resolved', methods: literals }
}
if (call && base.method === 'dynamic' && !base.descriptor) {
const parameter = checker.getResolvedSignature(call)?.parameters[0]
const parameterType = parameter && checker.getTypeOfSymbolAtLocation(parameter, call)
@@ -172,23 +71,24 @@ for (const file of paths) {
'mobile/app/connection-log.tsx',
'mobile/src/session/use-mobile-native-chat-terminal-stream.ts'
].includes(file)
)
) {
base.descriptor = {
kind: 'non-rpc-listener',
methods: [],
reason: 'store listener or terminal handle callback; raw RPC is inventoried at its sender'
}
else if (
} else if (
kind === 'subscribe' &&
parameterType &&
!(
parameterType.flags &
(ts.TypeFlags.String | ts.TypeFlags.StringLiteral | ts.TypeFlags.Any | ts.TypeFlags.Union)
)
)
) {
base.descriptor = { kind: 'non-rpc-listener', methods: [] }
else if (/\.test\./.test(file)) base.descriptor = { kind: 'test-reference', methods: [] }
else if (file.startsWith('mobile/src/transport/'))
} else if (/\.test\./.test(file)) {
base.descriptor = { kind: 'test-reference', methods: [] }
} else if (file.startsWith('mobile/src/transport/')) {
base.descriptor = {
kind: 'transport-exception',
methods: [
@@ -203,7 +103,7 @@ for (const file of paths) {
scenarioIds: ['transport:fulfilled', 'transport:refused', 'transport:rejection'],
reason: 'transport port, below the operation boundary'
}
else if (file.endsWith('/use-mobile-git-requests.ts')) {
} else if (file.endsWith('/use-mobile-git-requests.ts')) {
const descriptorSource = program.getSourceFile(
join(root, 'mobile/src/source-control/mobile-git-operation-descriptors.ts')
)!
@@ -213,8 +113,9 @@ for (const file of paths) {
ts.isPropertyAssignment(part) &&
part.name.getText() === 'method' &&
ts.isStringLiteral(part.initializer)
)
) {
literalMethods.push(part.initializer.text)
}
ts.forEachChild(part, collect)
}
collect(descriptorSource)
@@ -226,16 +127,22 @@ for (const file of paths) {
function visit(node: ts.Node): void {
if (ts.isCallExpression(node)) {
const kind = resolveKind(node.expression)
if (kind) add(node.expression, kind, node)
if (kind) {
add(node.expression, kind, node)
}
}
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
const kind = resolveKind(node)
if (kind) add(node, kind)
if (kind) {
add(node, kind)
}
} else if (ts.isIdentifier(node) || ts.isStringLiteral(node)) {
const parent = node.parent
if (!(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent))) {
const kind = ts.isIdentifier(node) ? resolveKind(node) : entryKind(node.text)
if (kind) add(node, kind)
if (kind) {
add(node, kind)
}
}
}
ts.forEachChild(node, visit)
@@ -245,15 +152,16 @@ for (const file of paths) {
entries.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column)
if (process.argv.includes('--require-no-dynamic-calls')) {
const unresolved = entries.filter((entry) => entry.method === 'dynamic' && !entry.descriptor)
if (unresolved.length)
if (unresolved.length) {
throw new Error(
`Dynamic calls lack descriptors:\n${unresolved.map((entry) => `${entry.file}:${entry.line} ${entry.symbol}`).join('\n')}`
)
}
}
emit('access-inventory', {
schemaVersion: 1,
scope: ['mobile/src', 'mobile/app'],
excludes: ['*.generated.*'],
excludes: [],
entries
})
console.log(
+171
View File
@@ -0,0 +1,171 @@
import ts from 'typescript'
export type RpcAccessKind = 'request' | 'subscribe'
export function createRpcAccessResolver(program: ts.Program, paths: string[]) {
const checker = program.getTypeChecker()
function entryKind(name: string | undefined): RpcAccessKind | undefined {
return name === 'sendRequest' ? 'request' : name === 'subscribe' ? 'subscribe' : undefined
}
function resolveKind(node: ts.Node, seen = new Set<ts.Node>()): RpcAccessKind | undefined {
if (seen.has(node)) {
return
}
seen.add(node)
if (ts.isPropertyAccessExpression(node)) {
return entryKind(node.name.text)
}
if (ts.isElementAccessExpression(node) && node.argumentExpression) {
const keyType = checker.getTypeAtLocation(node.argumentExpression)
if (keyType.isStringLiteral()) {
return entryKind(keyType.value)
}
if (checker.getTypeAtLocation(node.expression).getProperty('sendRequest')) {
return 'request'
}
}
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'bind'
) {
return resolveKind(node.expression.expression, seen)
}
if (!ts.isIdentifier(node)) {
return
}
const direct = entryKind(node.text)
if (direct) {
return direct
}
let symbol = checker.getSymbolAtLocation(node)
if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
symbol = checker.getAliasedSymbol(symbol)
}
for (const declaration of symbol?.declarations ?? []) {
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
const kind = resolveKind(declaration.initializer, seen)
if (kind) {
return kind
}
}
if (ts.isBindingElement(declaration)) {
return entryKind(
(declaration.propertyName ?? declaration.name).getText().replace(/['"]/g, '')
)
}
}
}
const calls: ts.CallExpression[] = []
for (const file of paths) {
const walk = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
calls.push(node)
}
ts.forEachChild(node, walk)
}
walk(program.getSourceFile(file)!)
}
function callers(owner: ts.SignatureDeclaration): ts.CallExpression[] {
const direct = calls.filter((call) => checker.getResolvedSignature(call)?.declaration === owner)
if (direct.length) {
return direct
}
if (
owner.getSourceFile().fileName.endsWith('use-mobile-structured-agent-session.ts') &&
owner.parameters[0]?.name.getText() === 'method'
) {
return calls.filter((call) => {
const declaration = checker.getResolvedSignature(call)?.declaration
return (
declaration?.parent &&
ts.isTypeAliasDeclaration(declaration.parent) &&
declaration.parent.name.text === 'StructuredAgentSessionMutate'
)
})
}
return []
}
function methods(node: ts.Expression | undefined, seen = new Set<ts.Node>()): string[] {
if (!node) {
return []
}
if (ts.isStringLiteralLike(node)) {
return [node.text]
}
if (seen.has(node)) {
return []
}
seen = new Set(seen).add(node)
const type = checker.getTypeAtLocation(node)
const types = type.isUnion() ? type.types : [type]
if (types.every((item) => item.isStringLiteral())) {
return types.map((item) => (item as ts.StringLiteralType).value).sort()
}
const symbol = checker.getSymbolAtLocation(node)
for (const declaration of symbol?.declarations ?? []) {
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
return methods(declaration.initializer, seen)
}
if (ts.isBindingElement(declaration) && ts.isObjectBindingPattern(declaration.parent)) {
const binding = declaration.parent.parent
if (ts.isVariableDeclaration(binding) && binding.initializer) {
const key = (declaration.propertyName ?? declaration.name).getText()
const target = checker.getSymbolAtLocation(binding.initializer)
for (const param of target?.declarations ?? []) {
if (!ts.isParameter(param) || !ts.isFunctionLike(param.parent)) {
continue
}
const index = param.parent.parameters.indexOf(param)
const values = callers(param.parent)
.map((call) => call.arguments[index])
.map((arg) => {
if (!arg || !ts.isObjectLiteralExpression(arg)) {
return []
}
return arg.properties
.filter((property) => property.name?.getText() === key)
.flatMap((property) =>
ts.isPropertyAssignment(property)
? methods(property.initializer, seen)
: ts.isShorthandPropertyAssignment(property)
? methods(property.name, seen)
: []
)
})
if (values.length && values.every((value) => value.length)) {
return [...new Set(values.flat())].sort()
}
}
}
}
if (ts.isShorthandPropertyAssignment(declaration)) {
const value = checker.getShorthandAssignmentValueSymbol(declaration)
for (const item of value?.declarations ?? []) {
if (ts.isParameter(item)) {
const owner = item.parent
if (ts.isFunctionLike(owner)) {
const index = owner.parameters.indexOf(item)
const values = callers(owner).map((call) => methods(call.arguments[index], seen))
if (values.length && values.every((value) => value.length)) {
return [...new Set(values.flat())].sort()
}
}
}
}
}
if (ts.isParameter(declaration)) {
const owner = declaration.parent
if (!ts.isFunctionLike(owner)) {
continue
}
const index = owner.parameters.indexOf(declaration)
const callerArgs = callers(owner).map((call) => call.arguments[index])
const resolved = callerArgs.map((arg) => methods(arg, seen))
if (resolved.length && resolved.every((values) => values.length)) {
return [...new Set(resolved.flat())].sort()
}
}
}
return []
}
return { entryKind, resolveKind, methods, calls }
}
+9 -6
View File
@@ -1,9 +1,8 @@
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { isDeepStrictEqual } from 'node:util'
import { runProcess } from '../../src/shared/child-process/run-process.ts'
export const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
export const root = resolve(import.meta.dirname, '../..')
export function files(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true })
.flatMap((entry) => {
@@ -14,21 +13,25 @@ export function files(directory: string): string[] {
}
export function option(name: string, fallback?: string): string {
const index = process.argv.indexOf(`--${name}`)
const value = index < 0 ? fallback : process.argv[index + 1]
if (!value || value.startsWith('--')) throw new Error(`Missing --${name}`)
const value = index === -1 ? fallback : process.argv[index + 1]
if (!value || value.startsWith('--')) {
throw new Error(`Missing --${name}`)
}
return value
}
export async function git(...args: string[]): Promise<string> {
const result = await runProcess({ program: 'git', args, cwd: root, maxOutputBytes: 8_000_000 })
if (result.code !== 0 || result.outputTruncated)
if (result.code !== 0 || result.outputTruncated) {
throw new Error(`git ${args.join(' ')}: ${result.stderr}`)
}
return result.stdout.trimEnd()
}
export function emit(name: string, value: unknown): void {
const path = resolve(root, option('output', `mobile/rpc-foundation/${name}.json`))
if (process.argv.includes('--check')) {
if (!isDeepStrictEqual(JSON.parse(readFileSync(path, 'utf8')), value))
if (!isDeepStrictEqual(JSON.parse(readFileSync(path, 'utf8')), value)) {
throw new Error(`Stale artifact: ${path}`)
}
console.log(`${name}: current`)
} else {
mkdirSync(dirname(path), { recursive: true })
+54 -24
View File
@@ -1,13 +1,12 @@
/** Ledger schema: { baseline, regression, entries: [{ id, description, status:
* 'reference-only' | 'ticket', branch: { sha, file, line }, main: { sha, file, line } }] }.
* Reference-only entries document unmerged behavior; tickets never authorize changes.
* Tickets require only main provenance; reference-only entries require both sides.
*/
import { readFileSync, existsSync } from 'node:fs'
import { dirname, isAbsolute, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { isAbsolute, resolve } from 'node:path'
import { runProcess } from '../../src/shared/child-process/run-process.ts'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const root = resolve(import.meta.dirname, '../..')
const artifacts = resolve(root, 'mobile/rpc-foundation')
const contracts: Record<string, string[]> = {
step0: [
@@ -55,7 +54,9 @@ const [step, ...args] = process.argv.slice(2)
const flags = new Map<string, string>()
function required(name: string): string {
const value = flags.get(name)
if (!value) throw new Error(`Missing --${name}`)
if (!value) {
throw new Error(`Missing --${name}`)
}
return value
}
function artifact(name: string): string {
@@ -73,12 +74,13 @@ async function run(args: string[]): Promise<void> {
})
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
if (result.code !== 0 || result.timedOut) throw new Error(`Check failed: ${args.join(' ')}`)
if (result.code !== 0 || result.timedOut) {
throw new Error(`Check failed: ${args.join(' ')}`)
}
}
async function generator(name: string, path: string, extra: string[] = []): Promise<void> {
await run([
'--import',
'tsx',
'mobile/node_modules/tsx/dist/cli.mjs',
`mobile/scripts/${name}.mts`,
'--check',
'--output',
@@ -88,17 +90,26 @@ async function generator(name: string, path: string, extra: string[] = []): Prom
}
async function checkStep0(): Promise<void> {
const ledger = JSON.parse(readFileSync(artifact('deltas'), 'utf8'))
if (ledger.baseline !== required('baseline') || ledger.regression !== required('regression'))
if (ledger.baseline !== required('baseline') || ledger.regression !== required('regression')) {
throw new Error('Ledger provenance differs')
if (!Array.isArray(ledger.entries) || ledger.entries.length < 7)
}
if (!Array.isArray(ledger.entries) || ledger.entries.length < 7) {
throw new Error('Incomplete named deltas')
}
for (const entry of ledger.entries) {
if (!entry.id || !entry.description || !['reference-only', 'ticket'].includes(entry.status))
if (!entry.id || !entry.description || !['reference-only', 'ticket'].includes(entry.status)) {
throw new Error('Invalid delta')
for (const side of ['main', 'branch']) {
}
for (const side of entry.status === 'ticket' ? ['main'] : ['main', 'branch']) {
const location = entry[side]
if (!location?.sha || !location.file || !Number.isInteger(location.line) || location.line < 1)
if (
!location?.sha ||
!location.file ||
!Number.isInteger(location.line) ||
location.line < 1
) {
throw new Error(`Invalid ${side} provenance: ${entry.id}`)
}
}
}
if (flags.has('require-all-current-calls') || flags.has('require-no-dynamic-calls')) {
@@ -111,26 +122,38 @@ async function checkStep0(): Promise<void> {
if (
flags.has('require-floor-derived-from-protocol-gate') ||
flags.has('require-retirement-decision')
)
) {
await generator('rpc-support-floor', artifact('support-floor'))
if (flags.has('slice'))
}
if (flags.has('slice')) {
await generator('rpc-slice-manifest', artifact('slice'), ['--pr-head', required('pr-head')])
if (flags.has('require-native-rpc-inventory'))
}
if (flags.has('require-native-rpc-inventory')) {
await generator('rpc-native-inventory', resolve(artifacts, 'native-rpc-inventory.json'))
}
for (const flag of ['require-gate-fail-closed', 'require-below-floor-block-e2e']) {
if (!flags.has(flag)) continue
if (!flags.has('allow-lane-b-deferred'))
if (!flags.has(flag)) {
continue
}
if (!flags.has('allow-lane-b-deferred')) {
throw new Error(`--${flag}: not implemented; lane B required`)
}
console.log(`--${flag}: deferred to lane B`)
}
if (flags.has('require-regressions')) {
const seeds = JSON.parse(readFileSync(resolve(artifacts, 'regression-seeds.json'), 'utf8'))
if (seeds.baseline !== required('baseline') || seeds.regression !== required('regression'))
if (seeds.baseline !== required('baseline') || seeds.regression !== required('regression')) {
throw new Error('Regression provenance differs')
}
await generator('rpc-reference-regressions', resolve(artifacts, 'reference-regressions.json'), [
'--regression',
required('regression')
])
for (const id of required('require-regressions').split(',')) {
const seed = seeds.tests.find((test: { id: string }) => test.id === id)
if (!seed || !readFileSync(resolve(root, seed.file), 'utf8').includes(seed.name))
if (!seed || !readFileSync(resolve(root, seed.file), 'utf8').includes(seed.name)) {
throw new Error(`Missing regression: ${id}`)
}
await run([
'mobile/node_modules/vitest/vitest.mjs',
'run',
@@ -143,7 +166,9 @@ async function checkStep0(): Promise<void> {
}
}
const ratchet = 'mobile/src/transport/screen-rpc-ratchet.test.ts'
if (!existsSync(resolve(root, ratchet))) throw new Error('Missing main ratchet')
if (!existsSync(resolve(root, ratchet))) {
throw new Error('Missing main ratchet')
}
await run([
'mobile/node_modules/vitest/vitest.mjs',
'run',
@@ -153,14 +178,19 @@ async function checkStep0(): Promise<void> {
])
}
try {
if (!contracts[step]) throw new Error(`Expected subcommand: ${Object.keys(contracts).join(', ')}`)
if (!contracts[step]) {
throw new Error(`Expected subcommand: ${Object.keys(contracts).join(', ')}`)
}
for (let i = 0; i < args.length; i++) {
const name = args[i].replace(/^--/, '')
const contract = contracts[step].find((value) => value.replace(/=$/, '') === name)
if (!args[i].startsWith('--') || !contract || flags.has(name))
if (!args[i].startsWith('--') || !contract || flags.has(name)) {
throw new Error(`Unknown or duplicate flag: ${args[i]}`)
}
const value = contract.endsWith('=') ? args[++i] : 'true'
if (!value || value.startsWith('--')) throw new Error(`Missing value for --${name}`)
if (!value || value.startsWith('--')) {
throw new Error(`Missing value for --${name}`)
}
flags.set(name, value)
}
if (step !== 'step0') {
@@ -0,0 +1,130 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import ts from 'typescript'
import { emit, git, option } from './rpc-artifact-io.mts'
import { rankSuggestions } from '../src/session/mobile-native-chat-autocomplete.ts'
import { buildGithubPrParams, githubPrRepoSlugParam } from '../src/session/github-pr-rpc.ts'
const revision = await git('rev-parse', `${option('regression', 'bcba08b3e4')}^{commit}`)
const sources: { file: string; sha256: string }[] = []
async function reference(
file: string,
dependencies: Record<string, unknown> = {}
): Promise<
Record<string, (...args: unknown[]) => Record<string, (...args: unknown[]) => Promise<unknown>>>
> {
const source = await git('show', `${revision}:${file}`)
sources.push({ file, sha256: createHash('sha256').update(source).digest('hex') })
const code = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
}).outputText
const exports = {}
const require = (name: string): unknown =>
dependencies[name] ??
new Proxy(
{},
{
get: (_target, key) => {
throw new Error(`Unprovided reference dependency ${name}:${String(key)}`)
}
}
)
new Function('exports', 'require', code)(exports, require)
return exports
}
const native = await reference('mobile/src/session/native-host-session-native-chat-operations.ts', {
'./mobile-native-chat-autocomplete': { rankSuggestions }
})
const mutations = await reference('mobile/src/session/github-pr-mutations.ts', {
'./github-pr-rpc': { buildGithubPrParams, githubPrRepoSlugParam }
})
const project = await reference(
'mobile/src/tasks/native-host-task-project-mutation-operations.ts',
{ '../session/github-pr-mutations': mutations }
)
const detail = await reference('mobile/src/tasks/native-host-task-detail-operations.ts')
const flush = async (): Promise<void> => {
for (let index = 0; index < 8; index++) {
await Promise.resolve()
}
}
const results: { id: string; killed: boolean; observed: string }[] = []
const pending: Array<(response: unknown) => void> = []
const operations = native.nativeHostSessionNativeChatOperations({
sendRequest: (method: string) =>
method === 'files.searchPaths'
? Promise.resolve({ ok: false, error: { code: 'method_not_found' } })
: new Promise((resolve) => pending.push(resolve))
})
const target = { workspaceId: 'A' }
const stale = operations.searchFiles(target, 'app')
await flush()
operations.resetFileSearchCache('A')
operations.resetFileSearchCache('B')
operations.resetFileSearchCache('A')
const fresh = operations.searchFiles(target, 'app')
await flush()
assert.equal(pending.length, 2)
pending[0]({ ok: true, result: { files: [{ relativePath: 'stale/app.ts' }] } })
await stale
const third = await operations.searchFiles(target, 'app')
results.push({
id: 'b1',
killed: Array.isArray(third) && third.includes('stale/app.ts'),
observed: JSON.stringify(third)
})
pending[1]({ ok: true, result: { files: [{ relativePath: 'fresh/app.ts' }] } })
await fresh
let accepted = false
try {
await project
.nativeHostTaskProjectMutationOperations({
sendRequest: async () => ({ ok: true, result: null })
})
.merge({ number: 42 }, 'repo-1', 'squash')
accepted = true
} catch {
/* The main oracle requires rejection. */
}
results.push({
id: 'b2',
killed: accepted,
observed: accepted ? 'null result accepted' : 'null result rejected'
})
let refuseIssue!: (value: unknown) => void, rejectComments!: (error: Error) => void
const issue = new Promise((resolve) => {
refuseIssue = resolve
})
const comments = new Promise((_resolve, reject) => {
rejectComments = reject
})
let error: unknown
const loading = detail
.nativeHostTaskDetailOperations({
sendRequest: (method: string) => (method === 'linear.getIssue' ? issue : comments)
})
.loadLinear({ issueId: 'issue-1', workspaceId: 'workspace-1' })
.catch((failure: unknown) => {
error = failure
})
refuseIssue({ ok: false, error: { message: 'issue refused' } })
await flush()
rejectComments(new Error('comments transport failed'))
await loading
results.push({
id: 'b3',
killed: error instanceof Error && error.message === 'issue refused',
observed: error instanceof Error ? error.message : String(error)
})
for (const result of results) {
assert.equal(result.killed, true, `${result.id}: reference no longer exhibits the known defect`)
}
emit('reference-regressions', {
revision,
sources,
supportingFunctions: ['rankSuggestions', 'buildGithubPrParams', 'githubPrRepoSlugParam'],
supportingSource:
'current main; ranking and sender parameter construction only, never reply acceptance or lifecycle',
results
})
console.log('reference regressions: b1,b2,b3 killed against pinned source')
+29 -10
View File
@@ -11,7 +11,9 @@ function constants(source: string): Record<string, number> {
/export const ((?:RUNTIME|DESKTOP|MOBILE)_PROTOCOL_VERSION|MIN_COMPATIBLE_(?:RUNTIME_CLIENT|RUNTIME_SERVER|MOBILE|DESKTOP)_VERSION)\s*=\s*(\w+)/g
)) {
const value = /^\d+$/.test(match[2]) ? Number(match[2]) : result[match[2]]
if (value !== undefined) result[match[1]] = value
if (value !== undefined) {
result[match[1]] = value
}
}
return result
}
@@ -33,8 +35,11 @@ for (const value of [
desktop.MIN_COMPATIBLE_RUNTIME_SERVER_VERSION,
mobile.MOBILE_PROTOCOL_VERSION,
mobile.MIN_COMPATIBLE_DESKTOP_VERSION
])
if (!Number.isInteger(value)) throw new Error('Missing protocol gate constant')
]) {
if (!Number.isInteger(value)) {
throw new Error('Missing protocol gate constant')
}
}
const tags = (
await git(
'tag',
@@ -72,7 +77,9 @@ const namespaces = [
const floors = []
for (const namespace of namespaces) {
const candidates = tags.filter((tag) => namespace.pattern.test(tag))
if (!candidates.length) throw new Error(`No tags for ${namespace.name}`)
if (!candidates.length) {
throw new Error(`No tags for ${namespace.name}`)
}
const inspected = []
let floor: unknown, atMinimum3: unknown
for (const tag of candidates) {
@@ -86,12 +93,16 @@ for (const namespace of namespaces) {
}
const values = constants(source)
const version = values[namespace.protocol] ?? values.DESKTOP_PROTOCOL_VERSION
if (!Number.isInteger(version)) throw new Error(`Missing protocol at ${tag}:${namespace.file}`)
if (!Number.isInteger(version)) {
throw new Error(`Missing protocol at ${tag}:${namespace.file}`)
}
const peerMinimum =
namespace.file === desktopPath
? (values.MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION ?? values.MIN_COMPATIBLE_MOBILE_VERSION)
: values.MIN_COMPATIBLE_DESKTOP_VERSION
if (!Number.isInteger(peerMinimum)) throw new Error(`Missing peer minimum at ${tag}`)
if (!Number.isInteger(peerMinimum)) {
throw new Error(`Missing peer minimum at ${tag}`)
}
const clientProtocol =
namespace.file === desktopPath
? mobile.MOBILE_PROTOCOL_VERSION
@@ -105,11 +116,19 @@ for (const namespace of namespaces) {
values[namespace.protocol] === undefined ? 'DESKTOP_PROTOCOL_VERSION' : namespace.protocol
}
inspected.push(evidence)
if (!floor && version >= namespace.minimum && clientProtocol >= peerMinimum) floor = evidence
if (!atMinimum3 && version >= 3 && clientProtocol >= peerMinimum) atMinimum3 = evidence
if (floor && atMinimum3) break
if (!floor && version >= namespace.minimum && clientProtocol >= peerMinimum) {
floor = evidence
}
if (!atMinimum3 && version >= 3 && clientProtocol >= peerMinimum) {
atMinimum3 = evidence
}
if (floor && atMinimum3) {
break
}
}
if (!floor || !atMinimum3) {
throw new Error(`No compatible floor in ${namespace.name}`)
}
if (!floor || !atMinimum3) throw new Error(`No compatible floor in ${namespace.name}`)
floors.push({
namespace: namespace.name,
consideredTags: candidates,
@@ -31,11 +31,15 @@ describe('Linear detail settlement', () => {
setItems: vi.fn()
} as unknown as Parameters<typeof useMobileTasksItemDetailLoading>[0])
refuseIssue({ ok: false, error: { message: 'issue refused' } })
for (let i = 0; i < 8; i++) await Promise.resolve()
for (let i = 0; i < 8; i++) {
await Promise.resolve()
}
expect(setDetailError).toHaveBeenLastCalledWith('')
expect(setDetailLoading).toHaveBeenLastCalledWith(true)
rejectComments(new Error('comments transport failed'))
for (let i = 0; i < 8; i++) await Promise.resolve()
for (let i = 0; i < 8; i++) {
await Promise.resolve()
}
expect(setDetailError).toHaveBeenLastCalledWith('comments transport failed')
expect(setDetailLoading).toHaveBeenLastCalledWith(false)
expect(setDetailPayload).toHaveBeenCalledTimes(1)
@@ -0,0 +1,65 @@
import ts from 'typescript'
import { describe, expect, it } from 'vitest'
import { createRpcAccessResolver } from '../../scripts/rpc-access-resolution.mts'
function fixture(source: string) {
const file = 'rpc-fixture.ts'
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true)
const host = ts.createCompilerHost({ noLib: true, types: [] })
host.getSourceFile = (name) => (name === file ? sourceFile : undefined)
const program = ts.createProgram([file], { noLib: true, types: [] }, host)
return createRpcAccessResolver(program, [file])
}
describe('RPC access symbol resolution', () => {
it('follows aliases, destructuring, bracket constants and bound senders', () => {
const resolver = fixture(`
declare const client: { sendRequest(method: string): void; subscribe(method: string): void }
const direct = client.sendRequest
const alias = direct
const { sendRequest: destructured } = client
const bound = client.sendRequest.bind(client)
const key = 'sendRequest'
alias('git.status'); destructured('files.list'); bound('settings.get'); client[key]('repo.list')
const { subscribe: stream } = client
stream('terminal.subscribe')
`)
const recognized = resolver.calls.filter((call) => resolver.resolveKind(call.expression))
expect(recognized.map((call) => resolver.methods(call.arguments[0]))).toEqual([
['git.status'],
['files.list'],
['settings.get'],
['repo.list'],
['terminal.subscribe']
])
})
it('fails closed when one caller supplies an unresolved method', () => {
const resolver = fixture(`
declare const client: { sendRequest(method: string): void }
declare const unknownMethod: string
function forward(method: string) { client.sendRequest(method) }
forward('git.status'); forward(unknownMethod)
`)
const raw = resolver.calls.find((call) => resolver.resolveKind(call.expression) === 'request')!
expect(resolver.methods(raw.arguments[0])).toEqual([])
})
it('derives the complete literal family from forwarder callers', () => {
const resolver = fixture(`
declare const client: { sendRequest(method: string): void }
function forward(method: string) { client.sendRequest(method) }
forward('git.status'); forward('git.fetch')
`)
const raw = resolver.calls.find((call) => resolver.resolveKind(call.expression) === 'request')!
expect(resolver.methods(raw.arguments[0])).toEqual(['git.fetch', 'git.status'])
})
it('does not hide an unresolved caller behind an object binding', () => {
const resolver = fixture(`
declare const client: { sendRequest(method: string): void }
declare const unknownMethod: string
function forward(args: { method: string }) { const { method } = args; client.sendRequest(method) }
forward({ method: 'git.status' }); forward({ method: unknownMethod })
`)
const raw = resolver.calls.find((call) => resolver.resolveKind(call.expression) === 'request')!
expect(resolver.methods(raw.arguments[0])).toEqual([])
})
})