-
Notifications
You must be signed in to change notification settings - Fork 1
review: a downvoted finding joins the adjudicated corpus; document the feedback contract #333
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
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0f3ca12
[jwies-review-downvote-adjudication] review: a downvoted finding join…
jwbron 02b65e0
[jwies-review-downvote-adjudication] Merge remote-tracking branch 'or…
jwbron 1c2a123
[jwies-review-adjudicated-suppression] review: split the text-similar…
jwbron 7e6cf11
[jwies-review-downvote-adjudication] Merge remote-tracking branch 'or…
jwbron 391004c
[jwies-review-adjudicated-suppression] review: human-resolved bot thr…
jwbron 422dccc
[jwies-review-adjudicated-suppression] Merge remote-tracking branch '…
jwbron 1794e34
[jwies-review-downvote-adjudication] review: only attributable non-bo…
jwbron 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,5 @@ | ||
| --- | ||
| "review": minor | ||
| --- | ||
|
|
||
| Resolving a bot thread now means "settled", not "open season for a rephrase". The staging collects the bot's threads a HUMAN resolved into a new adjudicated corpus (`adjudicated-threads.json`), and the dispatcher suppresses any non-blocking candidate that re-derives a defect that corpus already settled (same defect-identity match as open-thread suppression). Previously, resolution removed the thread from the only suppression corpus, so the next run could re-post the same concern with fresh wording as a brand-new thread, which every later accountability recap then reported as "still unaddressed" (webapp#41290: six resolved variants of one concern, then a seventh). Two safety asymmetries: a thread the BOT resolved (the reconciler, after a fix) never joins the corpus, and a BLOCKING candidate is never suppressed by it, so a fixed-then-regressed defect worth stopping the PR for always posts. |
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,5 @@ | ||
| --- | ||
| "review": minor | ||
| --- | ||
|
|
||
| A 👎 on a bot finding now adjudicates it, exactly like resolving its thread: the staging reads each thread opener's THUMBS_DOWN reaction count, and a bot-opened thread with a downvoted opener joins the adjudicated suppression corpus whatever its resolution state, so the settled defect cannot re-post under fresh wording (blocking re-flags still always post, and a still-open downvoted thread stays in the open corpus, which keeps the verdict-floor bookkeeping). Previously the downvote channel the bot advertises (the thumbs sweep asks "why?" on exactly this signal) dead-ended in counters and changed nothing about what posts. Also documents the full feedback signal contract (reply / resolve / 👎 / hide) in the README. |
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
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,240 @@ | ||
| import {describe, it, expect} from "vitest"; | ||
|
|
||
| import { | ||
| adjudicatedThreadsFromStaged, | ||
| suppressAdjudicatedDuplicates, | ||
| suppressTrackedDuplicates, | ||
| } from "./dedup-adjudicated"; | ||
| import type {Claim} from "./dispatch-contracts"; | ||
|
|
||
| /** | ||
| * Adjudicated-thread suppression tests, split from dedup.test.ts for its | ||
| * max-lines budget (the dedup-cluster.test.ts precedent); the `claim` factory | ||
| * mirrors that file's. The scenario throughout is webapp#41290's: a human | ||
| * resolved the bot's thread, and a later run re-derived the same defect with | ||
| * fresh wording at a nearby line, which the adjudicated corpus must absorb | ||
| * without ever absorbing a blocking regression re-flag. | ||
| */ | ||
|
|
||
| const claim = (over: Partial<Claim> & {id: string; source: string}): Claim => ({ | ||
| path: "services/ai-guide/memory/expiration.go", | ||
| line: 38, | ||
| label: "issue (blocking)", | ||
| subject: "s", | ||
| discussion: "d", | ||
| failure_scenario: "f", | ||
| confidence: 0.7, | ||
| ...over, | ||
| }); | ||
|
|
||
| describe("adjudicatedThreadsFromStaged", () => { | ||
| const adjudicated = (over: Record<string, unknown> = {}) => ({ | ||
| thread_id: "T1", | ||
| path: "a.ts", | ||
| resolved: true, | ||
| resolvedBy: "sxkosone", | ||
| comments: [ | ||
| { | ||
| author: "github-actions", | ||
| body: "**suggestion (non-blocking):** opener", | ||
| }, | ||
| ], | ||
| ...over, | ||
| }); | ||
|
|
||
| it("admits only bot-opened threads a human resolved", () => { | ||
| expect(adjudicatedThreadsFromStaged([adjudicated()])).toEqual([ | ||
| { | ||
| thread_id: "T1", | ||
| path: "a.ts", | ||
| body: "**suggestion (non-blocking):** opener", | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("admits a bot thread whose opener a reviewer downvoted, whatever its resolution state", () => { | ||
| // The 👎 channel: the same judgment as resolving, delivered through | ||
| // the reaction the thumbs sweep advertises. Resolution state does not | ||
| // gate it; a still-open downvoted thread is also in the open corpus, | ||
| // and the composed pass attributes a double match to the open thread. | ||
| for (const state of [ | ||
| {resolved: false, resolvedBy: ""}, | ||
| {resolved: true, resolvedBy: "github-actions"}, | ||
| {resolved: undefined, resolvedBy: undefined}, | ||
| ]) { | ||
| expect( | ||
| adjudicatedThreadsFromStaged([ | ||
| adjudicated({...state, openerDownvotes: 1}), | ||
| ]), | ||
| ).toEqual([ | ||
| { | ||
| thread_id: "T1", | ||
| path: "a.ts", | ||
| body: "**suggestion (non-blocking):** opener", | ||
| }, | ||
| ]); | ||
| } | ||
| }); | ||
|
|
||
| it("fails closed on every guard: unresolved, bot-resolved, unattributable resolver, human opener, malformed staging", () => { | ||
| // Each rejected shape degrades to a duplicate comment, never to a | ||
| // suppression the staging cannot justify: this corpus grants the | ||
| // strongest suppression in the pipeline (a human's explicit "settled" | ||
| // outlives rephrasings), so membership must be unmanufacturable. | ||
| const rejected: unknown[] = [ | ||
| adjudicated({resolved: false}), | ||
| adjudicated({resolved: undefined}), | ||
| adjudicated({resolved: "true"}), | ||
| // A downvote count must be an explicit positive number: absent, | ||
| // zero, or malformed reads as no downvote, and a downvote alone | ||
| // never launders a thread that fails the bot-opener guard. | ||
| adjudicated({resolved: false, openerDownvotes: 0}), | ||
| adjudicated({resolved: false, openerDownvotes: "1"}), | ||
| adjudicated({ | ||
| resolved: false, | ||
| openerDownvotes: 1, | ||
| comments: [{author: "jwbron", body: "human opener"}], | ||
| }), | ||
| // The bot resolving its own thread is the reconciler marking a | ||
| // defect FIXED; a fixed defect that reappears is a fresh finding. | ||
| adjudicated({resolvedBy: "github-actions"}), | ||
| adjudicated({resolvedBy: "github-actions[bot]"}), | ||
| adjudicated({resolvedBy: ""}), | ||
| adjudicated({resolvedBy: undefined}), | ||
| adjudicated({ | ||
| comments: [{author: "jwbron", body: "human opener"}], | ||
| }), | ||
| adjudicated({comments: []}), | ||
| adjudicated({thread_id: undefined}), | ||
| "not a record", | ||
| ]; | ||
| for (const thread of rejected) { | ||
| expect(adjudicatedThreadsFromStaged([thread])).toEqual([]); | ||
| } | ||
| expect(adjudicatedThreadsFromStaged(undefined)).toEqual([]); | ||
| expect(adjudicatedThreadsFromStaged({not: "an array"})).toEqual([]); | ||
| }); | ||
| }); | ||
|
|
||
| describe("suppressAdjudicatedDuplicates", () => { | ||
| const adjudicatedThread = (over: Record<string, unknown> = {}) => ({ | ||
| thread_id: "T-adj", | ||
| path: "services/ai-guide/memory/expiration.go", | ||
| body: "**suggestion (non-blocking):** No test exercises the deletion path: TestExpiration only asserts that expired keys are identified, so a regression that identifies but never deletes expired memories stays green.", | ||
| ...over, | ||
| }); | ||
| const rederivation = (over: Partial<Claim> = {}) => | ||
| claim({ | ||
| id: "correctness-reviewer-2", | ||
| source: "correctness-reviewer", | ||
| line: 42, | ||
| label: "suggestion (non-blocking)", | ||
| subject: | ||
| "Missing deletion test: the expiration path has no test covering the delete.", | ||
| discussion: | ||
| "No test exercises the deletion path; TestExpiration asserts expired keys are identified but a regression that never deletes expired memories stays green.", | ||
| failure_scenario: | ||
| "A regression that identifies expired memories but skips the deletion is not caught by TestExpiration and ships green.", | ||
| ...over, | ||
| }); | ||
|
|
||
| it("suppresses a non-blocking re-derivation of an adjudicated defect, marked as adjudicated", () => { | ||
| const {kept, suppressed} = suppressAdjudicatedDuplicates( | ||
| [rederivation()], | ||
| [adjudicatedThread()], | ||
| ); | ||
| expect(kept).toEqual([]); | ||
| expect(suppressed).toEqual([ | ||
| { | ||
| id: "correctness-reviewer-2", | ||
| source: "correctness-reviewer", | ||
| label: "suggestion (non-blocking)", | ||
| path: "services/ai-guide/memory/expiration.go", | ||
| line: 42, | ||
| thread_id: "T-adj", | ||
| threadBlocking: false, | ||
| adjudicated: true, | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("never suppresses a blocking candidate: a regression re-flag must stay visible", () => { | ||
| // The adjudicated thread is closed and floors nothing, so suppressing | ||
| // a blocker on it would let a re-confirmed blocking defect vanish | ||
| // without a trace. This asymmetry is also the regression escape | ||
| // hatch: a fixed-then-regressed defect worth stopping the PR for | ||
| // re-presents at blocking severity and posts. | ||
| const blocking = rederivation({label: "issue (blocking)"}); | ||
| const {kept, suppressed} = suppressAdjudicatedDuplicates( | ||
| [blocking], | ||
| [adjudicatedThread()], | ||
| ); | ||
| expect(kept).toEqual([blocking]); | ||
| expect(suppressed).toEqual([]); | ||
| }); | ||
|
|
||
| it("keeps unrelated and pathless claims, and everything when the corpus is empty", () => { | ||
| const unrelated = rederivation({ | ||
| subject: "Retention window subtracts months, not days.", | ||
| discussion: | ||
| "AddDate(0, -MemoryTTLDays, 0) subtracts 180 months so the window never expires anything.", | ||
| failure_scenario: | ||
| "Memories never expire because the cutoff is 15 years in the past.", | ||
| }); | ||
| const pathless = rederivation({path: undefined, line: undefined}); | ||
| const empty = suppressAdjudicatedDuplicates([rederivation()], []); | ||
| expect(empty.kept).toHaveLength(1); | ||
| expect(empty.suppressed).toEqual([]); | ||
| const {kept, suppressed} = suppressAdjudicatedDuplicates( | ||
| [unrelated, pathless], | ||
| [adjudicatedThread()], | ||
| ); | ||
| expect(kept).toEqual([unrelated, pathless]); | ||
| expect(suppressed).toEqual([]); | ||
| }); | ||
|
|
||
| it("attributes a candidate matching BOTH corpora to the OPEN thread (the verdict floor reads its blocking state)", () => { | ||
| // The composed pass order is the guarantee dispatch.ts relies on: the | ||
| // open corpus runs first, so a defect that is simultaneously tracked | ||
| // by an open thread and settled on an older resolved one suppresses | ||
| // against the OPEN thread, whose blocking state floors the verdict. | ||
| const openStaged = [ | ||
| { | ||
| thread_id: "T-open", | ||
| path: "services/ai-guide/memory/expiration.go", | ||
| resolved: false, | ||
| comments: [ | ||
| { | ||
| author: "github-actions", | ||
| body: adjudicatedThread().body, | ||
| }, | ||
| ], | ||
| }, | ||
| ]; | ||
| const adjudicatedStaged = [ | ||
| { | ||
| thread_id: "T-adj", | ||
| path: "services/ai-guide/memory/expiration.go", | ||
| resolved: true, | ||
| resolvedBy: "octo", | ||
| comments: [ | ||
| { | ||
| author: "github-actions", | ||
| body: adjudicatedThread().body, | ||
| }, | ||
| ], | ||
| }, | ||
| ]; | ||
| const result = suppressTrackedDuplicates( | ||
| [rederivation()], | ||
| openStaged, | ||
| adjudicatedStaged, | ||
| new Set(), | ||
| ); | ||
| expect(result.kept).toEqual([]); | ||
| expect(result.suppressed).toHaveLength(1); | ||
| expect(result.suppressed[0].thread_id).toBe("T-open"); | ||
| expect(result.suppressed[0].adjudicated).toBeUndefined(); | ||
| expect(result.shapeFailure).toBeUndefined(); | ||
| }); | ||
| }); |
Oops, something went wrong.
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.