Repository navigation
fix(review): recover bilingual register from the live PR when the plan omits the Han flag #7739
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
Changes from 5 commits
531a382
c13e11e
5d2f44d
35d61b3
431cb75
082b6f3
9148756
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 |
|---|---|---|
|
|
@@ -30,6 +30,8 @@ import { | |
| TranscriptsUnavailableError, | ||
| } from './lib/coverage.js'; | ||
| import { shellQuotePath } from './lib/shell-quote.js'; | ||
| import { gh, setGhHost } from './lib/gh.js'; | ||
| import { isPositivePrNumber } from './lib/roster.js'; | ||
| import { | ||
| CRITICAL_PREFIX, | ||
| SUGGESTION_PREFIX, | ||
|
|
@@ -40,6 +42,13 @@ import { | |
|
|
||
| export type ReviewEvent = 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT'; | ||
|
|
||
| /** | ||
| * Reads a PR's description body, given its `owner/repo` and number. The one | ||
| * production implementation calls `gh pr view`; the bilingual fallback uses it | ||
| * to recover the Han signal from the live PR when the plan does not carry it. | ||
| */ | ||
| export type PrBodyFetcher = (ownerRepo: string, prNumber: string) => string; | ||
|
|
||
| export interface ComposeReviewInput { | ||
| /** | ||
| * Critical findings anchored as inline `comments` entries. | ||
|
|
@@ -95,6 +104,18 @@ export interface ComposeReviewInput { | |
| * anything that would change where the transcripts are found on a real run. | ||
| */ | ||
| env?: NodeJS.ProcessEnv; | ||
| /** | ||
| * How the bilingual fallback reads the live PR body when the plan carries a | ||
| * PR identity but no `prDescriptionHasHan` (a `plan-diff` plan, or one an | ||
| * improvising orchestrator wired in place of `fetch-pr`'s report). A test | ||
| * seam ONLY: production leaves it undefined and the CLI reads the PR with | ||
| * `gh pr view`. The handler **strips it from the input JSON** before use (the | ||
| * same way it strips `env`), so a model cannot supply one — not even a | ||
| * non-function value that would throw past the default and drop the fold. It | ||
| * can neither force nor suppress the Chinese fold, which is the whole point of | ||
| * keeping the signal the CLI's own. | ||
| */ | ||
| prBodyFetcher?: PrBodyFetcher; | ||
|
wenshao marked this conversation as resolved.
Resolved
|
||
| /** Step 1's lightweight `pr-context` fetch failed. */ | ||
| contextUnavailable?: boolean; | ||
| presubmit?: { | ||
|
|
@@ -589,11 +610,13 @@ export function composeReview(input: ComposeReviewInput): ComposeReviewResult { | |
| // description contains Han characters, the posted body carries the complete | ||
| // Chinese version collapsed under the English one — the shape this repo's | ||
| // own PR descriptions use, decided by the plan the CLI wrote, never by the | ||
| // caller. Fragments with no deterministic translation (model-written | ||
| // findings, caller echoes, error interpolations) ride verbatim in both | ||
| // halves. The footer stays outside the fold, once. A `zh === en` body has | ||
| // nothing translated, so no empty fold is published. | ||
| const bilingual = bilingualFromPlan(input.planPath); | ||
| // caller. When the plan does not record the signal but still names the PR, | ||
| // the switch recovers it from the live description (see `bilingualFromPlan`). | ||
| // Fragments with no deterministic translation (model-written findings, caller | ||
| // echoes, error interpolations) ride verbatim in both halves. The footer | ||
| // stays outside the fold, once. A `zh === en` body has nothing translated, so | ||
| // no empty fold is published. | ||
| const bilingual = bilingualFromPlan(input.planPath, input.prBodyFetcher); | ||
|
wenshao marked this conversation as resolved.
Resolved
|
||
| const render = (parts: Bi[], sep: string): string => { | ||
| const en = parts.map((p) => p.en).join(sep); | ||
| if (en === '') return ''; | ||
|
|
@@ -1041,21 +1064,70 @@ interface Bi { | |
| zh: string; | ||
| } | ||
|
|
||
| /** The production reader: one `gh pr view` for the description body. */ | ||
| const fetchPrBodyViaGh: PrBodyFetcher = (ownerRepo, prNumber) => { | ||
|
Collaborator
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. [P2]
Two options, either is fine:
Option 1 is simpler and the path is rare enough that this is a suggestion, not a blocker. |
||
| const json = gh( | ||
| 'pr', | ||
| 'view', | ||
| prNumber, | ||
| '--repo', | ||
| ownerRepo, | ||
| '--json', | ||
| 'body', | ||
| ); | ||
|
wenshao marked this conversation as resolved.
Resolved
|
||
| return (JSON.parse(json) as { body?: string }).body ?? ''; | ||
| }; | ||
|
|
||
| /** | ||
| * Whether the posted body carries the collapsed Chinese version: the plan | ||
| * (fetch-pr's report) recorded Han characters in the PR description. The | ||
| * signal is the CLI's own — never the caller's, who could otherwise toggle | ||
| * the register of a certified body. A local plan has no such field, and a | ||
| * plan that cannot be read defaults to English-only: the language must never | ||
| * take the review down. | ||
| * | ||
| * A recorded `false` is authoritative: `fetch-pr` fetched the body and found | ||
| * no Han, so English-only is the answer and no network is spent — every | ||
| * English-authored PR review takes this path. | ||
| * | ||
| * The field being *absent* is a different state, and the one that shipped an | ||
| * English-only review over a Chinese-authored PR (#7686): `fetch-pr` always | ||
| * writes it, but a `plan-diff` plan never does, and an orchestrator that | ||
| * improvises the pipeline can wire `compose-review` at a plan that is not | ||
| * `fetch-pr`'s report at all. So when the flag is missing yet the plan still | ||
| * carries the PR's identity, recover the signal from the live PR — the real | ||
| * description, which the caller cannot fake, so this hardens the "signal is | ||
| * the CLI's own" property rather than loosening it. Any failure of that fetch | ||
| * falls back to English: the language must never take the review down. | ||
| */ | ||
| function bilingualFromPlan(planPath: string | undefined): boolean { | ||
| function bilingualFromPlan( | ||
| planPath: string | undefined, | ||
| fetchPrBody: PrBodyFetcher = fetchPrBodyViaGh, | ||
| ): boolean { | ||
| if (!planPath) return false; | ||
| let plan: { | ||
| prDescriptionHasHan?: unknown; | ||
| ownerRepo?: unknown; | ||
| prNumber?: unknown; | ||
| }; | ||
| try { | ||
| const plan = JSON.parse(readFileSync(planPath, 'utf8')) as { | ||
| prDescriptionHasHan?: unknown; | ||
| }; | ||
| return plan?.prDescriptionHasHan === true; | ||
| plan = JSON.parse(readFileSync(planPath, 'utf8')); | ||
| } catch { | ||
| return false; | ||
| } | ||
| if (typeof plan?.prDescriptionHasHan === 'boolean') { | ||
| return plan.prDescriptionHasHan; | ||
| } | ||
| const ownerRepo = | ||
| typeof plan?.ownerRepo === 'string' && plan.ownerRepo | ||
| ? plan.ownerRepo | ||
| : undefined; | ||
| const prNumber = isPositivePrNumber(plan?.prNumber) | ||
| ? String(plan.prNumber) | ||
| : undefined; | ||
| if (!ownerRepo || !prNumber) return false; | ||
| try { | ||
| return /\p{Script=Han}/u.test(fetchPrBody(ownerRepo, prNumber)); | ||
| } catch { | ||
|
Collaborator
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. [P2] Silent catch on the recovery path — no observability when the fallback fires The design is correct ("the language must never take the review down"), but this catch is completely silent. When Consider adding a one-line stderr warning so the fallback is observable: } catch (err) {
process.stderr.write(
`compose-review: bilingual recovery failed (${ownerRepo}#${prNumber}): ${err instanceof Error ? err.message : String(err)}\n`,
);
return false;
}This keeps the fail-safe (English-only, review still posts) while giving the operator a breadcrumb. The plan-read catch on line 1115 could get the same treatment for consistency, though it fires less often in practice. |
||
| return false; | ||
| } | ||
|
|
@@ -1065,6 +1137,8 @@ interface ComposeReviewCliArgs { | |
| input: string | undefined; | ||
| comments: string; | ||
| out: string | undefined; | ||
| /** GitHub Enterprise host — routes this command's `gh` calls via GH_HOST. */ | ||
| host?: string; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -1140,9 +1214,22 @@ export const composeReviewCommand: CommandModule = { | |
| .option('out', { | ||
| type: 'string', | ||
| describe: 'Also write the {event, body} JSON to this path', | ||
| }) | ||
| .option('host', { | ||
| type: 'string', | ||
| describe: | ||
| 'GitHub Enterprise host (routes gh via GH_HOST) — needed only when ' + | ||
| 'the bilingual body-language recovery has to fetch the PR description', | ||
| }), | ||
| handler: (argv) => { | ||
| const { input, comments, out } = argv as unknown as ComposeReviewCliArgs; | ||
| const { input, comments, out, host } = | ||
| argv as unknown as ComposeReviewCliArgs; | ||
| // Route this command's own `gh` call — the bilingual recovery's `gh pr view` | ||
| // (see `fetchPrBodyViaGh`) — via the PR's host, exactly as fetch-pr and submit | ||
| // do. Without it a GHE review whose plan lacks the Han flag fetches the body | ||
| // from github.com, fails, and composes an English-only body that disagrees | ||
| // with what `submit` (which routes by host) posts. | ||
| setGhHost(host); | ||
| // yargs enforces --comments on the real command line; this covers every | ||
| // other way in (tests, programmatic calls) with the same sentence instead | ||
| // of an ENOENT on `undefined`. | ||
|
|
@@ -1162,6 +1249,13 @@ export const composeReviewCommand: CommandModule = { | |
| // always resolves the transcripts from the environment the CLI exported. | ||
| const parsed = JSON.parse(raw) as ComposeReviewInput; | ||
| delete parsed.env; | ||
| // Same reasoning for the bilingual body-language fetcher: it is a unit-test | ||
| // seam (production reads the PR with `gh pr view`). A state JSON carrying it — | ||
| // even a non-function value like `"suppress"` — would otherwise reach | ||
| // `bilingualFromPlan`, be called, throw, and drop the Chinese fold through the | ||
| // fail-safe. Stripping it here keeps the register the CLI's own, not the | ||
| // caller's, which is the whole point of the seam. | ||
| delete parsed.prBodyFetcher; | ||
| // The inline counts are counted, not accepted — `submit` has refused them | ||
| // since the count-beside-the-comments bug, and this boundary refusing them | ||
| // too is what makes the Step 6 line and the posted verdict the same | ||
|
|
||
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.
[Nit] Strong test battery for the recovery path
Seven tests covering the fallback matrix: Han present/absent in recovered body, recorded
falseshort-circuits (no network), no PR identity short-circuits, fetch failure → English, production reader verifies exactghargs, and the handler strip guard against a model-suppliedprBodyFetcher. TherecordingFetcherpattern (proving it was NOT reached in the short-circuit tests) is a nice touch.One gap worth considering: there's no test for the case where
prDescriptionHasHanis present but with a non-boolean type (e.g."true"as string, ornull). The code correctly falls through to the recovery path in that case (line 1119typeof === 'boolean'check), but a test asserting "string\"true\"triggers recovery, not silent acceptance" would guard against a future loosening of the type check.