A2-A7 全部补齐到验收线;顺手把几处「门拦住却不给路」的坑填了

活动:A2 选型 / A3 寻源 / A4 定价 / A6 议价 / A7 归档五件各补成真活动包 ——
契约宽进(input.required 空,归一在活动内)、台账必填声明(含行级 requiredWhen)、
入场哨兵与 L1/L2 判据一律读基座数、两份件(在办可编辑 + 办结静态快照)、
三案例常备、包内自测。e2e 各三案例 released、行动被拒 0、发口正确、提交闸每单真拦。

基座(共享面,改一处影响所有活动,故每处都带回归):
· 重发判断走在站上场之前 —— 人答完那步常已收掉,readyToWork 先抛「不能再往它上面挂活」,
  于是「重发已答过的卡 = 无事发生」这条唯一活路在最需要时够不着,A1 基线当场熔断;
· 决策卡补幂等兜底(与执行工单同一把尺):头两次无事发生,第三次起才算被拒;
· 输出没成形先就地重问一次,原话进工作日志、不再摆上卡面;
· 工具调用读数 tool_<名>_calls —— 「机检只读基座数」这条纪律第一次覆盖到工具;
· 执行卡 steps/standard 补进出参 schema(从前能到卡面全靠厂商 JSON 模式宽松);
· 件的就地更新改逐节点 morph,保存先把新值镜像进只读那份 —— 不闪、不等回声。

A1(基线,全程守住):措辞三档对齐(必填/建议/选填)、禁止拿历史价当客户目标价、
卡骨架从步骤剧本搬进说明书(压成一拍连做时剧本没摊开,会发出没有指导书的裸卡)。

契约两处**新增可选字段**(非破坏):FieldDeclaration.requiredWhen、工单 finalHtml;
api-report 同批更新。docs/adr 与 docs/archive 的 13 份文件在工作树里不见了,
本次予以恢复而非提交删除。

