Skip to content

feat(relations): deterministic PR/issue classification via closing edges - #21

Merged
StressTestor merged 2 commits into
mainfrom
feat/relational-roles
Jul 18, 2026
Merged

feat(relations): deterministic PR/issue classification via closing edges#21
StressTestor merged 2 commits into
mainfrom
feat/relational-roles

Conversation

@StressTestor

Copy link
Copy Markdown
Owner

Closes #20

what

clusters now say how their members relate, deterministically from github's closing edges:

  • scan fetches closingIssuesReferences per PR (one field on the existing query, no extra requests) and persists closesIssues in item metadata, filtered to same-repo refs (a cross-repo ref must not alias onto local issue numbers)
  • every cluster gets relation: pr-issue-linked | pr-issue-unlinked | prs-only | issues-only, plus resolved in-cluster closingEdges
  • flows into the starmap JSON (additive, STARMAP_SCHEMA_VERSION stays 1), dupes --json NDJSON rows, dupes --cluster detail, and the report's verbose section
  • relation is omitted for clusters holding pre-upgrade rows: absent means unknown, never "closes nothing"
  • scan now refreshes metadata for unchanged items when it drifts (ciStatus, reviewCount, labels, closesIssues) without re-embedding, so one re-scan populates closing refs on an existing db. dirty-checked: a quiet repo costs zero extra writes

why

odysseus triage feedback classified cluster roles by hand ("PR/Issue pair with closing edge" / "no closing edge" / sibling issues) and asked for exactly this. github already knows the edges; we just never fetched them.

validation

  • 343 tests green (was 321), typecheck + build + biome clean, TDD throughout (every guard has a test that fails without it)
  • live e2e on StressTestor/pr-prism itself: the feat: support OpenAI-compatible endpoints #18/Support configurable OpenAI-compatible endpoints #17 cluster at 0.75 threshold emits "relation":"pr-issue-linked","closingEdges":[{"pr":18,"issue":17}] in both NDJSON and starmap, with closes: [17] on the PR item only
  • adversarially verified in a throwaway worktree (build + RED proofs + full suite)

follow-ups noted in #20, out of scope here: PR-stack detection (needs baseRefName), cross-repo edge resolution, complementary-vs-competing heuristics.

fetch closingIssuesReferences per PR in the scan, persist closesIssues in
item metadata, and label every cluster with a deterministic relation
(pr-issue-linked/-unlinked, prs-only, issues-only) plus resolved in-cluster
closingEdges. flows into starmap JSON (additive, schema v1), dupes NDJSON,
cluster detail, and the report. relation is omitted for clusters holding
pre-upgrade rows: absent means unknown, never "closes nothing".

scan now refreshes metadata for unchanged items without re-embedding, so
ciStatus/reviewCount/labels/closesIssues stay current on every scan and
existing dbs pick up closing refs with one re-scan.

closes #20
…fresh

review fixes: cross-repo closingIssuesReferences were mapped by bare number,
aliasing another repo's issue onto local numbering and risking fabricated
closing edges - the query now carries repository.nameWithOwner and the mapper
filters to the scanned repo. metadata refresh for unchanged items now writes
only on drift (a quiet repo costs zero UPDATEs per scan), and closing-ref
truncation past the 100-node page is warned instead of silent.
@StressTestor
StressTestor merged commit e5fbb46 into main Jul 18, 2026
2 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2962ba431b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pipeline.ts
bodyLength: item.body.length,
nodeId: item.nodeId,
headRefOid: item.headRefOid,
closesIssues: item.closesIssues,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve closing metadata during REST scans

When a database previously populated by GraphQL is scanned with prism scan --rest, REST PRs do not provide closesIssues (or the other deep signals), but this metadata object is still used by the unchanged-item refresh. The refresh therefore replaces known closing edges with an object where closesIssues is omitted, making subsequent clusters report the relation as unknown despite the data having been fetched earlier. Preserve existing deep metadata when the selected fetcher cannot supply it.

Useful? React with 👍 / 👎.

Comment thread src/github.ts
@@ -174,6 +186,7 @@ export class GitHubClient {
changedFiles

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark truncated closing references as unknown

For a PR with more than 100 closing references, this query persists only the first page as though it were complete. Although the warning below notes truncation, classifyClusterRelation receives a defined array and can emit pr-issue-unlinked when the relevant in-cluster issue is on a later page. Paginate the connection or retain an unknown state whenever totalCount exceeds the fetched nodes.

Useful? React with 👍 / 👎.

Comment thread src/github.ts
refs: { nodes?: Array<{ number: number; repository?: { nameWithOwner: string } }> } | null | undefined,
repoFull: string,
): number[] {
return (refs?.nodes || []).filter((n) => n.repository?.nameWithOwner === repoFull).map((n) => n.number);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare repository identities case-insensitively

Repository names supplied through config/--repo are not normalized, while repository.nameWithOwner is returned in GitHub's canonical display casing. If a user supplies a valid casing variant (for example, a lowercased repository name), this exact comparison drops every same-repository reference and stores [], causing linked PR/issue clusters to be classified as unlinked. Normalize both sides before comparing.

Useful? React with 👍 / 👎.

@StressTestor
StressTestor deleted the feat/relational-roles branch July 18, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deterministic PR/issue relational classification via closing edges

1 participant