From ce00b134268bd7bce92f15b5fdc23cfd7f55f5bd Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Mon, 12 Oct 2026 05:11:30 +0800 Subject: [PATCH 1/2] fix(run-completion): align ZH claim/request synonyms with EN (O110) --- src/cli/run-completion.ts | 6 +- test/o110-zh-en-claim-parity.spec.mjs | 123 ++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 test/o110-zh-en-claim-parity.spec.mjs diff --git a/src/cli/run-completion.ts b/src/cli/run-completion.ts index 87b7115d..2f9ab8d5 100644 --- a/src/cli/run-completion.ts +++ b/src/cli/run-completion.ts @@ -236,7 +236,7 @@ export function noteGoalVerifyResult( const CLAIM_PATTERNS: Array<{ re: RegExp; requires: VerificationCheck[] }> = [ { - re: /\ball\s+(?:the\s+)?checks?\s+(?:are\s+|is\s+)?(?:green|pass(?:ed|ing)?|clean|ok)\b|(?:全部|所有)(?:的)?检查(?:都|均|全部)?(?:已)?(?:通过|是绿的|绿了|变绿)|\bnpm\s+run\s+check\b[^.\n。]*\b(?:pass(?:es|ed)?|green|succeed(?:s|ed)?|clean)\b|npm\s+run\s+check[^。\n]*(?:通过|绿)/i, + re: /\ball\s+(?:the\s+)?checks?\s+(?:are\s+|is\s+)?(?:green|pass(?:ed|ing)?|clean|ok)\b|(?:全部|所有)(?:的)?检查(?:都|均|全部)?(?:已)?(?:通过|是绿的|绿了|变绿)|检查(?:都|均|全部)(?:已)?(?:通过|是绿的|绿了|变绿)|检查(?:都|均)?全绿|\bnpm\s+run\s+check\b[^.\n。]*\b(?:pass(?:es|ed)?|green|succeed(?:s|ed)?|clean)\b|npm\s+run\s+check[^。\n]*(?:通过|绿)/i, requires: ['check'], }, { @@ -252,7 +252,7 @@ const CLAIM_PATTERNS: Array<{ re: RegExp; requires: VerificationCheck[] }> = [ requires: ['typecheck'], }, { - re: /\b(?:all\s+)?tests?\s+(?:are\s+|now\s+)?(?:pass(?:es|ed|ing)?|green)\b|\d+\s*\/\s*\d+\s+(?:tests?\s+)?passed\b|测试(?:都|均|全部)?(?:已)?(?:通过|绿了|变绿)/i, + re: /\b(?:all\s+)?tests?\s+(?:are\s+|now\s+)?(?:pass(?:es|ed|ing)?|green)\b|\d+\s*\/\s*\d+\s+(?:tests?\s+)?passed\b|测试(?:都|均|全部)?(?:已)?(?:通过|绿了|变绿)|\bnpm\s+(?:run\s+)?test\b[^。\n]*(?:通过|绿)/i, requires: ['test'], }, ]; @@ -290,7 +290,7 @@ const CHECK_LABEL: Record = { }; const EXPECTS_CHANGES = - /(?:修复|修改|实现|改成|改为|改掉|新增|添加|增加|补上|补一|重构|删掉|删除|\bfix(?:es)?\b|\bimplement\b|\brefactor\b|\bchange\b|\bupdate\b|\bedit\b|\bwrite\s+(?:a\s+)?(?:test|file|function)|\badd\s+(?:a\s+|an\s+)?(?:test|option|flag|function|file|check|support))/i; + /(?:修复|修好|修一下|修掉|改好|修改|实现|改成|改为|改掉|新增|添加|增加|补上|补一|重构|删掉|删除|\bfix(?:es)?\b|\bimplement\b|\brefactor\b|\bchange\b|\bupdate\b|\bedit\b|\bwrite\s+(?:a\s+)?(?:test|file|function)|\badd\s+(?:a\s+|an\s+)?(?:test|option|flag|function|file|check|support))/i; const QUESTION_ONLY = /^(?:how|why|what|which|explain|describe|can you explain|为什么|怎么|如何|解释|说明一下|是什么)|[??]\s*$/i; diff --git a/test/o110-zh-en-claim-parity.spec.mjs b/test/o110-zh-en-claim-parity.spec.mjs new file mode 100644 index 00000000..e58047cc --- /dev/null +++ b/test/o110-zh-en-claim-parity.spec.mjs @@ -0,0 +1,123 @@ +#!/usr/bin/env node +/** + * O110: EN/ZH synonymous claims must yield the same outcome, last-line kind, + * reason and exit code. EN outcomes are pinned (unchanged); ZH aligns to EN. + * Spec: /workspace/specs/O110.md (A–D). + */ +import assert from 'node:assert/strict'; +import { + createRunCompletionTracker, + findUnverifiedCheckClaims, + noteRunCompletionEvent, +} from '../dist/cli/run-completion.js'; +import { formatOutcomeLine, resolveRunOutcome } from '../dist/cli/run-outcome.js'; + +function wrote(t) { + noteRunCompletionEvent(t, { + type: 'tool_start', + toolName: 'write_file', + toolCallId: 'w', + input: { path: 'src/a.ts', content: 'x' }, + }); + noteRunCompletionEvent(t, { + type: 'tool_end', + toolName: 'write_file', + toolCallId: 'w', + result: 'ok', + isError: false, + }); +} +function run(lang, { request, finalText, write = false }) { + const t = createRunCompletionTracker(); + if (write) wrote(t); + const o = resolveRunOutcome({ tracker: t, finalText, request }); + return { o, t, line: formatOutcomeLine(o, lang) }; +} +const kind = (line) => line.split(/[::]/)[0]; +const KIND = { Incomplete: '未完成', Unverified: '未验证', Completed: '完成' }; +function pair(name, en, zh, expect) { + const e = run('en', en); + const z = run('zh', zh); + // Pin current EN behavior. + assert.equal(e.o.status, expect.status, `${name} EN status pinned`); + assert.equal(e.o.exitCode, expect.exit, `${name} EN exit pinned`); + assert.equal(kind(e.line), expect.kindEn, `${name} EN last-line kind pinned`); + // ZH matches EN. + assert.equal(z.o.status, e.o.status, `${name} ZH status == EN`); + assert.equal(z.o.exitCode, e.o.exitCode, `${name} ZH exit == EN`); + assert.equal(kind(z.line), KIND[kind(e.line)], `${name} ZH last-line kind == EN`); + assert.equal( + /没有改动任何文件/.test(z.line), + /no files were modified/.test(e.line), + `${name} same no-files reason` + ); + assert.equal( + /没有跑检查/.test(z.line), + /no check was run/.test(e.line), + `${name} same no-check reason` + ); + assert.deepEqual( + findUnverifiedCheckClaims(zh.finalText, z.t), + findUnverifiedCheckClaims(en.finalText, e.t), + `${name} same unverified claim set` + ); + return { e, z }; +} + +// A. 0 writes + claimed fixed → Incomplete,「没有改动任何文件」, exit 9 +{ + const { e, z } = pair( + 'A', + { + request: 'Fix the bug in src/a.ts', + finalText: 'Fixed it. npm test passed and all checks are green.', + }, + { + request: '把 src/a.ts 的 bug 修好', + finalText: '已修好。npm test 已通过,所有检查都是绿的。', + }, + { status: 'incomplete', exit: 9, kindEn: 'Incomplete' } + ); + assert.match(e.line, /no files were modified/); + assert.match(z.line, /没有改动任何文件/); +} +// B. wrote files, no checks run, claimed green → Unverified, exit 9 +{ + const { z } = pair( + 'B', + { request: 'Fix the bug in src/a.ts', finalText: 'All checks green.', write: true }, + { request: '把 src/a.ts 的 bug 修好', finalText: '检查全部通过。', write: true }, + { status: 'unverified', exit: 9, kindEn: 'Unverified' } + ); + assert.match(z.line, /^未验证:声称检查已通过,但没有跑检查:npm run check$/); + pair( + 'B2', + { request: 'Fix the bug', finalText: 'npm test passed.', write: true }, + { request: '修好这个 bug', finalText: 'npm test 已通过。', write: true }, + { status: 'unverified', exit: 9, kindEn: 'Unverified' } + ); + pair( + 'B3', + { request: 'Fix the bug', finalText: 'All checks are green.', write: true }, + { request: '修复这个 bug', finalText: '检查全绿。', write: true }, + { status: 'unverified', exit: 9, kindEn: 'Unverified' } + ); +} +// C. pure Q&A, no claims → Completed, exit 0, no warning line +{ + const { e, z } = pair( + 'C', + { request: 'What does src/a.ts export?', finalText: 'It exports a constant `a`.' }, + { request: 'src/a.ts 导出了什么?', finalText: '它导出常量 `a`。' }, + { status: 'completed', exit: 0, kindEn: 'Completed' } + ); + assert.doesNotMatch(e.line, /Unverified|Incomplete/); + assert.doesNotMatch(z.line, /未验证|未完成/); +} +// Negations still don't count as claims (both languages). +{ + const t = createRunCompletionTracker(); + assert.deepEqual(findUnverifiedCheckClaims('检查都未通过。', t), []); + assert.deepEqual(findUnverifiedCheckClaims('npm test 没有通过。', t), []); +} +console.log('o110-zh-en-claim-parity: ok'); From 843e760bcb48c7326b219565060f44269bdc62a4 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Mon, 12 Oct 2026 05:40:21 +0800 Subject: [PATCH 2/2] fix(run-completion): ignore ZH Q&A/conditional frames for claim parity (O110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep EN/ZH synonym alignment (修好↔Fixed, 检查全绿↔All checks green), but do not treat question/hypothetical soft verbs (能不能修好 / 改好之后) as expecting file writes, and do not treat explanatory/conditional 通过/全绿 (才算/表示/等…后; means / wait until … then merge) as success claims. Adds EN/ZH paired cases C2–C7 (exit 0). Existing EN outcomes and O98 assertions unchanged; exit 9 semantics unchanged. --- src/cli/run-completion.ts | 18 +++++- test/o110-zh-en-claim-parity.spec.mjs | 82 +++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/cli/run-completion.ts b/src/cli/run-completion.ts index 2f9ab8d5..ab421dba 100644 --- a/src/cli/run-completion.ts +++ b/src/cli/run-completion.ts @@ -261,6 +261,10 @@ const CLAIM_PATTERNS: Array<{ re: RegExp; requires: VerificationCheck[] }> = [ * Success claims in `text` that the run does not back up: the matching check * never ran, or its latest run failed. */ +/** Explanatory / conditional framing — not a success claim (O110). */ +const CLAIM_NON_ASSERTIVE = + /才算|表示|意味着|说明(?:了|着)?|等.{0,16}后再|等.{0,16}后|如果|的话|\bmeans?\b|\bmust\b[\s\S]{0,40}?\bbefore\b|\bwait\s+until\b|\bbefore\s+(?:it\s+)?counts\b|\bthen\s+merge\b/i; + export function findUnverifiedCheckClaims( text: string, tracker: RunCompletionTracker @@ -272,6 +276,8 @@ export function findUnverifiedCheckClaims( const match = re.exec(sentence); if (!match) continue; if (/\b(?:not|n't|never)\b|未|没|尚未|无法/.test(match[0])) continue; + // Questions / explanations / conditionals are not claims that checks passed. + if (CLAIM_NON_ASSERTIVE.test(sentence)) continue; for (const check of requires) { const run = tracker.checks.get(check); if (!run || !run.passed) missing.add(check); @@ -294,11 +300,21 @@ const EXPECTS_CHANGES = const QUESTION_ONLY = /^(?:how|why|what|which|explain|describe|can you explain|为什么|怎么|如何|解释|说明一下|是什么)|[??]\s*$/i; +/** + * Soft ZH change verbs (修好/改好/修一下/修掉) only count in imperative use. + * Strip question / hypothetical frames (能不能/是否/之后…) before matching (O110). + */ +const SOFT_ZH_CHANGE_NON_IMPERATIVE = + /(?:能不能|可不可以|是否|能否)[^。!?\n?]{0,24}?(?:修好|改好|修一下|修掉)|(?:修好|改好|修一下|修掉)(?:之后|了没有|了吗|了没)/g; + /** Heuristic: does this request expect the run to change files? */ export function requestExpectsFileChanges(request: string): boolean { const text = request.trim(); if (!text || QUESTION_ONLY.test(text)) return false; - return EXPECTS_CHANGES.test(text); + if (!EXPECTS_CHANGES.test(text)) return false; + // If the only hits were soft ZH verbs in Q/hypothetical frames, do not expect writes. + const scrubbed = text.replace(SOFT_ZH_CHANGE_NON_IMPERATIVE, ' '); + return EXPECTS_CHANGES.test(scrubbed); } /** Decide whether the run is complete; never upgrades an error run to success. */ diff --git a/test/o110-zh-en-claim-parity.spec.mjs b/test/o110-zh-en-claim-parity.spec.mjs index e58047cc..db54b764 100644 --- a/test/o110-zh-en-claim-parity.spec.mjs +++ b/test/o110-zh-en-claim-parity.spec.mjs @@ -120,4 +120,86 @@ function pair(name, en, zh, expect) { assert.deepEqual(findUnverifiedCheckClaims('检查都未通过。', t), []); assert.deepEqual(findUnverifiedCheckClaims('npm test 没有通过。', t), []); } + +// C2–C7 (O110 acceptance C / QA): question / hypothetical / explanatory +// frames must stay Completed exit 0 (EN/ZH paired). Soft ZH verbs and +// 通过/全绿 in 才算/表示/等…后 must not false-positive. +{ + pair( + 'C2', + { + request: 'Tell me if this bug can be fixed', + finalText: 'Maybe, if the root cause is clear.', + }, + { + request: '说说这个 bug 能不能修好', + finalText: '可以修好,需要改 src/a.ts。', + }, + { status: 'completed', exit: 0, kindEn: 'Completed' } + ); + pair( + 'C3', + { + request: 'What happens after this function is fixed?', + finalText: 'It would return an array.', + }, + { + request: '这个函数改好之后会怎样', + finalText: '会返回数组。', + }, + { status: 'completed', exit: 0, kindEn: 'Completed' } + ); + pair( + 'C4', + { + request: 'Can you look at whether this error can be fixed a bit?', + finalText: 'It is a network issue; no code change needed.', + }, + { + request: '帮我看看能不能修一下这个报错', + finalText: '是网络问题,不需要改代码。', + }, + { status: 'completed', exit: 0, kindEn: 'Completed' } + ); + pair( + 'C5', + { + request: 'How do I know whether the bug is fixed?', + finalText: 'Usually run npm test; tests must be green before it counts as fixed.', + }, + { + request: '怎么判断 bug 修好了没有?', + finalText: '一般要跑 npm test,测试全绿才算修好。', + }, + { status: 'completed', exit: 0, kindEn: 'Completed' } + ); + pair( + 'C6', + { + request: 'What does CI all green mean?', + finalText: 'CI all green means all checks passed.', + }, + { + request: '解释一下 CI 全绿是什么意思', + finalText: 'CI 全绿表示检查都通过。', + }, + { status: 'completed', exit: 0, kindEn: 'Completed' } + ); + // Wrote files; conditional "wait until green then merge" is not a claim. + pair( + 'C7', + { + request: 'Fix the bug', + finalText: 'Done for now; wait until checks are green then merge.', + write: true, + }, + { + request: '修复 bug', + finalText: '改好了,等检查全绿后再合。', + write: true, + }, + { status: 'completed', exit: 0, kindEn: 'Completed' } + ); +} + console.log('o110-zh-en-claim-parity: ok');