From 01f3fa4a0db8a17dc8028f55474695a255565973 Mon Sep 17 00:00:00 2001 From: mkdev11 Date: Fri, 8 May 2026 05:20:36 +0200 Subject: [PATCH 1/3] Fix label actor role evidence --- packages/das/package.json | 1 + packages/das/src/db-view-contracts.spec.ts | 57 +++++++++++++++++ .../das/src/webhook/github-fetcher.service.ts | 4 +- .../das/src/webhook/handlers/label.handler.ts | 4 +- .../db/20_view_contributor_repo_roles.sql | 63 +++++++++++++++++-- packages/db/24_view_pr_labels_by_actor.sql | 4 +- 6 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 packages/das/src/db-view-contracts.spec.ts diff --git a/packages/das/package.json b/packages/das/package.json index 36691c0..4ef9f03 100644 --- a/packages/das/package.json +++ b/packages/das/package.json @@ -13,6 +13,7 @@ "dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", + "test": "node --test -r ts-node/register \"src/**/*.spec.ts\"", "lint": "eslint \"src/**/*.ts\"", "lint:fix": "eslint \"src/**/*.ts\" --fix" }, diff --git a/packages/das/src/db-view-contracts.spec.ts b/packages/das/src/db-view-contracts.spec.ts new file mode 100644 index 0000000..c01e692 --- /dev/null +++ b/packages/das/src/db-view-contracts.spec.ts @@ -0,0 +1,57 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import test from "node:test"; + +const dbDir = join(__dirname, "../../db"); + +function readDbSql(fileName: string): string { + return readFileSync(join(dbDir, fileName), "utf8"); +} + +function normalizeSql(sql: string): string { + return sql.replace(/\s+/g, " ").trim(); +} + +void test("contributor_repo_roles includes review and comment association evidence", (): void => { + const sql = normalizeSql(readDbSql("20_view_contributor_repo_roles.sql")); + + assert.match(sql, /FROM pull_requests\b/); + assert.match(sql, /FROM issues\b/); + assert.match(sql, /FROM reviews\b/); + assert.match(sql, /reviewer_github_id AS author_github_id/); + assert.match(sql, /reviewer_login AS author_login/); + assert.match(sql, /reviewer_association AS author_association/); + assert.match(sql, /submitted_at AS observed_at/); + assert.match(sql, /FROM comments\b/); + assert.match(sql, /COALESCE\(updated_at, created_at\) AS observed_at/); +}); + +void test("contributor_repo_roles keeps latest known non-null roles deterministic", (): void => { + const sql = normalizeSql(readDbSql("20_view_contributor_repo_roles.sql")); + + assert.match(sql, /author_association IS NOT NULL/); + assert.match(sql, /reviewer_association IS NOT NULL/); + assert.match(sql, /'pr:' \|\| pr_number::text AS source_key/); + assert.match(sql, /'issue:' \|\| issue_number::text AS source_key/); + assert.match(sql, /'review:' \|\| pr_number::text/); + assert.match(sql, /'comment:' \|\| comment_id::text AS source_key/); + assert.match( + sql, + /ORDER BY repo_full_name, author_github_id, observed_at DESC, source_rank DESC, source_key DESC/, + ); +}); + +void test("label actor views resolve roles through contributor_repo_roles", (): void => { + const prLabelsSql = normalizeSql(readDbSql("24_view_pr_labels_by_actor.sql")); + const issueLabelsSql = normalizeSql( + readDbSql("25_view_issue_labels_by_actor.sql"), + ); + + for (const sql of [prLabelsSql, issueLabelsSql]) { + assert.match(sql, /LEFT JOIN contributor_repo_roles crr/); + assert.match(sql, /crr.author_github_id = le.actor_github_id/); + assert.match(sql, /crr.repo_full_name = le.repo_full_name/); + assert.match(sql, /crr.author_association AS actor_association/); + } +}); diff --git a/packages/das/src/webhook/github-fetcher.service.ts b/packages/das/src/webhook/github-fetcher.service.ts index 98c8de4..160ba13 100644 --- a/packages/das/src/webhook/github-fetcher.service.ts +++ b/packages/das/src/webhook/github-fetcher.service.ts @@ -969,8 +969,8 @@ export class GitHubFetcherService implements OnModuleInit { /** * Upsert a list of LABELED_EVENT / UNLABELED_EVENT timeline nodes into * the label_events table. Actor role is resolved at read time via - * contributor_repo_roles — GraphQL's actor type doesn't expose - * authorAssociation. + * contributor_repo_roles using stored PR/issue, review, and comment + * association evidence; GraphQL's actor type doesn't expose authorAssociation. */ private async saveLabelTimelineEvents( repoFullName: string, diff --git a/packages/das/src/webhook/handlers/label.handler.ts b/packages/das/src/webhook/handlers/label.handler.ts index 4a4eaaf..5304fa1 100644 --- a/packages/das/src/webhook/handlers/label.handler.ts +++ b/packages/das/src/webhook/handlers/label.handler.ts @@ -34,8 +34,8 @@ export class LabelHandler { source === "pr" ? payload.pull_request.number : payload.issue.number; // Append to label_events log. Actor's repo role is resolved at read time - // via contributor_repo_roles (see pr_labels_by_actor view) — neither the - // webhook sender nor GraphQL LabeledEvent.actor expose author_association. + // via contributor_repo_roles using stored PR/issue, review, and comment + // association evidence; label actors themselves don't expose it. await this.labelEventRepo.save({ repoFullName, targetNumber, diff --git a/packages/db/20_view_contributor_repo_roles.sql b/packages/db/20_view_contributor_repo_roles.sql index eb0f13f..1e084df 100644 --- a/packages/db/20_view_contributor_repo_roles.sql +++ b/packages/db/20_view_contributor_repo_roles.sql @@ -1,5 +1,8 @@ -- Latest known association per contributor per repo. --- Unions PRs and issues, takes the most recently created record. +-- Uses every table that stores GitHub's author_association/reviewer_association: +-- PR authors, issue authors, submitted reviews, and issue/PR thread comments. +-- Rows without a stored association are ignored; label views should use the +-- latest known role, not let a missing observation erase earlier evidence. CREATE OR REPLACE VIEW contributor_repo_roles AS SELECT DISTINCT ON (repo_full_name, author_github_id) @@ -8,10 +11,62 @@ SELECT DISTINCT ON (repo_full_name, author_github_id) author_login, author_association FROM ( - SELECT repo_full_name, author_github_id, author_login, author_association, created_at + SELECT + repo_full_name, + author_github_id, + author_login, + author_association, + created_at AS observed_at, + 10 AS source_rank, + 'pr:' || pr_number::text AS source_key FROM pull_requests + WHERE author_github_id IS NOT NULL + AND author_github_id <> '' + AND author_association IS NOT NULL + UNION ALL - SELECT repo_full_name, author_github_id, author_login, author_association, created_at + + SELECT + repo_full_name, + author_github_id, + author_login, + author_association, + created_at AS observed_at, + 10 AS source_rank, + 'issue:' || issue_number::text AS source_key FROM issues + WHERE author_github_id IS NOT NULL + AND author_github_id <> '' + AND author_association IS NOT NULL + + UNION ALL + + SELECT + repo_full_name, + reviewer_github_id AS author_github_id, + reviewer_login AS author_login, + reviewer_association AS author_association, + submitted_at AS observed_at, + 20 AS source_rank, + 'review:' || pr_number::text || ':' || submitted_at::text AS source_key + FROM reviews + WHERE reviewer_github_id IS NOT NULL + AND reviewer_github_id <> '' + AND reviewer_association IS NOT NULL + + UNION ALL + + SELECT + repo_full_name, + author_github_id, + author_login, + author_association, + COALESCE(updated_at, created_at) AS observed_at, + 30 AS source_rank, + 'comment:' || comment_id::text AS source_key + FROM comments + WHERE author_github_id IS NOT NULL + AND author_github_id <> '' + AND author_association IS NOT NULL ) combined -ORDER BY repo_full_name, author_github_id, created_at DESC; +ORDER BY repo_full_name, author_github_id, observed_at DESC, source_rank DESC, source_key DESC; diff --git a/packages/db/24_view_pr_labels_by_actor.sql b/packages/db/24_view_pr_labels_by_actor.sql index d6cbc88..8b10f27 100644 --- a/packages/db/24_view_pr_labels_by_actor.sql +++ b/packages/db/24_view_pr_labels_by_actor.sql @@ -2,8 +2,8 @@ -- Collapses label_events to the latest action per (repo, pr, label); only rows -- where the latest action was "labeled" are included (i.e. label still applied). -- actor_association is resolved from contributor_repo_roles (the actor's most --- recently observed role from PRs/issues they've authored in this repo). --- Actors who've never authored anything return NULL for actor_association. +-- recently observed role from authored PRs/issues, reviews, or comments in +-- this repo). Actors with no stored association evidence return NULL. CREATE OR REPLACE VIEW pr_labels_by_actor AS WITH latest_events AS ( From 4e6a412241dbabffa063e044b5080ce774ca54e4 Mon Sep 17 00:00:00 2001 From: mkdev11 Date: Fri, 8 May 2026 23:04:36 +0200 Subject: [PATCH 2/3] Run tests before DAS build --- packages/das/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/das/package.json b/packages/das/package.json index 4ef9f03..c189ff1 100644 --- a/packages/das/package.json +++ b/packages/das/package.json @@ -6,6 +6,7 @@ "private": true, "license": "UNLICENSED", "scripts": { + "prebuild": "npm test", "build": "nest build", "format": "prettier --write \"src/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\"", From 727456b0049ce8a057caad6c13169c4bffbd05af Mon Sep 17 00:00:00 2001 From: mkdev11 Date: Mon, 11 May 2026 00:26:16 +0200 Subject: [PATCH 3/3] Drop contributor DB view tests --- packages/das/package.json | 2 - packages/das/src/db-view-contracts.spec.ts | 57 ---------------------- 2 files changed, 59 deletions(-) delete mode 100644 packages/das/src/db-view-contracts.spec.ts diff --git a/packages/das/package.json b/packages/das/package.json index c189ff1..36691c0 100644 --- a/packages/das/package.json +++ b/packages/das/package.json @@ -6,7 +6,6 @@ "private": true, "license": "UNLICENSED", "scripts": { - "prebuild": "npm test", "build": "nest build", "format": "prettier --write \"src/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\"", @@ -14,7 +13,6 @@ "dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "test": "node --test -r ts-node/register \"src/**/*.spec.ts\"", "lint": "eslint \"src/**/*.ts\"", "lint:fix": "eslint \"src/**/*.ts\" --fix" }, diff --git a/packages/das/src/db-view-contracts.spec.ts b/packages/das/src/db-view-contracts.spec.ts deleted file mode 100644 index c01e692..0000000 --- a/packages/das/src/db-view-contracts.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import test from "node:test"; - -const dbDir = join(__dirname, "../../db"); - -function readDbSql(fileName: string): string { - return readFileSync(join(dbDir, fileName), "utf8"); -} - -function normalizeSql(sql: string): string { - return sql.replace(/\s+/g, " ").trim(); -} - -void test("contributor_repo_roles includes review and comment association evidence", (): void => { - const sql = normalizeSql(readDbSql("20_view_contributor_repo_roles.sql")); - - assert.match(sql, /FROM pull_requests\b/); - assert.match(sql, /FROM issues\b/); - assert.match(sql, /FROM reviews\b/); - assert.match(sql, /reviewer_github_id AS author_github_id/); - assert.match(sql, /reviewer_login AS author_login/); - assert.match(sql, /reviewer_association AS author_association/); - assert.match(sql, /submitted_at AS observed_at/); - assert.match(sql, /FROM comments\b/); - assert.match(sql, /COALESCE\(updated_at, created_at\) AS observed_at/); -}); - -void test("contributor_repo_roles keeps latest known non-null roles deterministic", (): void => { - const sql = normalizeSql(readDbSql("20_view_contributor_repo_roles.sql")); - - assert.match(sql, /author_association IS NOT NULL/); - assert.match(sql, /reviewer_association IS NOT NULL/); - assert.match(sql, /'pr:' \|\| pr_number::text AS source_key/); - assert.match(sql, /'issue:' \|\| issue_number::text AS source_key/); - assert.match(sql, /'review:' \|\| pr_number::text/); - assert.match(sql, /'comment:' \|\| comment_id::text AS source_key/); - assert.match( - sql, - /ORDER BY repo_full_name, author_github_id, observed_at DESC, source_rank DESC, source_key DESC/, - ); -}); - -void test("label actor views resolve roles through contributor_repo_roles", (): void => { - const prLabelsSql = normalizeSql(readDbSql("24_view_pr_labels_by_actor.sql")); - const issueLabelsSql = normalizeSql( - readDbSql("25_view_issue_labels_by_actor.sql"), - ); - - for (const sql of [prLabelsSql, issueLabelsSql]) { - assert.match(sql, /LEFT JOIN contributor_repo_roles crr/); - assert.match(sql, /crr.author_github_id = le.actor_github_id/); - assert.match(sql, /crr.repo_full_name = le.repo_full_name/); - assert.match(sql, /crr.author_association AS actor_association/); - } -});