Archived
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:
@@ -246,6 +246,7 @@ export function missingReadings(
|
||||
primaryKey: boolean;
|
||||
required?: boolean | undefined;
|
||||
expect?: boolean | undefined;
|
||||
requiredWhen?: { column: string; value: string } | undefined;
|
||||
}>;
|
||||
}>,
|
||||
): Record<string, string | number> {
|
||||
@@ -260,17 +261,23 @@ export function missingReadings(
|
||||
* · `required` → `_missing` 缺必填几行(**不齐不许放行**,是验收标准);
|
||||
* · `expect` → `_gaps` 缺选填几行(不拦单,但**那一问不能省**)。
|
||||
* 缺口摆的是**中文列名**:这两个数会进拒回与卡面,给人看的地方不摆列名。
|
||||
*
|
||||
* `requiredWhen` 把必填钉在**该轮到的那一行**(2026-08-02,逐颗派单的闸):
|
||||
* 行上那一列不等于指定值,这一格就还没轮到,不算缺 —— 排队的颗不拦当前的卡。
|
||||
*/
|
||||
for (const [mark, suffix] of [
|
||||
['required', 'missing'],
|
||||
['expect', 'gaps'],
|
||||
] as const) {
|
||||
const watched = fields.filter((f) => f[mark] === true);
|
||||
const watched = fields.filter((f) => f[mark] === true || (mark === 'required' && f.requiredWhen !== undefined));
|
||||
if (watched.length === 0) continue;
|
||||
const bad: string[] = [];
|
||||
for (const row of rows) {
|
||||
const one = (typeof row === 'object' && row !== null ? row : {}) as Record<string, unknown>;
|
||||
const gaps = watched.filter((f) => blank(one, f.name));
|
||||
const due = watched.filter(
|
||||
(f) => f.requiredWhen === undefined || String(one[f.requiredWhen.column] ?? '') === f.requiredWhen.value,
|
||||
);
|
||||
const gaps = due.filter((f) => blank(one, f.name));
|
||||
if (gaps.length === 0) continue;
|
||||
const id: Record<string, string> = {};
|
||||
for (const k of keys) if (!blank(one, k)) id[k] = String(one[k]);
|
||||
@@ -323,6 +330,40 @@ export function tallyReadings(
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* **这一单到底调没调那个工具** —— `{ 'tool_crm_history_price_read_calls': N }`。
|
||||
*
|
||||
* 「机检只读基座数」这条纪律此前只覆盖到台账(`_rows` / `_missing` / tally),
|
||||
* **覆盖不到工具**:一条判据若要说「转 A 的依据来自本单亲查的行情」,
|
||||
* 从前只能读活动自报的列 —— 而模型偶发压根没调工具就把料判成「行情稳」,
|
||||
* 自报的那一列照样写得漂亮(A2 实测约十次一次,去向就此走岔)。
|
||||
* 说明书已经写到头(「这一拍亲自查 alt_pn」「不许拿上游的数顶替」),
|
||||
* **措辞治不了的结构治**(设计律第六条)。
|
||||
*
|
||||
* 工具出参本来就入痕(`turn-runner` 每次调完记一条 `kind:'result'`、
|
||||
* 标题 `${tool} 返回`),所以「调没调」是**基座数得出来的事实**,不必让活动自报。
|
||||
*
|
||||
* 只数**成功返回**的那几次:调失败等于没查到行情,不该拿来当依据。
|
||||
* 名字取 `tool_<工具名去符号>_calls`(S-FEEL 不认点号与横杠);
|
||||
* 声明过的工具**一律先铺 0** —— 与 `_rows` 同一个理由:未绑定的名字参与比较求值为假,
|
||||
* 「没调过」与「调了 0 次」在业务上是同一件事,读数上也得是。
|
||||
*/
|
||||
export function toolCallReadings(
|
||||
trail: ReadonlyArray<{ readonly kind: string; readonly title: string }>,
|
||||
declared: ReadonlyArray<{ readonly name: string }>,
|
||||
): Record<string, number> {
|
||||
const key = (tool: string): string => `tool_${tool.replace(/[^0-9a-zA-Z]+/g, '_')}_calls`;
|
||||
const out: Record<string, number> = {};
|
||||
for (const { name } of declared) out[key(name)] = 0;
|
||||
for (const entry of trail) {
|
||||
if (entry.kind !== 'result') continue;
|
||||
for (const { name } of declared) {
|
||||
if (entry.title.startsWith(`${name} 返回`)) out[key(name)] = (out[key(name)] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function rowKeyList(
|
||||
mine: Readonly<Record<string, readonly unknown[]>>,
|
||||
declared: ReadonlyArray<{ domain: string; object: string; fields: ReadonlyArray<{ name: string; primaryKey: boolean }> }>,
|
||||
@@ -752,8 +793,19 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
* 而判据要写成 `quote_intake_record_rows = line_total` 这样一句才有用。
|
||||
* 只数**本单自己写下的行**(草稿层),别单放行过的记录不算在内。
|
||||
*/
|
||||
const declared = (await deps.registry.manifestOf(task.activityId)).declarations.ledger;
|
||||
return { ...scope, ...rowReadings(mine, declared), ...rowKeyList(mine, declared), ...missingReadings(mine, declared), ...tallyReadings(mine, declared), ...mine };
|
||||
const manifest = await deps.registry.manifestOf(task.activityId);
|
||||
const declared = manifest.declarations.ledger;
|
||||
// 工具调用是**留痕里的事实**:判据要说「亲查过行情」,得有一边不是活动自报的
|
||||
const trail = await deps.memory.window(taskId, 400);
|
||||
return {
|
||||
...scope,
|
||||
...rowReadings(mine, declared),
|
||||
...rowKeyList(mine, declared),
|
||||
...missingReadings(mine, declared),
|
||||
...tallyReadings(mine, declared),
|
||||
...toolCallReadings(trail, manifest.declarations.tools),
|
||||
...mine,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 本活动声明过的台账对象 —— 清单 `declarations.ledger` 就是这份名单。
|
||||
@@ -1447,7 +1499,13 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
await cardSpan?.end({ output: { cardId: submitted.workOrderId, precheck: submitted.report.verified.length } });
|
||||
return { id: submitted.workOrderId };
|
||||
}
|
||||
const fields = declaredFields ?? [{ key: 'result', label: '结果', kind: 'textarea' as const, required: true }];
|
||||
/*
|
||||
* **执行卡不带表单字段**(2026-08-02 用户定:「填写全去工作区」)——
|
||||
* 填报的地方是工作区那份件(editGate + 孪生件 + 增删行),卡上只剩
|
||||
* 指导书、依据与提交钮。从前这里兜底一格必填「结果」textarea:
|
||||
* 活动没声明字段就替它造一个,人对着一张没处下笔的卡还得编一句话才按得下提交。
|
||||
*/
|
||||
const fields = declaredFields ?? [];
|
||||
/*
|
||||
* 卡面正文块 —— **人要在数据上作判断,数据就得摆在表单旁边**。
|
||||
*
|
||||
@@ -1859,7 +1917,24 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
},
|
||||
},
|
||||
})
|
||||
: createAgentRunner({ gateway: tracedGateway, onUsage: onAgentUsage, modelOf });
|
||||
: createAgentRunner({
|
||||
gateway: tracedGateway,
|
||||
onUsage: onAgentUsage,
|
||||
modelOf,
|
||||
/*
|
||||
* 输出不成形就地重问时,把模型原文存进工作日志(内部痕)——
|
||||
* 卡面从此不摆 JSON 原话,尸检去日志里看。stamp 与 memoryRecorder 同款兜底。
|
||||
*/
|
||||
note: async (taskId, title, raw) => {
|
||||
await deps.memory.append({
|
||||
taskId,
|
||||
kind: 'result',
|
||||
title,
|
||||
detail: { internal: true, raw },
|
||||
stamp: { contractVersion: '1', businessModelVersion: '1', skillVersions: [] },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/* ── loop-engine ──────────────────────────────────────────────── */
|
||||
const loop = createTurnRunner({
|
||||
|
||||
Reference in New Issue
Block a user