This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
ChaiHaoandClaude Opus 5 259192f736 人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的:

件上就地改
· 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名,
  于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 ——
  而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。
· **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。
  删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)——
  「没回传就是删了」会把一次不完整的回传变成一次删表。
· **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。
· 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。

判据与门
· 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来,
  人填好的目标价它照样判「客户未提供」的毛病由此消掉。
· 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。
· 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开,
  于是一单没打标就进了放行门。三个数全由基座现数。
· 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。
  **指向两扇打不开的门,比不指还糟。**

界面不撒谎
· 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。
· 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、
  往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。
· 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。
· 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。
· 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。

A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。
五道门禁全绿(971 passed)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
2026-08-02 13:41:44 +08:00

149 lines
5.7 KiB
TypeScript

/**
* 本地开发宿主 —— mock 工具表 + 模拟工位(工单 / 卡)+ 回放夹具(编码规范 §8.2)。
*
* 「系统外对着声明开发(本地 mock)」:开发者不必起系统就能跑活动;
* 夹具录下的出入参可直接喂给回放审(§7.3 两缝的同一副夹具)。
*/
import type { ActivityBase, ContractSpec } from './base.ts';
import type { ActivityDefinition, ActivityOutcome } from './define.ts';
import type { ChatCardDraft, LedgerKey, LedgerRow, ViewBlockLike, WorkOrderDraft } from './types.ts';
export type HostRecording = {
readonly views: ViewBlockLike[][];
readonly htmls: string[];
readonly says: string[];
readonly workOrders: WorkOrderDraft[];
readonly cards: ChatCardDraft[];
readonly toolCalls: Array<{ tool: string; input: Record<string, unknown> }>;
readonly intents: Array<{ tool: string; input: Record<string, unknown>; label: string }>;
readonly listeners: Array<{ eventType: string; keyFilter: Record<string, string>; deadline: string }>;
readonly llmCalls: Array<{ model: string; prompt: string }>;
readonly writes: Array<{ address: string; key: LedgerKey; patch: LedgerRow }>;
};
export type HostOptions = {
readonly contract: ContractSpec;
/** mock 工具表:工具名 → 固定出参 */
readonly tools?: Readonly<Record<string, (input: Record<string, unknown>) => Record<string, unknown>>>;
/** mock 模型:模型名 → 固定出参 */
readonly models?: Readonly<Record<string, (prompt: string) => { text: string; object?: unknown }>>;
/** 台账初值:地址 → 行 */
readonly ledger?: Readonly<Record<string, LedgerRow[]>>;
readonly memory?: ReadonlyArray<{ layer: 'fact' | 'pattern' | 'verdict' | 'exception'; statement: string }>;
};
export type MockHost = {
readonly base: ActivityBase;
readonly recording: HostRecording;
run(definition: ActivityDefinition): Promise<ActivityOutcome>;
};
export function createMockHost(options: HostOptions): MockHost {
const recording: HostRecording = {
views: [],
htmls: [],
says: [],
workOrders: [],
cards: [],
toolCalls: [],
intents: [],
listeners: [],
llmCalls: [],
writes: [],
};
const ledger = new Map<string, LedgerRow[]>(Object.entries(options.ledger ?? {}));
let cardSeq = 0;
const base: ActivityBase = {
contract: options.contract,
interaction: {
async pushView(blocks) {
recording.views.push([...blocks]);
},
async pushHtml(html) {
if (/<\s*script\b/i.test(html)) throw new Error('视图模版不得含 <script> —— 容器无脚本执行权');
recording.htmls.push(html);
},
async say(text) {
recording.says.push(text);
},
async issueWorkOrder(order) {
if (order.basis.length === 0) throw new Error('调用必附上下文:裸调用基座拒发');
recording.workOrders.push(order);
return { id: `wo-${(cardSeq += 1)}` };
},
async issueCard(card) {
recording.cards.push(card);
return { id: `card-${(cardSeq += 1)}` };
},
},
memory: {
async window() {
return [];
},
async recall(opts) {
const all = options.memory ?? [];
const layers = opts?.layers;
return all.filter((m) => layers === undefined || layers.includes(m.layer)).slice(0, opts?.limit ?? 50);
},
},
ledger: {
async read(address, where) {
const rows = ledger.get(address) ?? [];
if (where === undefined) return rows;
return rows.filter((row) => Object.entries(where).every(([k, v]) => row[k] === v));
},
async write(address, key, patch) {
recording.writes.push({ address, key, patch });
const rows = ledger.get(address) ?? [];
const index = rows.findIndex((row) => Object.entries(key).every(([k, v]) => row[k] === v));
if (index < 0) rows.push({ ...key, ...patch });
else rows[index] = { ...rows[index], ...patch };
ledger.set(address, rows);
},
},
tools: {
async invoke(tool, input) {
recording.toolCalls.push({ tool, input: { ...input } });
const impl = options.tools?.[tool];
if (impl === undefined) return { ok: false, error: { code: 'not-mocked', message: `工具未 mock:${tool}` } };
return { ok: true, output: impl({ ...input }) };
},
async intend(tool, input, label) {
recording.intents.push({ tool, input: { ...input }, label });
},
async listen(input) {
if (input.deadline.trim() === '') throw new Error('监听时限必配 —— 无死等');
recording.listeners.push({ ...input, keyFilter: { ...input.keyFilter } });
return { id: `listener-${(cardSeq += 1)}` };
},
},
llm: {
async generate(input) {
recording.llmCalls.push({ model: input.model, prompt: input.prompt });
const impl = options.models?.[input.model];
if (impl === undefined) throw new Error(`模型未 mock:${input.model}`);
return impl(input.prompt);
},
},
};
return {
base,
recording,
/*
* 跑一个回合。**没有 `run` 的活动不是残包** —— 案例活动的推进由 Loop 驱动,
* 行为写在清单里(案例模型 + 说明书 + DMN + 技能 + 台账声明),代码入口只被静态审扫描。
* 所以这里如实报错,不静默返回一个假的 outcome:那会让自测「过了」而什么也没测。
*/
run: (definition) => {
if (definition.run === undefined) {
return Promise.reject(
new Error(`活动 ${definition.name} 没有 run —— 案例活动的推进由 Loop 驱动,自测请测清单与资产,不要跑回合`),
);
}
return definition.run(base);
},
};
}