Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.claude/
pnpm-lock.yaml
app/services/type.ts
app/services/tenant-type.ts
Expand Down
2 changes: 1 addition & 1 deletion .takt/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ persona_providers:

provider_profiles:
cursor:
default_permission_mode: edit
default_permission_mode: full
codex:
default_permission_mode: edit
claude:
Expand Down
88 changes: 87 additions & 1 deletion .takt/tasks.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,87 @@
tasks: []
tasks:
- worktree: false
branch: feat/webhook-realtime-pr-update
piece: implement-step
issue: 255
auto_pr: false
draft_pr: false
name: webhook-realtime-pr-update-impl
status: failed
slug: 20260331-113806-wh255
task_dir: .takt/tasks/20260331-113806-wh255
created_at: 2026-03-31T11:38:06.000Z
started_at: 2026-03-31T11:41:07.279Z
completed_at: 2026-03-31T12:09:48.132Z
owner_pid: null
failure:
movement: fix
error: "Movement execution failed: All parallel sub-movements failed:
arch-review: Report phase failed for architect-review.md: You've hit
your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro),
visit https://chatgpt.com/codex/settings/usage to purchase more credits
or try again at Apr 3rd, 2026 11:33 PM.; supervise: Report phase failed
for supervisor-validation.md: You've hit your usage limit. Upgrade to
Pro (https://chatgpt.com/explore/pro), visit
https://chatgpt.com/codex/settings/usage to purchase more credits or try
again at Apr 3rd, 2026 11:33 PM."
last_message: >-
レビュー報告を確認し、指摘を修正します。






レビュー指摘を修正します: PR オブジェクトの直接ミューテーションと、リポジトリスコープ付き refresh 時の full-org
`process` 誤起動。



PR オブジェクトの非ミュータブル化と、リポジトリスコープ付き refresh 時の full-org `process`
誤判定を修正します。ヘルパーと回帰テストを追加します。












対応内容は次のとおりです。


## 1. PR オブジェクトの直接ミューテーション(ARCH-NEW-direct-pr-files-mutation /
VAL-NEW-direct-mutation)

- **`crawl.server.ts`**: `prMetadata.files = files` をやめ、`const prForSave
= { ...prMetadata, files }` を `savePrData` に渡すように変更。

- **`$pull/index.tsx`**: 同様に `const prForSave = { ...prMetadata, files
}` を使用。

- **`backfill-repo.ts`**: `pr.files = files` をやめ、`const prWithFiles = {
...pr, files }` を `updatePrMetadata` に渡すように変更。


## 2. リポジトリ指定付き refresh で full-org `process` が走る問題(carry-over
ARCH-NEW-crawl-scoped-handoff)

- **`crawl-process-handoff.server.ts`**
を追加し、`shouldTriggerFullOrgProcessJob` で
「`refresh` かつ **`repositoryId` なし** かつ **PR 番号の指定なし**」のときだけ org 全体の `process` と判定。
- **`crawl.server.ts`**: 従来の `input.refresh && !input.prNumbers`
を、この関数呼び出しに置き換え。`--repository … --refresh` のときは常に scoped
側(`updatedPrNumbers` 由来の `scopes`)へ。


## 3. 回帰テスト

- **`crawl-process-handoff.server.test.ts`**: 上記判定のユニットテストを追加(リポジトリ付き
refresh が full-org にならないこと等)。


