fix(test): recognize ledger row-ID split candidates so collection stops at the last ACK

Run 36385945043's split-overflow case asked all five candidate decisions by
8m55s, but the live candidate check required the question to open with
"E1:" and every option to be a known disposition. The skill cited ledger
row IDs ("D2.1 — R-E1: …") and offered "Hold, discuss first", so no
candidate was recognized and the attempt ran the whole review (1302s).

Identity now comes from the native header; the question must open with that
candidate's ledger reference, name only that candidate, and offer exactly one
include, defer and cut disposition. The selected answer must still be one of
those three. The semantic evaluator and every existing negative control are
unchanged; a trimmed capture from the run adds the positive case and four
row-ID negative controls.
This commit is contained in:
garrytan committed 2026-09-29 15:18:46 +00:00
1 parent 2ba451a49d
commit dc5934ee54
3 files changed
+430 -11

No files matched your search

+14 -10
View File
@@ -18,20 +18,24 @@ export function ceoSplitOptionAction(label: string): 'include' | 'defer' | 'cut'
}
/** Candidate-shaped menus for live progress only. Final coverage, subject and
* independence are established by evaluatePlanReviewDecisions over every call. */
* independence are established by evaluatePlanReviewDecisions over every call.
* Identity comes from the native header. The question opens with that
* candidate's ledger reference (E1 or a row ID ending in it), names only that
* candidate, and offers exactly one include, defer and cut disposition. */
export function ceoSplitCandidate(question: NativeQuestion): string | null {
const header = /^E([1-5])\s+(.+)$/.exec(question.header.trim());
if (!header || question.multiSelect || question.options.length < 3 || question.options.length > 4) return null;
const index = Number(header[1]) - 1;
const lead = question.question.split(/\r?\n/, 1)[0]!
.replace(/^D[1-9]\d*(?:\.[1-9]\d*)?\s*[—–:-]\s*/, '');
const target = /^E([1-5])[):]\s+(.+\?)$/.exec(lead);
if (!target || question.multiSelect || question.options.length < 3 || question.options.length > 4) return null;
const id = `E${target[1]}`;
const platform = platforms[Number(target[1]) - 1]!;
if (!new RegExp(`^${id}\\s+${platform}$`, 'i').test(question.header.trim()) ||
!new RegExp(`\\b${platform}\\b`, 'i').test(target[2]!) ||
/\bE[1-5][):]/.test(target[2]!)) return null;
const names = (platform: string) => new RegExp(`\\b${platform}\\b`, 'i').test(lead);
if (!new RegExp(`^${platforms[index]}$`, 'i').test(header[2]!) ||
!new RegExp(`^\\S*\\bE${header[1]}[):]\\s+.+\\?$`).test(lead) || !names(platforms[index]!) ||
platforms.some((platform, i) => i !== index && names(platform)) ||
[...lead.matchAll(/\bE([1-9]\d*)\b/g)].some(match => match[1] !== header[1])) return null;
const actions = question.options.map(option => ceoSplitOptionAction(option.label));
return actions.every(Boolean) && new Set(actions).size === actions.length &&
['include', 'defer', 'cut'].every(action => actions.includes(action)) ? id : null;
return ['include', 'defer', 'cut'].every(action => actions.filter(found => found === action).length === 1)
? `E${header[1]}` : null;
}
export function isCeoSplitCandidateCall(fp: AskUserQuestionFingerprint): boolean {