Skip to content
Closed
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
329 changes: 204 additions & 125 deletions .agents/skills/open-pr2/SKILL.md

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions .agents/skills/open-pr2/scripts/provenance-audit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { afterEach, describe, expect, test } from 'bun:test'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { auditRepository, formatAuditMarkdown } from './provenance-audit'

const repositories: string[] = []

function git(repository: string, ...args: string[]): string {
const result = Bun.spawnSync({
cmd: ['git', '-C', repository, ...args],
stdout: 'pipe',
stderr: 'pipe',
})
if (result.exitCode !== 0) {
throw new Error(result.stderr.toString().trim() || `git ${args.join(' ')} failed`)
}
return result.stdout.toString().trim()
}

function repository(): string {
const path = mkdtempSync(join(tmpdir(), 'open-pr2-provenance-'))
repositories.push(path)
git(path, 'init', '-b', 'main')
git(path, 'config', 'user.name', 'OpenPR2 Test')
git(path, 'config', 'user.email', 'open-pr2@example.com')
writeFileSync(join(path, 'base.txt'), 'base\n')
git(path, 'add', 'base.txt')
git(path, 'commit', '-m', 'base')
return path
}

function commitFile(repository: string, path: string, content: string, subject: string): string {
writeFileSync(join(repository, path), content)
git(repository, 'add', path)
git(repository, 'commit', '-m', subject)
return git(repository, 'rev-parse', 'HEAD')
}

afterEach(() => {
for (const path of repositories.splice(0)) {
rmSync(path, { recursive: true, force: true })
}
})

describe('OpenPR2 provenance audit', () => {
test('keeps first-parent commits and excludes files imported by a merge', () => {
const path = repository()
git(path, 'checkout', '-b', 'feature')
const beforeMerge = commitFile(path, 'feature-before.txt', 'before\n', 'feature before merge')

git(path, 'checkout', '-b', 'other', 'main')
commitFile(path, 'imported.txt', 'imported\n', 'other branch feature')

git(path, 'checkout', 'feature')
git(path, 'merge', '--no-ff', 'other', '-m', 'merge other branch')
const afterMerge = commitFile(path, 'feature-after.txt', 'after\n', 'feature after merge')

const audit = auditRepository({ repository: path, baseRef: 'main', headRef: 'feature' })

expect(audit.eligibleCommits.map((commit) => commit.sha)).toEqual([beforeMerge, afterMerge])
expect(audit.excludedMerges).toHaveLength(1)
expect(audit.branchOnlyFiles).toEqual(['feature-after.txt', 'feature-before.txt'])
expect(audit.mergeOnlyFiles).toEqual(['imported.txt'])
expect(audit.overlappingFiles).toEqual([])
expect(audit.unexplainedFiles).toEqual([])
})

test('marks files changed by both a merge and a later branch commit as overlapping', () => {
const path = repository()
git(path, 'checkout', '-b', 'feature')
git(path, 'checkout', '-b', 'other', 'main')
commitFile(path, 'shared.txt', 'imported\n', 'other branch feature')

git(path, 'checkout', 'feature')
git(path, 'merge', '--no-ff', 'other', '-m', 'merge other branch')
commitFile(path, 'shared.txt', 'imported\nfeature adjustment\n', 'adjust imported feature')

const audit = auditRepository({ repository: path, baseRef: 'main', headRef: 'feature' })

expect(audit.branchOnlyFiles).toEqual([])
expect(audit.mergeOnlyFiles).toEqual([])
expect(audit.overlappingFiles).toEqual(['shared.txt'])
})

test('does not describe eligible changes that were later reverted', () => {
const path = repository()
git(path, 'checkout', '-b', 'feature')
commitFile(path, 'temporary.txt', 'temporary\n', 'add temporary feature')
git(path, 'rm', 'temporary.txt')
git(path, 'commit', '-m', 'remove temporary feature')

const audit = auditRepository({ repository: path, baseRef: 'main', headRef: 'feature' })

expect(audit.finalFiles).toEqual([])
expect(audit.revertedEligibleFiles).toEqual(['temporary.txt'])
})

test('excludes a branch commit whose patch is already present on the base', () => {
const path = repository()
git(path, 'checkout', '-b', 'feature')
const featureCommit = commitFile(path, 'same.txt', 'same\n', 'add shared change')

git(path, 'checkout', 'main')
commitFile(path, 'same.txt', 'same\n', 'land shared change upstream')

const audit = auditRepository({ repository: path, baseRef: 'main', headRef: 'feature' })

expect(audit.eligibleCommits).toEqual([])
expect(audit.patchEquivalentCommits.map((commit) => commit.sha)).toEqual([featureCommit])
expect(audit.finalFiles).toEqual(['same.txt'])
expect(audit.patchEquivalentFiles).toEqual(['same.txt'])
expect(audit.unexplainedFiles).toEqual([])
})

test('formats all provenance classifications for the agent audit', () => {
const path = repository()
git(path, 'checkout', '-b', 'feature')
commitFile(path, 'feature.txt', 'feature\n', 'add feature')
const markdown = formatAuditMarkdown(
auditRepository({ repository: path, baseRef: 'main', headRef: 'feature' }),
)

expect(markdown).toContain('## Eligible first-parent commits')
expect(markdown).toContain('## Merge commits excluded')
expect(markdown).toContain('## Branch-only final files')
expect(markdown).toContain('feature.txt')
})
})
241 changes: 241 additions & 0 deletions .agents/skills/open-pr2/scripts/provenance-audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
export type CommitAudit = {
sha: string
shortSha: string
subject: string
parents: string[]
changedFiles: string[]
patchEquivalent: boolean
}

