import { designFirstReviewAUQ, designReviewSetupAUQ } from './claude-pty-runner'; import type { AskUserQuestionFingerprint } from './claude-pty-runner'; import { pickDesignCountOutsideVoices } from './design-count-outside'; /** Choosing reviewer participation is setup, even when numbered or asked late. */ export function isDesignCountSetup(fp: AskUserQuestionFingerprint): boolean { if (designReviewSetupAUQ(fp)) return true; const call = fp.nativeCall; if (!call?.answered || call.failed || call.questions.length !== 1 || call.unansweredQuestionIndices?.length || fp.signature !== `${call.sessionId}:${call.toolUseId}`) return false; const q = call.questions[0]!; if (q.multiSelect || !/^outside(?: design)? voices$/i.test(q.header.trim()) || (q.question.match(/\s*$/.test(q.question) || (q.question.match(/\?/g)?.length ?? 0) !== 1 || !/^(?:D\s*\d+(?:\s*\(Step\s*0[A-Z]?\))?\s*[—–:-]\s*)?(?:Run|Want|Include|Enable)\s+outside(?: design)? voices\s+(?:before|for)\s+the\s+(?:detailed\s+)?(?:design\s+)?review(?:\s+passes)?\?/i.test(q.question.trim())) return false; const labels = q.options.map(option => option.label.trim().replace(/\s*\(recommended\)\s*$/i, '')); // Consume the entire menu, not just its opening question or action labels. // Unknown explanatory prose can contain a second product decision. const remainder = q.question.slice(q.question.indexOf('?') + 1).replace(/]+>\s*$/, '').trim(); if (remainder && !/^(?:Codex evaluates the design; a Claude subagent reviews completeness\.|Codex evaluates against OpenAI's design hard rules \+ litmus checks; a Claude subagent does an independent completeness review\. \(Requires Codex CLI to be installed\.\))$/.test(remainder)) return false; const descriptions = q.options.map(option => (option.description ?? '').trim().replace(/\s+/g, ' ')); const noDescription = /^(?:Skip Codex \+ Claude subagent outside pass\. Best for this case: it's a scoped settings form update with a complete DESIGN\.md; hard-rejection checks apply to marketing surfaces, not OPERATE\/settings UI\.|Skip outside voices and go straight to the 7 review passes\. Faster; sufficient for most plans\.)$/; const yesDescription = /^(?:Run Codex against OpenAI design hard rules \+ litmus checks, and a separate Claude subagent for an independent completeness review\. Adds time but catches anything a single-model pass misses\.|Launches Codex design critique \+ Claude subagent completeness review in parallel before the 7 passes\. Adds 1[–-]2 minutes\.)$/; if (labels.some((label, index) => descriptions[index] && !(/^No\b/.test(label) ? noDescription : yesDescription).test(descriptions[index]!))) return false; const no = labels.filter(label => /^No(?:\s*[,—–-]\s*|\s+)proceed without$/i.test(label)); const yes = labels.filter(label => /^Yes(?:\s*[,—–-]\s*|\s+)run (?:outside(?: design)? voices|Codex \+ Claude subagent)$/i.test(label)); return labels.length === 2 && no.length === 1 && yes.length === 1 && q.options.some(option => option.label === call.answers?.[q.question]); } /** A numbered design-system amendment can be the first review decision. */ function numberedVisualHierarchyFinding(fp: AskUserQuestionFingerprint): boolean { const call = fp.nativeCall; if (!call || call.answered !== true || call.failed !== false || !call.sessionId || !call.toolUseId || call.questions.length !== 1 || !Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length || fp.signature !== `${call.sessionId}:${call.toolUseId}` || (fp.nativeQuestionIndex !== undefined && fp.nativeQuestionIndex !== 0)) return false; const q = call.questions[0]!; const finding = /^Gap ([1-9]\d*) of ([1-9]\d*)\s*[—–-]\s*([A-Za-z][A-Za-z0-9_-]{0,39}) button visual hierarchy: apply DESIGN\.md primary button style\?$/i.exec(q.question.trim()); if (!finding || Number(finding[1]) > Number(finding[2]) || !new RegExp(`^Gap ${finding[1]}: Button$`, 'i').test(q.header.trim()) || q.multiSelect || q.options.length !== 2 || fp.options.length !== 2 || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) || !q.options.some(o => o.label === call.answers?.[q.question])) return false; const labels = q.options.map(o => o.label.trim().replace(/\s*\(recommended\)\s*$/i, '')); const apply = labels.findIndex(s => /^Apply DESIGN\.md fix$/i.test(s)); const defer = labels.findIndex(s => /^Defer to implementation$/i.test(s)); if (apply < 0 || defer < 0 || apply === defer) return false; const control = '[A-Za-z][A-Za-z0-9_-]{0,39}'; const amendment = new RegExp(`^Add to plan: ${finding[3]} gets #[0-9a-f]{6} filled \\+ (?:white|black) text \\(primary\\); ${control}(?:, ${control})*(?:,? and ${control})? get neutral ghost style\\. Closes the visual hierarchy gap exactly as DESIGN\\.md specifies\\. Implementation task T[1-9]\\d* becomes committed\\.$`, 'i'); // Both offered bodies describe the actual style amendment or its deferral; // readiness, a source-selection question, or an example is not this finding. return amendment.test(q.options[apply]!.description?.trim() ?? '') && /^Leave the gap named but unresolved\. Engineer decides the button styles at implementation time without a spec\. Risk: inconsistency with the design system or re-work after review\.$/i.test(q.options[defer]!.description?.trim() ?? ''); } interface PrimaryFindingFacts { primary: string; currentGap: boolean; controlCount: number; namedPeers?: string[]; remedy: { peers: string[]; role: boolean; tokens: boolean; authority: boolean; current: boolean }; alternative: { unresolved: boolean; retainedCounts: number[]; current: boolean }; } /** Presentation adapters supply facts; this is the shared finding boundary. */ function validPrimaryFinding(facts: PrimaryFindingFacts): boolean { const peers = facts.remedy.peers; return facts.currentGap && facts.controlCount > 1 && peers.length + 1 === facts.controlCount && peers.every(peer => /^[a-z][a-z0-9 _-]{0,39}$/i.test(peer)) && new Set(peers).size === peers.length && !peers.includes(facts.primary.toLowerCase()) && (!facts.namedPeers || JSON.stringify(peers) === JSON.stringify(facts.namedPeers)) && facts.remedy.role && facts.remedy.tokens && facts.remedy.authority && facts.remedy.current && facts.alternative.unresolved && facts.alternative.current && facts.alternative.retainedCounts.every(count => count === facts.controlCount); } /** A qidless Issue with its own design gap is a finding, independent of D numbering. */ function ordinaryDesignIssue(fp: AskUserQuestionFingerprint, scope: { primary: boolean; ownsPrimaryPremise?: boolean }): boolean { const call = fp.nativeCall; if (!call?.questions.length) return false; const nativeValid = call.answered === true && call.failed === false && !!call.sessionId && !!call.toolUseId && call.questions.length === 1 && Array.isArray(call.unansweredQuestionIndices) && !call.unansweredQuestionIndices.length && fp.signature === `${call.sessionId}:${call.toolUseId}` && (fp.nativeQuestionIndex === undefined || fp.nativeQuestionIndex === 0); const q = call.questions[0]!; const title = q.question.split('\n')[0]!.trim(); const headerIdentity = /^Issue ([1-9]\d*)(?:: ([A-Za-z][A-Za-z0-9 _-]{0,39}))?$/i.exec(q.header.trim()); const titleSubject = title.replace(/^D[1-9]\d*\s*[—–:-]\s*/i, '') .replace(/^Issue [1-9]\d*: /i, ''); // Identity can live in either native field. A local actor/role relation is // distinct from the surrounding decision wording; both fields must agree // when they name an issue or actor. The facts below still prove the finding. const actorRole = /^(?!(?:How|What|Which|Who|Why|Where|When)\b)([A-Za-z][A-Za-z0-9 _-]{0,39}) (?:become|be|is|as) (?:the )?(?:(?:only|single|visible|visually) )?(?:filled )?primary (?:header )?action\b/i; const roleSubject = (text: string) => text.replace(/^(?:Should|Can|Could|Must|Will|Would) /i, ''); const titleRole = actorRole.exec(roleSubject(titleSubject)); // Recognizing this actor/role family is separate from accepting evidence. // Quotation or invalid native identity must not reopen generic qid fallback. scope.primary = !!titleRole || actorRole.test(roleSubject(titleSubject.replace(/^["“'‘—–\s]+|["”'’\s]+$/g, ''))); const headerOwnedIssue = headerIdentity && titleRole && !/\bIssue [1-9]\d*\b/i.test(titleSubject) && !/\b(?:reviewer|scope|setup|routing|learnings|outside voices|next steps?)\b/i.test(titleSubject) ? [title, headerIdentity[1]!, titleSubject] : null; // This primary-action decision can name the control in its Issue header. // F labels annotate findings; they do not establish review identity alone. const headerActionIssue = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*)(?: \(F[1-9]\d*\))?: (How should the header action group establish the primary action)\?$/i.exec(title); const signaledPrimaryIssue = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*) \(G[1-9]\d*\): (How should the header action group signal that [A-Za-z][A-Za-z0-9 _-]{0,39} is the primary action)\?$/i.exec(title); // Parse the owned comparison independently of the following decision's prose. // Both forms return the same issue, primary and peer facts for the checks below. const comparisonTitle = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*): ([A-Za-z][A-Za-z0-9 _-]{0,39}) (?:is visually identical to|is indistinguishable from) ([A-Za-z][A-Za-z0-9 ,/_-]{0,119}?)(?: in the header)?\. ([^?]+\?)$/i.exec(title); const compoundPrimaryIssue = comparisonTitle && /\b(?:fix|resolve|distinguish(?:ed)?|primary action)\b/i.test(comparisonTitle[4]!) ? comparisonTitle : null; const distinguishedPrimaryIssue = /^D[1-9]\d*\s*[—–:-]\s*Issue ([1-9]\d*): How should ([A-Za-z][A-Za-z0-9 _-]{0,39}) (?:be distinguished|stand out) from ([A-Za-z][A-Za-z0-9 ,/_-]{0,119}?)(?: in the header)?\?$/i.exec(title) ?? compoundPrimaryIssue; const questionIssue = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*)(?: \((?:(?:G[1-9]\d*|Pass [1-7]), )?(?:Visual Hierarchy|Spacing|Color|Typography|Motion)\))?: ([^?]+)\?$/i.exec(title) ?? headerActionIssue ?? signaledPrimaryIssue; // A declaration can own the same primary-action decision. Its body and // native choices below must prove the gap, complete styling and deferral. const declaredPrimaryIssue = (!questionIssue || /\nELI10: (?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) header buttons currently /i.test(q.question)) && /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*)(?: \(G[1-9]\d*\))?: ([^?\n]+)\??$/i.exec(title) || (titleRole && (questionIssue ?? headerOwnedIssue)); const issue = questionIssue || declaredPrimaryIssue; if (!issue) return false; const subject = issue[2]!.replace(/\.$/, ''); const declaredGap = declaredPrimaryIssue && /\(G([1-9]\d*)\)/.exec(title)?.[1]; const descriptivePrimaryHeader = (signaledPrimaryIssue && /^(?!(?:focus|scope|setup|routing|learnings|outside voices|next steps?)$)[A-Za-z][A-Za-z _-]{0,39}$/i.test(q.header.trim())) || (distinguishedPrimaryIssue && /^(?:Visual )?Hierarchy$/i.test(q.header.trim())) || (compoundPrimaryIssue && (q.header.trim().toLowerCase() === `${compoundPrimaryIssue[2]} primary`.toLowerCase() || q.header.trim().toLowerCase() === `Issue ${compoundPrimaryIssue[1]} ${compoundPrimaryIssue[2]}`.toLowerCase())); // The numbered headline must ask about a concrete design requirement. // Reviewer participation or workflow navigation can also use Issue labels. if (!distinguishedPrimaryIssue && !/\b(?:buttons?|primary(?: header)? actions?|primary emphasis|primacy|hierarchy|spacing|contrast|colou?rs?|labels?|typography|fonts?|loading|spinner|skeleton|motion)\b/i.test(issue[2]!)) return false; const choiceLabel = (label: string) => label.trim().replace(/^[1-9]\d*[A-Z](?:\s*[—–).:]\s*|\s+)/, '').replace(/\s*\(recommended\)\s*$/i, ''); const opposed = q.options.filter(o => /^(?:Defer|Decline|Leave|Keep|Accept the gap)\b/i.test(choiceLabel(o.label)) || (distinguishedPrimaryIssue && /^(?:Keep|Leave)\b/i.test(o.description?.trim() ?? ''))); const repair = !headerActionIssue && !distinguishedPrimaryIssue && !declaredPrimaryIssue && /\b(?:fix|resolve|address)\b/i.test(title) && q.options.some(o => /\b(?:closing|closes|fixes|resolves?|applies?)\b/i.test(o.description ?? '')); // A source citation alone can describe a report or the next reviewer. // Bind the alternate wording to a named control's concrete style amendment // and the opposed choice that leaves the documented violation unresolved. const primary = /^Make ([A-Za-z][A-Za-z0-9 _-]{0,39}) the (?:visible|visually|(?:only|single)(?: filled| visually)?) primary (?:header )?action(?: in the header)?$/i.exec(subject) ?? /^([A-Za-z][A-Za-z0-9 _-]{0,39}) (?:has no|lacks) (?:visual primacy|primary emphasis)(?: in the header action group)?$/i.exec(subject) ?? /^Give ([A-Za-z][A-Za-z0-9 _-]{0,39}) primary emphasis in the header action group$/i.exec(subject) ?? /^How should the header action group signal that ([A-Za-z][A-Za-z0-9 _-]{0,39}) is the primary action$/i.exec(subject) ?? /^How should (?:the )?(?:header )?actions establish that ([A-Za-z][A-Za-z0-9 _-]{0,39}) is the primary action$/i.exec(subject) ?? titleRole ?? (distinguishedPrimaryIssue && [distinguishedPrimaryIssue[0], distinguishedPrimaryIssue[2]!]) ?? (headerActionIssue && new RegExp(`^Issue ${issue[1]}: ([A-Za-z][A-Za-z0-9 _-]{0,39})$`, 'i').exec(q.header.trim())); scope.primary ||= !!primary; // Recognize the ordinary parser's premise grammar before validating its // evidence. Invalid source/roles/status within that grammar must not fall // through to the broader native-field parser merely by adding gap prose. const rawAssessments = [...q.question.matchAll(/^(?:>\s*)?ELI10: (.+)$/gm)].map(match => match[1]!); scope.ownsPrimaryPremise = !!primary && (!!titleRole || !!declaredPrimaryIssue || !!compoundPrimaryIssue || rawAssessments.some(value => /^(?:(?:Right now|Today) )?[A-Za-z][A-Za-z0-9 ,/_-]{0,159}? (?:(?:all )?look (?:the same|identical)|are (?:all )?(?:(?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) )?identical buttons)\b/i.test(value) && !/^(?:Right now|Today) (?:all|the)\b|\bcurrently\b/i.test(value) || /^(?:Right now|Today) (?:all|the) (?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) header buttons (?:look (?:the same|identical)|(?:are|have|share) the same [A-Za-z ,]+)\./i.test(value))); if (!nativeValid) return false; const ownedPrimaryHeader = primary && (q.header.trim().toLowerCase() === `${primary[1]} primary`.toLowerCase() || q.header.trim().toLowerCase() === `Issue ${issue[1]} ${primary[1]}`.toLowerCase()); if (!(new RegExp(`^Issue ${issue[1]}(?:: [A-Za-z][A-Za-z0-9 _-]{0,39})?$`, 'i').test(q.header.trim()) || descriptivePrimaryHeader || ownedPrimaryHeader) || / o.label)).size !== q.options.length || fp.options.length !== q.options.length || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) || !q.options.some(o => o.label === call.answers?.[q.question])) return false; if (declaredPrimaryIssue && (!primary || q.options.length > 4 || Object.keys(call.answers ?? {}).length !== 1)) return false; // An attributed native option can put the fill before or after its color. // It still names the primary, every ghost peer and DESIGN.md in one action. const namedTokenStyle = primary && new RegExp(`^(?:✅\\s*)?(?:Matches DESIGN\\.md exactly|(?:Apply|Use) DESIGN\\.md(?: tokens)?): ${primary[1]} ` + '(?:#[0-9a-f]{6} filled(?: with)? (?:white|black) text|filled #[0-9a-f]{6}(?: with)? (?:white|black) text); ' + '([A-Za-z][A-Za-z0-9 ,/_-]{0,99}) (?:as )?neutral ghost(?: buttons)?\\.', 'i'); const namedTokenIssue = !!namedTokenStyle && q.options.some(o => namedTokenStyle.test(o.description ?? '')); const primaryEmphasisIssue = namedTokenIssue || !!signaledPrimaryIssue || !!distinguishedPrimaryIssue || !!declaredPrimaryIssue || /^Give [A-Za-z][A-Za-z0-9 _-]{0,39} primary emphasis in the header action group$/i.test(issue[2]!); const scopedPrimaryStatus = !!headerActionIssue || primaryEmphasisIssue; const explicitStyle = primary && `${primary[1]} filled (?:primary )?#[0-9a-f]{6}(?:/| with )(?:white|black)(?: text)?; ` + '[A-Za-z][A-Za-z0-9 ,/_-]{0,99} neutral ghost(?: buttons)?\\.'; const amendments = primary && [ new RegExp(`^(?:✅\\s*)?Matches DESIGN\\.md exactly: ${primary[1]} filled #[0-9a-f]{6} with (?:white|black) text; ` + '[A-Za-z][A-Za-z0-9 ,_-]{0,99} as neutral ghost buttons\\.', 'i'), new RegExp(`^(?:✅\\s*)?${primary[1]} becomes the (?:single|one|only) filled(?: primary)?(?: button)? \\(#[0-9a-f]{6}, (?:white|black) text\\); ` + '[A-Za-z][A-Za-z0-9 /,_-]{0,99} (?:become|are) neutral ghost buttons (?:exactly as DESIGN\\.md specifies|per DESIGN\\.md)\\b', 'i'), new RegExp(`^(?:✅\\s*)?${primary[1]} is the (?:single|only) filled #[0-9a-f]{6} button; ` + '[A-Za-z][A-Za-z0-9 /,_-]{0,99} become neutral ghosts, exactly (?:per DESIGN\\.md|as DESIGN\\.md prescribes)\\b', 'i'), new RegExp(`^(?:✅\\s*)?Apply DESIGN\\.md(?: tokens)?: ${primary[1]} #[0-9a-f]{6} filled(?: with)? (?:white|black) text; ` + '[A-Za-z][A-Za-z0-9 ,/_-]{0,99} neutral ghost(?: buttons)?\\.', 'i'), // The same concrete style can cite DESIGN.md before or after its tokens. new RegExp(`^(?:✅\\s*)?(?:Apply DESIGN\\.md(?: tokens)?: ${explicitStyle}|${explicitStyle} Exact DESIGN\\.md\\.)`, 'i'), new RegExp(`^(?:✅\\s*)?${primary[1]}\\s*=\\s*filled #[0-9a-f]{6} with (?:white|black) text; ` + '[A-Za-z][A-Za-z0-9 ,/_-]{0,99}\\s*=\\s*neutral ghost(?: buttons?)?,? per DESIGN\\.md\\b', 'i'), ]; const primaryHeader = !q.header.includes(':') || q.header.split(':')[1]!.trim().toLowerCase() === primary?.[1]?.toLowerCase(); const ownedStatus = (value: string, index: number, source: string) => /^(?:withdrawn|superseded|resolved|closed|historical|hypothetical|rejected|cancelled|canceled|not current|no longer current)$/i.test(value) && /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:(?:(?:This|That|The) (?:issue|finding|question|amendment|deferral|style|fix|remedy|choice|option|(?:DESIGN\.md |token )?(?:requirement|contract))|(?:Issue |G)[1-9]\d*) (?:is|was|has been)|(?:these|the|this) (?:tokens?|styles?|primary treatment) (?:are|is|were|was|have been|has been)) $/i.test(source.slice(0, index)); // The style wordings share one owned decision: a current equal-weight gap, // a named control's DESIGN.md amendment, and a different choice retaining it. // A following status assertion remains current after a parenthesized effort // estimate. Preserve the estimate and expose its boundary to the same guards. const currentText = (text: string) => (scopedPrimaryStatus ? text.replace(/(\(human: ~?[0-9]+(?:\.[0-9]+)?(?:h|min) \/ CC: ~?[0-9]+(?:\.[0-9]+)?(?:h|min)\))(?=\s+\S)/g, '$1.') .replace(/\(recommended\)(?=\s+\S)/gi, '$&.') : text) .replace(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, '') .replace(/^(?:\s*>| {4}|\t).*$/gm, '') .replace(/`([^`]+)`/g, (_, body: string, index: number, source: string) => scopedPrimaryStatus && ownedStatus(body, index, source) ? body : /\s/.test(body) ? '' : body) // A quoted status scalar remains a current assertion when its unquoted // subject names this decision; whole quoted historical prose stays absent. .replace(scopedPrimaryStatus ? /"[^"\n]*"|“[^”\n]*”|(? ownedStatus(quoted.slice(1, -1), index, source) ? quoted.slice(1, -1) : '').replace(/\*\*/g, ''); const questionText = currentText(q.question); const assessments = [...questionText.matchAll(/^ELI10: (.+)$/gm)]; const prefix = questionText.slice(0, assessments[0]?.index ?? 0) .split('\n').filter(line => line.trim()).slice(1); const sourceAssessment = /\b(?:historical|hypothetical|quoted|source|earlier review)\s+(?:example|excerpt|assessment|material|text)\b|\bnot\s+(?:the\s+)?current\s+(?:UI|assessment|finding|amendment|deferral|remedy|choice|option)\b|\bthis (?:finding|amendment|deferral|remedy|choice|option) (?:applies only to|belongs to) (?:an? )?(?:another|different) (?:project|plan|review)\b/i; const assessment = assessments.length === 1 && prefix.every(line => /^(?:Project\/branch\/task:|\[P[0-3]\])/.test(line)) && !/^(?:Project\/branch\/task:|\[P[0-3]\])\s*(?:If|When|Unless|Provided|Assuming)\b/im.test(prefix.join('\n')) && !sourceAssessment.test(prefix.join(' ')) && !sourceAssessment.test(assessments[0]![1]!) ? assessments[0]![1]! : ''; const headerPeers = primary && headerActionIssue && new RegExp(`^${primary[1]}, ([A-Za-z][A-Za-z0-9 _-]{0,39}(?:, [A-Za-z][A-Za-z0-9 _-]{0,39})*(?:,? and [A-Za-z][A-Za-z0-9 _-]{0,39})?) currently look (?:the same|identical)\\.`, 'i').exec(assessment); // Equal visual properties can establish the same current lack of hierarchy. // Shared geometry alone is not a claim that the actions look equally primary. const properties = '(?:size|weight|colou?r|fill|emphasis)(?:(?:, ?|,? and )(?:size|weight|colou?r|fill|emphasis))*'; const equalProperties = distinguishedPrimaryIssue && new RegExp('^(?:Right now|Today) (?:all|the) (two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) header buttons (?:are|have|share) the same (' + properties + ')\\.', 'i').exec(assessment); const countedHeader = equalProperties && /\b(?:weight|colou?r|fill|emphasis)\b/i.test(equalProperties[2]!) && equalProperties || distinguishedPrimaryIssue && /^(?:Right now|Today) (?:all|the) (two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) header buttons look (?:the same|identical)\./i.exec(assessment) || distinguishedPrimaryIssue && /^The header shows (two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) buttons that look exactly alike\./i.exec(assessment) || (distinguishedPrimaryIssue || declaredPrimaryIssue) && /^(two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) buttons (?:sit in a row and |in a row )all look the same[,.]/i.exec(assessment) || declaredPrimaryIssue && /^(two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) header buttons currently (?:share one style|look identical)\./i.exec(assessment); const primaryAssessment = distinguishedPrimaryIssue || declaredPrimaryIssue ? countedHeader?.[0] : headerActionIssue ? headerPeers?.[0] : primary && new RegExp(`^(?:Right now|Today) ${primary[1]}(?:, [A-Za-z][A-Za-z0-9 _-]{0,39})+(?:,? and [A-Za-z][A-Za-z0-9 _-]{0,39})? (?:(?:all )?look (?:the same|identical)|are (?:all )?(?:(?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) )?identical buttons)\\b`, 'i').exec(assessment)?.[0]; const premiseSentence = assessment.split(/[.!?](?:\s|$)/)[0] ?? ''; const currentPremise = !/\b(?:archived|historical|hypothetical|quoted|example|previous|earlier)\b/i.test(premiseSentence) && !/\bPLAN\.md (?:onboarding|post-review TODO|engineering review)\b/i.test(prefix.join(' ')); const currentPrimary = !!primaryAssessment && !/\b(?:not|never|no longer)\b/i.test(primaryAssessment) && currentPremise; // The current assessment can state the full token contract while an offered // amendment names the existing component variants that implement it. const numberValue = (value: string) => /^\d+$/.test(value) ? Number(value) : ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'].indexOf(value.toLowerCase()); const controlNames = (text: string) => text.toLowerCase().split(/\s*\/\s*|,\s*(?:and\s+)?|\s+and\s+/).map(s => s.trim()).sort(); const validControls = (controls: string[]) => controls.length > 0 && controls.every(control => /^[a-z][a-z0-9 _-]{0,39}$/i.test(control)) && new Set(controls).size === controls.length; const headerControls = distinguishedPrimaryIssue ? controlNames(distinguishedPrimaryIssue[3]!) : headerPeers ? controlNames(headerPeers[1]!) : []; const namedPremiseMatch = /^(?:(?:Right now|Today) )?([A-Za-z][A-Za-z0-9 ,/_-]{0,159}?) (?:(?:all )?look (?:the same|identical)|are (?:all )?((?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) )?identical buttons)\b/i.exec(assessment); const namedPremise = primary && namedPremiseMatch && new RegExp(`\\b${primary[1]}\\b`, 'i').test(namedPremiseMatch[1]!) ? namedPremiseMatch : null; const premiseActors = namedPremise ? controlNames(namedPremise[1]!) : []; const namedCurrentGap = primary && namedPremise && validControls(premiseActors) && premiseActors.includes(primary[1]!.toLowerCase()) && premiseActors.length > 1 && currentPremise && !/\b(?:not|never|no longer)\b/i.test(namedPremise[0]) && (!namedPremise[2] || numberValue(namedPremise[2].trim()) === premiseActors.length); const premiseCount = countedHeader ? numberValue(countedHeader[1]!) : namedCurrentGap ? premiseActors.length : 0; const otherControls = headerActionIssue || distinguishedPrimaryIssue ? headerControls.length : primary && primaryAssessment ? primaryAssessment.replace(new RegExp(`^(?:Right now|Today) ${primary[1]},\\s*`, 'i'), '') .replace(/\s+(?:(?:all )?look (?:the same|identical)|are (?:all )?(?:(?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) )?identical buttons)$/, '') .split(/,\s*(?:and\s+)?|\s+and\s+/).length : 0; const variantContract = primary && new RegExp(`(?:^|[.!?]\\s+)DESIGN\\.md already says ${primary[1]} is the only filled button ` + '\\(#[0-9a-f]{6} with (?:white|black) text(?:, about [0-9]+(?:\\.[0-9]+)?:1 contrast)?\\) and the other ' + '(two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) are neutral ghost buttons\\.', 'i').exec(assessment); const statusBoundary = scopedPrimaryStatus ? '[.!?;]' : '[.!?]'; const invalidContract = new RegExp(`(?:^|${statusBoundary}\\s+|\\n)(?:[✅❌]\\s*)?(?:Correction:\\s*)?(?:this|that|the) (?:(?:DESIGN\\.md|token) )?(?:requirement|contract) (?:is|was|has been) (?:withdrawn|superseded|rejected|cancelled|canceled|not current|no longer current)\\b`, 'i'); const namedContract = primary && new RegExp(`(?:^|[.!?]\\s+)DESIGN\\.md already says ${primary[1]} is the only filled primary button and the other (two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) are neutral ghost buttons\\.`, 'i').exec(assessment); const headerContract = primary && headerActionIssue && new RegExp(`(?:^|[.!?]\\s+)DESIGN\\.md already answers it: ${primary[1]} is the only filled primary button, the other (two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) are neutral ghost buttons\\.`, 'i').exec(assessment); const conditionalHeader = (text: string) => /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:If|When|Unless|Assuming|Provided)\b/i.test(text) || /\b(?:only if|unless|pending approval|subject to approval)\b/i.test(text); // Approval conditions suspend this offered decision; explanatory conditions // about user behavior do not make an otherwise current amendment optional. const pendingPrimaryApproval = (text: string) => primaryEmphasisIssue && ( /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:If|When|Once|Provided|Assuming|Pending)\s+(?:approval|approved|acceptance|accepted|(?:we|you)\s+(?:approve|accept))\b/i.test(text) || (declaredPrimaryIssue && new RegExp(`(?:^|[.!?;]\\s+|\\n)(?:Correction:\\s*)?(?:This (?:issue|finding|amendment|deferral|option)|Issue ${issue[1]}${declaredGap ? `|G${declaredGap}` : ''}) (?:requires approval|applies only if approved)\\b`, 'i').test(text))); const currentHeaderContract = declaredPrimaryIssue || distinguishedPrimaryIssue ? (countedHeader || namedCurrentGap) && !conditionalHeader(questionText) : !headerActionIssue || (headerContract && headerControls.length > 0 && validControls(headerControls) && !headerControls.includes(primary![1]!.toLowerCase()) && numberValue(headerContract[1]!) === headerControls.length && !conditionalHeader(questionText)); const statedVariant = (variantContract || namedContract) && numberValue((variantContract || namedContract)![1]!) === otherControls && !/\b(?:proposed|hypothetical|quoted|historical|source)\s+(?:example|contract|requirement)\b/i.test(assessment.slice(0, (variantContract || namedContract)!.index)) && !invalidContract.test(questionText); const withdrawn = new RegExp(`(?:^|${statusBoundary}\\s+|\\n)(?:[✅❌]\\s*)?(?:Correction:\\s*)?(?:(?:(?:This|That|The) (?:issue|finding|question|amendment|deferral|style|fix|remedy|choice|option)|Issue ${issue[1]}${declaredGap ? `|G${declaredGap}` : ''}) (?:is|was|has been) (?:withdrawn|superseded|resolved|closed|historical|hypothetical|rejected|cancelled|canceled|not current|no longer current)|We have (?:resolved|closed|withdrawn) this (?:issue|finding)|No current (?:issue|finding|gap|violation) (?:remains|exists))\\b`, 'i'); const closedGap = /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:this|the|that) (?:(?:design )?debt|gap|violation) (?:is|was|has been) (?:already\s+|now\s+)?(?:resolved|fixed|closed)\b/i; const cancelledStyle = /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw)\s+(?:apply|use|add|keep)\s+(?:(?:these|the|this)\s+)?(?:tokens?|styles?|primary treatment)\b/i; const withdrawnStyles = /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:these|the|this) (?:tokens?|styles?|primary treatment) (?:are|is|were|was|have been|has been) (?:withdrawn|rejected|cancelled|canceled|not current|no longer current)\b/i; const currentOptionEvidence = (text: string) => !pendingPrimaryApproval(text) && !conditionalHeader(text) && !sourceAssessment.test(text) && !withdrawn.test(text) && !closedGap.test(text) && !invalidContract.test(text) && !cancelledStyle.test(text) && !withdrawnStyles.test(text); const contradictsDesign = (text: string) => /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:these|the|this) (?:tokens?|styles?|variants?) (?:(?:do|does) not match DESIGN\.md|(?:are|is|were|was) (?:not approved|unapproved))\b/i.test(text); const consistentRemedyEvidence = (text: string, owner: string, peers: string[]) => currentOptionEvidence(text) && !contradictsDesign(text) && !new RegExp(`\\b${owner}(?:\\s*[:=]\\s*|\\s+)(?:(?:is|as|becomes) )?(?:the |a )?(?:neutral )?(?:ghost|outlined|secondary)\\b`, 'i').test(text) && !peers.some(peer => new RegExp(`\\b(?:primary(?: button| action)? ${peer}(?=$|[\\s,.;])|${peer}(?:\\s*[:=]\\s*|\\s+)(?:(?:is|as|becomes) )?(?:the |a )?(?:filled(?: primary)?|primary|outlined))\\b`, 'i').test(text)); const choiceIds = q.options.map(o => /^([1-9]\d*)[A-Z](?:\s*[—–).:]\s*|\s+)/.exec(o.label)); const primaryRepair = primaryHeader && amendments && (declaredPrimaryIssue || distinguishedPrimaryIssue ? currentPrimary || namedCurrentGap : currentPrimary) && currentHeaderContract && prefix.filter(line => /^Project\/branch\/task:/.test(line)).length === 1 && !!call.answeredAt && Number.isFinite(Date.parse(call.answeredAt)) && choiceIds.every(id => id?.[1] === issue[1]) && !pendingPrimaryApproval(questionText) && !conditionalHeader(titleSubject) && !sourceAssessment.test(titleSubject) && currentText(titleSubject) === titleSubject && !sourceAssessment.test(questionText) && !withdrawn.test(questionText) && !closedGap.test(questionText) && !withdrawnStyles.test(questionText) && !invalidContract.test(questionText) && q.options.some(amendment => { const body = currentText(amendment.description ?? ''); if (pendingPrimaryApproval(body)) return false; // Roles and their concrete tokens belong to one native option; a familiar // label alone cannot supply the style or borrow DESIGN.md from a peer. const roleLabel = currentText(amendment.label); if (!currentOptionEvidence(roleLabel)) return false; const propertyStyle = primary && distinguishedPrimaryIssue && new RegExp(`^(?:✅\\s*)?${primary[1]} (?:is|becomes) the (?:only|single) filled(?: primary)? #[0-9a-f]{6}(?: button)? with (?:white|black) text; ([A-Za-z][A-Za-z0-9 ,/_-]{0,119}) (?:are|become) neutral ghost(?: buttons)?\\.`, 'i').exec(body); const optionAuthority = /(?:^|[.;]\s+)(?:✅\s*)?(?:Matches DESIGN\.md exactly|Per DESIGN\.md)(?=[:.;,]|$)/i.test(body); const roleAuthority = new RegExp(`^${issue[1]}[A-Z][).:]?\\s+(?:(?:Apply|Use|Reuse) )?DESIGN\\.md\\b`, 'i').test(roleLabel) || optionAuthority; const roleStyle = propertyStyle && roleAuthority && !conditionalHeader(roleLabel) && !/\b(?:not|never|no|if|historical|hypothetical|source|quoted|withdrawn|superseded|cancelled|canceled)\b/i.test(roleLabel) && !headerControls.some(peer => new RegExp(`\\b(?:primary(?: button| action)? ${peer}|${peer} (?:as )?(?:the )?(?:filled )?primary)\\b`, 'i').test(roleLabel)) ? propertyStyle : null; const declaredStyle = primary && declaredPrimaryIssue && new RegExp(`^(?:✅\\s*)?${primary[1]} filled #[0-9a-f]{6}(?: with)? (?:white|black)(?: text)?; ([A-Za-z][A-Za-z0-9 ,/_-]{0,119}) neutral ghost(?: buttons)?\\.`, 'i').exec(body); const headerStyle = primary && headerActionIssue && new RegExp(`^(?:✅\\s*)?${primary[1]} becomes the only filled #[0-9a-f]{6} button with (?:white|black) text; ([A-Za-z][A-Za-z0-9 ,_-]{0,119}) become neutral ghost buttons, exactly as DESIGN\\.md states\\.`, 'i').exec(body); // A descriptive header still owns a concrete primary and every peer. // Extract the primary token clause and peer clause independently of their // separator. Their DESIGN.md authority must be in this same native option. const primaryClause = primary && (distinguishedPrimaryIssue || declaredPrimaryIssue) && new RegExp(`^(?:✅\\s*)?(?:Matches DESIGN\\.md exactly: )?${primary[1]}(?:\\s*[:=]\\s*|\\s+)` + '(?:(?:is|becomes) (?:the (?:only|single) )?)?(filled(?: primary)?(?: button)? )?' + '(?:\\(#[0-9a-f]{6}, (?:white|black) text\\)|#[0-9a-f]{6}(?: button)?(?: with)? (?:white|black)(?: text)?)' + '(?:,\\s*[1-9]\\d*(?:\\.\\d+)?px)?[.;,]\\s+', 'i').exec(body); const peerClause = primaryClause && /^([A-Za-z][A-Za-z0-9 ,/_-]{0,119}?)(?:\s*[:=]\s*|\s+)(?:(?:are|become|as) )?neutral ghost(?: buttons)?[.;,]/i.exec(body.slice(primaryClause[0].length)); const semanticLabel = choiceLabel(roleLabel); const labelledRole = primary && (new RegExp(`^${primary[1]} filled primary(?:,|$)`, 'i').test(semanticLabel) || new RegExp(`^Filled (?:primary (?:(?:\\+|and|with) ghosts|${primary[1]})|${primary[1]}, ghost others)$`, 'i').test(semanticLabel)); const designAction = /^(?:Apply|Use|Reuse) DESIGN\.md (?:tokens?|styles)$/i.test(semanticLabel); const namedDesignRole = /^(?:(?:Apply|Use|Reuse) )?DESIGN\.md (?:primary|tokens?|styles?)\b/i.test(semanticLabel); const approvedTokens = /(?:^|[.;]\s+)(?:✅\s*)?(?:Uses?|Applies?|Reuses?|Matches?) (?:the )?(?:exact )?approved (?:tokens?|styles?)(?=[.;]|$)/i.test(body); const clauseAuthority = optionAuthority || designAction || (namedDesignRole && approvedTokens) || (primaryClause && peerClause && /^exactly per DESIGN\.md(?:[.;]|\n|$)/i.test(body.slice(primaryClause[0].length + peerClause[0].length).trimStart())); const clauseRole = !!labelledRole || !!primaryClause?.[1]; const styleLabel = labelledRole || designAction || roleStyle || namedDesignRole || (/\b(?:filled|primary)\b/i.test(semanticLabel) && !/\b(?:review|reviewer|prepare|start|next|setup|source|example)\b/i.test(semanticLabel)); const clauseStyle = primaryClause && peerClause && clauseAuthority && clauseRole && styleLabel ? [primaryClause[0] + peerClause[0], peerClause[1]!] : null; const distinguishedStyle = primary && distinguishedPrimaryIssue && ( clauseStyle ?? new RegExp(`^(?:✅\\s*)?${primary[1]} is #[0-9a-f]{6} with (?:white|black) text; ([A-Za-z][A-Za-z0-9 ,_-]{0,119}) are neutral ghost buttons per DESIGN\\.md\\.`, 'i').exec(body) ?? new RegExp(`^(?:✅\\s*)?${primary[1]} becomes the only filled button \\(#[0-9a-f]{6}, (?:white|black) text\\); ([A-Za-z][A-Za-z0-9 ,/_-]{0,119}) use the existing neutral ghost variant` + '(?: \\(human: ~?[0-9]+(?:\\.[0-9]+)?(?:h|min) / CC: ~?[0-9]+(?:\\.[0-9]+)?(?:h|min)\\))?\\. (?:✅\\s*)?Matches DESIGN\\.md exactly\\b', 'i').exec(body) ?? roleStyle); const findingStyle = clauseStyle ?? (declaredPrimaryIssue && designAction ? declaredStyle : null) ?? distinguishedStyle; const attributedStyle = namedTokenStyle?.exec(body); let namedTokenValid = false; if (namedTokenIssue) { const peers = primaryAssessment?.replace(new RegExp(`^(?:Right now|Today) ${primary![1]},\\s*`, 'i'), '') .replace(/\s+(?:(?:all )?look (?:the same|identical)|are (?:all )?(?:(?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) )?identical buttons)$/, ''); const sources = (prefix.join(' ') + ' ' + assessment).match(/[\w./-]+\.md\b/g) ?? []; const sourceRoles = [...assessment.matchAll(new RegExp(`(?:^|[.!?]\\s+)DESIGN\\.md (?:already )?(?:says|states|specifies|requires|defines|names the treatment):? ${primary![1]} is the (?:only|single) filled button ` + '\\((#[0-9a-f]{6})(?:,| with) (white|black) text(?:, (?:~|about )?[0-9]+(?:\\.[0-9]+)?:1 contrast)?\\)(?:,| and) (?:the )?other ' + '(two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) are neutral ghost(?:s| buttons)\\.', 'gi'))]; const assertedRoles = sourceRoles.length === 1 ? sourceRoles[0] : undefined; // Additional imperative work cannot borrow this styling decision's ACK. // Explanatory subjects, negated work and quoted history are not commands. const extraAction = /(?:^|[.!?;]\s+|\n|[✅❌]\s*|\b(?:and|but|while)\s+)(?:(?:also|then|now|first|next|please)\s+)*(?:approve|add|build|create|implement|replace|remove|delete|deploy|install|configure|rewrite|migrate|launch|fix|repair|resolve)\b/i; const wrongTokens = !assertedRoles || !attributedStyle || numberValue(assertedRoles[3]!) !== otherControls || attributedStyle[0].match(/#[0-9a-f]{6}/i)?.[0].toLowerCase() !== assertedRoles[1]!.toLowerCase() || attributedStyle[0].match(/\b(?:white|black) text\b/i)?.[0].toLowerCase() !== assertedRoles[2]!.toLowerCase() + ' text' || extraAction.test(questionText) || q.options.some(o => extraAction.test(currentText(o.label + '\n' + (o.description ?? '')))); namedTokenValid = Boolean(!wrongTokens && attributedStyle && peers && q.options.length <= 4 && Object.keys(call.answers ?? {}).length === 1 && (questionText.match(/^Project\/branch\/task:/gm)?.length ?? 0) === 1 && !conditionalHeader(questionText) && !conditionalHeader(body) && sources.includes('DESIGN.md') && sources.every(source => ['PLAN.md', 'DESIGN.md'].includes(source)) && JSON.stringify(controlNames(attributedStyle[1]!.replaceAll('/', ','))) === JSON.stringify(controlNames(peers)) && new Set(controlNames(peers)).size === otherControls && !invalidContract.test(body)); } const style = declaredPrimaryIssue || distinguishedPrimaryIssue ? findingStyle?.[0] : headerActionIssue ? headerStyle?.[0] : (namedTokenValid ? attributedStyle?.[0] : undefined) ?? amendments.map(pattern => pattern.exec(body)).find(Boolean)?.[0]; if ((declaredPrimaryIssue || distinguishedPrimaryIssue) && /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:the|this) (?:current )?(?:amendment|fix) keeps (?:all )?(?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) (?:header )?buttons identical\b/i.test(body)) return false; if ((declaredPrimaryIssue || distinguishedPrimaryIssue) && (!findingStyle || conditionalHeader(body) || invalidContract.test(body) || !(roleStyle || labelledRole || designAction || clauseStyle))) return false; if (headerActionIssue && (!headerStyle || conditionalHeader(body) || invalidContract.test(body) || JSON.stringify(controlNames(headerStyle[1]!)) !== JSON.stringify(headerControls))) return false; const variantLine = /^✅\s*Uses the existing Button primary and ghost variants from DESIGN\.md; no new styles\./m.exec(body); const benefits = variantLine ? body.slice(0, variantLine.index).trim().split('\n').filter(Boolean) : []; const labelledRoles = /^✅ Matches DESIGN\.md exactly: one filled primary, (two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) neutral ghosts, [1-9]\d*px targets kept\./.exec(body); const labelledStyle = statedVariant && namedContract && labelledRoles && numberValue(labelledRoles[1]!) === otherControls && new RegExp(`^[1-9]\\d*[A-Z]\\) ${primary![1]} filled #[0-9a-f]{6}/(?:white|black), others ghost(?: \\(recommended\\))?$`, 'i').test(amendment.label); const variantRepair = !headerActionIssue && (labelledStyle || (statedVariant && variantContract && variantLine && new RegExp(`^[1-9]\\d*[A-Z] Filled ${primary![1]}, ghost others(?: \\(recommended\\))?$`, 'i').test(amendment.label) && benefits.every(line => /^✅\s*(?!(?:If|When|Unless|Historical|Hypothetical|Quoted|Source|Example)\b)\S/i.test(line)) && !/\b(?:archived|historical|hypothetical|quoted|previous|earlier)\b/i.test(benefits.join(' ')))) && !/(?:^|[.!?]\s+|\n)(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw) (?:apply|use|add|keep) (?:the |these )?(?:Button )?primary and ghost variants\b/i.test(body) && !contradictsDesign(body) && !/(?:^|[.!?]\s+|\n)(?:Correction:\s*)?(?:the|this) (?:current )?amendment keeps all (?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) buttons identical\b/i.test(body); // A named primary cannot simultaneously occur in the ghost-control list. const secondaryStyle = findingStyle?.[1] ?? style?.slice(style.indexOf(';') + 1); if ((!style && !variantRepair) || (secondaryStyle && new RegExp(`\\b${primary![1]}\\b`, 'i').test(secondaryStyle)) || sourceAssessment.test(body) || withdrawn.test(body) || closedGap.test(body) || cancelledStyle.test(body) || withdrawnStyles.test(body) || contradictsDesign(body)) return false; return opposed.some(defer => { const declined = currentText(defer.description ?? ''); const declinedLabel = currentText(defer.label); if (!currentOptionEvidence(declinedLabel)) return false; if (pendingPrimaryApproval(declined) || (namedTokenIssue && (conditionalHeader(declined) || /(?:^|[.!?;]\s+|\n)(?:this|the) (?:option|deferral) (?:(?:now|already|actually) )?(?:fixes|resolves|closes) (?:the |this )?(?:hierarchy |primary-action )?gap\b/i.test(declined)))) return false; const declaredDeferral = declaredPrimaryIssue && /^Defer$/i.test(choiceLabel(declinedLabel)) && new RegExp(`^Leave ${declaredGap ? `G${declaredGap}` : `Issue ${issue[1]}`} open and record it as unresolved\\.`, 'i').test(declined) && !new RegExp(`(?:^|[.!?;]\\s+|\\n)(?:Correction:\\s*)?(?:do not|don't|never|skip|cancel|withdraw) (?:leave|keep|defer) (?:${declaredGap ? `G${declaredGap}|` : ''}Issue ${issue[1]})\\b`, 'i').test(declined) && !conditionalHeader(declined) && !sourceAssessment.test(declined) && !withdrawn.test(declined) && !closedGap.test(declined) && !invalidContract.test(declined) && !cancelledStyle.test(declined) && !withdrawnStyles.test(declined); // Native menus can list current benefits before the gap retained by // declining. Only consume a complete affirmative pro/con prefix; prose // framing a source example or a future condition cannot expose an icon. const headerDeferral = /^(?:Leave|Keep) the header unchanged and record the gap as (?:debt|an open issue)\.\s*/i.exec(declined); const deferralBody = headerDeferral ? declined.slice(headerDeferral[0].length) : declined; const cancelledHeaderDeferral = headerDeferral && /(?:^|[.!?;]\s+|\n)(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw) (?:keep|leave) (?:the )?header unchanged\b/i.test(declined); const pros = /^(?:✅(?!\s*(?:If|When|Unless|Historical|Hypothetical|Quoted|Source|Example)\b)\s*[^✅❌]+)+❌\s*/i.exec(deferralBody); const remaining = pros && !sourceAssessment.test(pros[0]) ? deferralBody.slice(pros[0].length) : deferralBody; const retainedEmphasis = distinguishedPrimaryIssue && ( new RegExp(`^(?:Keep|Leave) identical (?:header )?buttons, bold (?:the )?${primary![1]} (?:text|label)\\. (?:Weak(?: visual)? signal, )?off-token\\.`, 'i').test(remaining) || new RegExp(`^Weight alone is a weak signal at a glance and violates DESIGN\\.md, which names ${primary![1]} the only filled action\\. (?:❌\\s*)?Leaves the primary action undiscoverable for scanning users\\.`, 'i').test(remaining)); const retainedHierarchyGap = distinguishedPrimaryIssue && /^(?:❌\s*)?Ships a (?:known|documented) DESIGN\.md violation and the plan['’]s own Visual Hierarchy gap (?:stays|remains) open\./i.test(remaining); const retainedRoleGap = roleStyle && /^(?:No change[.;]\s*)?(?:the |this )?(?:finding|issue|gap) (?:stays|remains) (?:open|unresolved)\b/i.test(remaining); const retainedControls = /^(?:Leave|Keep) (?:the |all )?(two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) (?:header )?buttons (?:uniform|identical|equal)(?: for now)?(?:[.;]\s+| and )/i.exec(declined); const unresolvedDebt = retainedControls && numberValue(retainedControls[1]!) === headerControls.length + 1 && /^record (?:it|(?:the|this) gap) as (?:unresolved|open) design debt\./i.test(declined.slice(retainedControls[0].length)); const cancelledRetention = /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw) (?:keep|leave) (?:the |all )?(?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) (?:header )?buttons (?:uniform|identical|equal)\b/i.test(declined); const cancelledDebt = /(?:^|[.!?;]\s+|\n)(?:[✅❌]\s*)?(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw) (?:record|log|track) (?:it|this|(?:the|this) gap) as (?:unresolved|open) design debt\b/i.test(declined); const retainedLabel = /^(?:Keep|Leave) (?:all |the )?(two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) (?:(?:identical|equal|uniform) (?:header )?buttons|(?:header )?buttons (?:identical|equal|uniform))\b/i.exec(choiceLabel(declinedLabel)); const retainedPrimaryGap = retainedLabel && /^Violates DESIGN\.md\b/i.test(remaining) && /\bleaves the primary action (?:indistinguishable|undiscoverable)\b/i.test(remaining); const retainedNoPrimary = retainedLabel && /^Ships the documented violation; no primary action;/i.test(remaining); // The opposed option's label and body share ownership too. A retained // actor count can precede ordinary tradeoffs before the explicit gap. const retainedViolation = retainedLabel && declined.split(/[.!?;]\s+/).some(clause => /^(?:❌\s*)?(?:Leaves?|Ships?|Keeps?|Retains?) (?:a )?(?:known |documented )?DESIGN\.md violation\b/i.test(clause) && /\bno primary action\b/i.test(clause)); if (declaredPrimaryIssue || distinguishedPrimaryIssue) return defer !== amendment && validPrimaryFinding({ primary: primary![1]!, currentGap: !!(currentPrimary || namedCurrentGap) && !!currentHeaderContract && (!namedPremise || !!namedCurrentGap) && (!namedCurrentGap || !countedHeader || premiseActors.length === premiseCount) && (!namedCurrentGap || !distinguishedPrimaryIssue || JSON.stringify(premiseActors.filter(actor => actor !== primary![1]!.toLowerCase())) === JSON.stringify(headerControls)), controlCount: premiseCount, namedPeers: distinguishedPrimaryIssue ? headerControls : namedCurrentGap ? premiseActors.filter(actor => actor !== primary![1]!.toLowerCase()) : undefined, remedy: { peers: controlNames(findingStyle![1]!), role: clauseStyle ? clauseRole : !!(roleStyle || labelledRole || (declaredStyle && designAction)), tokens: !!style, authority: clauseStyle ? !!clauseAuthority : !!(distinguishedStyle || (declaredStyle && designAction)), current: [roleLabel, body].every(text => consistentRemedyEvidence(text, primary![1]!, controlNames(findingStyle![1]!))), }, alternative: { unresolved: !!(declaredDeferral || retainedEmphasis || retainedHierarchyGap || retainedRoleGap || unresolvedDebt || retainedPrimaryGap || retainedNoPrimary || retainedViolation), retainedCounts: [retainedLabel?.[1], retainedControls?.[1]].filter((count): count is string => !!count).map(numberValue), current: !cancelledRetention && !cancelledDebt && currentOptionEvidence(declined) && currentOptionEvidence(declinedLabel) && !/(?:^|[.!?;]\s+|\n)(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw) (?:keep|leave) identical (?:header )?buttons\b/i.test(declined) && !closedGap.test(declined) && !invalidContract.test(declined) && !cancelledStyle.test(declined) && !withdrawnStyles.test(declined), }, }); const retainedButtons = /^(?:❌\s*)?Keep all (two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) (?:header )?buttons identical; gap stays documented\./i.exec(remaining); const cancelledRetainedButtons = /(?:^|[.!?;]\s+|\n)(?:Correction:\s*)?(?:do not|don't|never|skip|cancel|withdraw) (?:keep|leave) (?:all )?(two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) (?:header )?buttons identical\b/i.exec(declined); if (headerActionIssue) { const keep = /^[1-9]\d*[A-Z]: Keep all (two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) identical(?: \(recommended\))?$/i.exec(defer.label); return defer !== amendment && keep && numberValue(keep[1]!) === headerControls.length + 1 && !conditionalHeader(declined) && !sourceAssessment.test(declined) && !withdrawn.test(declined) && !invalidContract.test(declined) && !closedGap.test(declined) && !cancelledRetainedButtons && /^(?:❌\s*)?Ships a (?:known|documented) DESIGN\.md violation; the review score stays capped and users keep scanning a flat row\./i.test(remaining); } return defer !== amendment && !sourceAssessment.test(declined) && !withdrawn.test(declined) && !closedGap.test(declined) && !cancelledHeaderDeferral && ((namedTokenValid && /^(?:Leaves|Keeps|Retains) (?:a documented|the(?: documented)?) DESIGN\.md violation (?:in place|unresolved|open)\b/i.test(remaining)) || (retainedButtons && numberValue(retainedButtons[1]!) === otherControls + 1 && (!cancelledRetainedButtons || numberValue(cancelledRetainedButtons[1]!) !== otherControls + 1)) || /^(?:❌\s*)?(?:Leaves a documented DESIGN\.md violation in place|Keeps the documented DESIGN\.md violation and the scan problem|Violates DESIGN\.md and leaves the mis-click on [A-Za-z][A-Za-z /_-]{0,79} unaddressed|Ships a (?:known|documented) DESIGN\.md violation and the primary action (?:stays|remains) undiscoverable|Ships a header with no primary action; PLAN\.md['’]s own gap stays open|Ships the documented violation;[^.\n]*\bthe gap remains open|Primary-action ambiguity ships; documented DESIGN\.md violation remains|Decline the fix; gap stays documented and lowers the score|Decline the fix; document the violation as accepted|Keep all (?:two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) identical; record as an open DESIGN\.md violation)\b/i.test(remaining) || (variantRepair && /^(?:Violates DESIGN\.md and leaves users guessing which action is primary; Pass [1-7] stays at [0-9](?:\.[0-9]+)?\/10|Documented DESIGN\.md violation ships and Pass [1-7] stays at [0-9](?:\.[0-9]+)?\/10)\.$/i.test(remaining))); }); }); return opposed.length > 0 && !!(repair || primaryRepair); } /** A design-system choice can name the gap without using an imperative repair verb. */ function designSystemChoiceIssue(fp: AskUserQuestionFingerprint): boolean { const call = fp.nativeCall; if (!call || call.answered !== true || call.failed !== false || !call.sessionId || !call.toolUseId || call.questions.length !== 1 || !Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length || fp.signature !== `${call.sessionId}:${call.toolUseId}` || (fp.nativeQuestionIndex !== undefined && fp.nativeQuestionIndex !== 0) || !call.answeredAt || !Number.isFinite(Date.parse(call.answeredAt))) return false; const q = call.questions[0]!; const lines = q.question.trim().split('\n'); const namedGap = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*) \(G([1-9]\d*)\): ([^?]+)\?$/.exec(lines[0]!); const findingIssue = /^([1-9]\d*)\s*[—–:-]\s*Finding ([1-9]\d*) \(([A-Za-z][A-Za-z &/-]*)\): ([^?]+)\?$/i.exec(lines[0]!); const fieldIssue = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*)(?: \(Pass ([1-7])(?:, [A-Za-z][A-Za-z &/-]*)?\))?: ([^?]+)\?$/.exec(lines[0]!) ?? (findingIssue ? [findingIssue[0], findingIssue[1], undefined, `${findingIssue[3]}: ${findingIssue[4]}`] : null); const source = /^Project\/branch\/task: (.+)$/m.exec(q.question)?.[1] ?? ''; const sourceGaps = [...source.matchAll(/\bgap G([1-9]\d*)\b/gi)]; const ownGap = sourceGaps[0]?.[1]; const nativeIssue = fieldIssue && (q.header.trim() === `Issue ${fieldIssue[1]}` || findingIssue); // The native Issue/option IDs own the current decision. A pass can be in // its title or source field, and a G label is optional. If a G is present, // another source row cannot lend this question its identity or evidence. const scopedIssue = fieldIssue && (fieldIssue[2] || /\bPass [1-7]\b/.test(source) || nativeIssue) && sourceGaps.length <= 1 && [...q.question.matchAll(/\bG([1-9]\d*)\b/g)].every(m => m[1] === ownGap) ? [fieldIssue[0], fieldIssue[1], ownGap, fieldIssue[3]] : null; const gapIssue = namedGap ?? scopedIssue; if (gapIssue && (() => { const [, issueNumber, gapNumber, subject] = gapIssue; const headerIssue = /\bIssue ([1-9]\d*)\b/i.exec(q.header); if (!q.header.trim() || (headerIssue && headerIssue[1] !== issueNumber) || /^(?:focus|scope|setup|routing|learnings|outside(?: design)? voices|next steps?)\b/i.test(q.header.trim()) || / 4 || fp.options.length !== q.options.length || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) || !q.options.some(o => o.label === call.answers?.[q.question])) return false; // The issue and option IDs bind a decision; its descriptive menu header // and the wording/line count of each decision field do not supply evidence. const ids = q.options.map(o => new RegExp(`^(${issueNumber}[A-Z])(?:[).:]?\\s+)`).exec(o.label)?.[1]); if (ids.some(id => !id) || new Set(ids).size !== ids.length) return false; // Count the acknowledged design decision, not optional summary formatting. const fields = ['Project/branch/task:', 'ELI10:', 'Stakes if we pick wrong:', 'Recommendation:', 'Completeness:']; if (q.question.includes('Net:')) fields.push('Net:'); const positions = fields.map(field => q.question.indexOf(field)); if (positions.some((position, i) => position < 0 || q.question.lastIndexOf(fields[i]!) !== position || (i > 0 && position <= positions[i - 1]!)) || q.question.slice(0, positions[0]).trim() !== lines[0]) return false; const values = fields.map((field, i) => q.question.slice(positions[i]! + field.length, positions[i + 1] ?? q.question.length).trim()); const sourceOnly = /^(?:[>"“`]|Historical|Previously|Hypothetical|Quoted|Source|Archived|Earlier|Example|If|When|Once|Unless|Assuming|Provided|Pending approval)\b|^[>"“`]/i; const inactive = /(?:^|[.!?;]\s+|\n)(?:Correction:\s*)?(?:(?:this|the) (?:finding|gap|issue|amendment|fix|decision)|G[1-9]\d*|Issue [1-9]\d*) (?:is|was|has been) (?:already |now )?["'‘“`]*(?:withdrawn|resolved|closed|superseded|hypothetical|not current|no longer current)\b|\bno current (?:defect|gap|finding|issue)\b/i; const namedOwner = findingIssue ? `Finding ${findingIssue[2]}` : /^D[1-9]\d*/.exec(lines[0]!)?.[0]; const namedStatusPrefix = new RegExp(`(?:^|[.!?;]\\s+|\\n)(?:Correction:\\s*)?${namedOwner ?? '(?!)'} (?:is|was|has been) (?:already |now )?$`, 'i'); const namedInactive = new RegExp(namedStatusPrefix.source.replace(/\$$/, '') + '["\'‘“`]*(?:withdrawn|resolved|closed|superseded|hypothetical|not current|no longer current)\\b', 'i'); const inactiveCurrent = (text: string) => inactive.test(text) || namedInactive.test(text); const current = (value: string) => value.replace(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, '') .replace(/^\s*>.*$/gm, '').replace(/"[^"\n]+"|“[^”\n]+”|`[^`\n]+`|'[^'\n]+'|‘[^’\n]+’/g, (quoted, index, source) => /^(?:withdrawn|resolved|closed|superseded|hypothetical|not current|no longer current)$/i.test(quoted.slice(1, -1)) && (/\b(?:(?:this|the) (?:finding|gap|issue|amendment|fix|decision)|G[1-9]\d*|Issue [1-9]\d*) (?:is|was|has been) (?:already |now )?$/i.test(source.slice(0, index)) || namedStatusPrefix.test(source.slice(0, index))) ? quoted.slice(1, -1) : ''); const sourceText = current(values[0]!.replace(/`PLAN\.md`/g, 'PLAN.md')); // A review can name its current plan instead of repeating PLAN.md. Treat // the title as a title only inside the review's own provenance field; a // pass number corroborates that ownership but cannot supply it by itself. const namedPlan = /(?:^|[,;]\s*)(?:\/plan-design-review of|reviewing|design review of)\s+(?:"([^"\n]+)"|“([^”\n]+)”|`([^`\n]+)`)(?=[,;.\s]|$)/i.exec(values[0]!); const title = namedPlan?.slice(1).find(Boolean); const namedCurrentPlan = !!title && !/\b[\w.-]+\.md\b/i.test(title) && !/\b(?:other|another|different|unrelated|foreign|historical|archived|quoted|copied|example)\b/i.test(title) && /\bPass [1-7]\s*\([A-Za-z][A-Za-z &/-]*\)/.test(sourceText); const ownedSource = /\bPLAN\.md\b/.test(sourceText) || (!/\b[\w.-]+\.md\b/i.test(values[0]!) && (namedCurrentPlan || /\bPass [1-7]\s*\([A-Za-z][A-Za-z &/-]*\) of the [A-Za-z][A-Za-z -]* plan\.$/.test(sourceText))); if (values.some(value => !value || sourceOnly.test(value)) || inactiveCurrent(current(q.question)) || !ownedSource || /\b(?:other|another|different|unrelated|foreign|historical|archived|quoted|copied) (?:[A-Za-z-]+ )?(?:plan|review|source)\b/i.test(sourceText) || /\b(?:planning|review|workflow) setup\b|\b(?:setup|onboarding|routing|posture|learnings) (?:stage|phase|step|decision)\b/i.test(current(values[0]!)) || (scopedIssue && !ownedSource) || !ids.some(id => values[3]!.startsWith(`${id} `))) return false; const assessment = current(values[1]!); if (/\b(?:historical|archived|hypothetical|quoted)\b|\b(?:not|isn't) (?:the )?current\b/i.test(assessment) || (!nativeIssue && !/\b(?:now|today|currently|proposed)\b/i.test(assessment))) return false; // The complete comparison may live in the current native brief while // the rendered menu uses short captions. Keep each detail block bound // to its own native option ID; never pool evidence across alternatives. const detailedOptions = new Map(); const details = values[4]!.split(/\n(?:Pros\s*\/\s*cons|Options):\s*\n/i); if (nativeIssue && details.length === 2) { const body = details[1]!; const starts = [...body.matchAll(/^([1-9]\d*[A-Z])[).:]\s+\S/gm)]; if (starts.length === ids.length && starts.every(start => ids.includes(start[1])) && new Set(starts.map(start => start[1])).size === ids.length && !body.split('\n').some(line => sourceOnly.test(line.trim()))) { for (const [index, start] of starts.entries()) { detailedOptions.set(start[1]!, body.slice(start.index, starts[index + 1]?.index ?? body.length)); } } } // Recognize the fixture's design-defect classes, not a G-number or a // prescribed sentence: ambiguous hierarchy, absent pending feedback, // inconsistent type/spacing, or unreadable error contrast. Every class // still needs a concrete native remedy and its own opposed open gap. const classes: Array<{ subject: RegExp; defect: RegExp; remedy: RegExp }> = [ { subject: /\b(?:distinguished|primary|header|hierarchy)\b/i, defect: /\b(?:look (?:the )?(?:same|identical)|share (?:the )?same visual weight|visually identical)\b/i, remedy: /\bfilled\b[^;\n]*#[0-9a-f]{6}[^;\n]*(?:white|black)\b[^\n]*\bghost\b/i }, { subject: /\b(?:pending|request|loading)\b/i, defect: /\b(?:page|request|button)\b[^.!?]*(?:just sits|freezes|no (?:visible )?(?:feedback|signal|indicator))|\b(?:shows?|gives?) no (?:pending |visible )?(?:feedback|signal|indicator)\b|\bnothing changes\b/i, remedy: /\b(?:inline )?spinner\b[^\n]*\baria-busy\s*=\s*true\b[^\n]*\breduced.motion\b/i }, { subject: /\b(?:type|typography|labels|headings)\b/i, defect: /\b(?:form|labels?|type)\b[^.!?]*(?:no (?:consistent )?(?:rule|role)|inconsisten\w*|accidental|(?:three|[3-9]\d*) sizes)/i, remedy: /\b\d+px\b[^\n]*\blabels?\b[^\n]*\b\d+px\b[^\n]*\b(?:headings?|h[1-6])\b/i }, { subject: /\b(?:spacing|rhythm|gaps)\b/i, defect: /\b(?:form|gaps?|spacing)\b[^.!?]*(?:no rule|without a spacing rule|random|inconsisten\w*)|\b(?:uneven|mixed|inconsistent|random) (?:spacing|gaps)\b/i, remedy: /\bsections?\s+\d+px\b[^\n]*\bfield groups?\s+\d+px\b[^\n]*\blabel(?:\W*to\W*|\W+)(?:input|control)\s+\d+px\b/i }, { subject: /\b(?:errors?|contrast|colou?rs?)\b/i, defect: /\b(?:error|text|contrast)\b[^.!?]*(?:fails? WCAG|below (?:WCAG|AA)|cannot read|can't read)/i, remedy: /#[0-9a-f]{6}\b[^\n]*#[0-9a-f]{6}\b[^\n]*\b(?:icon|text)\b/i }, ]; // A numbered current Issue may state its gap in the title, then explain // its impact in ELI10. Source/status/field ownership still apply to both. const assertedGap = nativeIssue ? `${current(subject!)}\n${assessment}` : assessment; const lowContrast = nativeIssue && /\b(?:error|contrast|message|text)\b/i.test(subject!) && [...assertedGap.matchAll(/(?:\bat\b|\babout\b|\bapproximately\b|~)\s*([0-9]+(?:\.[0-9]+)?)\s*:\s*1\b/gi)] .some(match => Number(match[1]) < 4.5) && /\b(?:WCAG|AA)\b/.test(assessment); const kind = classes.find((kind, index) => kind.subject.test(subject!) && (kind.defect.test(assertedGap) || index === 4 && lowContrast)); if (!kind || /\b(?:do not|don't|does not|doesn't) look identical\b/i.test(assessment)) return false; return q.options.some((option, index) => { const body = option.description?.trim() ?? ''; const detail = detailedOptions.get(ids[index]!) ?? ''; const remedy = nativeIssue ? current(`${option.label}\n${body}\n${detail}`) : current(body); if (sourceOnly.test(body) || inactiveCurrent(remedy) || !kind.remedy.test(remedy) || !values[3]!.startsWith(`${ids[index]} `)) return false; return q.options.some((other, otherIndex) => { const declined = `${other.description?.trim() ?? ''}\n${detailedOptions.get(ids[otherIndex]!) ?? ''}`.trim(); const opposed = current(declined); const ownedOpposition = !/\b(?:other|another|different|unrelated|foreign) (?:gap|issue|finding|decision)\b/i.test(opposed) && [...opposed.matchAll(/\bIssue ([1-9]\d*)\b/gi)].every(match => match[1] === issueNumber); // A retained violation must be an affirmative current consequence, // not words inside a prohibition or a consequence awaiting approval. // Read the whole option so a later correction can withdraw the claim. const retainedViolation = /(?:^|[.!?;]\s+|\n|[✅❌]\s*)(?:Leaves|Keeps) (?:the |this )?(?:plan|design|page|header) violating DESIGN\.md\b/i.test(opposed) && !/\b(?:not|never|no longer|cannot|can't|don't|doesn't|didn't|won't)\b[^.!?;\n]*\b(?:leaves?|keeps?) (?:the |this )?(?:plan|design|page|header) violating DESIGN\.md\b/i.test(opposed) && !/\b(?:if|when|once|unless|assuming|provided|pending|contingent|conditional)\b|\b(?:before|after|requires?|needs?|subject to|depends? on)\s+(?:(?:user|later|further|your|owner|explicit)\s+)?(?:approval|acceptance)\b/i.test(opposed); return ownedOpposition && other !== option && /^(?:Keep|Leave|Defer|Decline|No)\b/i.test(other.label.replace(new RegExp(`^${ids[otherIndex]}[).:]?\\s+`), '')) && !sourceOnly.test(declined) && !inactiveCurrent(current(declined)) && !/\b(?:(?:does?|did) not|no longer|never) violates? DESIGN\.md\b/i.test(opposed) && (new RegExp(`\\b(?:gap\\s+)?G${gapNumber}\\s+(?:stays|remains|is)\\s+(?:open|unresolved)\\b`, 'i').test(current(declined)) || (!!scopedIssue && (/\b(?:the |[a-z-]+ )?gap (?:stays|remains|is) (?:open|unresolved)\b/i.test(current(declined)) || (!!nativeIssue && /\b(?:known|documented) (?:WCAG )?AA failure ships\b/i.test(current(declined)) && /\bstays open\b/i.test(current(declined))) || (!!nativeIssue && /\b(?:stays|remains) (?:open|unresolved)\b/i.test(current(declined)) && !/\b(?:other|another|different|unrelated) (?:gap|issue|finding|decision)\b/i.test(current(declined)) && [...current(declined).matchAll(/\bIssue ([1-9]\d*)\b/gi)].every(match => match[1] === issueNumber)) || (!!nativeIssue && /^Record as unresolved[.;]/i.test(current(declined))) || // A kept violation may name the relevant contract, rather than // use the exact phrase "gap remains open". It still belongs to // this issue and the same design-defect class as the remedy. (!!nativeIssue && /\bviolates DESIGN\.md(?:'s)?\b/i.test(opposed) && kind.subject.test(opposed) && !/\b(?:(?:does?|did) not|no longer|never) violates?\b|\b(?:historical|previous|earlier|example|quoted|hypothetical)\b/i.test(opposed)) || (!!nativeIssue && /\bviolates DESIGN\.md(?:'s)? (?:stated |existing |documented )?(?:primary treatment|two-role rule|spacing scale|contrast requirement)\b/i.test(current(declined))) || /\b(?:plan|design|page|header)\b[^.!?]*\b(?:keeps|retains|leaves|ships)\b[^.!?]*\bDESIGN\.md violation\b/i.test(current(declined)) || retainedViolation) && [...q.options.flatMap(o => [...`${o.label} ${o.description ?? ''}`.matchAll(/\bG([1-9]\d*)\b/g)])] .every(m => m[1] === gapNumber))); }); }); })()) return true; const issue = /^(?:D[1-9]\d*\s*[—–:-]\s*)?Issue ([1-9]\d*): (.+)\?$/.exec(lines[0]!); if (!issue || q.header.trim() !== `Issue ${issue[1]}` || lines.length !== 7 || !/^Project\/branch\/task: [^\n,]+ on [^\n,]+, PLAN\.md design review, Pass [1-7] [A-Za-z][A-Za-z &()-]+\.$/.test(lines[1]!) || !/^ELI10: \S/.test(lines[2]!) || !/\bDESIGN\.md\b/.test(lines[2]!) || !/^Stakes if we pick wrong: \S/.test(lines[3]!) || !/^Recommendation: \S/.test(lines[4]!) || !/^Completeness: \S/.test(lines[5]!) || !/^Net: \S/.test(lines[6]!) || / 4 || new Set(q.options.map(o => o.label)).size !== q.options.length || !q.options.every(o => new RegExp(`^${issue[1]}[A-Z]: \\S`).test(o.label)) || fp.options.length !== q.options.length || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) || !q.options.some(o => o.label === call.answers?.[q.question])) return false; // These are current visual/interaction choices, not reviewer participation or next-step routing. const subjects = [ /^How should [A-Z][A-Za-z0-9 _/-]{0,79} be distinguished from [A-Z][A-Za-z0-9 ,/_-]{0,119}$/, /^What does the user see while [A-Z][A-Za-z0-9 _/-]{0,79} is pending for [1-9]\d*(?:[-–][1-9]\d*)? seconds$/, /^What type scale should (?:form )?labels(?: and section headings)? use$/, /^What vertical spacing rhythm should the form use$/, /^How should (?:the )?error message meet WCAG AA contrast$/, ]; const subject = subjects.findIndex(pattern => pattern.test(issue[2]!)); if (subject < 0) return false; const assessments = [/^ELI10: The header shows\b/, /^ELI10: After clicking\b/, /^ELI10: Labels on the form are set\b/, /^ELI10: Gaps between sections are\b/, /^ELI10: The error message is\b/]; if (!assessments[subject]!.test(lines[2]!) || /(?:^|[.!?]\s+)(?:This (?:issue|finding) (?:is|has been) (?:withdrawn|resolved|closed)|We have (?:resolved|closed|withdrawn) this (?:issue|finding)|No current (?:issue|finding|gap|defect|violation) (?:remains|exists))\b/i.test(lines[2]!.slice(7))) return false; const control = /^How should (.+) be distinguished from /.exec(issue[2]!)?.[1]; const concrete = [new RegExp(`^${control}\\b[^\\n]*\\b(?:filled|ghost|outlined|primary)\\b`, 'i'), /^(?:Spinner|InlineStatus|Static indicator)\b/i, /^[1-9]\d*px\b/i, /^[1-9]\d*px\b/i, /^#[0-9a-f]{6}\b/i][subject]!; const conforming = q.options.filter(o => concrete.test(o.label.replace(/^[1-9]\d*[A-Z]: /, '')) && (/^✅ Exact(?:ly)? (?:the (?:two )?)?DESIGN\.md\b/.test(o.description ?? '') || (subject === 0 && new RegExp(`^✅ ${control} is [^\\n]+\\bexactly per DESIGN\\.md\\b`).test(o.description ?? '')))); return conforming.some(choice => q.options.some(o => o !== choice && /^❌ (?:Ships (?:the documented violation|a known WCAG AA failure)\b|Deviates from the DESIGN\.md\b)/m.test(o.description ?? ''))); } /** Named decision fields may be compact prose; native choices still own the finding. */ function compactPrimaryDecision(fp: AskUserQuestionFingerprint): boolean { const call = fp.nativeCall; if (!call || call.answered !== true || call.failed !== false || !call.sessionId || !call.toolUseId || !call.answeredAt || !Number.isFinite(Date.parse(call.answeredAt)) || call.questions.length !== 1 || !Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length || fp.signature !== `${call.sessionId}:${call.toolUseId}` || (fp.nativeQuestionIndex !== undefined && fp.nativeQuestionIndex !== 0)) return false; const q = call.questions[0]!; if (q.multiSelect || q.options.length !== 2 || new Set(q.options.map(o => o.label)).size !== 2 || fp.options.length !== 2 || !fp.options.every((o, i) => o.index === i + 1 && o.label === q.options[i]!.label) || !q.options.some(o => o.label === call.answers?.[q.question]) || / value .replace(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, '') .replace(/^(?:\s*>| {4}|\t).*$/gm, '') .replace(/`[^`\n]*`|"[^"\n]*"|“[^”\n]*”|(? new RegExp(`^(?:${inactive})$`, 'i').test(quoted.slice(1, -1)) && scalarPrefix.test(source.slice(0, index)) ? quoted.slice(1, -1) : '') .replace(/\*\*/g, ''); const invalid = (value: string) => new RegExp(`${boundary}${owner} (?:is|was|are|were|has been|have been) (?:${inactive})\\b`, 'i').test(value) || new RegExp(`${boundary}(?:(?:This|The) (?:gap|violation) (?:is|was|has been) (?:already |now )?(?:resolved|fixed|closed)|No current (?:gap|issue|finding|violation) (?:remains|exists))\\b`, 'i').test(value) || new RegExp(`${boundary}(?:If|When|Once|Provided|Assuming|Pending) (?:approval|approved|acceptance|accepted|(?:we|you) (?:approve|accept))\\b`, 'i').test(value) || new RegExp(`${boundary}(?:Do not|Don't|Never|Skip|Cancel|Withdraw) (?:apply|use|add|keep) (?:this (?:fix|amendment)|(?:the |these )?(?:tokens?|styles?|primary treatment))\\b`, 'i').test(value) || new RegExp(`${boundary}(?:${control} (?:already (?:is|has)|is already) (?:the (?:only |visible )?primary action|primary[- ]action hierarchy)|This (?:issue|finding) has no current (?:gap|defect)|(?:This|The) (?:amendment|fix) keeps (?:all )?(?:[a-z]+|[1-9]\\d*) buttons identical)\\b`, 'i').test(value) || /(?:^|[.!?;]\s+|\n)(?:Historical|Hypothetical|Quoted|Source|Archived|Example)(?:\s+(?:review|example|excerpt|assessment|material|text))?\s*:/i.test(value); const text = current(q.question); if (invalid(text)) return false; // These are the skill's existing decision fields, not a particular sentence // or line layout. Duplicate/missing fields cannot borrow a neighboring issue. const fields = ['Project/branch/task:', 'ELI10:', 'Stakes if we pick wrong:', 'Recommendation:', 'Completeness:', 'Net:']; const positions = fields.map(field => text.indexOf(field)); if (positions.some((position, i) => position < 0 || text.lastIndexOf(fields[i]!) !== position || (i > 0 && position <= positions[i - 1]!)) || text.slice(0, positions[0]).trim() !== headline[0] || (text.match(/\?/g)?.length ?? 0) !== 1 || !/\?\s*$/.test(text)) return false; const values = fields.map((field, i) => text.slice(positions[i]! + field.length, positions[i + 1] ?? text.length).trim()); if (values.some(value => !value) || values.some(value => /^(?:If|When|Once|Unless|Assuming|Provided|Historical|Hypothetical|Quoted|Source|Example)\b/i.test(value)) || !/\bDESIGN\.md\b/.test(values[3]!)) return false; const count = (value: string) => /^\d+$/.test(value) ? Number(value) : ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'].indexOf(value.toLowerCase()); const names = (value: string) => value.toLowerCase().split(/\s*[,/]\s*(?:and\s+)?|\s+and\s+/).map(s => s.trim()).sort(); const same = (a: string[], b: string[]) => JSON.stringify(a) === JSON.stringify(b); const assessment = /^The header shows ([A-Za-z][A-Za-z0-9 ,/_-]{0,159}) as (two|three|four|five|six|seven|eight|nine|ten|[1-9]\d*) identical buttons\./i.exec(values[1]!); if (!assessment) return false; const actors = names(assessment[1]!); if (new Set(actors).size !== actors.length || actors.length !== count(assessment[2]!) || !actors.includes(control.toLowerCase())) return false; const peers = actors.filter(actor => actor !== control.toLowerCase()); const ids = q.options.map(o => new RegExp(`^(${issue}[A-Z])[).:]\\s+`).exec(o.label)?.[1]); if (ids.some(id => !id) || new Set(ids).size !== 2 || !ids.some(id => values[3]!.startsWith(`${id} `))) return false; const offered = ids.map(id => [...values[4]!.matchAll(new RegExp(`(?:^|\\s)${id}[).:]\\s+`, 'g'))]); if (offered.some(matches => matches.length !== 1)) return false; return q.options.some((option, index) => { const body = current(option.description ?? ''), other = q.options[1 - index]!, declined = current(other.description ?? ''); const style = /^([A-Za-z][A-Za-z0-9 _-]{0,39}): filled (#[0-9a-f]{6}) with (white|black) text\. ([A-Za-z][A-Za-z0-9 ,/_-]{0,159}): neutral ghost(?: buttons)?\./i.exec(body); const keep = new RegExp(`^${ids[1 - index]}[).:] Keep (two|three|four|five|six|seven|eight|nine|ten|[1-9]\\d*) equal buttons(?: \\(recommended\\))?$`, 'i').exec(other.label); if (!style || style[1]!.toLowerCase() !== control.toLowerCase() || !same(names(style[4]!), peers) || !new RegExp(`^${ids[index]}[).:] Filled primary ${control}(?: \\(recommended\\))?$`, 'i').test(option.label) || !keep || count(keep[1]!) !== actors.length || invalid(body) || invalid(declined) || !/^No change\. Documented as a declined fix; Pass [1-7] stays below 10\./i.test(declined)) return false; // The detailed offered action must agree with its native menu's tokens and // actors; prose about another control cannot lend this choice a remedy. const start = offered[index]![0]!.index!, next = offered[1 - index]![0]!.index!; const action = values[4]!.slice(start, next > start ? next : undefined); const detail = new RegExp(`(?:^|[✅]\\s*)${control} becomes the only filled button \\((#[0-9a-f]{6}), (white|black) text\\); ([A-Za-z][A-Za-z0-9 ,/_-]{0,159}) become neutral ghost buttons`, 'i').exec(action); return !!detail && detail[1]!.toLowerCase() === style[2]!.toLowerCase() && detail[2]!.toLowerCase() === style[3]!.toLowerCase() && same(names(detail[3]!), peers); }); } /** A completed finding can start the passes when the caller already supplied the focus. */ export function isDesignCountFirstReview(fp: AskUserQuestionFingerprint): boolean { const call = fp.nativeCall; if (!call?.answered || call.failed) return false; if (isDesignCountSetup(fp)) return false; if (numberedVisualHierarchyFinding(fp)) return true; const findingScope = { primary: false, ownsPrimaryPremise: false }; if (ordinaryDesignIssue(fp, findingScope)) return true; if (findingScope.ownsPrimaryPremise) return false; // A complete native decision can supply its own source, current defect, // remedy and opposition in review fields. Validate those independently // before closing the loose marker fallback for recognized primary issues. if (designSystemChoiceIssue(fp)) return true; if (findingScope.primary) return false; if (compactPrimaryDecision(fp)) return true; if (designFirstReviewAUQ(fp)) return true; return call.questions.some(q => { if (!call.answers?.[q.question] || q.options.length < 2) return false; if (/^(?:focus|scope|learnings|routing|next steps?|outside(?: design)? voices)$/i.test(q.header.trim())) return false; const id = //i.exec(q.question)?.[1] ?? ''; if (/(?:^|-)(?:focus|scope|setup|routing|learnings|onboarding|next-steps?|posture|mockups?|target)(?:-|$)/i.test(id)) return false; // Native fingerprints prepend the menu header. Inspect the actual question // for an explicit finding that offers a plan amendment and deferral. if (call.answered === true && call.failed === false && /^Pass\s*[1-7]\s*\([^)]*\)\s*[—–:]\s*Finding\s*[1-9]\d*:\s+\S/i.test(q.question.trim()) && /^plan-design-review-[a-z0-9-]+$/i.test(id) && (q.question.match(/]+>\s*$/i.test(q.question) && q.options.some(option => /^(?:Apply|Add|Fix|Specify|Define|Restore)\b/i.test(option.label)) && q.options.some(option => /^(?:Defer|Leave|Keep as-is|Accept the gap)\b/i.test(option.label)) && q.options.some(option => option.label === call.answers?.[q.question]) && Array.isArray(call.unansweredQuestionIndices) && !call.unansweredQuestionIndices.length && fp.signature === `${call.sessionId}:${call.toolUseId}`) return true; // A named or scored pass can ask for a missing design requirement before a // numbered finding heading appears. Its actual decision and opposed // choices establish review; a score or familiar qid alone cannot. const scoredPass = /^(?:D\s*\d+\s*[—–:-]\s*)?Pass\s*[1-7]\s*\([^)]*\)\s*[—–:-]\s*(?:10|[0-9])(?:\.[0-9]+)?\/10[.!:]/i.test(q.question.trim()); const namedPass = /^(?:D\s*\d+\s*[—–:-]\s*)?Pass\s*[1-7]\s*[—–:-]\s*[A-Za-z][A-Za-z ]{3,60}:\s+/i.test(q.question.trim()); const chosen = q.options.some(option => option.label === call.answers?.[q.question]); const fixChoice = q.options.some(option => /^(?:Add|Fix|Specify|Define|Restore)\b/i.test(option.label)); const leaveChoice = q.options.some(option => /^(?:Leave as-is|Keep as-is|Defer|Accept the gap)\b/i.test(option.label) || /^Skip\s*[—–-]\s*implied by\s+[^.!?]+\bgap$/i.test(option.label)); if ((scoredPass || namedPass) && /^plan-design-review-[a-z0-9-]+$/i.test(id) && (q.question.match(/]+>\s*$/i.test(q.question) && fixChoice && leaveChoice && chosen && !(call.unansweredQuestionIndices?.length) && fp.signature === `${call.sessionId}:${call.toolUseId}`) return true; // Native pass decisions can carry a D-number before the pass title and // use plan-design-passN rather than plan-design-review-... identities. // Bind both forms to the same explicit pass and an offered choice that // leaves a named gap unresolved. Pass readiness is only setup. const numberedPass = /^D\s*\d+\s*[—–:-]\s*Pass\s*([1-7])\s*\([^)]*\)\s*:/i.exec(q.question.trim()); const passId = /^plan-design-pass([1-7])-/i.exec(id); const unresolvedChoice = q.options.some(option => /\b(?:leave|keep|defer|accept)\b/i.test(option.label) && /\b(?:gap|problem|defect|inconsisten\w*)\b/i.test(`${option.label} ${option.description ?? ''}`)); if (numberedPass && passId && numberedPass[1] === passId[1] && unresolvedChoice && /\?/.test(q.question)) return true; // These are issue-bearing pass statements in actual answered calls, // not a setup request that merely mentions the seven review passes. return /^Pass\s*[1-7]\s+(?:surfaces|(?:also\s+)?(?:found|flagged))\b/i.test(q.question.trim()) && /\?/.test(q.question); }); } /** A closed recap may explain why Eng is next; it cannot request another fix. */ function closedDesignGateRecap(tail: string, descriptions: string[]): boolean { const navigation = /\bWhat(?:['’]s)?\s+next\?\s*\s*$/i.exec(tail); if (!navigation) return false; const body = tail.slice(0, navigation.index).trim(); const gate = /^(?:Eng(?:ineering)? Review is (?:the )?required (?:shipping gate|gate before shipping))[.!]?$/i; const sentences = (text: string) => text.split(/[.!]\s+|[.!]$/).map(s => s.trim()).filter(Boolean); const recap = (text: string): boolean => { // Each count describes completed or explicitly absent work. A positive // deferred/open count is not a closed review, regardless of its title. const count = /^(?:(?:\d+|all)\s+(?:design\s+)?(?:decisions|findings|issues)\s+(?:(?:are|were)\s+)?(?:resolved|approved|addressed|closed)|\d+\s+(?:implementation\s+)?tasks\s+(?:(?:are|were)\s+)?(?:added|recorded|ready)|(?:no|zero|0)\s+(?:deferred(?:\s+(?:decisions|findings|issues|tasks|items))?|(?:unresolved|open|pending|outstanding)\s+(?:decisions|findings|issues|tasks|items)))$/i; if (text.split(/,\s*(?:and\s+)?|\s+and\s+/i).every(part => count.test(part))) return true; // Only a declarative completed-review subject can introduce explanatory // content. Separate clauses, questions and conditional/future work fail. if (!/^(?:The|This)\s+(?:design\s+)?review\s+(?:has\s+)?(?:added|recorded|approved|addressed|specified|covered|resolved)\s+\S/i.test(text)) return false; if (/[;?<>]|\b(?:if|unless|until|once|when|should|must|need|needs|will|would|could|please|then|also|still|missing|unresolved)\b|\b(?:and|but)\s+(?:first\s+)?(?:do|add|fix|repair|implement|resolve|decide|configure|remove|delete|pick|choose)\b/i.test(text)) return false; const clauses = text.split(/\s+[—–]\s+/); return clauses.length <= 2 && (clauses.length === 1 || /^(?:architectural|engineering|implementation)\s+(?:implications|considerations|details)\b/i.test(clauses[1]!)); }; const parts = sentences(body); if (parts.filter(part => gate.test(part)).length !== 1 || !parts.every(part => gate.test(part) || recap(part))) return false; return descriptions.every(description => sentences(description).every(part => gate.test(part) || recap(part) || /^Exit plan mode and proceed on your own$/i.test(part) || /^You have \d+ (?:concrete )?(?:implementation )?tasks ready to build from$/i.test(part))); } /** A qidless closed handoff must consume every question/description clause. */ function resolvedDesignHandoff(q: NonNullable['questions'][number]): number | null { if (!/^next review$/i.test(q.header.trim()) || q.options.length !== 2) return null; const completed = /^Design review complete [—–-] (?:10|[0-9](?:\.\d+)?)\/10 (?:→|->) (?:10|[0-9](?:\.\d+)?)\/10\. All ([1-9]\d*) decisions resolved\. The plan is design-complete; next is the required shipping gate\. What['’]s next\?$/.exec(q.question.trim()); if (!completed) return null; const labels = q.options.map(o => o.label.trim().replace(/\s*\(recommended\)\s*$/i, '')); const review = labels.findIndex(label => /^Run \/plan-eng-review$/i.test(label)); const manual = labels.findIndex(label => /^Skip\s*[—–-]\s*I['’]ll handle next steps manually$/i.test(label)); if (review < 0 || manual < 0 || review === manual) return null; const description = (index: number) => (q.options[index]!.description ?? '').trim().replace(/\s+/g, ' '); const topics = '(?:spinner|skeleton|(?:button|switch|field) (?:keyboard|focus|loading|error|disabled|pending|success)|(?:keyboard|focus|loading|error|disabled|pending|success) (?:states?|behavior|navigation))'; const recap = new RegExp('^Eng review is the required shipping gate\\. It validates architecture, component wiring, tests, and accessibility implementation against the ' + completed[1] + ' approved design decisions\\. This design review added interaction specs \\(' + topics + '(?:, ' + topics + ')*\\), so eng review needs to validate their architectural fit\\.$'); if (!recap.test(description(review)) || !/^End the review workflow here\. The improved plan is at the (?:e2e output|approved plan) path; implementation can begin\. Run \/plan-eng-review later before shipping\.$/.test(description(manual))) return null; return manual + 1; } function designHandoff(fp: AskUserQuestionFingerprint): { manualIndex: number | null } | null { const call = fp.nativeCall; if (!call || call.failed || call.questions.length !== 1 || fp.signature !== `${call.sessionId}:${call.toolUseId}`) return null; const q = call.questions[0]!; if (q.multiSelect || q.options.length < 2) return null; const pending = call.answered === false && call.answers === undefined && call.answeredAt === undefined && (call.unansweredQuestionIndices === undefined || (Array.isArray(call.unansweredQuestionIndices) && call.unansweredQuestionIndices.length === 1 && call.unansweredQuestionIndices[0] === 0)); const resolvedManual = call.failed === false && (call.answered === true || pending) ? resolvedDesignHandoff(q) : null; if (resolvedManual !== null) return { manualIndex: resolvedManual }; if (!/^next\s+steps?$/i.test(q.header.trim())) return null; const ids = [...q.question.matchAll(//gi)].map(match => match[1]); if ((q.question.match(/|to)\s*)?\d+(?:\.\d+)?\/10(?:,\s*\d+\s+decisions?(?:\s+(?:made|added))?)?\)[.!])(?:\s|$)/i.exec(declaration); if (!completed) return null; const requiredGateOffer = /^The required next gate is Eng(?:ineering)? Review\s*[—–-]\s*want me to run it now\?\s*\s*$/i.test(declaration.slice(completed[0].length).trim()); const closedRecap = q.options.length === 2 && closedDesignGateRecap( declaration.slice(completed[0].length).trim(), q.options.map(option => option.description ?? '')); const requiredGateQuestion = requiredGateOffer || closedRecap || /^(?:\d+ implementation tasks ready\.\s*)?Eng(?:ineering)? Review is the required shipping gate\.\s*What next\?\s*\s*$/i.test(declaration.slice(completed[0].length).trim()); // The offered Eng action can carry the required-gate declaration while the // closed question asks only what is next. Its descriptions remain part of // the decision, so they cannot conceal a new repair or conditional closure. const describedRequiredGate = /^What['’]s\s+next\?\s*\s*$/i.test(declaration.slice(completed[0].length).trim()) && q.options.some(option => /^Run \/plan-eng-review(?:\s*\(recommended\))?$/i.test(option.label.trim()) && /^Required gate before shipping[.!]/i.test(option.description ?? '')); const guardedNavigation = requiredGateQuestion || describedRequiredGate; if (!requiredGateQuestion && !/\bWhat['’]s\s+next\?\s*\s*$/i.test(declaration)) return null; // A routing label cannot conceal a new repair in its description. if (guardedNavigation && q.options.some(option => /(?:^|[.!?;]\s+|\b(?:proceed to|continue to|must|need to)\s+)(?:(?:please|first|then|also)\s+)*(?:add|fix|repair|implement|resolve|decide)\b|\b(?:(?:should|could|can|would)\s+(?:we|I)|(?:we|I)\s+(?:should|could|can|would))\s+(?:add|fix|repair|implement|resolve|decide)\b/i.test(option.description ?? '') || /\b(?:Design|the|this)\s+review\s+(?:(?:is|remains)\s+)?(?:not\s+(?:complete|done|resolved)|incomplete|unfinished)\b|\bnot\s+all\s+(?:decisions|findings|issues|gaps)\s+(?:are\s+)?(?:resolved|complete|done)\b|\b(?:decisions|findings|issues|gaps)\s+(?:are\s+)?not\s+(?:resolved|complete|done)\b/i.test(option.description ?? '') || /\b(?:once|after|when|if|unless|until)\b[^.!?]*\b(?:review|decisions?|findings?|issues?|gaps?)\b[^.!?]*\b(?:complete|done|resolved)\b|\b(?:review|decisions?|findings?|issues?|gaps?)\b[^.!?]*\b(?:complete|done|resolved)\b[^.!?]*\b(?:once|after|when|if|unless|until)\b/i.test(option.description ?? ''))) return null; // A closed heading does not override an affirmative outstanding-work claim // in its recap. Zero/no outstanding work is a compatible completion claim. const outstanding = (guardedNavigation ? [declaration, ...q.options.map(o => o.description ?? '')].join('\n') : declaration) .replace(/\b(?:no|zero|0)\s+(?:unresolved|open|pending|unaddressed|remaining|outstanding)\s+(?:[a-z-]+\s+){0,3}(?:gaps?|issues?|decisions?|requirements?|work)\b/gi, '') .replace(/\bno\s+(?:gaps?|issues?|decisions?|requirements?|work)\s+remains?\b/gi, ''); if (/\b(?:unresolved|open|pending|unaddressed|remaining|outstanding)\s+(?:[a-z-]+\s+){0,3}(?:gaps?|issues?|decisions?|requirements?|work)\b|\b(?:gaps?|issues?|decisions?|requirements?|work)\s+(?:still\s+)?remains?\b|\b(?:gaps?|issues?|decisions?|requirements?|work)\s+(?:is|are)\s+still\s+(?:unresolved|open|pending|unaddressed)\b/i.test(outstanding)) return null; const labels = q.options.map(o => o.label.trim().replace(/^[A-Z][).]\s*/i, '') .replace(/\s*\(recommended\)\s*$/i, '').trim()); const manual = labels.map(label => /^(?:Handle next steps manually|Skip\s*[—–-]\s*I['’]ll handle next steps manually)$/i.test(label) || (guardedNavigation && /^Skip\s*[—–-]\s*handle (?:next steps )?manually$/i.test(label))); const review = labels.map(label => /^Run \/plan-eng-review(?: next)?(?: \(required gate\))?$/i.test(label)); const navigation = labels.map(label => /^(?:Skip to implementation|Run \/plan-ceo-review(?: first)?|Run \/design-(?:shotgun|html))$/i.test(label)); if (manual.filter(Boolean).length > 1 || !review.some(Boolean) || !labels.every((_, i) => manual[i] || review[i] || navigation[i])) return null; // Classification does not invent a missing stop option. Only an offered // manual action can steer a pending question away from another workflow. const index = manual.findIndex(Boolean); return { manualIndex: index < 0 ? null : index + 1 }; } /** Completed handoffs retain raw evidence and their own administrative count. */ export function isDesignCompletionHandoff(fp: AskUserQuestionFingerprint): boolean { const call = fp.nativeCall; if (!call?.answered || call.failed || !Array.isArray(call.unansweredQuestionIndices) || call.unansweredQuestionIndices.length || designHandoff(fp) === null) return false; const q = call.questions[0]!; return q.options.some(option => call.answers?.[q.question] === option.label); } /** Preserve the native-only outside opt-out, then finish this review at its actual handoff. */ export function pickDesignCountQuestion( routing: AskUserQuestionFingerprint, active: AskUserQuestionFingerprint, ): number | null { const outside = pickDesignCountOutsideVoices(routing, active); if (outside !== null) return outside; return active.nativeCall?.answered ? null : designHandoff(active)?.manualIndex ?? null; }