Archived
手工走通整条流,揪出五处编排层问题 —— 最要命的是假成功
把 `unified-quote` 从起单一路手工答到底(这是逐个开发活动之前该做的验证),
一路撞出五处,按撞到的顺序:
① 七个活动派出七张一模一样的「请处理」
槽位骨架给的是 `card: {}`,工单标题/字段全落回默认。收卡的人不知道自己走到
哪一步、这一步要干什么,整条流没法手工走通。→ 卡面自述:标题就是步骤标签,
依据带「第几步 / 共几步」,并如实说「这一步的自动化还没开发,先由你来办」。
② 走到第 3 步就 `exhausted` 终态、工作流转 abnormal
六个槽位共用 `humanCalls: 3`,而 A1 声明了 6 个人类步 —— 活动**结构上永远走不完**。
→ `budgetFor(n)` 按各槽位自己的步数给。步/项清单提成 `STEPS` 常量,
预算与骨架共用同一个数组,数不上不会漂移。治理台申报值也同源(否则台上摆假数)。
③ 六步答完,任务卡在「等人,却没有任何东西等人答」
`flow-runner` 写死了 `released = lastStepIsAcceptance && lastVerdictPassed` ——
末步不是验收工单,活动就永远不 release。→ 声明的六步来自 MVP 作业指导书,
不篡改语义,在其后**追加**一步「收口验收」。
④ 只走了 A1(一个节点实例)却报 `completed` ← **最危险的一处**
状态聚合把「没有在跑的了」当成完成。界面上是一单已完成的报价,业务上一步没走。
→ 「完成」凭**到过终点节点**;收敛而未到终点 = 卡住 = `abnormal`,进驾驶舱异常卡。
`aggregateWorkflowState` 收第三个入参(终点 id 集),不传退回旧口径。
⑤ 异或网关一条边都选不中时,流无声停住
④ 让它至少报异常了,但只说「有问题」不说「哪儿」。→ 发 `route-dead-end` 事件:
哪个节点、各出边的条件、判据里当时有哪些键。只记键名不记值 —— 诊断够用,
载荷不进事件明文。实测这条痕直接指出「G1 要 needAlternative,而键里没有」。
顺带:改槽位内容必须升包版(0.1.0 → 0.4.0)。「安装恒为新增、版本并存」是硬不变量,
同版重装是 no-op、manifest 一字不动 —— 我第一次改完重播种,屏幕报「已装入」而库里还是旧的。
`installSlots` 现在先问库、如实报「已装」还是「已在库,未改动 —— 要换内容请升包版」。
**走查后的诚实状态**:A1 走得完、活动 released;流**过不了 G1**,因为出口的必填去向旗
产不出来。这不是缺陷 —— 出口载荷由台账落地事实推导,槽位没实现台账写入就没有事实,
凭空捏一个就是在系统内部 mock。于是 A1 的开发完成判据也具体了:必须让
`needAlternative` / `needSourcing` 成为落地事实,否则 G1 永远判不了。债务簿已记。
另记一条闸的盲点:api:check 只快照 index/contract/port.d.ts,而 index 是 re-export 名单 ——
给纯函数加形参它看不见(本次实测)。改签名要手动同步 CONTRACT.md,别指望闸提醒。
typecheck ✓ lint ✓ depcruise 374 模块 ✓ api:check ✓ 490 测试 ✓(均以退出码验)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -24,22 +24,70 @@ export const SLOT_ARTIFACT = [
|
||||
|
||||
const checksum = createHash('sha256').update(SLOT_ARTIFACT).digest('hex');
|
||||
|
||||
/** flow 槽位的业务模型骨架:MVP 作业指导书的步骤标题 —— 未实现期一步一张执行工单,人工可兜 */
|
||||
const flowSkeleton = (steps: readonly string[]) => ({
|
||||
paradigm: 'flow' as const,
|
||||
steps: steps.map((label, i) => ({
|
||||
/**
|
||||
* flow 槽位的业务模型骨架:MVP 作业指导书的步骤标题 —— 未实现期一步一张执行工单,人工可兜。
|
||||
*
|
||||
* 卡面**必须自述**。空卡(`card: {}`)会让工单落回默认的「请处理」+ 一个「结果」框:
|
||||
* 七个活动派出七张一模一样的卡,收卡的人不知道自己走到哪一步、这一步要干什么 ——
|
||||
* 整条流就没法手工走通,而手工走通恰恰是逐个开发活动之前最该做的验证。
|
||||
* 所以这里把步骤标签、第几步、以及「实现待开发、先人工兜」的实情都摆到卡面上。
|
||||
* 认的键见 `wiring/engines.ts` 的 `cards.issue`:`title` / `ask` / `fields` / `basis`。
|
||||
*
|
||||
* **末步必须是验收工单**,否则活动永远收不了口。`flow-runner` 写死了这条:
|
||||
* `released = lastStepIsAcceptance(steps) && lastVerdictPassed(position)` ——
|
||||
* 六步全写成 execution 时,人把六张单都答完,任务却停在 `awaiting-human`:
|
||||
* 没有待答工单、没有待答卡,**等人却没东西给人答**,必交出口也发不出去,
|
||||
* 下游网关判不了、整条流走不出 A1(实测)。
|
||||
*
|
||||
* 声明的步骤来自 MVP 作业指导书,不篡改它的语义 —— 所以在其后**追加**一步收口验收:
|
||||
* 放行即活动办结、按出口选择发射;驳回即退回重做。
|
||||
*/
|
||||
const flowSkeleton = (steps: readonly string[]) => {
|
||||
const total = steps.length + 1;
|
||||
const failure = { retry: { times: 0, backoff: 'PT30S' }, fallback: { kind: 'blocked' as const } };
|
||||
const source = { k: '来源', v: '活动槽位的步序声明(实现待开发)' };
|
||||
|
||||
const work = steps.map((label, i) => ({
|
||||
name: `S${i + 1}`,
|
||||
label,
|
||||
input: {},
|
||||
kind: 'human' as const,
|
||||
workOrder: 'execution' as const,
|
||||
card: {},
|
||||
card: {
|
||||
title: label,
|
||||
ask: `这一步的自动化还没开发,先由你来办:${label}。办完把结果填回来,流程照常往下走。`,
|
||||
basis: [{ k: '步序', v: `第 ${i + 1} 步 / 共 ${total} 步` }, source],
|
||||
},
|
||||
deadline: 'P1D',
|
||||
// 失败策略声明给定,不临场:人步失败即受阻待人 —— 槽位期没有可重试的机器步
|
||||
failure: { retry: { times: 0, backoff: 'PT30S' }, fallback: { kind: 'blocked' as const } },
|
||||
})),
|
||||
parallelGroups: [],
|
||||
});
|
||||
failure,
|
||||
}));
|
||||
|
||||
return {
|
||||
paradigm: 'flow' as const,
|
||||
steps: [
|
||||
...work,
|
||||
{
|
||||
name: `S${total}`,
|
||||
label: '收口验收',
|
||||
input: {},
|
||||
kind: 'human' as const,
|
||||
workOrder: 'acceptance' as const,
|
||||
card: {
|
||||
title: '收口验收',
|
||||
basis: [
|
||||
{ k: '步序', v: `第 ${total} 步 / 共 ${total} 步(收口)` },
|
||||
{ k: '前置', v: `${steps.length} 步已由人工代办:${steps.join(' · ')}` },
|
||||
source,
|
||||
],
|
||||
},
|
||||
deadline: 'P1D',
|
||||
failure,
|
||||
},
|
||||
],
|
||||
parallelGroups: [],
|
||||
};
|
||||
};
|
||||
|
||||
/** loop 槽位:CMMN 骨架(MVP 案例图的项)+ 叙事一句话;DMN 待实现期再挂 */
|
||||
const loopSkeleton = (items: readonly string[], narrative: string) => ({
|
||||
@@ -52,7 +100,22 @@ const loopSkeleton = (items: readonly string[], narrative: string) => ({
|
||||
const identity = (activityId: string, name: string, paradigm: 'flow' | 'loop') => ({
|
||||
activityId,
|
||||
name,
|
||||
packageVersion: '0.1.0',
|
||||
/*
|
||||
* 包版本 —— 改了槽位携带的东西就必须往上走。
|
||||
*
|
||||
* 「安装恒为新增、版本并存」是硬不变量:同 (activityId, version) 再装是 no-op,
|
||||
* 库里的 manifest 一个字都不会动。所以 0.1.0 期间把卡面从空卡改成自述卡,
|
||||
* 重播种会「静默成功」—— 打印「已装入」,库里还是旧的(实测踩到)。
|
||||
*
|
||||
* 0.2.0:步骤卡面自述(标题 / 要你答 / 步序依据)。
|
||||
* 0.3.0:人工调用预算按各槽位自己的步数给 —— 共用 3 会让 6 步的 A1 走到第 3 步就
|
||||
* `exhausted` 终态、结构上永远走不完(实测)。授权面申报值同源。
|
||||
* 0.4.0:flow 骨架末步追加**收口验收**—— 没有它活动永远不 release,
|
||||
* 六步答完就卡在「等人却没东西给人答」(实测)。
|
||||
* 契约的入参出口一直没动,故 `contract.version` 始终 0.1.0。
|
||||
* 在途任务按启动时版本跑完,不受影响。
|
||||
*/
|
||||
packageVersion: '0.4.0',
|
||||
category: '统一报价',
|
||||
paradigm,
|
||||
sdkVersion: '0.1.0-m5',
|
||||
@@ -64,7 +127,46 @@ const harness = { memoryWindow: 20, stallAfter: 'PT45M', nudge: [] };
|
||||
/** 七件契约里逐槽位相同的空段 —— 槽位期没有必交付物/验收细则,实现期再补 */
|
||||
const contractRest = { requiredDeliverables: [], acceptanceCriteria: [], prohibitions: [] };
|
||||
|
||||
const budget = { steps: 40, tokens: 200_000, activeMinutes: 60, humanCalls: 3 };
|
||||
/**
|
||||
* 各槽位的步/项清单 —— **预算与骨架共用同一个数组**,数不上不会漂移。
|
||||
* flow 是声明式人类步(一步一张工单);loop 的项由 Agent 临场推进,
|
||||
* 最坏情况一项问一次人,所以也按项数给预算。
|
||||
*/
|
||||
const STEPS = {
|
||||
rfqIntake: ['接收登记', '追问补全信息', '逐条解析归一', '查历史价', '逐条定状况标', '定去向与派单'],
|
||||
altSelection: ['替代对标表', '初筛清单'],
|
||||
sourcingInquiry: ['寻源比价表', '供应商回价单'],
|
||||
costPricing: ['取价基准', '逐条定价'],
|
||||
quoteNegotiation: ['正式报价单'],
|
||||
intelArchive: ['结果登记', '画像更新', '落地率统计'],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 预算 —— `humanCalls` 必须**够这个槽位自己的人类步数**,不能六个槽位共用一个数。
|
||||
*
|
||||
* 曾经共用 `humanCalls: 3`,而 A1 声明了 6 个人类步:手工走到第 3 步就 `exhausted`
|
||||
* 终态、工作流转 abnormal,活动**结构上永远走不完**(实测:used 3 / cap 3,停在 A1)。
|
||||
* 槽位期是一步一张工单,所以按步数给:声明的 n 步 + 1 张收口验收 + 1 次余量。
|
||||
* 实现期活动自己声明真实预算时,这条约束不变:实际要问人的次数 ≤ humanCalls。
|
||||
*/
|
||||
const budgetFor = (declaredSteps: number) => ({
|
||||
steps: 40,
|
||||
tokens: 200_000,
|
||||
activeMinutes: 60,
|
||||
// n 张执行 + 1 张验收(flowSkeleton 追加的收口步)+ 1 次余量
|
||||
humanCalls: declaredSteps + 2,
|
||||
});
|
||||
|
||||
/**
|
||||
* 授权面申报的人工调用预算 —— 与运行时真正吃的那个数**同源**。
|
||||
* 运行时只认 `resources.budget.humanCalls`(见 `wiring/engines.ts` 的 `caps`);
|
||||
* `authorization.humanCallBudget` 只给治理台显示。两者不一致,治理台就会摆一个假数。
|
||||
*/
|
||||
const authorizationFor = (humanSteps: number) => ({
|
||||
discretion: [],
|
||||
preauthorizedActions: [],
|
||||
humanCallBudget: budgetFor(humanSteps).humanCalls,
|
||||
});
|
||||
|
||||
export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
/* ── A1 客户RFQ受理(SOP · BD) ──────────────────────────────────── */
|
||||
@@ -108,8 +210,8 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
|
||||
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'], budget },
|
||||
authorization: authorizationFor(STEPS.rfqIntake.length),
|
||||
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'], budget: budgetFor(STEPS.rfqIntake.length) },
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
@@ -127,7 +229,7 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
]),
|
||||
},
|
||||
assets: {
|
||||
businessModel: flowSkeleton(['接收登记', '追问补全信息', '逐条解析归一', '查历史价', '逐条定状况标', '定去向与派单']),
|
||||
businessModel: flowSkeleton(STEPS.rfqIntake),
|
||||
skills: [],
|
||||
viewTemplates: [],
|
||||
cardSchemas: {},
|
||||
@@ -178,8 +280,8 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
|
||||
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['FAE'], budget },
|
||||
authorization: authorizationFor(STEPS.altSelection.length),
|
||||
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['FAE'], budget: budgetFor(STEPS.altSelection.length) },
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
@@ -190,7 +292,7 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
},
|
||||
assets: {
|
||||
businessModel: loopSkeleton(
|
||||
['替代对标表', '初筛清单'],
|
||||
STEPS.altSelection,
|
||||
'MVP §1.5:上接 G1 是否需要选型;齐了即收 —— 必做项完成且无进行中项即结案,办结经 G2 定去向。',
|
||||
),
|
||||
skills: [],
|
||||
@@ -243,13 +345,13 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
|
||||
authorization: authorizationFor(STEPS.sourcingInquiry.length),
|
||||
resources: {
|
||||
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
|
||||
skills: [],
|
||||
models: [],
|
||||
roles: ['PRM'],
|
||||
budget,
|
||||
budget: budgetFor(STEPS.sourcingInquiry.length),
|
||||
},
|
||||
},
|
||||
declarations: {
|
||||
@@ -266,7 +368,7 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
},
|
||||
assets: {
|
||||
businessModel: loopSkeleton(
|
||||
['寻源比价表', '供应商回价单'],
|
||||
STEPS.sourcingInquiry,
|
||||
'MVP §1.7:上接 G2 是否需要寻源;齐了即收 —— 必做项完成且无进行中项即结案,办结转 A4 成本定价。',
|
||||
),
|
||||
skills: [],
|
||||
@@ -318,8 +420,8 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
|
||||
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'], budget },
|
||||
authorization: authorizationFor(STEPS.costPricing.length),
|
||||
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'], budget: budgetFor(STEPS.costPricing.length) },
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
@@ -329,7 +431,7 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
ledger: declarationsFor(['intel.history_price', 'customer.profile', 'project.quote_draft']),
|
||||
},
|
||||
assets: {
|
||||
businessModel: flowSkeleton(['取价基准', '逐条定价']),
|
||||
businessModel: flowSkeleton(STEPS.costPricing),
|
||||
skills: [],
|
||||
viewTemplates: [],
|
||||
cardSchemas: {},
|
||||
@@ -379,13 +481,13 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
|
||||
authorization: authorizationFor(STEPS.quoteNegotiation.length),
|
||||
resources: {
|
||||
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
|
||||
skills: [],
|
||||
models: [],
|
||||
roles: ['BD'],
|
||||
budget,
|
||||
budget: budgetFor(STEPS.quoteNegotiation.length),
|
||||
},
|
||||
},
|
||||
declarations: {
|
||||
@@ -397,7 +499,7 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
},
|
||||
assets: {
|
||||
businessModel: loopSkeleton(
|
||||
['正式报价单'],
|
||||
STEPS.quoteNegotiation,
|
||||
'MVP §1.4:上接审批通过进入;发出标准报价并跟谈,办结经 G4 议价结果定去向(成交 / 有异议 / 拒绝或超时)。',
|
||||
),
|
||||
skills: [],
|
||||
@@ -449,8 +551,8 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
|
||||
resources: { tools: [], skills: [], models: [], roles: ['BD'], budget },
|
||||
authorization: authorizationFor(STEPS.intelArchive.length),
|
||||
resources: { tools: [], skills: [], models: [], roles: ['BD'], budget: budgetFor(STEPS.intelArchive.length) },
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
@@ -460,7 +562,7 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
ledger: declarationsFor(['intel.history_price', 'customer.profile', 'project.landing_stats']),
|
||||
},
|
||||
assets: {
|
||||
businessModel: flowSkeleton(['结果登记', '画像更新', '落地率统计']),
|
||||
businessModel: flowSkeleton(STEPS.intelArchive),
|
||||
skills: [],
|
||||
viewTemplates: [],
|
||||
cardSchemas: {},
|
||||
@@ -479,10 +581,23 @@ export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
|
||||
export async function installSlots(container: Container): Promise<void> {
|
||||
for (const manifest of MVP_SLOTS) {
|
||||
const { activityId, packageVersion, name, paradigm } = manifest.identity;
|
||||
|
||||
/*
|
||||
* 先问库里有没有这一版 —— 因为「安装恒为新增、版本并存」:同版再装是 no-op,
|
||||
* manifest 一个字不动。不问就报「已装入」等于**撒谎**:改了槽位内容重播种,
|
||||
* 屏幕说装好了,库里还是旧的,人拿着旧行为查新代码(实测踩到,那次是卡面没换)。
|
||||
*/
|
||||
const already = await container.registry
|
||||
.manifestOf(activityId, packageVersion)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
const verdict = await container.registry.install(manifest, SLOT_ARTIFACT);
|
||||
const errors = verdict.staticFindings.filter((finding) => finding.severity === 'error');
|
||||
process.stdout.write(
|
||||
` 槽位 ${manifest.identity.name}(${manifest.identity.paradigm})静态审 ${verdict.staticFindings.length} 条` +
|
||||
` 槽位 ${name}(${paradigm})${already ? ` v${packageVersion} 已在库,未改动 —— 要换内容请升包版` : ` v${packageVersion} 已装`}` +
|
||||
` · 静态审 ${verdict.staticFindings.length} 条` +
|
||||
(errors.length > 0 ? ` —— ERROR: ${errors.map((e) => e.message).join('; ')}` : '') +
|
||||
'\n',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user