-
Notifications
You must be signed in to change notification settings - Fork 1
review: hand off out-of-lane observations instead of dropping them #245
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
base: jwbron/review-rereview-accountability
Are you sure you want to change the base?
Changes from all commits
7b5318c
2be8ede
3a8fc5c
1438a67
1bc9e08
51691de
7eb5501
7de0635
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "review": minor | ||
| --- | ||
|
|
||
| Out-of-lane observations are handed off, never dropped. Observed on the review-v1.4.0 re-run lifecycle: the skill-auditor found a real eventual-consistency bug (dedup reads via a Query immediately after PutMulti) and discarded it, verbatim "that's a correctness concern, not a quotable skill-rule violation, so I'll leave it"; correct under quote-the-rule, but the observation died and the defect shipped unflagged. The skill-auditor and every specialist lens may now return `out_of_lane_observations[]` (path, optional line, the concern stated concretely, a required concrete `failure_scenario`, optional `suggested_lane`) for real concerns their own mandate does not let them report. The orchestrator converts each one into a candidate comment with the code-assigned label `question (non-blocking)` (a handoff is not a vetted finding and can never block on its own; the claim-validator never upgrades severity) and routes it through the identical provenance gate, scope filter, claims.json, and validation path as every other candidate. The shape is validated by the new `validateOutOfLaneObservation` in `lib/finding-schema.ts` (a sibling type, so `FINDING_SCHEMA_VERSION` stays 2: no serialized finding is invalidated). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -170,6 +170,46 @@ export type Finding = { | |
| model_authored_prose: string; | ||
| }; | ||
|
|
||
| /** | ||
| * An out-of-lane observation: a real concern a reviewer surfaced that its own | ||
| * mandate does not let it report as a finding — e.g. the skill-auditor notices | ||
| * a correctness problem while checking a rule, but quote-the-rule rightly | ||
| * forbids reporting it as a skill violation. Production motivation: on the | ||
| * review-v1.4.0 re-run lifecycle the skill-auditor found a real | ||
| * eventual-consistency bug (dedup reads via a Query immediately after | ||
| * PutMulti) and dropped it — "that's a correctness concern, not a quotable | ||
| * skill-rule violation, so I'll leave it". Correct per quote-the-rule, but the | ||
| * observation died. | ||
| * | ||
| * The skill-auditor and every specialist lens may return these alongside | ||
| * `findings[]`; the orchestrator routes each one into claim validation as a | ||
| * non-blocking candidate (label code-assigned, never model-chosen), so a | ||
| * declined-as-out-of-lane observation is validated and surfaced rather than | ||
| * discarded. An observation is a handoff, not a vetted finding: it can never | ||
| * block on its own. | ||
| */ | ||
| export type OutOfLaneObservation = { | ||
| /** Repo-relative path the observation anchors on. */ | ||
| path: string; | ||
| /** RIGHT-side diff line, when the concern is line-specific. */ | ||
| line?: number; | ||
| /** One sentence: the concern, stated concretely. Model-authored. */ | ||
| observation: string; | ||
| /** | ||
| * One sentence: the concrete inputs/state and the wrong outcome they | ||
| * produce — the claim the claim-validator attacks, exactly as on a | ||
| * finding. An observation whose scenario cannot be stated concretely is | ||
| * not worth handing off. | ||
| */ | ||
| failure_scenario: string; | ||
| /** The lane the producer thinks owns this, e.g. `correctness`. Optional. */ | ||
| suggested_lane?: string; | ||
| }; | ||
|
|
||
| export type ObservationValidationResult = | ||
| | {ok: true; observation: OutOfLaneObservation} | ||
| | {ok: false; errors: string[]}; | ||
|
|
||
| export type ValidationResult = | ||
| | {ok: true; finding: Finding} | ||
| | {ok: false; errors: string[]}; | ||
|
|
@@ -328,6 +368,60 @@ export const validateFinding = (input: unknown): ValidationResult => { | |
| return {ok: true, finding: input as Finding}; | ||
| }; | ||
|
|
||
| /** | ||
| * Validate an untrusted value against the out-of-lane observation shape. | ||
| * Same error-collecting contract as {@link validateFinding}: every problem is | ||
| * returned, so a producer's malformed handoff is diagnosable from the run | ||
| * artifact rather than silently dropped. | ||
| */ | ||
| export const validateOutOfLaneObservation = ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question (non-blocking): |
||
| input: unknown, | ||
| ): ObservationValidationResult => { | ||
| const errors: string[] = []; | ||
|
|
||
| if (!isRecord(input)) { | ||
| return {ok: false, errors: ["observation: must be an object"]}; | ||
| } | ||
|
|
||
| if (!isNonEmptyString(input["path"])) { | ||
| errors.push("path: required non-empty string"); | ||
| } | ||
|
|
||
| const line = input["line"]; | ||
| if ( | ||
| line !== undefined && | ||
| (!Number.isInteger(line) || (line as number) < 1) | ||
| ) { | ||
| errors.push("line: must be a positive integer when present"); | ||
| } | ||
|
|
||
| if (!isNonEmptyString(input["observation"])) { | ||
| errors.push("observation: required non-empty string"); | ||
| } | ||
|
|
||
| if (!isNonEmptyString(input["failure_scenario"])) { | ||
| errors.push("failure_scenario: required non-empty string"); | ||
| } | ||
|
|
||
| if ( | ||
| input["suggested_lane"] !== undefined && | ||
| !isNonEmptyString(input["suggested_lane"]) | ||
| ) { | ||
| errors.push("suggested_lane: must be a non-empty string when present"); | ||
| } | ||
|
|
||
| if (errors.length > 0) { | ||
| return {ok: false, errors}; | ||
| } | ||
|
|
||
| return {ok: true, observation: input as OutOfLaneObservation}; | ||
| }; | ||
|
|
||
| /** Narrowing boolean wrapper around {@link validateOutOfLaneObservation}. */ | ||
| export const isValidOutOfLaneObservation = ( | ||
| input: unknown, | ||
| ): input is OutOfLaneObservation => validateOutOfLaneObservation(input).ok; | ||
|
|
||
| /** Narrowing boolean wrapper around {@link validateFinding}. */ | ||
| export const isValidFinding = (input: unknown): input is Finding => | ||
| validateFinding(input).ok; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought (non-blocking): The production orchestrator now routes out-of-lane observations, but the live-eval producer (
eval/live-producer.tsparseAgentFindings) reads onlyfindings[]. So in a review-trial a handed-off observation is dropped and the arm scores as if it never happened — the same drop this PR closes on the production path. Worth teaching the eval harness to readout_of_lane_observations[]in a follow-up so the trial can measure this feature.