export type ProvenanceAudit = {
repository: string
baseRef: string
headRef: string
mergeBase: string
eligibleCommits: CommitAudit[]
patchEquivalentCommits: CommitAudit[]
excludedMerges: CommitAudit[]
finalFiles: string[]
branchOnlyFiles: string[]
mergeOnlyFiles: string[]
patchEquivalentFiles: string[]
overlappingFiles: string[]
unexplainedFiles: string[]
revertedEligibleFiles: string[]
}

type AuditOptions = {
repository: string
baseRef: string
headRef?: string
}

function runGit(repository: string, args: string[]): string {
const result = Bun.spawnSync({
cmd: ['git', '-C', repository, ...args],
stdout: 'pipe',
stderr: 'pipe',
})
const stdout = result.stdout.toString()
if (result.exitCode !== 0) {
const stderr = result.stderr.toString().trim()
throw new Error(`git ${args.join(' ')} failed${stderr ? `: ${stderr}` : ''}`)
}
return stdout
}

function lines(value: string): string[] {
return value
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
}

function nulSeparated(value: string): string[] {
return value.split('\0').filter(Boolean).sort()
}

function changedFiles(repository: string, from: string | null, to: string): string[] {
if (!from) {
return nulSeparated(
runGit(repository, ['diff-tree', '--root', '--no-commit-id', '--name-only', '-r', '-z', to]),
)
}
return nulSeparated(runGit(repository, ['diff', '--name-only', '-z', from, to]))
}

function intersection(left: Set<string>, right: Set<string>): string[] {
return [...left].filter((value) => right.has(value)).sort()
}

function difference(left: Set<string>, right: Set<string>): string[] {
return [...left].filter((value) => !right.has(value)).sort()
}

function commitAudit(
repository: string,
sha: string,
patchEquivalentShas: Set<string>,
): CommitAudit {
const parents = lines(runGit(repository, ['show', '-s', '--format=%P', sha]).trim().replaceAll(' ', '\n'))
const subject = runGit(repository, ['show', '-s', '--format=%s', sha]).trim()
return {
sha,
shortSha: sha.slice(0, 10),
subject,
parents,
changedFiles: changedFiles(repository, parents[0] ?? null, sha),
patchEquivalent: patchEquivalentShas.has(sha),
}
}

