Files
gstack/test/helpers/third-party-actions.ts
Garry Tanandcapy-ai[bot] 3ab7b149c9 v1.91.249.0 fix(evals): repair five paid-case flakes at their causes (GSTA-107 daily flaky pass) (#3472)
* fix(evals): repair five paid-case flakes at their causes: docsync --help slip, state-dir artifacts, preamble asset, defer paraphrase, probe --help (GSTA-107)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

offersDefer: the wait/hold-off-for-now paraphrase must open the option or a
sentence and end it, so a drive option's "I wait for now while you sign in"
or a manual option's "wait for now and do it yourself" no longer reads as the
defer option (red-team finding on #3472). Negative controls added.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (round 2)

- offersDefer: the wait/hold-off-for-now paraphrase never counts inside a
  browser drive option, accepts an em dash without spaces and a curly
  apostrophe in let's; negative and positive controls added (adversarial
  review on #3472).
- docsync-authority: the interface assertion expects the rendered
  `<home>/remote.git` form so it holds on Windows; the captured Linux
  replay is POSIX-only like the late-result replay (outside voice P1).
- touchfiles: register the two new docsync captures with the paid cases
  they replay (ship-docsync-store, ship-docsync-completion), which
  test/free-fixtures.test.ts requires (remote gate red at 470ac55).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-26 16:55:25 +00:00

66 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The NEEDS_ASIDE: Darwin download pitch is a recommendation, so it counts only where a recommendation can start: a
* sentence, clause (after , : ; or a dash), bullet or line start, optionally led by "then/please/just/so/first/now" and
* "you can/could/may/should/need to". A quoted, parenthesised or reported mention ("the line 'download Aside ...'",
* "says to download ...") and a clause with a negation before the phrase ("you don't need to download ...", or "do not"
* ending the previous line) never count. The canonical phrase may name Aside as "it" once the macOS 15+ floor follows
* ("If you want to use Aside: download it (macOS 15+) at aside.com", gate 39315041557). The short forms without the
* floor count only at the start of a sentence, bullet or quote.
*/
const ASIDE_DOWNLOAD_PITCH = String.raw`(?:^|[.!?;:]\s*|\n)\s*(?:[>*-]\s*)*["\u201C]?download (?:it|Aside) at aside\.com\b|(?:^|[.!?;:,\u2014\u2013\n])[\s>*_-]*(?<!(?:\b(?:not|never|no|cannot)|n['\u2019]t)\s+)(?:(?:then|please|just|so|first|now)\s+)?(?:you\s+(?:can|could|may|should|need\s+to)\s+)?download (?:Aside|it) \(macOS 15\+\) at aside\.com\b`;
export const asideDownloadPitch = new RegExp(ASIDE_DOWNLOAD_PITCH, 'im');
export const asideDownloadPitchCount = (text: string): number => [...text.matchAll(new RegExp(ASIDE_DOWNLOAD_PITCH, 'gim'))].length;
/** Lettered consent options (A-D), bulleted, bold or blockquoted, with indented continuation lines. */
export function letteredOptions(text: string): string[] {
const options: string[] = [];
let current: string | undefined;
for (const line of text.replaceAll('**', '').split('\n')) {
const option = line.match(/^[ \t]*(?:>[ \t]*)?(?:[-*+][ \t]+)?[A-D][).][ \t]+(.*)$/);
if (option) {
if (current !== undefined) options.push(current);
current = option[1];
} else if (current !== undefined && /^[ \t]+\S/.test(line)) {
current += ` ${line.trim()}`;
} else if (current !== undefined) {
options.push(current);
current = undefined;
}
}
if (current !== undefined) options.push(current);
return options;
}
/** Inspect lettered consent options, not narration that quotes an unavailable option. */
export function asideDriveOptions(text: string): string[] {
return letteredOptions(text).filter(option => {
const currentOffer = option.replace(
/\b(?:I|we)(?:['’]ll| will) (?:re[- ]?ask|ask again)(?: (?:you|this question))? with (?:the )?(?:"[^"]+"|“[^”]+”|[\w -]+?) option(?: included)?\b/gi,
'',
);
return /\bAside\b/.test(option) && (/\b(?:drive|driving|browse|browsing|navigate|click)\b/i.test(currentOffer)
|| /\bI(?:['’]ll| will)? (?:do|handle|create|complete) (?:it|this)\b/i.test(currentOffer));
});
}
/** Any lettered option offering a browser drive, for flows where no drive may be offered. */
export const offersDriveOption = (text: string): boolean => letteredOptions(text).some(option => /drive|browse|Aside/i.test(option));
/**
* The defer option: a lettered option that defers, postpones or sets the step aside for now. "Wait for now" and
* "hold off for now" count only as the option's own opening or sentence (gate 40290033102: "D) Wait for now. I'll
* mark this step as blocked on you."), never as a drive option's "I wait while you sign in": the phrase must open the
* option or a sentence and end it, and the option must not be a browser drive, so "I wait for now while you sign in",
* "wait for now and do it yourself" and "I drive it in Aside. I wait for now." do not count.
*/
export const offersDefer = (text: string): boolean => letteredOptions(text).some(option =>
/\b(?:defer\w*|postpone\w*|put (?:this|it)(?: step)? off|skip (?:this|it)(?: step)? for now|come back to (?:this|it)(?: step)? later|leave (?:this|it)(?: step)? for (?:now|later))\b/i.test(option)
|| !/drive|browse|Aside/i.test(option)
&& /(?:^|[.!?;:]\s+)(?:(?:we|I|let['\u2019]s) )?(?:wait|hold off)(?: on (?:this|it)(?: step)?)? for now\b(?=\s*(?:[.!?;:]|[-\u2013\u2014]|$))/i.test(option));
/** The manual option: "manual", or one sentence offering instructions or steps the user follows themselves. */
export function offersManualSteps(text: string): boolean {
return /manual/i.test(text) || text.split(/[.!?](?:\s|$)|\n/).some(sentence => /\b(?:instructions?|steps?)\b/i.test(sentence) &&
(/\byourself\b/i.test(sentence) || (/\bby\s+hand\b/i.test(sentence) && /\byour?\b/i.test(sentence))));
}