-
Notifications
You must be signed in to change notification settings - Fork 1
review: add an opt-in documentation reviewer
#299
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 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
69daf1f
[jwies/documentation-lens] review: add an opt-in `documentation` revi…
jwbron f64f10e
[jwies/documentation-lens] review: name the documentation label, sinc…
jwbron 18fc3ec
[jwies/documentation-lens] review: reconcile the documentation review…
jwbron c2e3594
[jwies/documentation-lens] review: an absent opt-in reviewer is an as…
jwbron 53d7ef2
[jwies/documentation-lens] review: carry the arm-asymmetry caveat int…
jwbron 102ee15
[jwies/documentation-lens] review: corpus cases that can actually dis…
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,13 @@ | ||
| --- | ||
| "review": minor | ||
| --- | ||
|
|
||
| Add an opt-in `documentation` reviewer: an advisory-only whole-change reviewer that checks the comments and prose docs a diff adds or changes against a documentation policy. It flags a comment that restates the code, one that narrates the change rather than describing the code, one this diff falsified, commented-out code, and (the other direction) a specific unexplained constant, workaround, or ordering constraint the change introduces. The policy lives inline in the reviewer's definition rather than in a fifth required consumer import: the baseline is universal, and repo-specific calibration already has a home in the per-directory `REVIEW.md` contracts. It never reasons about whether a human or a model wrote the text (it cannot tell, and the policy is the same either way), and it does not review the PR title or description. Off everywhere until a repo adds `enable documentation` to its `ROUTING` file. | ||
|
|
||
| Its findings render with a new non-blocking label, `suggestion (non-blocking, documentation)`, code-assigned from the lens exactly as the `, best-practice` variants are. The variant is not cosmetic: the autofix workflow selects threads by parsing the Conventional-Comment label off each posted comment, so this label is the only channel by which a documentation-scoped autofix can tell a documentation thread from any other nit. The scripted dispatcher now assigns the lens of a label-shape reviewer by name (`dispatch-contracts.ts`: previously `conventions` for `skill-auditor` and `correctness` for every other one), so a documentation finding carries `lens: documentation` downstream and `labelForFinding` agrees with the label the reviewer emitted. There is deliberately no blocking documentation variant, so `BLOCKING_LABELS` (what the blocking autofix scope acts on) is unchanged. `thumbs-sweep-github.ts` picks the new label up automatically; `verdict.ts` is unaffected, since the verdict counts blocking labels only. | ||
|
|
||
| Two supporting changes. The `claim-validator` gains a documentation-claim rule: verify the quoted comment text and the code it is contrasted with, and refute whenever the comment carries information the code does not show, which is this reviewer's characteristic false positive. And the live eval producer now dispatches the opt-in reviewers a case `enable`s (`routerConfig.enabledReviewers`, canonical order, unknown names throw) and maps their label-shape output; before this, no opt-in reviewer had a live arm at all and so could not earn its `enable` line through the eval suite the way the policy says it must. Cases that enable nothing are unaffected. | ||
|
|
||
| New eval corpus pair, both live-enabled: `golden-documentation-stale-and-narrated` (a sound behavior change that leaves a falsified comment and a change-narrating comment, and carries a must-not-flag trap whose comment reads like a restatement but records a real constraint) and `clean-documentation-earned-comments` (every added comment earns its line; the run must post nothing). | ||
|
|
||
| Eval-harness fix that this PR's own corpus pair needs: an opt-in reviewer a case `enable`s but the arm's `review.md` does not define is now recorded as an absent dimension instead of throwing. The baseline arm of the A/B that graduates any new reviewer is built from the base tip and so cannot define it, and `runArm` does not wrap its produce call, so the throw killed the whole run before any report. The name is already validated against `ENABLEABLE_REVIEWERS`, so tolerating absence cannot mask a typo; every other roster member (the always-on finders, the validator, the reconciler, a routed lens) still hard-errors. The A/B report calls the asymmetry out under `Arm asymmetry`, since the candidate arm's findings on that dimension are gain by construction rather than a measured improvement. |
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
53 changes: 53 additions & 0 deletions
53
workflows/review/eval/corpus/clean/clean-documentation-earned-comments/case.json
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,53 @@ | ||
| { | ||
| "id": "clean-documentation-earned-comments", | ||
| "tags": ["clean", "documentation", "live"], | ||
| "category": "clean", | ||
| "description": "The documentation reviewer's precision guard. Every comment this change adds earns its line by recording something the code cannot show: why ties break on id (cross-service determinism), and where the 0.35 threshold came from. Each one is anchored on an added line and sits next to the code it explains, which is exactly the shape a reviewer reaching for a finding would flag. The run must post nothing.", | ||
| "changedFiles": [{"path": "src/search/ranking.ts", "status": "modified"}], | ||
| "routerConfig": { | ||
| "enabledReviewers": ["documentation"] | ||
| }, | ||
| "findings": [], | ||
| "policyConflicts": [], | ||
| "expected": { | ||
| "verdict": "APPROVE", | ||
| "postedCommentCount": 0, | ||
| "mustNotPost": [] | ||
| }, | ||
| "diff": "diff --git a/src/search/ranking.ts b/src/search/ranking.ts\n--- a/src/search/ranking.ts\n+++ b/src/search/ranking.ts\n@@ -5,6 +5,18 @@\n-/** Order results for display. */\n-export const rankResults = (results: readonly Result[]): Result[] =>\n- [...results].sort((a, b) => b.score - a.score);\n+/**\n+ * Order results for display.\n+ *\n+ * Ties break on id rather than relying on sort stability: ranking runs on two\n+ * services with different V8 versions, and a differing tie order surfaces as a\n+ * phantom diff in the cached page.\n+ */\n+export const rankResults = (results: readonly Result[]): Result[] =>\n+ [...results].sort(\n+ (a, b) => b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),\n+ );\n \n+// 0.35 came out of the 2026-05 relevance sweep: below it, keyword-only matches\n+// pushed curated content off the first page.\n+export const MIN_SCORE = 0.35;\n+\n export const visible = (results: readonly Result[]): Result[] =>\n- rankResults(results);\n+ rankResults(results).filter((result) => result.score >= MIN_SCORE);\n", | ||
| "live": { | ||
| "prContext": { | ||
| "title": "search: break ranking ties deterministically and drop low-score results", | ||
| "description": "Two ranking fixes: ties now break on id so the two ranking services agree, and results below the relevance floor stop reaching the page.", | ||
| "author": "dev-search", | ||
| "baseBranch": "main" | ||
| }, | ||
| "mustNotFlagSpecs": [ | ||
| { | ||
| "key": "doc-trap-tiebreak-docstring", | ||
| "path": "src/search/ranking.ts", | ||
| "lineStart": 5, | ||
| "lineEnd": 11, | ||
| "lens": "documentation", | ||
| "mechanism": [ | ||
| "docstring restates the signature", | ||
| "comment repeats what the sort does", | ||
| "verbose docstring", | ||
| "delete the tie-break comment" | ||
| ] | ||
| }, | ||
| { | ||
| "key": "doc-trap-threshold-rationale", | ||
| "path": "src/search/ranking.ts", | ||
| "lineStart": 17, | ||
| "lineEnd": 19, | ||
| "lens": "documentation", | ||
| "mechanism": [ | ||
| "comment restates the constant", | ||
| "0.35 comment is redundant", | ||
| "comment names the value the line already shows" | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
22 changes: 22 additions & 0 deletions
22
...s/review/eval/corpus/clean/clean-documentation-earned-comments/tree/src/search/ranking.ts
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,22 @@ | ||
| /** Result ranking for the search page. */ | ||
|
|
||
| export type Result = {id: string; score: number}; | ||
|
|
||
| /** | ||
| * Order results for display. | ||
| * | ||
| * Ties break on id rather than relying on sort stability: ranking runs on two | ||
| * services with different V8 versions, and a differing tie order surfaces as a | ||
| * phantom diff in the cached page. | ||
| */ | ||
| export const rankResults = (results: readonly Result[]): Result[] => | ||
| [...results].sort( | ||
| (a, b) => b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0), | ||
| ); | ||
|
|
||
| // 0.35 came out of the 2026-05 relevance sweep: below it, keyword-only matches | ||
| // pushed curated content off the first page. | ||
| export const MIN_SCORE = 0.35; | ||
|
|
||
| export const visible = (results: readonly Result[]): Result[] => | ||
| rankResults(results).filter((result) => result.score >= MIN_SCORE); |
115 changes: 115 additions & 0 deletions
115
workflows/review/eval/corpus/golden/golden-documentation-stale-and-narrated/case.json
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,115 @@ | ||
| { | ||
| "id": "golden-documentation-stale-and-narrated", | ||
| "tags": ["golden", "documentation", "live"], | ||
| "category": "golden", | ||
| "description": "A correct behaviour change (expiry window 30 -> 90 days, clock injected) that leaves two documentation defects behind: a comment the change falsified, and a comment that narrates the change instead of the code. The change is otherwise sound, so the run must APPROVE while posting both as non-blocking documentation suggestions. The trap is the EXPIRY_SLACK_MS comment: it sits on a constant and looks like a restatement, but it records a real constraint (a reader holding an expiring note) that the code does not show, so it must not be flagged.", | ||
| "changedFiles": [{"path": "src/notes/expiry.ts", "status": "modified"}], | ||
| "routerConfig": { | ||
| "enabledReviewers": ["documentation"] | ||
| }, | ||
| "findings": [ | ||
| { | ||
| "source": "documentation", | ||
| "finding": { | ||
| "schema_version": 2, | ||
| "id": "doc-stale-expiry-days", | ||
| "lens": "documentation", | ||
| "anchor": { | ||
| "type": "line", | ||
| "path": "src/notes/expiry.ts", | ||
| "line": 10, | ||
| "side": "RIGHT" | ||
| }, | ||
| "severity": "advisory", | ||
| "confidence": 0.85, | ||
| "evidence_trace": [ | ||
| "src/notes/expiry.ts:9 comment reads \"// Notes expire after 30 days.\"", | ||
| "src/notes/expiry.ts:10 sets EXPIRY_DAYS to 90 in this change" | ||
| ], | ||
| "failure_scenario": "The next reader takes the 30-day figure from the comment and reasons about retention with a window three times shorter than the one the code enforces.", | ||
| "producing_hunt": "documentation:falsified-by-diff", | ||
| "model_authored_prose": "The comment above still says notes expire after 30 days, but this line changes the window to 90." | ||
| } | ||
| }, | ||
| { | ||
| "source": "documentation", | ||
| "finding": { | ||
| "schema_version": 2, | ||
| "id": "doc-narrated-clock", | ||
| "lens": "documentation", | ||
| "anchor": { | ||
| "type": "line", | ||
| "path": "src/notes/expiry.ts", | ||
| "line": 12, | ||
| "side": "RIGHT" | ||
| }, | ||
| "severity": "advisory", | ||
| "confidence": 0.8, | ||
| "evidence_trace": [ | ||
| "src/notes/expiry.ts:12 adds \"// Updated to take the clock from the caller instead of reading Date.now().\"", | ||
| "src/notes/expiry.ts:13 the signature already shows `now` is a parameter" | ||
| ], | ||
| "failure_scenario": "The comment describes a past the next reader cannot see; once the PR is merged it documents a Date.now() call that no longer exists anywhere in the file.", | ||
| "producing_hunt": "documentation:narrates-the-change", | ||
| "model_authored_prose": "This comment describes the change rather than the code: the signature already shows the clock is a parameter, and \"updated to\" stops meaning anything once this merges." | ||
| } | ||
| } | ||
| ], | ||
| "policyConflicts": [], | ||
| "expected": { | ||
| "verdict": "APPROVE", | ||
| "mustCatch": ["doc-stale-expiry-days", "doc-narrated-clock"], | ||
| "postedCommentCount": 2 | ||
| }, | ||
| "diff": "diff --git a/src/notes/expiry.ts b/src/notes/expiry.ts\n--- a/src/notes/expiry.ts\n+++ b/src/notes/expiry.ts\n@@ -3,12 +3,18 @@\n const DAY_MS = 86_400_000;\n \n+// Rounded up: a note that expires mid-request must stay readable for the rest\n+// of that request, or the delete job races a reader holding it open.\n+const EXPIRY_SLACK_MS = 60_000;\n+\n // Notes expire after 30 days.\n-export const EXPIRY_DAYS = 30;\n+export const EXPIRY_DAYS = 90;\n \n-export const isExpired = (createdAt: number): boolean =>\n- Date.now() - createdAt > EXPIRY_DAYS * DAY_MS;\n+// Updated to take the clock from the caller instead of reading Date.now().\n+export const isExpired = (createdAt: number, now: number): boolean =>\n+ now - createdAt > EXPIRY_DAYS * DAY_MS + EXPIRY_SLACK_MS;\n \n // Return the notes that have not expired yet.\n export const liveNotes = <T extends {createdAt: number}>(\n notes: readonly T[],\n-): T[] => notes.filter((note) => !isExpired(note.createdAt));\n+ now: number,\n+): T[] => notes.filter((note) => !isExpired(note.createdAt, now));\n", | ||
| "live": { | ||
| "prContext": { | ||
| "title": "notes: extend the expiry window to 90 days and inject the clock", | ||
| "description": "Retention asked for 90 days. Also takes `now` from the caller so the sweep job and its tests agree on the clock.", | ||
| "author": "dev-notes", | ||
| "baseBranch": "main" | ||
| }, | ||
| "mustCatchSpecs": [ | ||
| { | ||
| "key": "doc-stale-expiry-days", | ||
| "path": "src/notes/expiry.ts", | ||
| "lineStart": 9, | ||
| "lineEnd": 10, | ||
| "lens": "documentation", | ||
| "mechanism": [ | ||
| "comment says 30 days but the constant is 90", | ||
| "stale comment", | ||
| "falsified by the change", | ||
| "comment contradicts EXPIRY_DAYS" | ||
| ] | ||
| }, | ||
| { | ||
| "key": "doc-narrated-clock", | ||
| "path": "src/notes/expiry.ts", | ||
| "lineStart": 12, | ||
| "lineEnd": 13, | ||
| "lens": "documentation", | ||
| "mechanism": [ | ||
| "comment narrates the change rather than the code", | ||
| "updated to.*instead of", | ||
| "change-log comment", | ||
| "meaningful only at the time of the diff" | ||
| ] | ||
| } | ||
| ], | ||
| "mustNotFlagSpecs": [ | ||
| { | ||
| "key": "doc-trap-slack-rationale", | ||
| "path": "src/notes/expiry.ts", | ||
| "lineStart": 5, | ||
| "lineEnd": 7, | ||
| "lens": "documentation", | ||
| "mechanism": [ | ||
| "EXPIRY_SLACK_MS comment is redundant", | ||
| "restates the constant", | ||
| "delete the slack comment" | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
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.