From 735c37eb96fa61e521fbd839e68b7a5ef9d21e0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:05:28 +0900 Subject: [PATCH 01/24] test(workflow-registry): fail closed on active-PR path ambiguity --- ...-registry-active-pr-path-ambiguity.test.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test/workflow-registry-active-pr-path-ambiguity.test.ts diff --git a/test/workflow-registry-active-pr-path-ambiguity.test.ts b/test/workflow-registry-active-pr-path-ambiguity.test.ts new file mode 100644 index 000000000..c0bdfbebb --- /dev/null +++ b/test/workflow-registry-active-pr-path-ambiguity.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import { classifyWorkflowRegistry } from "../scripts/workflow-registry-audit.mjs"; + +const repository = "ContextualWisdomLab/noema"; +const defaultBranchSha = "1fbe857a5cf52b5af31e2db5e4676876289e3e23"; +const observedAt = "2026-08-18T00:00:00.000Z"; + +function classify(registryPath: string, activePullRequestPath: string) { + return classifyWorkflowRegistry({ + repository, + defaultBranchSha, + observedAt, + workflows: [ + { + id: 900, + name: "Bounded repair", + path: registryPath, + state: "active", + }, + ], + trackedWorkflowPaths: [], + activePullRequestWorkflowPaths: [activePullRequestPath], + pagination: { + totalCount: 1, + receipts: [{ page: 1, itemCount: 1, hasNext: false }], + }, + }); +} + +describe("workflow registry active-PR path ambiguity", () => { + it("fails closed instead of orphaning a registry path that differs only by case from an active PR path", () => { + const result = classify( + ".github/workflows/bounded-current-repair.yml", + ".github/workflows/Bounded-Current-Repair.yml", + ); + + expect(result.status).toBe("FAIL"); + expect(result.workflows[0]).toMatchObject({ + workflow_id: 900, + classification: "unresolved_registry_record", + }); + expect(result.failures).toContainEqual( + expect.objectContaining({ + code: "active_pr_workflow_path_case_mismatch", + workflow_id: 900, + }), + ); + expect(result.failures).not.toContainEqual( + expect.objectContaining({ code: "active_orphan_workflow" }), + ); + }); + + it("fails closed instead of orphaning a registry path that differs only by Unicode normalization from an active PR path", () => { + const result = classify( + ".github/workflows/résumé-repair.yml", + ".github/workflows/résumé-repair.yml", + ); + + expect(result.status).toBe("FAIL"); + expect(result.workflows[0]).toMatchObject({ + workflow_id: 900, + classification: "unresolved_registry_record", + }); + expect(result.failures).toContainEqual( + expect.objectContaining({ + code: "active_pr_workflow_path_normalization_mismatch", + workflow_id: 900, + }), + ); + expect(result.failures).not.toContainEqual( + expect.objectContaining({ code: "active_orphan_workflow" }), + ); + }); +}); From 649332fa3b5c0af58b7925aefea4ec46ef093629 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:08:06 +0900 Subject: [PATCH 02/24] fix(workflow-registry): preserve active-PR path ambiguity --- scripts/workflow-registry-audit.mjs | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/scripts/workflow-registry-audit.mjs b/scripts/workflow-registry-audit.mjs index b861786e6..86babb466 100644 --- a/scripts/workflow-registry-audit.mjs +++ b/scripts/workflow-registry-audit.mjs @@ -217,6 +217,36 @@ function classifyRecord( return { ...base, classification: "active_pr_owned", failure: null }; } + const activePullRequestCaseCollision = [...activePullRequestWorkflowPaths].find( + (activePath) => activePath.toLowerCase() === lowerPath, + ); + if (activePullRequestCaseCollision) { + return { + ...base, + classification: "unresolved_registry_record", + failure: { + code: "active_pr_workflow_path_case_mismatch", + workflow_id: record.id, + detail: `Workflow path ${path} differs by case from active-PR path ${activePullRequestCaseCollision}.`, + }, + }; + } + + const activePullRequestNormalizationCollision = [...activePullRequestWorkflowPaths].find( + (activePath) => activePath.normalize("NFC").toLowerCase() === normalizedPath, + ); + if (activePullRequestNormalizationCollision) { + return { + ...base, + classification: "unresolved_registry_record", + failure: { + code: "active_pr_workflow_path_normalization_mismatch", + workflow_id: record.id, + detail: `Workflow path ${path} differs by Unicode normalization from active-PR path ${activePullRequestNormalizationCollision}.`, + }, + }; + } + if (record.state === "disabled_manually" || record.state === "disabled_inactivity") { return { ...base, classification: "disabled_registry_record", failure: null }; } From 74e9ea6c441d681b78391f852ce178186b1352e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:08:20 +0900 Subject: [PATCH 03/24] test(workflow-registry): preserve unrelated orphan classification --- ...-registry-active-pr-path-ambiguity.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/workflow-registry-active-pr-path-ambiguity.test.ts b/test/workflow-registry-active-pr-path-ambiguity.test.ts index c0bdfbebb..dbb0f1eaf 100644 --- a/test/workflow-registry-active-pr-path-ambiguity.test.ts +++ b/test/workflow-registry-active-pr-path-ambiguity.test.ts @@ -71,4 +71,23 @@ describe("workflow registry active-PR path ambiguity", () => { expect.objectContaining({ code: "active_orphan_workflow" }), ); }); + + it("still identifies an active orphan when open-PR workflow paths are unrelated", () => { + const result = classify( + ".github/workflows/orphaned-repair.yml", + ".github/workflows/unrelated-current-repair.yml", + ); + + expect(result.status).toBe("FAIL"); + expect(result.workflows[0]).toMatchObject({ + workflow_id: 900, + classification: "active_orphan", + }); + expect(result.failures).toContainEqual( + expect.objectContaining({ + code: "active_orphan_workflow", + workflow_id: 900, + }), + ); + }); }); From e472e7f8dde41948c8723c8fdd7117d3cb6b72cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:11:21 +0900 Subject: [PATCH 04/24] test(workflow-registry): refuse active-PR adoption race --- ...flow-registry-live-disable-pr-race.test.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 test/workflow-registry-live-disable-pr-race.test.ts diff --git a/test/workflow-registry-live-disable-pr-race.test.ts b/test/workflow-registry-live-disable-pr-race.test.ts new file mode 100644 index 000000000..b649c75be --- /dev/null +++ b/test/workflow-registry-live-disable-pr-race.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it, vi } from "vitest"; +import { runWorkflowRegistryDisablement } from "../scripts/workflow-registry-live-disable.mjs"; + +const repository = "ContextualWisdomLab/noema"; +const mainSha = "a".repeat(40); +const workflowPath = ".github/workflows/obsolete-repair.yml"; + +function activeOrphanAudit() { + return { + schema_version: 1, + repository_full_name: repository, + default_branch_sha: mainSha, + observed_at: "2026-08-18T00:00:00.000Z", + pagination_receipts: [{ page: 1, itemCount: 1, hasNext: false }], + status: "FAIL", + failures: [{ + code: "active_orphan_workflow", + workflow_id: 101, + detail: "Active workflow is absent from protected main.", + }], + workflows: [{ + workflow_id: 101, + workflow_path: workflowPath, + workflow_state: "active", + classification: "active_orphan", + }], + }; +} + +function activePullRequestOwnedAudit() { + return { + schema_version: 1, + repository_full_name: repository, + default_branch_sha: mainSha, + observed_at: "2026-08-18T00:00:01.000Z", + pagination_receipts: [{ page: 1, itemCount: 1, hasNext: false }], + status: "PASS", + failures: [], + workflows: [{ + workflow_id: 101, + workflow_path: workflowPath, + workflow_state: "active", + classification: "active_pr_owned", + }], + }; +} + +describe("workflow-registry disablement active-PR race", () => { + it("collects the live registry before the authorizing audit so a newly opened PR blocks disablement", async () => { + let activePullRequestAdopted = false; + const collectLiveWorkflows = vi.fn(async () => { + activePullRequestAdopted = true; + return [{ id: 101, path: workflowPath, state: "active" }]; + }); + const collectAudit = vi.fn(async () => ( + activePullRequestAdopted ? activePullRequestOwnedAudit() : activeOrphanAudit() + )); + const disableWorkflow = vi.fn(); + + await expect(runWorkflowRegistryDisablement({ + repository, + workflowId: 101, + collectAudit, + collectLiveWorkflows, + transport: { + revalidateDefaultBranch: vi.fn().mockResolvedValue({ sha: mainSha }), + revalidateWorkflow: vi.fn().mockResolvedValue({ + id: 101, + path: workflowPath, + state: "active", + }), + disableWorkflow, + }, + })).rejects.toThrow("fresh workflow disablement plan is non-authorizing"); + + expect(collectLiveWorkflows).toHaveBeenCalledTimes(1); + expect(collectAudit).toHaveBeenCalledTimes(1); + expect(disableWorkflow).not.toHaveBeenCalled(); + }); +}); From 4e985437fb913dc3987aa3396b43eacdf3f14620 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:12:11 +0900 Subject: [PATCH 05/24] fix(workflow-registry): refresh PR ownership before mutation --- scripts/workflow-registry-live-disable.mjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/workflow-registry-live-disable.mjs b/scripts/workflow-registry-live-disable.mjs index 6448ae885..1cb36dbe5 100644 --- a/scripts/workflow-registry-live-disable.mjs +++ b/scripts/workflow-registry-live-disable.mjs @@ -219,9 +219,10 @@ export async function collectLiveWorkflowRecords(input) { /** * Execute one and only one requested active-orphan workflow disablement. - * The operator collects a full exact-main audit, immediately refreshes raw registry - * identities, builds process-local mutation authority, revalidates main plus workflow - * state around the mutation, and then performs a second full audit before returning a receipt. + * The operator refreshes the raw registry first, then collects the full exact-main + * audit so active-PR ownership is the freshest broad state before mutation authority + * is built. The executor then revalidates main plus workflow state around the + * mutation and performs a second full audit before returning a receipt. * * @param {object} input exact repository, workflow id, audit/live collectors, and transport * @returns {Promise} bounded postcondition receipt @@ -247,9 +248,9 @@ export async function runWorkflowRegistryDisablement(input) { throw new Error("workflow disablement operator is missing authorized transport"); } + const liveWorkflows = await input.collectLiveWorkflows(); const audit = await input.collectAudit(); const exactMain = audit?.default_branch_sha; - const liveWorkflows = await input.collectLiveWorkflows(); const plan = buildWorkflowDisablementPlan({ audit, expectedRepository: repository, From 1ee9c955711cf3e98713880fa9e5ca82a98a3792 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:14:04 +0900 Subject: [PATCH 06/24] test(workflow-registry): detect moving open-PR ownership snapshot --- ...ow-registry-live-audit-pr-snapshot.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/workflow-registry-live-audit-pr-snapshot.test.ts diff --git a/test/workflow-registry-live-audit-pr-snapshot.test.ts b/test/workflow-registry-live-audit-pr-snapshot.test.ts new file mode 100644 index 000000000..b5eb1ccf1 --- /dev/null +++ b/test/workflow-registry-live-audit-pr-snapshot.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { collectLiveWorkflowRegistryAudit } from "../scripts/workflow-registry-live-audit.mjs"; + +const repository = "ContextualWisdomLab/noema"; +const mainSha = "071d116fff8a856809a3553d57506e6e9703b8b4"; +const firstPullHead = "b".repeat(40); +const movedPullHead = "c".repeat(40); + +describe("live workflow-registry open-PR snapshot", () => { + it("fails closed when an open PR head moves while workflow ownership is collected", async () => { + let pullReads = 0; + const result = await collectLiveWorkflowRegistryAudit({ + repository, + defaultBranch: "main", + now: () => "2026-08-18T00:00:00.000Z", + ghJson: async (endpoint: string) => { + if (endpoint === `repos/${repository}/branches/main`) { + return { commit: { sha: mainSha } }; + } + if (endpoint === `repos/${repository}/git/trees/${mainSha}?recursive=1`) { + return { truncated: false, tree: [] }; + } + if (endpoint === `repos/${repository}/actions/workflows?per_page=100&page=1`) { + return { + total_count: 1, + workflows: [{ + id: 11, + path: ".github/workflows/bounded-repair.yml", + state: "active", + }], + }; + } + if (endpoint === `repos/${repository}/pulls?state=open&per_page=100&page=1`) { + pullReads += 1; + return [{ + number: 99, + head: { sha: pullReads === 1 ? firstPullHead : movedPullHead }, + }]; + } + if (endpoint === `repos/${repository}/pulls/99/files?per_page=100&page=1`) { + return [{ filename: ".github/workflows/bounded-repair.yml" }]; + } + throw new Error(`unexpected endpoint ${endpoint}`); + }, + }); + + expect(pullReads).toBe(2); + expect(result.status).toBe("FAIL"); + expect(result.failures).toContainEqual( + expect.objectContaining({ code: "workflow_registry_collection_failed" }), + ); + expect(String(result.failures[0]?.detail ?? "")) + .toContain("Open pull-request inventory changed during workflow-path collection"); + }); +}); From 8a0a7b867a313f0cecef4373287915d4edebbb28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:15:05 +0900 Subject: [PATCH 07/24] fix(workflow-registry): bind open-PR ownership to stable heads --- scripts/workflow-registry-live-audit.mjs | 44 ++++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/scripts/workflow-registry-live-audit.mjs b/scripts/workflow-registry-live-audit.mjs index 9d3579736..d2d7d407f 100644 --- a/scripts/workflow-registry-live-audit.mjs +++ b/scripts/workflow-registry-live-audit.mjs @@ -12,6 +12,7 @@ import { const EXPECTED_REPOSITORY = "ContextualWisdomLab/noema"; const EXPECTED_DEFAULT_BRANCH = "main"; const REPOSITORY_WORKFLOW_PREFIX = ".github/workflows/"; +const LOWERCASE_SHA_40 = /^[0-9a-f]{40}$/; const MAX_GH_OUTPUT_BYTES = 8 * 1024 * 1024; const MAX_GH_REQUEST_MILLISECONDS = 20_000; const MAX_PAGES = 1_000; @@ -151,17 +152,38 @@ async function listArrayPages(ghJson, endpointForPage, label) { throw new Error(`${label} pagination exceeded ${MAX_PAGES} pages without a terminal short page.`); } -async function activePullRequestWorkflowPaths(repository, ghJson) { - const pulls = await listArrayPages( +function openPullRequestSnapshot(pulls) { + const identities = []; + const seenNumbers = new Set(); + for (const pull of pulls) { + if (!Number.isSafeInteger(pull?.number) || pull.number <= 0) { + throw new Error("Open pull-request inventory contains an invalid pull number."); + } + if (!LOWERCASE_SHA_40.test(pull?.head?.sha ?? "")) { + throw new Error("Open pull-request inventory contains an invalid head SHA."); + } + if (seenNumbers.has(pull.number)) { + throw new Error("Open pull-request inventory contains a duplicate pull number."); + } + seenNumbers.add(pull.number); + identities.push(`${pull.number}:${pull.head.sha}`); + } + return identities.sort(); +} + +async function listOpenPullRequests(repository, ghJson) { + return listArrayPages( ghJson, (page) => `repos/${repository}/pulls?state=open&per_page=${PER_PAGE}&page=${page}`, "Open pull requests", ); +} + +async function activePullRequestWorkflowPaths(repository, ghJson) { + const pulls = await listOpenPullRequests(repository, ghJson); + const initialSnapshot = openPullRequestSnapshot(pulls); const workflowPaths = new Set(); for (const pull of pulls) { - if (!Number.isSafeInteger(pull?.number) || pull.number <= 0) { - throw new Error("Open pull-request inventory contains an invalid pull number."); - } const files = await listArrayPages( ghJson, (page) => `repos/${repository}/pulls/${pull.number}/files?per_page=${PER_PAGE}&page=${page}`, @@ -176,13 +198,21 @@ async function activePullRequestWorkflowPaths(repository, ghJson) { } } } + + const finalSnapshot = openPullRequestSnapshot( + await listOpenPullRequests(repository, ghJson), + ); + if (JSON.stringify(finalSnapshot) !== JSON.stringify(initialSnapshot)) { + throw new Error("Open pull-request inventory changed during workflow-path collection."); + } + return [...workflowPaths].sort(); } /** * Collect the live Actions registry against independently re-resolved protected - * main and the workflow paths owned by open pull requests. This function is - * read-only; it produces orphan findings but never disables workflow identities. + * main and one stable open-PR head snapshot. This function is read-only; it + * produces orphan findings but never disables workflow identities. * @param {object} input Collector dependencies. * @returns {Promise} Exact-main-bound workflow-registry audit evidence. */ From f00e8b12d60b27512abc6fdc63d07ebd1c52dfbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:15:40 +0900 Subject: [PATCH 08/24] test(workflow-registry): bind live collector fixture to PR head --- test/workflow-registry-live-audit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/workflow-registry-live-audit.test.ts b/test/workflow-registry-live-audit.test.ts index ba18afddd..8466dc7f3 100644 --- a/test/workflow-registry-live-audit.test.ts +++ b/test/workflow-registry-live-audit.test.ts @@ -64,7 +64,7 @@ describe("live workflow-registry collector", () => { }; } if (endpoint === "repos/ContextualWisdomLab/noema/pulls?state=open&per_page=100&page=1") { - return [{ number: 99 }]; + return [{ number: 99, head: { sha: "b".repeat(40) } }]; } if (endpoint === "repos/ContextualWisdomLab/noema/pulls/99/files?per_page=100&page=1") { return [{ filename: ".github/workflows/bounded-repair.yml" }]; From 7a753e1dba46fe0431cac1210fbcebfe5c275c74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:18:57 +0900 Subject: [PATCH 09/24] test(workflow-registry): detect moving PR base snapshot --- ...ow-registry-live-audit-pr-snapshot.test.ts | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/test/workflow-registry-live-audit-pr-snapshot.test.ts b/test/workflow-registry-live-audit-pr-snapshot.test.ts index b5eb1ccf1..82e01e217 100644 --- a/test/workflow-registry-live-audit-pr-snapshot.test.ts +++ b/test/workflow-registry-live-audit-pr-snapshot.test.ts @@ -5,6 +5,40 @@ const repository = "ContextualWisdomLab/noema"; const mainSha = "071d116fff8a856809a3553d57506e6e9703b8b4"; const firstPullHead = "b".repeat(40); const movedPullHead = "c".repeat(40); +const firstPullBase = "d".repeat(40); +const movedPullBase = "e".repeat(40); + +function commonResponse(endpoint: string) { + if (endpoint === `repos/${repository}/branches/main`) { + return { commit: { sha: mainSha } }; + } + if (endpoint === `repos/${repository}/git/trees/${mainSha}?recursive=1`) { + return { truncated: false, tree: [] }; + } + if (endpoint === `repos/${repository}/actions/workflows?per_page=100&page=1`) { + return { + total_count: 1, + workflows: [{ + id: 11, + path: ".github/workflows/bounded-repair.yml", + state: "active", + }], + }; + } + if (endpoint === `repos/${repository}/pulls/99/files?per_page=100&page=1`) { + return [{ filename: ".github/workflows/bounded-repair.yml" }]; + } + return undefined; +} + +function expectMovingSnapshotFailure(result: Awaited>) { + expect(result.status).toBe("FAIL"); + expect(result.failures).toContainEqual( + expect.objectContaining({ code: "workflow_registry_collection_failed" }), + ); + expect(String(result.failures[0]?.detail ?? "")) + .toContain("Open pull-request inventory changed during workflow-path collection"); +} describe("live workflow-registry open-PR snapshot", () => { it("fails closed when an open PR head moves while workflow ownership is collected", async () => { @@ -14,42 +48,46 @@ describe("live workflow-registry open-PR snapshot", () => { defaultBranch: "main", now: () => "2026-08-18T00:00:00.000Z", ghJson: async (endpoint: string) => { - if (endpoint === `repos/${repository}/branches/main`) { - return { commit: { sha: mainSha } }; - } - if (endpoint === `repos/${repository}/git/trees/${mainSha}?recursive=1`) { - return { truncated: false, tree: [] }; - } - if (endpoint === `repos/${repository}/actions/workflows?per_page=100&page=1`) { - return { - total_count: 1, - workflows: [{ - id: 11, - path: ".github/workflows/bounded-repair.yml", - state: "active", - }], - }; - } + const common = commonResponse(endpoint); + if (common !== undefined) return common; if (endpoint === `repos/${repository}/pulls?state=open&per_page=100&page=1`) { pullReads += 1; return [{ number: 99, head: { sha: pullReads === 1 ? firstPullHead : movedPullHead }, + base: { sha: firstPullBase }, }]; } - if (endpoint === `repos/${repository}/pulls/99/files?per_page=100&page=1`) { - return [{ filename: ".github/workflows/bounded-repair.yml" }]; + throw new Error(`unexpected endpoint ${endpoint}`); + }, + }); + + expect(pullReads).toBe(2); + expectMovingSnapshotFailure(result); + }); + + it("fails closed when an open PR base changes while workflow ownership is collected", async () => { + let pullReads = 0; + const result = await collectLiveWorkflowRegistryAudit({ + repository, + defaultBranch: "main", + now: () => "2026-08-18T00:00:00.000Z", + ghJson: async (endpoint: string) => { + const common = commonResponse(endpoint); + if (common !== undefined) return common; + if (endpoint === `repos/${repository}/pulls?state=open&per_page=100&page=1`) { + pullReads += 1; + return [{ + number: 99, + head: { sha: firstPullHead }, + base: { sha: pullReads === 1 ? firstPullBase : movedPullBase }, + }]; } throw new Error(`unexpected endpoint ${endpoint}`); }, }); expect(pullReads).toBe(2); - expect(result.status).toBe("FAIL"); - expect(result.failures).toContainEqual( - expect.objectContaining({ code: "workflow_registry_collection_failed" }), - ); - expect(String(result.failures[0]?.detail ?? "")) - .toContain("Open pull-request inventory changed during workflow-path collection"); + expectMovingSnapshotFailure(result); }); }); From d36a285d48bdcc083d95fabaa12764728661d39e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:20:07 +0900 Subject: [PATCH 10/24] fix(workflow-registry): bind PR ownership to exact base SHA --- scripts/workflow-registry-live-audit.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/workflow-registry-live-audit.mjs b/scripts/workflow-registry-live-audit.mjs index d2d7d407f..3b350c2ab 100644 --- a/scripts/workflow-registry-live-audit.mjs +++ b/scripts/workflow-registry-live-audit.mjs @@ -162,11 +162,14 @@ function openPullRequestSnapshot(pulls) { if (!LOWERCASE_SHA_40.test(pull?.head?.sha ?? "")) { throw new Error("Open pull-request inventory contains an invalid head SHA."); } + if (!LOWERCASE_SHA_40.test(pull?.base?.sha ?? "")) { + throw new Error("Open pull-request inventory contains an invalid base SHA."); + } if (seenNumbers.has(pull.number)) { throw new Error("Open pull-request inventory contains a duplicate pull number."); } seenNumbers.add(pull.number); - identities.push(`${pull.number}:${pull.head.sha}`); + identities.push(`${pull.number}:${pull.head.sha}:${pull.base.sha}`); } return identities.sort(); } @@ -211,7 +214,7 @@ async function activePullRequestWorkflowPaths(repository, ghJson) { /** * Collect the live Actions registry against independently re-resolved protected - * main and one stable open-PR head snapshot. This function is read-only; it + * main and one stable open-PR head/base snapshot. This function is read-only; it * produces orphan findings but never disables workflow identities. * @param {object} input Collector dependencies. * @returns {Promise} Exact-main-bound workflow-registry audit evidence. From 3eae1804ccce504b1dbc213fc6cf3fcae4aab83d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:20:57 +0900 Subject: [PATCH 11/24] test(workflow-registry): bind live fixture to exact PR base --- test/workflow-registry-live-audit.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/workflow-registry-live-audit.test.ts b/test/workflow-registry-live-audit.test.ts index 8466dc7f3..30bd10a42 100644 --- a/test/workflow-registry-live-audit.test.ts +++ b/test/workflow-registry-live-audit.test.ts @@ -64,7 +64,11 @@ describe("live workflow-registry collector", () => { }; } if (endpoint === "repos/ContextualWisdomLab/noema/pulls?state=open&per_page=100&page=1") { - return [{ number: 99, head: { sha: "b".repeat(40) } }]; + return [{ + number: 99, + head: { sha: "b".repeat(40) }, + base: { sha: mainSha }, + }]; } if (endpoint === "repos/ContextualWisdomLab/noema/pulls/99/files?per_page=100&page=1") { return [{ filename: ".github/workflows/bounded-repair.yml" }]; From f377fe12627dfabfe36c1b664ea0863ae90d6550 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:22:03 +0900 Subject: [PATCH 12/24] test(workflow-registry): detect truncated PR-file inventory --- ...ow-registry-live-audit-pr-snapshot.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/workflow-registry-live-audit-pr-snapshot.test.ts b/test/workflow-registry-live-audit-pr-snapshot.test.ts index 82e01e217..3787e6deb 100644 --- a/test/workflow-registry-live-audit-pr-snapshot.test.ts +++ b/test/workflow-registry-live-audit-pr-snapshot.test.ts @@ -25,6 +25,14 @@ function commonResponse(endpoint: string) { }], }; } + if (endpoint === `repos/${repository}/pulls/99`) { + return { + number: 99, + head: { sha: firstPullHead }, + base: { sha: firstPullBase }, + changed_files: 1, + }; + } if (endpoint === `repos/${repository}/pulls/99/files?per_page=100&page=1`) { return [{ filename: ".github/workflows/bounded-repair.yml" }]; } @@ -90,4 +98,39 @@ describe("live workflow-registry open-PR snapshot", () => { expect(pullReads).toBe(2); expectMovingSnapshotFailure(result); }); + + it("fails closed when GitHub's PR-file listing retains fewer files than the PR advertises", async () => { + const result = await collectLiveWorkflowRegistryAudit({ + repository, + defaultBranch: "main", + now: () => "2026-08-18T00:00:00.000Z", + ghJson: async (endpoint: string) => { + if (endpoint === `repos/${repository}/pulls/99`) { + return { + number: 99, + head: { sha: firstPullHead }, + base: { sha: firstPullBase }, + changed_files: 2, + }; + } + const common = commonResponse(endpoint); + if (common !== undefined) return common; + if (endpoint === `repos/${repository}/pulls?state=open&per_page=100&page=1`) { + return [{ + number: 99, + head: { sha: firstPullHead }, + base: { sha: firstPullBase }, + }]; + } + throw new Error(`unexpected endpoint ${endpoint}`); + }, + }); + + expect(result.status).toBe("FAIL"); + expect(result.failures).toContainEqual( + expect.objectContaining({ code: "workflow_registry_collection_failed" }), + ); + expect(String(result.failures[0]?.detail ?? "")) + .toContain("Pull request #99 file inventory retained 1 of 2 advertised changed files"); + }); }); From 9aa20171007222aa60dca30e6a0c122c16a3fcd8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:28:18 +0900 Subject: [PATCH 13/24] test(workflow-registry): align live-audit fixture with PR detail check --- test/workflow-registry-live-audit.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/workflow-registry-live-audit.test.ts b/test/workflow-registry-live-audit.test.ts index 30bd10a42..29444f2a5 100644 --- a/test/workflow-registry-live-audit.test.ts +++ b/test/workflow-registry-live-audit.test.ts @@ -70,6 +70,14 @@ describe("live workflow-registry collector", () => { base: { sha: mainSha }, }]; } + if (endpoint === "repos/ContextualWisdomLab/noema/pulls/99") { + return { + number: 99, + head: { sha: "b".repeat(40) }, + base: { sha: mainSha }, + changed_files: 1, + }; + } if (endpoint === "repos/ContextualWisdomLab/noema/pulls/99/files?per_page=100&page=1") { return [{ filename: ".github/workflows/bounded-repair.yml" }]; } From 2d1f8f8f9bf9cee11dd0554e4d73770c7e917829 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:29:13 +0900 Subject: [PATCH 14/24] fix(workflow-registry): verify complete PR file inventory --- scripts/workflow-registry-live-audit.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/workflow-registry-live-audit.mjs b/scripts/workflow-registry-live-audit.mjs index 3b350c2ab..9dd69f9b0 100644 --- a/scripts/workflow-registry-live-audit.mjs +++ b/scripts/workflow-registry-live-audit.mjs @@ -192,6 +192,22 @@ async function activePullRequestWorkflowPaths(repository, ghJson) { (page) => `repos/${repository}/pulls/${pull.number}/files?per_page=${PER_PAGE}&page=${page}`, `Pull request #${pull.number} files`, ); + const detail = await ghJson(`repos/${repository}/pulls/${pull.number}`); + if ( + detail?.number !== pull.number + || detail?.head?.sha !== pull.head.sha + || detail?.base?.sha !== pull.base.sha + ) { + throw new Error(`Pull request #${pull.number} identity changed during file inventory.`); + } + if (!Number.isSafeInteger(detail?.changed_files) || detail.changed_files < 0) { + throw new Error(`Pull request #${pull.number} advertised an invalid changed-file count.`); + } + if (files.length !== detail.changed_files) { + throw new Error( + `Pull request #${pull.number} file inventory retained ${files.length} of ${detail.changed_files} advertised changed files.`, + ); + } for (const file of files) { if ( typeof file?.filename === "string" From 9e9950e21af0496b943c789bf00fb979d5d7332f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:30:32 +0900 Subject: [PATCH 15/24] test(workflow-registry): bind active PR ownership to immutable head tree --- ...ow-registry-live-audit-pr-snapshot.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/workflow-registry-live-audit-pr-snapshot.test.ts b/test/workflow-registry-live-audit-pr-snapshot.test.ts index 3787e6deb..141357cba 100644 --- a/test/workflow-registry-live-audit-pr-snapshot.test.ts +++ b/test/workflow-registry-live-audit-pr-snapshot.test.ts @@ -133,4 +133,41 @@ describe("live workflow-registry open-PR snapshot", () => { expect(String(result.failures[0]?.detail ?? "")) .toContain("Pull request #99 file inventory retained 1 of 2 advertised changed files"); }); + + it("binds active-PR workflow ownership to the exact immutable head tree despite an ABA-shaped file listing", async () => { + const result = await collectLiveWorkflowRegistryAudit({ + repository, + defaultBranch: "main", + now: () => "2026-08-18T00:00:00.000Z", + ghJson: async (endpoint: string) => { + if (endpoint === `repos/${repository}/git/trees/${firstPullHead}?recursive=1`) { + return { + truncated: false, + tree: [{ path: ".github/workflows/bounded-repair.yml", type: "blob" }], + }; + } + if (endpoint === `repos/${repository}/pulls/99/files?per_page=100&page=1`) { + return [{ filename: "README.md" }]; + } + const common = commonResponse(endpoint); + if (common !== undefined) return common; + if (endpoint === `repos/${repository}/pulls?state=open&per_page=100&page=1`) { + return [{ + number: 99, + head: { sha: firstPullHead }, + base: { sha: firstPullBase }, + }]; + } + throw new Error(`unexpected endpoint ${endpoint}`); + }, + }); + + expect(result.workflows[0]).toMatchObject({ + workflow_id: 11, + classification: "active_pr_owned", + }); + expect(result.failures).not.toContainEqual( + expect.objectContaining({ code: "active_orphan_workflow" }), + ); + }); }); From c4d35bafee17ce43efbdf6d206af642d2fd9123b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:31:10 +0900 Subject: [PATCH 16/24] test(workflow-registry): align collector fixture with exact PR head tree --- test/workflow-registry-live-audit.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/workflow-registry-live-audit.test.ts b/test/workflow-registry-live-audit.test.ts index 29444f2a5..c9293c118 100644 --- a/test/workflow-registry-live-audit.test.ts +++ b/test/workflow-registry-live-audit.test.ts @@ -39,6 +39,7 @@ describe("live workflow-registry collector", () => { it("collects registry pages, active-PR workflow ownership, and exact protected-main identity", async () => { const calls: string[] = []; let branchReads = 0; + const pullHeadSha = "b".repeat(40); const ghJson = vi.fn(async (endpoint: string) => { calls.push(endpoint); if (endpoint === "repos/ContextualWisdomLab/noema/branches/main") { @@ -54,6 +55,12 @@ describe("live workflow-registry collector", () => { ], }; } + if (endpoint === `repos/ContextualWisdomLab/noema/git/trees/${pullHeadSha}?recursive=1`) { + return { + truncated: false, + tree: [{ path: ".github/workflows/bounded-repair.yml", type: "blob" }], + }; + } if (endpoint === "repos/ContextualWisdomLab/noema/actions/workflows?per_page=100&page=1") { return { total_count: 2, @@ -66,14 +73,14 @@ describe("live workflow-registry collector", () => { if (endpoint === "repos/ContextualWisdomLab/noema/pulls?state=open&per_page=100&page=1") { return [{ number: 99, - head: { sha: "b".repeat(40) }, + head: { sha: pullHeadSha }, base: { sha: mainSha }, }]; } if (endpoint === "repos/ContextualWisdomLab/noema/pulls/99") { return { number: 99, - head: { sha: "b".repeat(40) }, + head: { sha: pullHeadSha }, base: { sha: mainSha }, changed_files: 1, }; @@ -103,6 +110,7 @@ describe("live workflow-registry collector", () => { classification: "active_pr_owned", })); expect(calls).toContain(`repos/ContextualWisdomLab/noema/git/trees/${mainSha}?recursive=1`); + expect(calls).toContain(`repos/ContextualWisdomLab/noema/git/trees/${pullHeadSha}?recursive=1`); }); it("fails closed when the protected branch moves during collection", async () => { From c30c3eb9cc70fab370c4d759cdcfc2ee774b4a0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:33:06 +0900 Subject: [PATCH 17/24] fix(workflow-registry): bind active PR ownership to exact head trees --- scripts/workflow-registry-live-audit.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/workflow-registry-live-audit.mjs b/scripts/workflow-registry-live-audit.mjs index 9dd69f9b0..6342c8fc9 100644 --- a/scripts/workflow-registry-live-audit.mjs +++ b/scripts/workflow-registry-live-audit.mjs @@ -208,6 +208,14 @@ async function activePullRequestWorkflowPaths(repository, ghJson) { `Pull request #${pull.number} file inventory retained ${files.length} of ${detail.changed_files} advertised changed files.`, ); } + + const exactHeadTree = await ghJson( + `repos/${repository}/git/trees/${pull.head.sha}?recursive=1`, + ); + for (const workflowPath of repositoryWorkflowPathsFromTree(exactHeadTree)) { + workflowPaths.add(workflowPath); + } + for (const file of files) { if ( typeof file?.filename === "string" From ea92040823c8969a0ef4b335d5e5d43b6807f577 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:40:23 +0900 Subject: [PATCH 18/24] test(workflow-registry): reject stale inherited PR workflow ownership --- ...ow-registry-live-audit-pr-snapshot.test.ts | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/test/workflow-registry-live-audit-pr-snapshot.test.ts b/test/workflow-registry-live-audit-pr-snapshot.test.ts index 141357cba..56a5eb1d6 100644 --- a/test/workflow-registry-live-audit-pr-snapshot.test.ts +++ b/test/workflow-registry-live-audit-pr-snapshot.test.ts @@ -140,10 +140,18 @@ describe("live workflow-registry open-PR snapshot", () => { defaultBranch: "main", now: () => "2026-08-18T00:00:00.000Z", ghJson: async (endpoint: string) => { + if (endpoint === `repos/${repository}/git/trees/${firstPullBase}?recursive=1`) { + return { truncated: false, tree: [] }; + } if (endpoint === `repos/${repository}/git/trees/${firstPullHead}?recursive=1`) { return { truncated: false, - tree: [{ path: ".github/workflows/bounded-repair.yml", type: "blob" }], + tree: [{ + path: ".github/workflows/bounded-repair.yml", + type: "blob", + mode: "100644", + sha: "1".repeat(40), + }], }; } if (endpoint === `repos/${repository}/pulls/99/files?per_page=100&page=1`) { @@ -170,4 +178,47 @@ describe("live workflow-registry open-PR snapshot", () => { expect.objectContaining({ code: "active_orphan_workflow" }), ); }); + + it("does not let an unchanged workflow inherited from a stale PR base suppress a real orphan", async () => { + const unchangedWorkflow = { + path: ".github/workflows/bounded-repair.yml", + type: "blob", + mode: "100644", + sha: "1".repeat(40), + }; + const result = await collectLiveWorkflowRegistryAudit({ + repository, + defaultBranch: "main", + now: () => "2026-08-18T00:00:00.000Z", + ghJson: async (endpoint: string) => { + if ( + endpoint === `repos/${repository}/git/trees/${firstPullBase}?recursive=1` + || endpoint === `repos/${repository}/git/trees/${firstPullHead}?recursive=1` + ) { + return { truncated: false, tree: [unchangedWorkflow] }; + } + if (endpoint === `repos/${repository}/pulls/99/files?per_page=100&page=1`) { + return [{ filename: "README.md" }]; + } + const common = commonResponse(endpoint); + if (common !== undefined) return common; + if (endpoint === `repos/${repository}/pulls?state=open&per_page=100&page=1`) { + return [{ + number: 99, + head: { sha: firstPullHead }, + base: { sha: firstPullBase }, + }]; + } + throw new Error(`unexpected endpoint ${endpoint}`); + }, + }); + + expect(result.workflows[0]).toMatchObject({ + workflow_id: 11, + classification: "active_orphan", + }); + expect(result.failures).toContainEqual( + expect.objectContaining({ code: "active_orphan_workflow" }), + ); + }); }); From abd643a3edda5beab955040a588fb5fbce1c6504 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:41:01 +0900 Subject: [PATCH 19/24] test(workflow-registry): align snapshot fixtures with base-tree proof --- test/workflow-registry-live-audit-pr-snapshot.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/workflow-registry-live-audit-pr-snapshot.test.ts b/test/workflow-registry-live-audit-pr-snapshot.test.ts index 56a5eb1d6..6486271a1 100644 --- a/test/workflow-registry-live-audit-pr-snapshot.test.ts +++ b/test/workflow-registry-live-audit-pr-snapshot.test.ts @@ -15,6 +15,12 @@ function commonResponse(endpoint: string) { if (endpoint === `repos/${repository}/git/trees/${mainSha}?recursive=1`) { return { truncated: false, tree: [] }; } + if ( + endpoint === `repos/${repository}/git/trees/${firstPullBase}?recursive=1` + || endpoint === `repos/${repository}/git/trees/${firstPullHead}?recursive=1` + ) { + return { truncated: false, tree: [] }; + } if (endpoint === `repos/${repository}/actions/workflows?per_page=100&page=1`) { return { total_count: 1, From c9ceaeddb787cfd530ed6ce8f0c2c99b12d49f59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:41:48 +0900 Subject: [PATCH 20/24] fix(workflow-registry): scope PR ownership to immutable tree delta --- scripts/workflow-registry-live-audit.mjs | 63 ++++++++++++++---------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/scripts/workflow-registry-live-audit.mjs b/scripts/workflow-registry-live-audit.mjs index 6342c8fc9..2813784ac 100644 --- a/scripts/workflow-registry-live-audit.mjs +++ b/scripts/workflow-registry-live-audit.mjs @@ -78,30 +78,49 @@ export function workflowPageFromResponse(payload, page, perPage) { }; } -/** - * Extract exact repository workflow blobs from one complete recursive Git tree. - * A truncated tree cannot prove absence and therefore fails closed. - * @param {unknown} payload GitHub recursive tree response. - * @returns {string[]} Exact tracked workflow paths sorted for deterministic evidence. - */ -export function repositoryWorkflowPathsFromTree(payload) { +function repositoryWorkflowEntryMap(payload) { if (!payload || payload.truncated === true) { - throw new Error("Protected-main recursive Git tree is truncated; workflow absence is not provable."); + throw new Error("Recursive Git tree is truncated; workflow absence is not provable."); } if (!Array.isArray(payload.tree)) { - throw new Error("Protected-main recursive Git tree response is invalid."); + throw new Error("Recursive Git tree response is invalid."); } - const paths = []; + const entries = new Map(); for (const entry of payload.tree) { if ( - entry?.type === "blob" - && typeof entry.path === "string" - && entry.path.startsWith(REPOSITORY_WORKFLOW_PREFIX) + entry?.type !== "blob" + || typeof entry.path !== "string" + || !entry.path.startsWith(REPOSITORY_WORKFLOW_PREFIX) ) { - paths.push(entry.path); + continue; + } + const identity = `${entry.type}\u0000${String(entry.mode ?? "")}\u0000${String(entry.sha ?? "")}`; + const prior = entries.get(entry.path); + if (prior !== undefined && prior !== identity) { + throw new Error(`Recursive Git tree contains conflicting workflow entries for ${entry.path}.`); } + entries.set(entry.path, identity); } - return [...new Set(paths)].sort(); + return entries; +} + +/** + * Extract exact repository workflow blobs from one complete recursive Git tree. + * A truncated tree cannot prove absence and therefore fails closed. + * @param {unknown} payload GitHub recursive tree response. + * @returns {string[]} Exact tracked workflow paths sorted for deterministic evidence. + */ +export function repositoryWorkflowPathsFromTree(payload) { + return [...repositoryWorkflowEntryMap(payload).keys()].sort(); +} + +function changedWorkflowPathsBetweenTrees(basePayload, headPayload) { + const baseEntries = repositoryWorkflowEntryMap(basePayload); + const headEntries = repositoryWorkflowEntryMap(headPayload); + const paths = new Set([...baseEntries.keys(), ...headEntries.keys()]); + return [...paths] + .filter((path) => baseEntries.get(path) !== headEntries.get(path)) + .sort(); } function createGhJsonReader(delegatedGithubToken) { @@ -209,21 +228,15 @@ async function activePullRequestWorkflowPaths(repository, ghJson) { ); } + const exactBaseTree = await ghJson( + `repos/${repository}/git/trees/${pull.base.sha}?recursive=1`, + ); const exactHeadTree = await ghJson( `repos/${repository}/git/trees/${pull.head.sha}?recursive=1`, ); - for (const workflowPath of repositoryWorkflowPathsFromTree(exactHeadTree)) { + for (const workflowPath of changedWorkflowPathsBetweenTrees(exactBaseTree, exactHeadTree)) { workflowPaths.add(workflowPath); } - - for (const file of files) { - if ( - typeof file?.filename === "string" - && file.filename.startsWith(REPOSITORY_WORKFLOW_PREFIX) - ) { - workflowPaths.add(file.filename); - } - } } const finalSnapshot = openPullRequestSnapshot( From fcb007c6ed8f8d10f960a65ecade96fc871774da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 09:02:26 +0900 Subject: [PATCH 21/24] test(workflow-registry): reject current-base-only ownership --- ...ow-registry-live-audit-pr-snapshot.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/workflow-registry-live-audit-pr-snapshot.test.ts b/test/workflow-registry-live-audit-pr-snapshot.test.ts index 6486271a1..ace9b871d 100644 --- a/test/workflow-registry-live-audit-pr-snapshot.test.ts +++ b/test/workflow-registry-live-audit-pr-snapshot.test.ts @@ -227,4 +227,54 @@ describe("live workflow-registry open-PR snapshot", () => { expect.objectContaining({ code: "active_orphan_workflow" }), ); }); + + it("does not treat a workflow removed only on the current base as active-PR-owned when the PR never changed it from the merge base", async () => { + const mergeBaseSha = "f".repeat(40); + const inheritedWorkflow = { + path: ".github/workflows/bounded-repair.yml", + type: "blob", + mode: "100644", + sha: "1".repeat(40), + }; + const result = await collectLiveWorkflowRegistryAudit({ + repository, + defaultBranch: "main", + now: () => "2026-08-18T00:00:00.000Z", + ghJson: async (endpoint: string) => { + if (endpoint === `repos/${repository}/compare/${firstPullBase}...${firstPullHead}`) { + return { merge_base_commit: { sha: mergeBaseSha } }; + } + if (endpoint === `repos/${repository}/git/trees/${firstPullBase}?recursive=1`) { + return { truncated: false, tree: [] }; + } + if ( + endpoint === `repos/${repository}/git/trees/${mergeBaseSha}?recursive=1` + || endpoint === `repos/${repository}/git/trees/${firstPullHead}?recursive=1` + ) { + return { truncated: false, tree: [inheritedWorkflow] }; + } + if (endpoint === `repos/${repository}/pulls/99/files?per_page=100&page=1`) { + return [{ filename: "README.md" }]; + } + const common = commonResponse(endpoint); + if (common !== undefined) return common; + if (endpoint === `repos/${repository}/pulls?state=open&per_page=100&page=1`) { + return [{ + number: 99, + head: { sha: firstPullHead }, + base: { sha: firstPullBase }, + }]; + } + throw new Error(`unexpected endpoint ${endpoint}`); + }, + }); + + expect(result.workflows[0]).toMatchObject({ + workflow_id: 11, + classification: "active_orphan", + }); + expect(result.failures).toContainEqual( + expect.objectContaining({ code: "active_orphan_workflow" }), + ); + }); }); From c30142ee7bd620503340be1ca57ba85214dac636 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 09:05:17 +0900 Subject: [PATCH 22/24] fix(workflow-registry): bind ownership to merge base --- scripts/workflow-registry-live-audit.mjs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/workflow-registry-live-audit.mjs b/scripts/workflow-registry-live-audit.mjs index 2813784ac..847bfc758 100644 --- a/scripts/workflow-registry-live-audit.mjs +++ b/scripts/workflow-registry-live-audit.mjs @@ -228,13 +228,20 @@ async function activePullRequestWorkflowPaths(repository, ghJson) { ); } - const exactBaseTree = await ghJson( - `repos/${repository}/git/trees/${pull.base.sha}?recursive=1`, + const comparison = await ghJson( + `repos/${repository}/compare/${pull.base.sha}...${pull.head.sha}`, + ); + const mergeBaseSha = comparison?.merge_base_commit?.sha; + if (!LOWERCASE_SHA_40.test(mergeBaseSha ?? "")) { + throw new Error(`Pull request #${pull.number} comparison is missing a valid merge-base SHA.`); + } + const exactMergeBaseTree = await ghJson( + `repos/${repository}/git/trees/${mergeBaseSha}?recursive=1`, ); const exactHeadTree = await ghJson( `repos/${repository}/git/trees/${pull.head.sha}?recursive=1`, ); - for (const workflowPath of changedWorkflowPathsBetweenTrees(exactBaseTree, exactHeadTree)) { + for (const workflowPath of changedWorkflowPathsBetweenTrees(exactMergeBaseTree, exactHeadTree)) { workflowPaths.add(workflowPath); } } @@ -251,8 +258,9 @@ async function activePullRequestWorkflowPaths(repository, ghJson) { /** * Collect the live Actions registry against independently re-resolved protected - * main and one stable open-PR head/base snapshot. This function is read-only; it - * produces orphan findings but never disables workflow identities. + * main and one stable open-PR head/base snapshot. Active-PR workflow ownership + * is derived from each immutable merge-base→head tree delta. This function is + * read-only; it produces orphan findings but never disables workflow identities. * @param {object} input Collector dependencies. * @returns {Promise} Exact-main-bound workflow-registry audit evidence. */ From 796cc53bb68e2153131c876da2416cd60b9dd7b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 09:05:50 +0900 Subject: [PATCH 23/24] test(workflow-registry): provide immutable merge bases --- test/workflow-registry-live-audit-pr-snapshot.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/workflow-registry-live-audit-pr-snapshot.test.ts b/test/workflow-registry-live-audit-pr-snapshot.test.ts index ace9b871d..4d6b41f43 100644 --- a/test/workflow-registry-live-audit-pr-snapshot.test.ts +++ b/test/workflow-registry-live-audit-pr-snapshot.test.ts @@ -15,6 +15,9 @@ function commonResponse(endpoint: string) { if (endpoint === `repos/${repository}/git/trees/${mainSha}?recursive=1`) { return { truncated: false, tree: [] }; } + if (endpoint === `repos/${repository}/compare/${firstPullBase}...${firstPullHead}`) { + return { merge_base_commit: { sha: firstPullBase } }; + } if ( endpoint === `repos/${repository}/git/trees/${firstPullBase}?recursive=1` || endpoint === `repos/${repository}/git/trees/${firstPullHead}?recursive=1` From 6946decded34aa9747b9d0e6e59311477cf779fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 09:06:14 +0900 Subject: [PATCH 24/24] test(workflow-registry): model merge-base comparison --- test/workflow-registry-live-audit.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/workflow-registry-live-audit.test.ts b/test/workflow-registry-live-audit.test.ts index c9293c118..b0aacb698 100644 --- a/test/workflow-registry-live-audit.test.ts +++ b/test/workflow-registry-live-audit.test.ts @@ -55,6 +55,9 @@ describe("live workflow-registry collector", () => { ], }; } + if (endpoint === `repos/ContextualWisdomLab/noema/compare/${mainSha}...${pullHeadSha}`) { + return { merge_base_commit: { sha: mainSha } }; + } if (endpoint === `repos/ContextualWisdomLab/noema/git/trees/${pullHeadSha}?recursive=1`) { return { truncated: false,