MVP底座③:六活动槽位清单 —— 契约就绪,实现留白,工具按名解耦

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 22:45:32 +08:00
co-authored by Claude Fable 5
parent d00e29091e
commit 90ca92aa0e
2 changed files with 534 additions and 0 deletions
@@ -0,0 +1,44 @@
/**
* 六个活动槽位 —— 契约就绪、实现留白。守:范式/岗位/出口与 MVP 一致;
* 声明只有名(工具按名引用、域对象按名取)—— 活动与工具的解耦就靠这一条。
* A5 定价审批是**内置审批点**,零开发、不入库 —— 不在槽位之列。
*/
import { describe, expect, it } from 'vitest';
import { MVP_SLOTS, SLOT_ARTIFACT } from '../seed-mvp-slots.ts';
describe('MVP_SLOTS', () => {
it('六槽位,id 与范式如 MVP(A5 审批点内置,不入库)', () => {
const byId = new Map(MVP_SLOTS.map((m) => [m.identity.activityId as string, m.identity.paradigm]));
expect(MVP_SLOTS).toHaveLength(6);
expect(byId.get('rfq-intake')).toBe('flow');
expect(byId.get('alt-selection')).toBe('loop');
expect(byId.get('sourcing-inquiry')).toBe('loop');
expect(byId.get('cost-pricing')).toBe('flow');
expect(byId.has('pricing-approval')).toBe(false);
expect(byId.get('quote-negotiation')).toBe('loop');
expect(byId.get('intel-archive')).toBe('flow');
});
it('A1 出口带两枚去向旗;A2 透传 needSourcing —— 网关条件的数据来源', () => {
const a1 = MVP_SLOTS.find((m) => m.identity.activityId === 'rfq-intake')!;
const p1 = (a1.contract.output.mandatory[0]!.payload as { properties?: Record<string, unknown> }).properties ?? {};
expect(Object.keys(p1)).toEqual(expect.arrayContaining(['needAlternative', 'needSourcing']));
const a2 = MVP_SLOTS.find((m) => m.identity.activityId === 'alt-selection')!;
const p2 = (a2.contract.output.mandatory[0]!.payload as { properties?: Record<string, unknown> }).properties ?? {};
expect(Object.keys(p2)).toContain('needSourcing');
});
it('A6 出口带 outcome 三值 —— G4 议价结果的判据', () => {
const a6 = MVP_SLOTS.find((m) => m.identity.activityId === 'quote-negotiation')!;
const p6 = (a6.contract.output.mandatory[0]!.payload as { properties?: Record<string, Record<string, unknown>> })
.properties ?? {};
expect(p6['outcome']?.['enum']).toEqual(['won', 'revise', 'lost']);
});
it('槽位不携实现:入口是抛「未实现」的占位,工具声明只有名', () => {
expect(SLOT_ARTIFACT).toContain('未实现');
for (const m of MVP_SLOTS) {
for (const t of m.declarations.tools) expect(typeof t.name).toBe('string');
}
});
});
+490
View File
@@ -0,0 +1,490 @@
/**
* 六个活动槽位(docs/MVP示例工作流.html §1.1–1.7)—— **契约就绪、实现留白**。
*
* 槽位 = 活动的契约面:身份 / 七件契约(入参、出口)/ 声明(域对象、工具名)/ 业务模型骨架。
* 与工具的解耦就在声明:`declarations.tools` 只写**名**,注册、连接值、白名单全在治理侧。
* A5 定价审批是**内置审批点**(审批不是任务,零开发、不入库),不在槽位之列。
*
* 实现待底座完备后逐个开发:届时以新包版本经安装通道换入,槽位契约不动。
*/
import { createHash } from 'node:crypto';
import { activityManifestSchema } from '@contracts';
import type { ActivityManifest } from '@contracts';
import type { Container } from './container.ts';
import { declarationsFor } from './seed-mvp-data.ts';
/**
* 槽位占位入口 —— **不是实现**:装载它只会得到「未实现」的诚实报错。
* mock 只许在外界:这里绝不发既定载荷装作跑过(README 平移规则)。
*/
export const SLOT_ARTIFACT = [
'// 活动槽位:契约已就绪,实现待底座完备后逐个开发。',
"throw new Error('活动尚未实现 —— 这是槽位,契约见治理台活动库');",
].join('\n');
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) => ({
name: `S${i + 1}`,
label,
input: {},
kind: 'human' as const,
workOrder: 'execution' as const,
card: {},
deadline: 'P1D',
// 失败策略声明给定,不临场:人步失败即受阻待人 —— 槽位期没有可重试的机器步
failure: { retry: { times: 0, backoff: 'PT30S' }, fallback: { kind: 'blocked' as const } },
})),
parallelGroups: [],
});
/** loop 槽位:CMMN 骨架(MVP 案例图的项)+ 叙事一句话;DMN 待实现期再挂 */
const loopSkeleton = (items: readonly string[], narrative: string) => ({
paradigm: 'loop' as const,
cmmnXml: `<case><casePlanModel autoComplete="true">${items.map((it) => `<task name="${it}"/>`).join('')}</casePlanModel></case>`,
narrative,
dmn: { drd: [], tables: [] },
});
const identity = (activityId: string, name: string, paradigm: 'flow' | 'loop') => ({
activityId,
name,
packageVersion: '0.1.0',
category: '统一报价',
paradigm,
sdkVersion: '0.1.0-m5',
checksum,
});
const harness = { memoryWindow: 20, stallAfter: 'PT45M', nudge: [] };
/** 七件契约里逐槽位相同的空段 —— 槽位期没有必交付物/验收细则,实现期再补 */
const contractRest = { requiredDeliverables: [], acceptanceCriteria: [], prohibitions: [] };
const budget = { steps: 40, tokens: 200_000, activeMinutes: 60, humanCalls: 3 };
export const MVP_SLOTS: readonly ActivityManifest[] = [
/* ── A1 客户RFQ受理(SOP · BD) ──────────────────────────────────── */
activityManifestSchema.parse({
identity: identity('rfq-intake', '客户RFQ受理', 'flow'),
contract: {
activityId: 'rfq-intake',
version: '0.1.0',
input: {
data: {
type: 'object',
required: ['customerCode', 'rfqRef'],
properties: {
customerCode: { type: 'string' },
rfqRef: { type: 'string' },
subject: { type: 'string' },
body: { type: 'string' },
quotedAt: { type: 'string' },
},
},
messageAllowed: true,
},
output: {
mandatory: [
{
name: 'intake',
description: '统一受理记录(打标)+ 两枚去向旗',
payload: {
type: 'object',
required: ['records', 'needAlternative', 'needSourcing'],
properties: {
records: { type: 'array' },
pn: { type: 'string' },
type_tag: { type: 'string' },
needAlternative: { type: 'boolean' },
needSourcing: { type: 'boolean' },
},
},
},
],
selective: [],
},
...contractRest,
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'], budget },
},
declarations: {
models: [],
listeners: [],
harness,
tools: [{ name: 'crm.history-price-read' }],
ledger: declarationsFor([
'project.rfq_file',
'project.intake_record',
'project.pn_class',
'project.alt_request',
'project.inquiry_request',
'intel.history_price',
'customer.profile',
]),
},
assets: {
businessModel: flowSkeleton(['接收登记', '追问补全信息', '逐条解析归一', '查历史价', '逐条定状况标', '定去向与派单']),
skills: [],
viewTemplates: [],
cardSchemas: {},
distillHooks: [],
},
samples: [
{
name: '契约样例:受理一单',
input: { customerCode: 'C001', rfqRef: 'RFQ-001' },
expect: { ports: ['intake'] },
},
],
entry: { file: 'index.js', checksum },
}),
/* ── A2 替代选型(Case · FAE) ──────────────────────────────────── */
activityManifestSchema.parse({
identity: identity('alt-selection', '替代选型', 'loop'),
contract: {
activityId: 'alt-selection',
version: '0.1.0',
input: {
data: {
type: 'object',
required: ['material'],
properties: {
material: { type: 'string' },
chip_type: { type: 'string' },
spec_params: { type: 'string' },
scenario: { type: 'string' },
needSourcing: { type: 'boolean' },
},
},
messageAllowed: true,
},
output: {
mandatory: [
{
name: 'benchmark',
description: '替代对标表(已签字)+ needSourcing 原样带出(G2 在本路径读它)',
payload: {
type: 'object',
required: ['rows', 'needSourcing'],
properties: { rows: { type: 'array' }, needSourcing: { type: 'boolean' } },
},
},
],
selective: [],
},
...contractRest,
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['FAE'], budget },
},
declarations: {
models: [],
listeners: [],
harness,
tools: [{ name: 'crm.history-price-read' }],
ledger: declarationsFor(['intel.chip_spec', 'intel.public_select', 'project.alt_benchmark']),
},
assets: {
businessModel: loopSkeleton(
['替代对标表', '初筛清单'],
'MVP §1.5:上接 G1 是否需要选型;齐了即收 —— 必做项完成且无进行中项即结案,办结经 G2 定去向。',
),
skills: [],
viewTemplates: [],
cardSchemas: {},
distillHooks: [],
},
samples: [
{
name: '契约样例:一件替代评估',
input: { material: 'LM358DR' },
expect: { ports: ['benchmark'] },
},
],
entry: { file: 'index.js', checksum },
}),
/* ── A3 寻源询价(Case · PRM) ──────────────────────────────────── */
activityManifestSchema.parse({
identity: identity('sourcing-inquiry', '寻源询价', 'loop'),
contract: {
activityId: 'sourcing-inquiry',
version: '0.1.0',
input: {
data: {
type: 'object',
required: ['pn'],
properties: {
pn: { type: 'string' },
brand: { type: 'string' },
era: { type: 'string' },
project_stage: { type: 'string' },
accept_alt: { type: 'boolean' },
},
},
messageAllowed: true,
},
output: {
mandatory: [
{
name: 'compare',
description: '寻源比价表 + 供应商底价表',
payload: {
type: 'object',
required: ['rows', 'floors'],
properties: { rows: { type: 'array' }, floors: { type: 'array' } },
},
},
],
selective: [],
},
...contractRest,
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
resources: {
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
skills: [],
models: [],
roles: ['PRM'],
budget,
},
},
declarations: {
models: [],
listeners: [],
harness,
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
ledger: declarationsFor([
'supplier.channel_dir',
'supplier.dead_stock',
'project.sourcing_compare',
'project.supplier_floor',
]),
},
assets: {
businessModel: loopSkeleton(
['寻源比价表', '供应商回价单'],
'MVP §1.7:上接 G2 是否需要寻源;齐了即收 —— 必做项完成且无进行中项即结案,办结转 A4 成本定价。',
),
skills: [],
viewTemplates: [],
cardSchemas: {},
distillHooks: [],
},
samples: [
{
name: '契约样例:一单询价',
input: { pn: 'LM358DR' },
expect: { ports: ['compare'] },
},
],
entry: { file: 'index.js', checksum },
}),
/* ── A4 成本定价(SOP · BD) ────────────────────────────────────── */
activityManifestSchema.parse({
identity: identity('cost-pricing', '成本定价', 'flow'),
contract: {
activityId: 'cost-pricing',
version: '0.1.0',
input: {
data: {
type: 'object',
required: ['records'],
properties: {
records: { type: 'array' },
floors: { type: 'array' },
benchmark: { type: 'array' },
customerCode: { type: 'string' },
},
},
messageAllowed: true,
},
output: {
mandatory: [
{
name: 'draft',
description: '报价草案:逐条定价,先过底价门禁再提审',
payload: {
type: 'object',
required: ['rows'],
properties: { rows: { type: 'array' }, quote_no: { type: 'string' } },
},
},
],
selective: [],
},
...contractRest,
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'], budget },
},
declarations: {
models: [],
listeners: [],
harness,
tools: [{ name: 'crm.history-price-read' }],
ledger: declarationsFor(['intel.history_price', 'customer.profile', 'project.quote_draft']),
},
assets: {
businessModel: flowSkeleton(['取价基准', '逐条定价']),
skills: [],
viewTemplates: [],
cardSchemas: {},
distillHooks: [],
},
samples: [
{
name: '契约样例:一批草案',
input: { records: [] },
expect: { ports: ['draft'] },
},
],
entry: { file: 'index.js', checksum },
}),
/* ── A6 报价议价(Case · BD) ───────────────────────────────────── */
activityManifestSchema.parse({
identity: identity('quote-negotiation', '报价议价', 'loop'),
contract: {
activityId: 'quote-negotiation',
version: '0.1.0',
input: {
data: {
type: 'object',
required: ['rows'],
properties: { rows: { type: 'array' }, quote_no: { type: 'string' }, customerCode: { type: 'string' } },
},
messageAllowed: true,
},
output: {
mandatory: [
{
name: 'formal',
description: '正式报价单(已送出)+ 跟进提醒 + 议价结果',
payload: {
type: 'object',
required: ['outcome'],
properties: {
outcome: { type: 'string', enum: ['won', 'revise', 'lost'] },
quote_no: { type: 'string' },
customer_code: { type: 'string' },
rows: { type: 'array' },
},
},
},
],
selective: [],
},
...contractRest,
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
resources: {
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
skills: [],
models: [],
roles: ['BD'],
budget,
},
},
declarations: {
models: [],
listeners: [],
harness,
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
ledger: declarationsFor(['project.formal_quote', 'project.followup']),
},
assets: {
businessModel: loopSkeleton(
['正式报价单'],
'MVP §1.4:上接审批通过进入;发出标准报价并跟谈,办结经 G4 议价结果定去向(成交 / 有异议 / 拒绝或超时)。',
),
skills: [],
viewTemplates: [],
cardSchemas: {},
distillHooks: [],
},
samples: [
{
name: '契约样例:一单议价',
input: { rows: [] },
expect: { ports: ['formal'] },
},
],
entry: { file: 'index.js', checksum },
}),
/* ── A7 情报归档(SOP · BD) ────────────────────────────────────── */
activityManifestSchema.parse({
identity: identity('intel-archive', '情报归档', 'flow'),
contract: {
activityId: 'intel-archive',
version: '0.1.0',
input: {
data: {
type: 'object',
required: ['outcome'],
properties: {
outcome: { type: 'string' },
quote_no: { type: 'string' },
customer_code: { type: 'string' },
quotedAt: { type: 'string' },
},
},
messageAllowed: true,
},
output: {
mandatory: [
{
name: 'stats',
description: '不成交也留资产:情报、客户画像、落地率',
payload: {
type: 'object',
required: ['landing_rate'],
properties: { landing_rate: { type: 'number' } },
},
},
],
selective: [],
},
...contractRest,
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
resources: { tools: [], skills: [], models: [], roles: ['BD'], budget },
},
declarations: {
models: [],
listeners: [],
harness,
tools: [],
ledger: declarationsFor(['intel.history_price', 'customer.profile', 'project.landing_stats']),
},
assets: {
businessModel: flowSkeleton(['结果登记', '画像更新', '落地率统计']),
skills: [],
viewTemplates: [],
cardSchemas: {},
distillHooks: [],
},
samples: [
{
name: '契约样例:一次归档',
input: { outcome: 'lost' },
expect: { ports: ['stats'] },
},
],
entry: { file: 'index.js', checksum },
}),
];
export async function installSlots(container: Container): Promise<void> {
for (const manifest of MVP_SLOTS) {
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} 条` +
(errors.length > 0 ? ` —— ERROR: ${errors.map((e) => e.message).join('; ')}` : '') +
'\n',
);
}
}