门禁:typecheck / lint / depcruise / test(1366)全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 12:28:31 +08:00
co-authored by Claude Opus 5
parent 85466e815f
commit ade0fa5a94
90 changed files with 17969 additions and 2316 deletions
@@ -81,13 +81,13 @@ describe('写行的形状', () => {
});
describe('看不懂就转人,不猜着动手', () => {
it('行动型不认识 → 发执行卡,并把**模型原话**摆上去', () => {
const action = normalizeAction({ kind: '我自己想的行动' }, ENABLED, '{"kind":"我自己想的行动"}');
it('行动型不认识 → 发执行卡,卡面只说人话 —— JSON 原话归工作日志,不上卡', () => {
const action = normalizeAction({ kind: '我自己想的行动' }, ENABLED);
expect(action.kind).toBe('human');
if (action.kind !== 'human') return;
expect(action.card['reason']).toBe('模型未给出合规的行动');
// 只写「不合规」的话,收卡的人分不清是模型跑偏、行动型少了一种、还是提示词没交代清楚
expect(String(action.card['modelReply'])).toContain('我自己想的行动');
// 2026-08-03 实测:原话上卡,人看到的是一坨代码(「卡片里又是乱七八糟的东西」)
expect(action.card['modelReply']).toBeUndefined();
expect(String(action.card['reason'])).toContain('工作日志');
expect(action.card['kindSeen']).toBe('我自己想的行动');
});
@@ -98,44 +98,38 @@ describe('看不懂就转人,不猜着动手', () => {
});
/**
* 补全卡的格子在哪儿 —— **两处都认**。
*
* 提示词里执行工单的写法是 `card.fields`,模型很自然把补全卡也那么写(实测就是),
* 于是每一拍都被「fields 为空」拒回来。空不空由引擎验,这里只负责不让一个位置差异吃掉一整拍。
* **补全卡已全局退役**(2026-08-02 用户定:「彻底全局退役补全卡,执行卡完全就是
* 包含补全语义的人类工具」)—— 归一层把旧输出里的 `ask` 原地转成零输入执行工单:
* 问句上卡,字段名并进指导书(人补在工作区的件上),`fields` 不上卡。
* 拒回只会让模型换着花样再发,归一才是把路铺平。
*/
describe('补全卡的格子', () => {
const fieldsOf = (raw: Record<string, unknown>) => {
const a = normalizeAction({ kind: 'ask', itemId: 'T1', ...raw }, ENABLED);
return a.kind === 'ask' ? a.fields : [];
};
describe('补全卡退役:ask 原地转成零输入执行工单', () => {
const norm = (raw: Record<string, unknown>) => normalizeAction({ kind: 'ask', itemId: 'T1', ...raw }, ENABLED);
it('顶层 fields', () => {
expect(fieldsOf({ ask: '补一下', fields: [{ key: 'lead_time', label: '交期' }] })).toEqual([
{ key: 'lead_time', label: '交期' },
]);
it('转成 human + execution,问句在 ask,字段名进指导书', () => {
const a = norm({ ask: '缺交期,补一下', fields: [{ key: 'lead_time', label: '交期' }] });
expect(a.kind).toBe('human');
if (a.kind !== 'human') return;
expect(a.workOrder).toBe('execution');
const card = a.card as Record<string, unknown>;
expect(card['ask']).toBe('缺交期,补一下');
expect(JSON.stringify(card['steps'])).toContain('交期');
// 零输入:fields 不上卡
expect(card['fields']).toBeUndefined();
});
it('落在 card 里也认(实测模型这么写过)', () => {
expect(fieldsOf({ card: { ask: '补一下', fields: [{ key: 'eau', label: '年用量' }] } })).toEqual([
{ key: 'eau', label: '年用量' },
]);
it('fields 落在 card 里也认(旧模型这么写过)—— 一样进指导书', () => {
const a = norm({ card: { ask: '补一下', fields: [{ key: 'eau', label: '年用量' }] } });
expect(a.kind).toBe('human');
if (a.kind !== 'human') return;
expect(JSON.stringify((a.card as Record<string, unknown>)['steps'])).toContain('年用量');
});
it('写成一串字段名也认 —— label 用 key 顶上,宁可显示字段名不显示空白', () => {
expect(fieldsOf({ ask: 'x', fields: ['eau', 'lead_time'] })).toEqual([
{ key: 'eau', label: 'eau' },
{ key: 'lead_time', label: 'lead_time' },
]);
});
it('没有 key 的格子丢掉 —— 没有 key 就没法回填', () => {
expect(fieldsOf({ ask: 'x', fields: [{ label: '只有标签' }, { key: 'ok', label: '行' }] })).toEqual([
{ key: 'ok', label: '行' },
]);
});
it('一个都收不到就是空数组,交引擎拒(不替它编格子)', () => {
expect(fieldsOf({ ask: 'x' })).toEqual([]);
it('连问句都没有:兜一个标题,不吃掉整拍', () => {
const a = norm({});
expect(a.kind).toBe('human');
if (a.kind !== 'human') return;
expect(String((a.card as Record<string, unknown>)['title'])).not.toBe('');
});
});
@@ -209,10 +203,16 @@ describe('一拍一串行动的归一', () => {
expect(turn.actions.map((a) => a.kind)).toEqual(['finish']);
});
it('整串一个都不认识 → 单张转人卡,模型原话摆上去', () => {
const turn = normalizeTurn({ actions: [{ kind: '胡编的' }] }, ENABLED, '{"kind":"胡编的"}');
it('整串一个都不认识 → 标 unrecognized(decide 先就地重问)+ 单张转人卡兜底', () => {
const turn = normalizeTurn({ actions: [{ kind: '胡编的' }] }, ENABLED);
expect(turn.actions).toHaveLength(1);
expect(turn.actions[0]?.kind).toBe('human');
expect(turn.unrecognized).toBe(true);
});
it('认出了行动就不标 unrecognized —— 别把好拍拿去白重问一遍', () => {
const turn = normalizeTurn({ actions: [{ kind: 'finish', itemId: 'T1' }] }, ENABLED);
expect(turn.unrecognized).toBeUndefined();
});
it('超出封顶截到 6 个 —— 引擎侧还会再守一道', () => {