export function auditRepository(options: AuditOptions): ProvenanceAudit {
const { repository, baseRef, headRef = 'HEAD' } = options
runGit(repository, ['rev-parse', '--verify', baseRef])
runGit(repository, ['rev-parse', '--verify', headRef])

const mergeBases = lines(runGit(repository, ['merge-base', '--all', baseRef, headRef]))
if (mergeBases.length !== 1) {
throw new Error(
`expected exactly one merge base for ${baseRef} and ${headRef}; found ${mergeBases.length}`,
)
}

const patchEquivalentShas = new Set(
lines(runGit(repository, ['cherry', baseRef, headRef]))
.filter((line) => line.startsWith('- '))
.map((line) => line.slice(2).split(' ')[0]!)
.filter(Boolean),
)
const firstParentShas = lines(
runGit(repository, ['rev-list', '--first-parent', '--reverse', `${baseRef}..${headRef}`]),
)
const commits = firstParentShas.map((sha) => commitAudit(repository, sha, patchEquivalentShas))
const excludedMerges = commits.filter((commit) => commit.parents.length > 1)
const branchCommits = commits.filter((commit) => commit.parents.length <= 1)
const patchEquivalentCommits = branchCommits.filter((commit) => commit.patchEquivalent)
const eligibleCommits = branchCommits.filter((commit) => !commit.patchEquivalent)

const finalFiles = nulSeparated(
runGit(repository, ['diff', '--name-only', '-z', `${baseRef}...${headRef}`]),
)
const finalSet = new Set(finalFiles)
const eligibleSet = new Set(eligibleCommits.flatMap((commit) => commit.changedFiles))
const patchEquivalentSet = new Set(
patchEquivalentCommits.flatMap((commit) => commit.changedFiles),
)
const importedSet = new Set(excludedMerges.flatMap((commit) => commit.changedFiles))
const branchCandidates = new Set(intersection(finalSet, eligibleSet))
const importCandidates = new Set(intersection(finalSet, importedSet))

return {
repository,
baseRef,
headRef,
mergeBase: mergeBases[0]!,
eligibleCommits,
patchEquivalentCommits,
excludedMerges,
finalFiles,
branchOnlyFiles: difference(branchCandidates, importedSet),
mergeOnlyFiles: difference(importCandidates, eligibleSet),
patchEquivalentFiles: intersection(finalSet, patchEquivalentSet),
overlappingFiles: intersection(branchCandidates, importedSet),
unexplainedFiles: difference(
difference(difference(finalSet, eligibleSet), importedSet),
patchEquivalentSet,
),
revertedEligibleFiles: difference(eligibleSet, finalSet),
}
}

function markdownList(values: string[]): string {
return values.length === 0 ? '- None' : values.map((value) => `- ${value}`).join('\n')
}

function commitTable(commits: CommitAudit[]): string {
if (commits.length === 0) return '_None_'
return [
'| Commit | Subject | Files |',
'|---|---|---:|',
...commits.map(
(commit) =>
`| \`${commit.shortSha}\` | ${commit.subject.replaceAll('|', '\\|')} | ${commit.changedFiles.length} |`,
),
].join('\n')
}

export function formatAuditMarkdown(audit: ProvenanceAudit): string {
return [
'# OpenPR2 provenance audit',
'',
`- Base: \`${audit.baseRef}\``,
`- Head: \`${audit.headRef}\``,
`- Merge base: \`${audit.mergeBase}\``,
'',
'## Eligible first-parent commits',
'',
commitTable(audit.eligibleCommits),
'',
'## Patch-equivalent commits excluded',
'',
commitTable(audit.patchEquivalentCommits),
'',
'## Merge commits excluded',
'',
commitTable(audit.excludedMerges),
'',
'## Branch-only final files',
'',
markdownList(audit.branchOnlyFiles),
'',
'## Merge-only final files',
'',
markdownList(audit.mergeOnlyFiles),
'',
'## Patch-equivalent final files',
'',
markdownList(audit.patchEquivalentFiles),
'',
'## Overlapping files requiring hunk review',
'',
markdownList(audit.overlappingFiles),
'',
'## Unexplained final files',
'',
markdownList(audit.unexplainedFiles),
'',
'## Eligible files absent from the final diff',
'',
markdownList(audit.revertedEligibleFiles),
].join('\n')
}

function readArgument(name: string): string | undefined {
const index = Bun.argv.indexOf(name)
return index >= 0 ? Bun.argv[index + 1] : undefined
}

if (import.meta.main) {
const baseRef = readArgument('--base')
const headRef = readArgument('--head') ?? 'HEAD'
const repository = readArgument('--repo') ?? process.cwd()
const format = readArgument('--format') ?? 'markdown'

if (!baseRef || !['json', 'markdown'].includes(format)) {
console.error(
'Usage: bun provenance-audit.ts --base <canonical-base-ref> [--head HEAD] [--repo path] [--format markdown|json]',
)
process.exit(1)
}

try {
const audit = auditRepository({ repository, baseRef, headRef })
process.stdout.write(
format === 'json' ? `${JSON.stringify(audit, null, 2)}\n` : `${formatAuditMarkdown(audit)}\n`,
)
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exit(2)
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ supabase/.temp/

# Build Outputs
.next/
.next-failed-cache/
out/
build
dist
Expand Down
1 change: 0 additions & 1 deletion apps/editor/lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,5 @@ registerEditorHostPanel({
...streetscapeHostPanel,
creator: { name: 'Sudhir Yadav', url: 'https://github.com/sudhir9297' },
})

loadBuiltinsSync()
void loadExternalPlugins()
Loading
Loading