Archived
手改是**兜 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
149 lines
5.7 KiB
TypeScript
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);
|
|
},
|
|
};
|
|
}
|