`pnpm validate` は問題なく完了しています(テスト 310 件)。
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function isRunActive(status: RunStatus): boolean {

const jobNameColors: Record<string, string> = {
crawl: 'bg-blue-100 text-blue-800',
recalculate: 'bg-purple-100 text-purple-800',
process: 'bg-purple-100 text-purple-800',
classify: 'bg-amber-100 text-amber-800',
backfill: 'bg-emerald-100 text-emerald-800',
}
Expand Down
40 changes: 22 additions & 18 deletions app/routes/$orgSlug/settings/data-management/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { getErrorMessage } from '~/app/libs/error-message'
import { orgContext } from '~/app/middleware/context'
import { durably } from '~/app/services/durably'
import { durably as serverDurably } from '~/app/services/durably.server'
import {
crawlConcurrencyKey,
processConcurrencyKey,
} from '~/app/services/jobs/concurrency-keys.server'
import type { JobSteps } from '~/app/services/jobs/shared-steps.server'
import ContentSection from '../+components/content-section'
import { JobHistory, isRunActive } from './+components/job-history'
Expand Down Expand Up @@ -41,7 +45,7 @@ export const action = async ({ request, context }: Route.ActionArgs) => {
await serverDurably.jobs.crawl.trigger(
{ organizationId: org.id, refresh: true },
{
concurrencyKey: `crawl:${org.id}`,
concurrencyKey: crawlConcurrencyKey(org.id),
labels: { organizationId: org.id },
},
)
Expand All @@ -53,7 +57,7 @@ export const action = async ({ request, context }: Route.ActionArgs) => {
)
}
})
.with('recalculate', async () => {
.with('process', async () => {
const selectedSteps = formData.getAll('steps').map(String)
const steps = {
upsert: selectedSteps.includes('upsert'),
Expand All @@ -63,27 +67,27 @@ export const action = async ({ request, context }: Route.ActionArgs) => {
if (!steps.upsert && !steps.export) {
return data(
{
intent: 'recalculate' as const,
intent: 'process' as const,
error: 'At least one step must be selected',
},
{ status: 400 },
)
}

try {
await serverDurably.jobs.recalculate.trigger(
await serverDurably.jobs.process.trigger(
{ organizationId: org.id, steps },
{
concurrencyKey: `recalculate:${org.id}`,
concurrencyKey: processConcurrencyKey(org.id),
labels: { organizationId: org.id },
},
)
return data({ intent: 'recalculate' as const, ok: true })
return data({ intent: 'process' as const, ok: true })
} catch {
return data(
{
intent: 'recalculate' as const,
error: 'Failed to start recalculation',
intent: 'process' as const,
error: 'Failed to start process job',
},
{ status: 500 },
)
Expand Down Expand Up @@ -126,28 +130,28 @@ function RefreshSection({ isRunning }: { isRunning: boolean }) {
)
}

// --- Recalculate Section ---
// --- Process Section ---

function RecalculateSection({ isRunning }: { isRunning: boolean }) {
function ProcessSection({ isRunning }: { isRunning: boolean }) {
const fetcher = useFetcher()
const [upsert, setUpsert] = useState(true)
const [exportData, setExportData] = useState(false)
const noneSelected = !upsert && !exportData
const isSubmitting = fetcher.state !== 'idle'
const triggerError =
fetcher.data?.intent === 'recalculate' ? fetcher.data?.error : null
fetcher.data?.intent === 'process' ? fetcher.data?.error : null

return (
<Stack>
<div className="space-y-1">
<p className="text-sm font-medium">Recalculate Cycle Times</p>
<p className="text-sm font-medium">Process Cycle Times</p>
<p className="text-muted-foreground text-xs">
Re-analyze PR data from stored raw data. Select which steps to run.
</p>
</div>
<fetcher.Form method="post">
<Stack gap="4">
<input type="hidden" name="intent" value="recalculate" />
<input type="hidden" name="intent" value="process" />
<div className="space-y-2">
<div className="flex items-center gap-2">
<Checkbox
Expand Down Expand Up @@ -182,7 +186,7 @@ function RecalculateSection({ isRunning }: { isRunning: boolean }) {
loading={isSubmitting}
disabled={noneSelected || isRunning}
>
Recalculate
Process
</Button>
</div>
</Stack>
Expand Down Expand Up @@ -271,18 +275,18 @@ export default function DataManagementPage({
const isCrawlRunning = runs.some(
(r) => r.jobName === 'crawl' && isRunActive(r.status),
)
const isRecalculateRunning = runs.some(
(r) => r.jobName === 'recalculate' && isRunActive(r.status),
const isProcessRunning = runs.some(
(r) => r.jobName === 'process' && isRunActive(r.status),
)

return (
<ContentSection
title="Data Management"
desc="Manage data refresh and recalculation for this organization."
desc="Manage data refresh and processing for this organization."
>
<Stack gap="6">
<RefreshSection isRunning={isCrawlRunning} />
<RecalculateSection isRunning={isRecalculateRunning} />
<ProcessSection isRunning={isProcessRunning} />
<ExportDataSection orgSlug={orgSlug} />
<JobHistory
runs={runs}
Expand Down
101 changes: 35 additions & 66 deletions app/routes/$orgSlug/settings/repositories/$repository/$pull/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,23 @@ import { href, useFetcher, useRevalidator } from 'react-router'
import { match } from 'ts-pattern'
import { z } from 'zod'
import { Badge, Button, HStack, Heading, Stack } from '~/app/components/ui'
import { getErrorMessage } from '~/app/libs/error-message'
import { orgContext } from '~/app/middleware/context'
import {
getGithubAppLink,
getIntegration,
} from '~/app/services/github-integration-queries.server'
import { resolveOctokitFromOrg } from '~/app/services/github-octokit.server'
import {
upsertPullRequest,
upsertPullRequestReview,
upsertPullRequestReviewers,
} from '~/batch/db/mutations'
import { getBotLogins } from '~/batch/db/queries'
import { processConcurrencyKey } from '~/app/services/jobs/concurrency-keys.server'
import { createFetcher } from '~/batch/github/fetcher'
import type { ShapedGitHubPullRequest } from '~/batch/github/model'
import { buildPullRequests } from '~/batch/github/pullrequest'
import { createStore } from '~/batch/github/store'
import type { Route } from './+types/index'
import {
getOrganizationSettings,
getPullRequest,
getPullRequestRawData,
getPullRequestReviewers,
getPullRequestReviews,
getRepository,
getShapedPullRequest,
} from './queries.server'

export const handle = {
Expand Down Expand Up @@ -137,72 +129,49 @@ export const action = async ({
}
})
.with('refresh', async () => {
// 1. Get existing PR shape from raw data
const shapedPr = await getShapedPullRequest(
organization.id,
repositoryId,
pullId,
)
if (!shapedPr) {
throw new Response('Raw PR data not found. Run Compare first.', {
status: 404,
})
}
const pr = shapedPr as unknown as ShapedGitHubPullRequest

// 2. Re-fetch commits/comments/reviews/timelineItems from GitHub
const [commits, comments, reviews, timelineItems] = await Promise.all([
fetcher.commits(pullId),
fetcher.comments(pullId),
fetcher.reviews(pullId),
fetcher.timelineItems(pullId),
])
const fetchedAt = new Date().toISOString()
const [prMetadata, commits, comments, reviews, timelineItems, files] =
await Promise.all([
fetcher.pullrequest(pullId),
fetcher.commits(pullId),
fetcher.comments(pullId),
fetcher.reviews(pullId),
fetcher.timelineItems(pullId),
fetcher.files(pullId),
])
const prForSave = { ...prMetadata, files }

// 3. Save raw data via store
const store = createStore({
organizationId: organization.id,
repositoryId,
})
await store.savePrData(pr, {
commits,
reviews,
discussions: comments,
timelineItems,
})

// 4. Get organization settings and bot logins for build config
const [settings, botLoginsList] = await Promise.all([
getOrganizationSettings(organization.id),
getBotLogins(organization.id),
])

// 5. Build pull request data (analyze)
const result = await buildPullRequests(
await store.savePrData(
prForSave,
{
organizationId: organization.id,
repositoryId,
botLogins: new Set(botLoginsList),
releaseDetectionMethod: settings?.releaseDetectionMethod ?? 'branch',
releaseDetectionKey: settings?.releaseDetectionKey ?? '',
commits,
reviews,
discussions: comments,
timelineItems,
},
[pr],
store.loader,
fetchedAt,
)

// 6. Upsert to DB
for (const pull of result.pulls) {
await upsertPullRequest(organization.id, pull)
}
for (const review of result.reviews) {
await upsertPullRequestReview(organization.id, review)
}
for (const reviewer of result.reviewers) {
await upsertPullRequestReviewers(
organization.id,
reviewer.repositoryId,
reviewer.pullRequestNumber,
reviewer.reviewers,
const { durably } = await import('~/app/services/durably.server')
try {
await durably.jobs.process.triggerAndWait(
{
organizationId: organization.id,
scopes: [{ repositoryId, prNumbers: [Number(pullId)] }],
},
{
concurrencyKey: processConcurrencyKey(organization.id),
labels: { organizationId: organization.id },
},
)
} catch (e) {
throw new Response(`Process job failed: ${getErrorMessage(e)}`, {
status: 500,
})
}

return { intent: 'refresh' as const, success: true }
Expand Down
Loading
Loading