-
Notifications
You must be signed in to change notification settings - Fork 0
[#46] AI 분석 대상 branch 조합과 코드 evidence를 구성한다 #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| import { Buffer } from "node:buffer" | ||
| import type { BranchComparisonPair } from "../branches/types.js" | ||
| import type { | ||
| GitMergeCodeContextPairResult, | ||
| GitMergeTreePairResult, | ||
| MergeCodeContextEvidence | ||
| } from "../git/types.js" | ||
| import type { BranchConflictGraphEdge } from "../risks/types.js" | ||
| import type { | ||
| AiPredictionPairCodeContext, | ||
| AiPredictionPairEvidencePayload, | ||
| AiPredictionPairMergeStatus, | ||
| AiPredictionPairTargetStatus | ||
| } from "./types.js" | ||
|
|
||
| // 한 branch 조합에 포함할 수 있는 코드 원문의 UTF-8 byte 상한 | ||
| const CODE_CONTEXT_MAX_BYTES = 128 * 1024 | ||
|
|
||
| // 선택된 branch 조합의 graph, merge, 코드 문맥을 하나의 AI evidence로 구성 | ||
| export function build( | ||
| edge: BranchConflictGraphEdge, | ||
| mergeResult: GitMergeTreePairResult, | ||
| codeContextResult?: GitMergeCodeContextPairResult | ||
| ): AiPredictionPairEvidencePayload { | ||
| const targetStatus = targetStatusFor(edge) | ||
|
|
||
| assertMatchingPair(edge.pair, mergeResult.pair) | ||
| assertMatchingCodeContext(edge.pair, codeContextResult) | ||
|
|
||
| const mergeStatus = mergeStatusFor(targetStatus, mergeResult) | ||
|
|
||
| return { | ||
| pair: edge.pair, | ||
| targetStatus, | ||
| reasons: edge.reasons, | ||
| branches: { | ||
| left: { | ||
| name: edge.pair.leftBranchName, | ||
| commitOid: mergeResult.leftCommitOid | ||
| }, | ||
| right: { | ||
| name: edge.pair.rightBranchName, | ||
| commitOid: mergeResult.rightCommitOid | ||
| } | ||
| }, | ||
| merge: { | ||
| status: mergeStatus, | ||
| mergedTreeOid: mergeResult.mergedTreeOid, | ||
| conflictFiles: mergeResult.conflictFiles, | ||
| conflicts: mergeResult.conflicts | ||
| }, | ||
| codeContext: codeContextFor(codeContextResult) | ||
| } | ||
| } | ||
|
|
||
| // graph edge가 확정 conflict 또는 critical potential overlap 대상인지 검증 | ||
| function targetStatusFor( | ||
| edge: BranchConflictGraphEdge | ||
| ): AiPredictionPairTargetStatus { | ||
| if (edge.status === "confirmed_conflict") { | ||
| return edge.status | ||
| } | ||
|
|
||
| if ( | ||
| edge.status === "potential_overlap" && | ||
| edge.reasons.some(reason => reason.code === "same_hunk_overlap") | ||
| ) { | ||
| return edge.status | ||
| } | ||
|
|
||
| throw new Error("AI prediction pair evidence requires selected branch pair") | ||
| } | ||
|
|
||
| // graph target 상태와 실제 merge 수집 상태가 일치하는지 검증 | ||
| function mergeStatusFor( | ||
| targetStatus: AiPredictionPairTargetStatus, | ||
| mergeResult: GitMergeTreePairResult | ||
| ): AiPredictionPairMergeStatus { | ||
| if (mergeResult.status === "merge_check_failed") { | ||
| throw new Error("AI prediction pair evidence requires successful merge collection") | ||
| } | ||
|
|
||
| const matchesTarget = targetStatus === "confirmed_conflict" | ||
| ? mergeResult.status === "confirmed_conflict" | ||
| : mergeResult.status === "clean" | ||
|
|
||
| if (!matchesTarget) { | ||
| throw new Error("AI prediction pair evidence must use matching merge status") | ||
| } | ||
|
|
||
| return mergeResult.status | ||
| } | ||
|
|
||
| // code context 결과와 내부 evidence가 같은 ordered pair에 속하는지 검증 | ||
| function assertMatchingCodeContext( | ||
| pair: BranchComparisonPair, | ||
| result: GitMergeCodeContextPairResult | undefined | ||
| ): void { | ||
| if (!result) { | ||
| return | ||
| } | ||
|
|
||
| assertMatchingPair(pair, result.pair) | ||
|
|
||
| for (const evidence of result.evidence) { | ||
| assertMatchingEvidence(pair, evidence) | ||
| } | ||
| } | ||
|
|
||
| // 코드 문맥 하나가 대상 ordered pair에 속하는지 검증 | ||
| function assertMatchingEvidence( | ||
| pair: BranchComparisonPair, | ||
| evidence: MergeCodeContextEvidence | ||
| ): void { | ||
| assertMatchingPair(pair, evidence.pair) | ||
| } | ||
|
|
||
| // 두 branch 조합의 좌우 이름과 순서가 모두 같은지 검증 | ||
| function assertMatchingPair( | ||
| pair: BranchComparisonPair, | ||
| candidate: BranchComparisonPair | ||
| ): void { | ||
| if ( | ||
| pair.leftBranchName !== candidate.leftBranchName || | ||
| pair.rightBranchName !== candidate.rightBranchName | ||
| ) { | ||
| throw new Error( | ||
| "AI prediction pair evidence must use matching ordered branch pair" | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // code context 수집 결과를 available, missing, failed 상태로 변환 | ||
| function codeContextFor( | ||
| result: GitMergeCodeContextPairResult | undefined | ||
| ): AiPredictionPairCodeContext { | ||
| if (!result) { | ||
| return { | ||
| status: "missing", | ||
| overlapFiles: [], | ||
| evidence: [], | ||
| includedFileCount: 0, | ||
| includedHunkCount: 0, | ||
| omittedFileCount: 0, | ||
| omittedHunkCount: 0 | ||
| } | ||
| } | ||
|
|
||
| if (result.errorMessage) { | ||
| return { | ||
| status: "failed", | ||
| overlapFiles: [], | ||
| evidence: [], | ||
| includedFileCount: 0, | ||
| includedHunkCount: 0, | ||
| omittedFileCount: 0, | ||
| omittedHunkCount: 0, | ||
| errorMessage: result.errorMessage | ||
| } | ||
| } | ||
|
|
||
| const limited = limitedCodeContextFor(result.evidence) | ||
|
|
||
| return { | ||
| status: "available", | ||
| overlapFiles: result.overlapFiles, | ||
| ...limited | ||
| } | ||
| } | ||
|
|
||
| // evidence 순서를 유지하며 hunk의 네 version을 분리하지 않고 조합 상한 적용 | ||
| function limitedCodeContextFor(evidence: MergeCodeContextEvidence[]) { | ||
| let includedByteCount = 0 | ||
| let includedHunkCount = 0 | ||
|
|
||
| for (const item of evidence) { | ||
| const byteCount = rawCodeByteCountFor(item) | ||
|
|
||
| if (CODE_CONTEXT_MAX_BYTES < includedByteCount + byteCount) { | ||
| break | ||
| } | ||
|
|
||
| includedByteCount += byteCount | ||
| includedHunkCount += 1 | ||
| } | ||
|
|
||
| const includedEvidence = evidence.slice(0, includedHunkCount) | ||
| const omittedEvidence = evidence.slice(includedHunkCount) | ||
|
|
||
| return { | ||
| evidence: includedEvidence, | ||
| includedFileCount: uniqueFileCountFor(includedEvidence), | ||
| includedHunkCount, | ||
| omittedFileCount: uniqueFileCountFor(omittedEvidence), | ||
| omittedHunkCount: omittedEvidence.length | ||
| } | ||
| } | ||
|
|
||
| // 한 hunk의 base, left, right, merged 코드 원문 UTF-8 byte 합산 | ||
| function rawCodeByteCountFor(evidence: MergeCodeContextEvidence): number { | ||
| const snippets = [ | ||
| evidence.baseSnippet, | ||
| evidence.leftSnippet, | ||
| evidence.rightSnippet, | ||
| evidence.mergedSnippet | ||
| ] | ||
|
|
||
| return snippets.reduce( | ||
| (byteCount, snippet) => byteCount + ( | ||
| typeof snippet.content === "string" | ||
| ? Buffer.byteLength(snippet.content, "utf8") | ||
| : 0 | ||
| ), | ||
| 0 | ||
| ) | ||
| } | ||
|
|
||
| // evidence 목록에 포함된 중복 없는 file 수 계산 | ||
| function uniqueFileCountFor(evidence: MergeCodeContextEvidence[]): number { | ||
| return new Set(evidence.map(item => item.filePath)).size | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import type { BranchComparisonPair } from "../branches/types.js" | ||
| import type { | ||
| GitMergeCodeContextPairResult, | ||
| GitMergeTreePairResult | ||
| } from "../git/types.js" | ||
| import type { BranchConflictGraphEdge } from "../risks/types.js" | ||
| import { build as buildEvidence } from "./predictionPairEvidenceBuilder.js" | ||
| import { select } from "./predictionPairTargetSelector.js" | ||
| import type { AiPredictionPairEvidencePayload } from "./types.js" | ||
|
|
||
| // graph, merge, 코드 문맥 결과를 선택된 branch 조합별 AI 요청 입력으로 조립 | ||
| export function build( | ||
| edges: BranchConflictGraphEdge[], | ||
| mergeResults: GitMergeTreePairResult[], | ||
| codeContextResults: GitMergeCodeContextPairResult[] = [] | ||
| ): AiPredictionPairEvidencePayload[] { | ||
| const targets = select(edges) | ||
| const mergeIndex = indexByPair( | ||
| mergeResults, | ||
| "AI prediction pair request requires unique merge result" | ||
| ) | ||
| const contextIndex = indexByPair( | ||
| codeContextResults, | ||
| "AI prediction pair request requires unique code context result" | ||
| ) | ||
| const seenKeys = new Set<string>() | ||
|
|
||
| return targets.map(edge => { | ||
| const key = pairKeyFor(edge.pair) | ||
|
|
||
| if (seenKeys.has(key)) { | ||
| throw new Error( | ||
| "AI prediction pair request requires unique selected branch pair" | ||
| ) | ||
| } | ||
|
|
||
| seenKeys.add(key) | ||
|
|
||
| const merge = mergeIndex.get(key) | ||
|
|
||
| if (!merge) { | ||
| throw new Error( | ||
| "AI prediction pair request requires merge result for selected branch pair" | ||
| ) | ||
| } | ||
|
|
||
| return buildEvidence(edge, merge, contextIndex.get(key)) | ||
| }) | ||
| } | ||
|
|
||
| // ordered pair별 결과를 색인하고 같은 조합의 중복 결과 거부 | ||
| function indexByPair<T extends { pair: BranchComparisonPair }>( | ||
| results: T[], | ||
| duplicateErrorMessage: string | ||
| ): Map<string, T> { | ||
| const index = new Map<string, T>() | ||
|
|
||
| for (const result of results) { | ||
| const key = pairKeyFor(result.pair) | ||
|
|
||
| if (index.has(key)) { | ||
| throw new Error(duplicateErrorMessage) | ||
| } | ||
|
|
||
| index.set(key, result) | ||
| } | ||
|
|
||
| return index | ||
| } | ||
|
|
||
| // 좌우 branch 순서를 보존하는 충돌 없는 조합 key 구성 | ||
| function pairKeyFor(pair: BranchComparisonPair): string { | ||
| return `${pair.leftBranchName}\u0000${pair.rightBranchName}` | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import type { BranchConflictGraphEdge } from "../risks/types.js" | ||
|
|
||
| // 확정 conflict와 critical potential overlap 조합만 AI 분석 대상으로 선별 | ||
| export function select( | ||
| edges: BranchConflictGraphEdge[] | ||
| ): BranchConflictGraphEdge[] { | ||
| return edges.filter(isTarget) | ||
| } | ||
|
|
||
| function isTarget(edge: BranchConflictGraphEdge): boolean { | ||
| if (edge.status === "confirmed_conflict") { | ||
| return true | ||
| } | ||
|
|
||
| return edge.status === "potential_overlap" && | ||
| edge.reasons.some(reason => reason.code === "same_hunk_overlap") | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.