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
BPOS-MVP/docs/archive/plan-2026-07-27-mvp-foundation.md
ChaiHaoandClaude Opus 5 1d2d5ebb53 归档过时文档,立仓库根 CLAUDE.md 记最新状态
**归档四份**(docs/archive/,每份头部带归档说明与现行去向,另配 archive/README.md 索引):
  · MVP规格包-README.md(原 docs/README.md)—— 从零实现的启动包,MVP 已建成,
    启动提示词与里程碑纪律不再是现行指引;代码注释「README《归一化对照表》」等引用
    的各节原文保留,查历史语境仍来这里;
  · PROGRESS.md —— M0-M6 全部完成后停止维护,「下一步」多数已做完或被后续决策取代;
  · plan-2026-07-27-{mvp-foundation,three-surfaces}.md —— 两份已执行完毕的实施计划
    (三面形态此后又有演进:运营撤数据区、触发器与大模型独立成区)。
  语义源三份 html(操作系统/活动模型/MVP示例)不归档 —— 仍是 OS/AM 条款的出处,
  只是 flow 相关章节读作历史记述。

**新立 CLAUDE.md**(仓库根,每会话装载):现状快照(三面一体 · 编排层测死 · 范式退役
一律案例模型 · 无模型密钥时三套 e2e 跳过 · 下一件大事 = 按活动开发规范第 12 章销账)、
文档权威地图(活文档/语义源/前端权威/归档)、八条铁规矩(五闸以退出码验 · 契约四步 ·
§12.5 禁止清单 · mock 只在外界 · 密钥只从环境 · 试跑用 AI 账号测完清数据 ·
安装恒新增改必升版 · 界面不撒谎)、常用命令与结构速查。

五闸绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-30 03:15:00 +08:00

51 KiB
Raw Permalink Blame History

⚠️ 已归档(2026-07-30) —— MVP 底座计划,已执行完毕。现行状态看仓库根 CLAUDE.md。

MVP 底座(四域数据 · 1:1 流程包 · 三类工具 · 卡片设计台)实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 按 docs/MVP示例工作流.html 一比一落一个流程包实例(统一报价:6 活动槽位 + 1 审批点 + 3 网关 + 双终点),数据按 客户/情报/供应商/项目 四域重划,工具箱呈现 功能/LLM/人类 三类已有工具与 自定义导入/MCP 接入,设计台四库全部卡片化;活动只落槽位契约,不刻画实现。

Architecture: 种子重写为三件套(seed-mvp-data 四域对象声明 / seed-mvp-slots 六槽位清单经安装通道装入 / seed-mvp-flow 流程包 v1 + 手绘图坐标 1:1 + 触发器与通道),声明物化自动建四域十八对象;活动与工具靠声明名解耦(declarations.tools 只写名字,白名单在治理台)。前端:domains.ts 换四域短名,设计台四页换 g-tile/卡片网格,工具箱一页四段。

Tech Stack: 既有栈零新增 —— zod 契约 + 安装通道(registry.install 顺带声明物化)、diagram-js 手绘图存证(pkg.diagram)、React + g-* 皮肤(console.css 已有类 + 既有「三面补充」节)。

Global Constraints

  • 五门全绿才算一个任务完成:pnpm typecheck && pnpm lint && pnpm depcruise && pnpm api:check && pnpm test(契约变更:同步 CONTRACT.md → pnpm api:check --update → git add api-report)。
  • §12.5 禁用:any、as unknown as、@ts-ignore、domain 层 IO、全局可变状态、删测试、改门禁配置。
  • mock 只允许出现在外界:槽位活动不得内置假执行(不发既定载荷、不装作跑过);未实现就是未实现,起单后任务停在那里是诚实状态。
  • 流程包只落一个实例:种子只产出 unified-quote v1(published),不再产出多版本;验收含「库里恰一行」。
  • 活动与工具完全解耦:槽位清单 declarations.tools 只引用工具名;工具注册、连接值、白名单全在治理侧(seed 的 toolbox 段),槽位不含任何工具实现或 import。
  • 活动细节不刻画:槽位的 assets.businessModel 只携 MVP 文档里的步骤/里程碑骨架(标题级),入口产物为注释性占位 JS(不可执行语义,运行时如实报「未实现」)。
  • 密钥只从环境读(JCAI_DEEPSEEK_API_KEY);种子口令走 JCAI_SEED_PASSWORD。
  • 提交信息中文一句话,结尾两行: Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> 与 Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m。
  • 服务:API :8787、Web :5273;账号 Chaihao / ch19900614;curl 先 POST /api/login 拿 cookie。
  • 本计划会整库重建(docker compose down -v)—— 现有运行数据(在途单/消息)全部清掉,这是四域重划的必然代价,执行前无需再确认(用户已在需求 2 中决定重划)。

版图(先读懂再动手)

四域十九对象(需求 2 的落法)

划分原则:流转单据(过程)归项目域;沉淀资产按内容归 客户/情报/供应商。 定价审批表不入台账:MVP 标其载体为「审批流」—— 它就是审批点的审批单记录,落在审批机制里(sys 侧),不物化为数据对象。

域 键 短名 对象(object 键 · 中文名)
项目 project 项目 rfq_file RFQ/BOM文件 · intake_record 统一受理记录 · pn_class 料号分类表 · alt_request 替代评估请求 · inquiry_request 询价请求 · supplier_floor 供应商底价表 · alt_benchmark 替代对标表 · sourcing_compare 寻源比价表 · quote_draft 报价草案 · formal_quote 正式报价单 · followup 跟进提醒 · landing_stats 报价落地率统计
客户信息 customer 客户 profile 客户资料库
市场与销售情报 intel 情报 history_price 历史价格情报 · chip_spec 芯片规格书 · public_select 公网选型库
供应商信息 supplier 供应商 channel_dir 供应商渠道目录 · dead_stock 呆滞库存表

字段全表(name:type,*=主键;type ∈ text/number/boolean/date/timestamp/json,label 用 MVP 中文名):

project.rfq_file      RFQ/BOM文件   *rfq_ref:text *pn:text qty_eau:text brand:text analog_flag:boolean aec_cert:text target_price:number lead_time:text
project.intake_record 统一受理记录  *customer_code:text *pn:text brand:text eau:text type_tag:text target_price:number status:text
project.pn_class      料号分类表    *pn:text type_tag:text basis:text history_hit:boolean review_note:text
project.alt_request   替代评估请求  *material:text chip_type:text spec_params:text scenario:text requester:text
project.inquiry_request 询价请求    *pn:text brand:text era:text project_stage:text accept_alt:boolean unit_usage:text tp:number scenario:text
project.supplier_floor 供应商底价表 *pn:text *supplier:text floor_price:number moq:text lead_time:text
project.alt_benchmark 替代对标表    *original_pn:text *candidate_pn:text alt_grade:text param_diff:text diff_risk:text mod_eval:text verify_advice:text
project.sourcing_compare 寻源比价表 *pn:text *supplier:text quote_price:number moq:text lead_time:text channel_grade:text condition:text verdict:text
project.quote_draft   报价草案      *pn:text proposed_price:number margin:number deviation:number
project.formal_quote  正式报价单    *pn:text unit_price_cny:number lead_time:text spq:text moq:text trade_terms:text valid_until:date
project.followup      跟进提醒      *quote_no:text customer_code:text d3:date d7:date reply_status:text
project.landing_stats 报价落地率统计 *period:text landing_rate:number type_a_hit:number d3_reply_rate:number return_rate:number
customer.profile      客户资料库    *customer_code:text grade:text tags:text payment_terms:text industry:text risk_level:text annual_purchase:number
intel.history_price   历史价格情报  *pn:text *quoted_at:timestamp brand:text unit_price_cny:number lead_time:text moq:text valid_until:date
intel.chip_spec       芯片规格书    *pn:text key_params:text package:text version_date:date
intel.public_select   公网选型库    *alt_relation:text param_compare:text spec_ref:text lifecycle:text price_ref:text
supplier.channel_dir  供应商渠道目录 *supplier:text channel_type:text brand_lines:text contact:text
supplier.dead_stock   呆滞库存表    *pn:text stock_qty:number stock_age:text cost_price:number

六个活动槽位 + 一个审批点(契约面;MVP §1.1–1.7 一比一)

槽位 id 节点 名 范式 岗位 入(契约 input) 出口(output.mandatory) 声明的域对象(reads→writes)
rfq-intake A1 客户RFQ受理 flow BD subject/body/customerCode/rfqRef intake:统一受理记录(records)+ needAlternative:boolean + needSourcing:boolean 读 intel.history_price · customer.profile → 写 project.rfq_file · intake_record · pn_class · alt_request · inquiry_request
alt-selection A2 替代选型 loop FAE 替代评估请求(material/chip_type/spec_params/scenario)+ needSourcing 透传 benchmark:替代对标表 + needSourcing:boolean(原样带出) 读 intel.chip_spec · intel.public_select → 写 project.alt_benchmark
sourcing-inquiry A3 寻源询价 loop PRM 询价请求(pn/brand/era/…) compare:寻源比价表 + 供应商底价表 读 supplier.channel_dir · supplier.dead_stock → 写 project.sourcing_compare · project.supplier_floor
cost-pricing A4 成本定价 flow BD 受理记录 + 底价/对标(可空) draft:报价草案 读 intel.history_price · customer.profile → 写 project.quote_draft
quote-negotiation A6 报价议价 loop BD 报价草案(已批准)+ 定价审批表 formal:正式报价单 + 跟进提醒 + outcome:"won"|"revise"|"lost" 写 project.formal_quote · project.followup
intel-archive A7 情报归档 flow BD 议价结果与单据 stats:落地率统计 写 intel.history_price · customer.profile · project.landing_stats

A5 定价审批 = 内置审批点(教义:审批点与网关是系统内置要素,零开发、不入库;审批不是任务)。 审批点自带 通过/驳回 两出口:通过走 A5→A6 边;驳回走 reject: { kind:'reopen', nodeId:'A4', mapping } 声明(v1 旧包的 AP 节点即此范式,mapping 照抄其式样)。G3「审批结果」被审批点吸收 —— 判据就是审批结果本身,不另立网关;图上以琥珀审批框 + 通过边(ok)+ 底部「退回调整→A4」卡点呈现, 与 MVP 图差一颗菱形,语义同构(此取舍已与用户确认:A5 必须是审批点)。

流程包 1:1(节点 · 坐标取自 MVP SVG,已取整到 10px 珊格)

语义节点 13 + 图上卡点 2(卡点只入 diagram,不入语义):

id kind label x,y(w×h) 备注
start start 客户RFQ询价 30,62(36×36)
A1 activity 客户RFQ受理 150,60(100×40) sub:BD
G1 gateway(exclusive/fork) 是否需要选型 293,58(44×44) basis:受理判定 needAlternative
A2 activity 替代选型 390,60(100×40) sub:FAE
G2 gateway(exclusive/fork) 是否需要寻源 553,58(44×44) basis:受理判定 needSourcing
A3 activity 寻源询价 660,60(100×40) sub:PRM
A4 activity 成本定价 810,60(100×40) sub:BD
A5 approval 定价审批 810,260(100×40) sub:PM · 审批点(琥珀)
A6 activity 报价议价 370,260(100×40) sub:BD
G4 gateway(exclusive/fork) 议价结果 183,258(44×44) basis:A6 outcome(G3 已被审批点吸收,G4 保留)
A7 activity 情报归档 150,170(100×40) sub:BD
end_won end 转订单履约 30,262(36×36) 图上 end(绿)
end_arch end 归档闭环 30,172(36×36) 图上 end-bad(红)
(callout) callout 退回调整→A4 590,370(100×30) 仅 diagram
(callout) callout 返回调整→A4 150,370(100×30) 仅 diagram

边 15(label/tone 按图;条件按上表出参;mapping 见 Task 4 代码)。 A5 的驳回不是边:走审批点 reject 声明(reopen A4),图上由「退回调整→A4」卡点呈现:

start→A1                        (起单五映射:customerCode/rfqRef/subject/body/quotedAt)
A1→G1
G1→A2   label 需要选型    cond: needAlternative = true
G1→G2   label 不需要选型  cond: needAlternative = false     (走图顶部旁路)
A2→G2
G2→A3   label 需要寻源    cond: needSourcing = true
G2→A4   label 不需要寻源  cond: needSourcing = false
A3→A4
A4→A5   label 报价草案提审
A5→A6   label 通过   tone ok        (审批点通过出口;驳回走 reject 声明,非边)
A6→G4
G4→end_won label 成交       tone ok   cond: outcome = "won"
G4→A4      label 有异议     tone bad  cond: outcome = "revise"  reopen: true
G4→A7      label 拒绝/超时  tone bad  cond: outcome = "lost"
A7→end_arch label 归档闭环

网关条件的求值作用域是 {...initiation, ...vars, ...payload}(process-service.ts:184), payload 是上一发射节点的出口载荷 —— 所以 G2 在「A2 路径」上读的是 A2 的载荷, A2 契约必须把 needSourcing 原样带出(上表已列)。G1 旁路(G1→G2)时载荷穿网关不换, G2 读到的仍是 A1 的载荷,两条入路都取到 needSourcing。

工具箱四段(需求 4)

段 内容 来源 状态
功能工具 crm.history-price-read(读)· mail.send(写) toolbox 目录(种子注册,两审) 实装,白名单按槽位授
LLM 工具 deepseek-v4-flash / deepseek-v4-pro / quote-extractor llm 模块实例(listLlmInstancesRoute) 实装(调模型经 ModelGateway 一缝)
人类工具 执行工单 · 验收工单 · 决策卡 · 补全卡 · 评价卡 human-tool 模块(card.ts 五形态) 实装(调人经工单/卡族),目录静态呈现
接入 导入自定义工具(表单→toolbox.register 两审入目录)· 连接 MCP(说明卡) 新路由 registerToolRoute;MCP 无运行时 导入实装;MCP 仅呈现+记 debt

Task 1: 四域短名与业务序(前端命名层)

Files:

  • Modify: apps/web/src/shared/gov/domains.ts
  • Modify: apps/web/src/shared/gov/__tests__/domains.test.ts

Interfaces:

  • Produces: DOMAIN_SHORT 含 customer:'客户' intel:'情报' supplier:'供应商' project:'项目';DOMAIN_ORDER = ['project','customer','intel','supplier'](项目是每天看的,排第一);domainShort/sortDomains 签名不变。

  • Step 1: 改测试(先红)

domains.test.ts 整段替换两个用例的期望:

  it('已知域给 2-3 字人读名', () => {
    expect(domainShort('project')).toBe('项目');
    expect(domainShort('customer')).toBe('客户');
    expect(domainShort('intel')).toBe('情报');
    expect(domainShort('supplier')).toBe('供应商');
  });

sortDomains 用例期望改为:

    expect(sortDomains(['supplier', 'intel', 'zzz_custom', 'project'])).toEqual([
      'project',
      'intel',
      'supplier',
      'zzz_custom',
    ]);
  • Step 2: 跑测试确认失败

Run: pnpm exec vitest run apps/web/src/shared/gov 2>&1 | tail -5 Expected: FAIL(旧字典是 quote_* 键)。

  • Step 3: 改实现

domains.ts 的字典与序换成:

export const DOMAIN_SHORT: Readonly<Record<string, string>> = {
  project: '项目',
  customer: '客户',
  intel: '情报',
  supplier: '供应商',
};

/** 业务序:项目(每天看的)→ 客户 → 情报 → 供应商;字典外的域殿后 */
const DOMAIN_ORDER = ['project', 'customer', 'intel', 'supplier'];

(domainShort 回落逻辑与 sortDomains 本体不动。)

  • Step 4: 测试过 + 提交

Run: pnpm exec vitest run apps/web/src/shared/gov 2>&1 | tail -4 → PASS。

git add apps/web/src/shared/gov/domains.ts apps/web/src/shared/gov/__tests__/domains.test.ts
git commit -m "MVP底座①:数据四域命名 —— 项目/客户/情报/供应商"

Task 2: 四域对象声明(seed 数据件)

Files:

  • Create: apps/api/src/app/seed-mvp-data.ts
  • Test: apps/api/src/app/__tests__/seed-mvp-data.test.ts

Interfaces:

  • Consumes: objectDeclarationSchema(@contracts:domain/object/label/fields[{name,type,label,primaryKey}])。

  • Produces:

    export const MVP_OBJECTS: readonly ObjectDeclaration[];       // 19 个,即版图字段全表
    export function declarationsFor(objects: readonly `${string}.${string}`[]): ObjectDeclaration[];
    

    declarationsFor(['project.intake_record', ...]) 供槽位清单按名取声明子集。

  • Step 1: 写失败测试

/**
 * 四域十九对象 —— 声明即物化的输入。守三件事:数量与域别齐、主键齐、按名可取。
 */
import { describe, expect, it } from 'vitest';
import { MVP_OBJECTS, declarationsFor } from '../seed-mvp-data.ts';

describe('MVP_OBJECTS', () => {
  it('四域十八对象,不多不少 —— 定价审批表载体是审批流,不入台账', () => {
    expect(MVP_OBJECTS).toHaveLength(18);
    const byDomain = Object.groupBy(MVP_OBJECTS, (o) => o.domain);
    expect(byDomain['project']).toHaveLength(12);
    expect(byDomain['customer']).toHaveLength(1);
    expect(byDomain['intel']).toHaveLength(3);
    expect(byDomain['supplier']).toHaveLength(2);
  });

  it('每个对象至少一个主键字段 —— 只增不破坏靠主键立身', () => {
    for (const o of MVP_OBJECTS) {
      expect(o.fields.some((f) => f.primaryKey), `${o.domain}.${o.object}`).toBe(true);
    }
  });

  it('declarationsFor 按名取子集,名不存在即抛', () => {
    expect(declarationsFor(['project.intake_record', 'intel.history_price'])).toHaveLength(2);
    expect(() => declarationsFor(['project.nope'])).toThrow('未知对象');
  });
});
  • Step 2: 跑测试确认失败

Run: pnpm exec vitest run apps/api/src/app/__tests__/seed-mvp-data.test.ts 2>&1 | tail -4 → FAIL。

  • Step 3: 写实现

按「版图 · 字段全表」逐对象转录(objectDeclarationSchema.parse 包住,label 用中文名)。骨架 + 一个完整示例,其余 18 个照表机械展开:

/**
 * 四域十八对象(docs/MVP示例工作流.html · 动态数据 + 静态底座;定价审批表落审批机制,不入台账)。
 * 划分原则:流转单据归 project;沉淀资产按内容归 customer / intel / supplier。
 * 声明只写活动**要用**的字段;物化走安装通道的声明合并(最大化合并、只增不破坏)。
 */
import { objectDeclarationSchema } from '@contracts';
import type { ObjectDeclaration } from '@contracts';

const f = (name: string, type: 'text' | 'number' | 'boolean' | 'date' | 'timestamp', label: string, primaryKey = false) =>
  ({ name, type, label, primaryKey, masked: false });

export const MVP_OBJECTS: readonly ObjectDeclaration[] = [
  objectDeclarationSchema.parse({
    domain: 'project',
    object: 'intake_record',
    label: '统一受理记录',
    fields: [
      f('customer_code', 'text', '客户码', true),
      f('pn', 'text', 'PN', true),
      f('brand', 'text', '厂牌'),
      f('eau', 'text', 'EAU'),
      f('type_tag', 'text', 'Type标'),
      f('target_price', 'number', '目标价'),
      f('status', 'text', '状态'),
    ],
  }),
  // …其余 18 个:严格照「版图 · 字段全表」,*号列 primaryKey=true
];

export function declarationsFor(objects: readonly `${string}.${string}`[]): ObjectDeclaration[] {
  return objects.map((key) => {
    const [domain, object] = key.split('.');
    const hit = MVP_OBJECTS.find((o) => o.domain === domain && o.object === object);
    if (hit === undefined) throw new Error(`未知对象:${key} —— 先在 MVP_OBJECTS 登记`);
    return hit;
  });
}
  • Step 4: 测试过 + 提交

Run: pnpm exec vitest run apps/api/src/app/__tests__ 2>&1 | tail -4 → PASS;pnpm typecheck 归零。

git add apps/api/src/app/seed-mvp-data.ts apps/api/src/app/__tests__/seed-mvp-data.test.ts
git commit -m "MVP底座②:四域十八对象声明 —— 流转归项目,沉淀归客户/情报/供应商"

Task 3: 六槽位清单 + 经安装通道装入

Files:

  • Create: apps/api/src/app/seed-mvp-slots.ts
  • Test: apps/api/src/app/__tests__/seed-mvp-slots.test.ts

Interfaces:

  • Consumes: activityManifestSchema(identity/contract/declarations/assets/samples/entry)、declarationsFor(Task 2)。

  • Produces:

    export const MVP_SLOTS: readonly ActivityManifest[];   // 6 个,id 见版图槽位表(A5 是审批点,不在其列)
    export async function installSlots(container: Container): Promise<void>;  // 逐个 registry.install
    export const SLOT_ARTIFACT: string;                    // 占位入口 JS(注释体,不可执行语义)
    
  • Step 1: 写失败测试

/**
 * 六个活动槽位 —— 契约就绪、实现留白。守:范式/岗位/出口与 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, 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 props = (a1.contract.output.mandatory[0]!.payload as { properties?: Record<string, unknown> }).properties ?? {};
    expect(Object.keys(props)).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('槽位不携实现:入口是注释占位,工具声明只有名', () => {
    expect(SLOT_ARTIFACT).toContain('槽位');
    for (const m of MVP_SLOTS) {
      for (const t of m.declarations.tools) expect(typeof t.name).toBe('string');
    }
  });
});
  • Step 2: 跑测试确认失败 → FAIL(文件不存在)。

  • Step 3: 写实现

要点(完整写 A1,其余 6 个按版图槽位表机械展开):

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 作业指导书的步骤标题,一步一条 human 执行工单 —— 未实现期人工可兜 */
const flowSkeleton = (steps: readonly string[]) => ({
  paradigm: 'flow' as const,
  steps: steps.map((label, i) => ({
    id: `S${i + 1}`,
    label,
    kind: 'human' as const,
    workOrder: 'execution' as const,
    card: {},
    deadline: 'P1D',
  })),
  parallelGroups: [],
});

/** loop 槽位:CMMN 骨架(MVP 案例图的项),叙事 = 案例卡原文一句话 */
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: '1.0.0', checksum,
});

const harness = { memoryWindow: 20, stallAfter: 'PT30M', nudge: [] };

export const MVP_SLOTS: readonly ActivityManifest[] = [
  activityManifestSchema.parse({
    identity: identity('rfq-intake', '客户RFQ受理', 'flow'),
    contract: {
      activityId: 'rfq-intake', version: '0.1.0',
      // contractInputSchema:data 是 JSON schema;message 允许但不得携带扩大授权的效力
      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: [] },
      requiredDeliverables: [], acceptanceCriteria: [], prohibitions: [],
      authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
      resources: {
        tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'],
        budget: { steps: 40, tokens: 200_000, activeMinutes: 30, humanCalls: 3 },
      },
    },
    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/A3/A4/A6/A7 同构:范式/岗位见版图表;out 口 payload 至少含表中出参;
  //   A6 含 outcome:{enum:['won','revise','lost']};
  //   businessModel:A4 flowSkeleton(['取价基准','逐条定价']);
  //   A7 flowSkeleton(['结果登记','画像更新','落地率统计']);
  //   A2 loopSkeleton(['替代对标表','初筛清单'], 'MVP §1.5:齐了即收 —— 必做项完成且无进行中项即结案');
  //   A3 loopSkeleton(['寻源比价表','供应商回价单'], 'MVP §1.7:齐了即收');
  //   A6 loopSkeleton(['正式报价单'], 'MVP §1.4:上接审批通过,办结经议价结果定去向');
];

export async function installSlots(container: Container): Promise<void> {
  for (const manifest of MVP_SLOTS) {
    const verdict = await container.registry.install(manifest, SLOT_ARTIFACT);
    process.stdout.write(`   槽位 ${manifest.identity.name}(${manifest.identity.paradigm})静态审 ${verdict.staticFindings.length} 条\n`);
  }
}

注意两处形状要以真实 schema 为准(写前 grep 校对,发现不合就以 schema 为准调用例): contractInputSchema / outputPortsSchema(contract7.ts)与 authorization/resources 的必填名; 静态审若因「工具契约缺失」报 error,把该工具名先注册(Task 5 先行注册两件功能工具即可覆盖)。

  • Step 4: 测试过 + typecheck + 提交
pnpm exec vitest run apps/api/src/app/__tests__ && pnpm typecheck
git add apps/api/src/app/seed-mvp-slots.ts apps/api/src/app/__tests__/seed-mvp-slots.test.ts
git commit -m "MVP底座③:六活动槽位清单 —— 契约就绪,实现留白,工具按名解耦"

Task 4: 流程包 1:1 + 触发器/通道 + 种子切换 + 整库重建

Files:

  • Create: apps/api/src/app/seed-mvp-flow.ts
  • Create: apps/api/src/app/seed-mvp.ts(编排 ①LLM ②工具 ③槽位 ④流程包+触发器+通道)
  • Modify: apps/api/src/app/seed.ts(改调 seedMvp)
  • Delete: apps/api/src/app/seed-quote.ts
  • Test: apps/api/src/app/__tests__/seed-mvp-flow.test.ts

Interfaces:

  • Consumes: processPackageSchema(nodes/edges/diagram)、triggerConfigSchema、Task 3 的槽位 id。

  • Produces: export const MVP_PACKAGE: ProcessPackage(id unified-quote,version 1,含 diagram);export async function seedMvpFlow(container): Promise<void>。

  • Step 1: 写失败测试(拓扑即契约)

/**
 * 流程包 1:1 —— 守三件事:拓扑与 MVP 图一致;网关条件读得到出参;闭合校验可发布。
 */
import { describe, expect, it } from 'vitest';
import { MVP_PACKAGE } from '../seed-mvp-flow.ts';

describe('MVP_PACKAGE', () => {
  it('13 语义节点 · 15 边 · 三网关一审批点双终点', () => {
    expect(MVP_PACKAGE.nodes).toHaveLength(13);
    expect(MVP_PACKAGE.edges).toHaveLength(15);
    expect(MVP_PACKAGE.nodes.filter((n) => n.kind === 'end')).toHaveLength(2);
    expect(MVP_PACKAGE.nodes.filter((n) => n.kind === 'gateway')).toHaveLength(3);
    expect(MVP_PACKAGE.nodes.filter((n) => n.kind === 'approval')).toHaveLength(1);
  });

  it('A5 是审批点:PM 审,驳回 reopen 回 A4 —— 驳回不是边', () => {
    const a5 = MVP_PACKAGE.nodes.find((n) => n.id === 'A5');
    expect(a5?.kind).toBe('approval');
    if (a5?.kind !== 'approval') return;
    expect(a5.approver.position).toBe('PM');
    expect(a5.reject.kind).toBe('reopen');
    if (a5.reject.kind === 'reopen') expect(a5.reject.nodeId).toBe('A4');
    // 唯一的 reopen 边是议价「有异议」回 A4
    expect(MVP_PACKAGE.edges.filter((e) => e.reopen).map((e) => `${e.from}→${e.to}`)).toEqual(['G4→A4']);
  });

  it('网关条件按出参写:G1/G2 读旗,G4 读 outcome', () => {
    const cond = (from: string, to: string) => MVP_PACKAGE.edges.find((e) => e.from === from && e.to === to)?.condition;
    expect(cond('G1', 'A2')).toBe('needAlternative = true');
    expect(cond('G2', 'A4')).toBe('needSourcing = false');
    expect(cond('G4', 'A7')).toBe('outcome = "lost"');
  });

  it('手绘图存证在包上 —— 呈现 1:1 的真相', () => {
    expect(MVP_PACKAGE.diagram).toBeDefined();
    const nodes = (MVP_PACKAGE.diagram as { nodes: { id: string; kind: string }[] }).nodes;
    expect(nodes.filter((n) => n.kind === 'callout')).toHaveLength(2);
  });
});
  • Step 2: 确认失败 → FAIL。

  • Step 3: 写 seed-mvp-flow.ts

按「版图 · 节点表 + 边表」逐行转录。骨架:

export const MVP_PACKAGE = processPackageSchema.parse({
  id: 'unified-quote',
  name: '统一报价',
  version: 1,
  state: 'draft',
  callInterface: { input: { type: 'object', required: ['customerCode', 'rfqRef'], properties: {
    customerCode: { type: 'string' }, rfqRef: { type: 'string' },
    subject: { type: 'string' }, body: { type: 'string' }, quotedAt: { type: 'string' } } },
    output: { type: 'object', properties: { landingRate: { type: 'number' } } } },
  nodes: [ /* 节点表 13 行:activity 带 owner{position,strategy:'sole'} failure{kind:'terminate'};
             A5 审批点照 v1 的 AP 节点范式:
             { id:'A5', kind:'approval', label:'定价审批',
               approver:{ position:'PM', strategy:'sole' },
               reject:{ kind:'reopen', nodeId:'A4', mapping:[…照 v1 e-g4-a4 式样,从 intake/draft 口取] },
               nudge:{ steps:[{ after:'PT20M', action:'nudge' }], deadline:'PT30M' },
               layout:{ row:1, col:6, x:810, y:260 } }
             gateway 带 gateway:'exclusive' mode:'fork' basis;layout{row,col,x,y} 用坐标表 x,y */ ],
  edges: [ /* 边表 15 行:label/tone/condition/reopen 逐字照表;mapping:
      start→A1  五条 initiation 映射(customerCode/rfqRef/subject/body/quotedAt)
      G1→A2    { target:'material',source:{from:'output-port',port:'intake',path:'pn'} } 等按 A2 入参
      G2→A3    按 A3 入参自 intake 口取 pn/brand
      G2→A4 / A3→A4 / A2→G2:A4 入参自 intake 口 + 上游口(supplier_floor/benchmark 可空)
      A4→A5    { target:'draft',source:{from:'output-port',port:'draft',path:'rows'} }
      A5→A6    通过(审批点唯一出边):draft 口取报价草案,照 v1 e-ap-a6 式样
      G4→A4(reopen) 参照旧 v1 的 reopen 映射式样(定稿于库中 v1:e-g4-a4)
      G4→A7    outcome/quote_no/customer_code 自 formal 口
      末端两边 mapping: [] */ ],
  variables: [],
  diagram: { /* FlowGraph:节点表全部 15 项(13 语义 + 2 callout)带 x/y/w/h;
               edges 带 label/tone 与折点(A4→A5 走右侧 910,80→990,80→990,280→910,280;
               G1→G2 顶部旁路经 y≈20;G4→A4 回边走底部 y≈370 过「返回调整→A4」卡点;
               「退回调整→A4」卡点(590,370)是 A5 驳回声明的呈现,不带边) */ },
});

seed-mvp.ts 统筹(顺序:LLM → 工具+白名单 → 槽位 → 流程包发布 → 触发器/通道),其中 LLM 段与通道段整段沿用 seed-quote.ts 现文(deepseek 三实例、ch-rfq-mail/ch-rfq-review、 trg-rfq(event)与 trg-rfq-manual(registration)),白名单换新槽位 id:

await container.toolbox.setAllowlist('rfq-intake', ['crm.history-price-read']);
await container.toolbox.setAllowlist('cost-pricing', ['crm.history-price-read']);
await container.toolbox.setAllowlist('quote-negotiation', ['crm.history-price-read', 'mail.send']);
await container.toolbox.setAllowlist('sourcing-inquiry', ['crm.history-price-read', 'mail.send']);
await container.toolbox.setAllowlist('alt-selection', ['crm.history-price-read']);
await container.toolbox.setAllowlist('intel-archive', []);

功能工具只留 HISTORY_TOOL 与 MAIL_TOOL 两件(quote-* 私有纯工具随旧活动退场 —— 纯工具属活动实现,槽位期没有实现就没有纯工具)。seed.ts 把 seedQuote 换成 seedMvp;删除 seed-quote.ts。

  • Step 4: 整库重建 + 种子
pnpm db:down && docker compose down -v 2>/dev/null; pnpm db:up && sleep 4 && pnpm db:migrate && pnpm db:seed

Expected 种子日志:LLM 三实例;两件功能工具;七槽位装入(静态审无 error); 流程包「统一报价」v1 已发布 —— 13 节点;触发器两条 + 通道两条。

  • Step 5: 单实例与物化验收(curl + SQL)
docker exec jcai-pg psql -U jcai -d jcai -tAc "select id, version, state from sys.process_package;"
# 期望:恰一行 unified-quote|1|published
docker exec jcai-pg psql -U jcai -d jcai -tAc "select schemaname, count(*) from pg_tables where schemaname in ('project','customer','intel','supplier') group by 1 order by 1;"
# 期望:customer 1 · intel 3 · project 12 · supplier 2

再起服务用 Chaihao 登录 → GET /api/gov/process 恰一包;/design 流程包页恰一张卡, 点开画布 1:1(双终点、四网关、两卡点、边标签齐)。

  • Step 6: 五门 + 提交

api:check 若报 seed 相关 d.ts 变化照常 --update。

git add -A && git commit -m "MVP底座④:统一报价 1:1 流程包单实例 —— 四域物化,种子整体切换"

Task 5: 自定义工具导入路由(后端)

Files:

  • Modify: packages/contracts/src/api/governance.ts
  • Modify: apps/api/src/app/routes/governance-ops.ts
  • Modify: api-report/

Interfaces:

  • Consumes: container.toolbox.register(input): RegistrationVerdict(两审:静态 + 样例)。

  • Produces:

    export const registerToolRoute = {
      method: 'POST',
      path: '/api/gov/tools',
      body: z.object({
        contract: z.object({
          name: z.string().min(1), version: z.string().min(1), description: z.string().min(1),
          input: z.record(z.string(), z.unknown()), output: z.record(z.string(), z.unknown()),
          effect: z.enum(['read', 'write']),
          errors: z.array(z.object({ code: z.string(), retryable: z.boolean(), message: z.string() })).default([]),
          idempotent: z.boolean().default(true),
          timeoutMs: z.number().int().default(10_000), retries: z.number().int().default(1),
          connectionKeys: z.array(z.string()).default([]),
        }),
        /** 连接值只写不读 —— 键声明在契约,值密封在治理台(OS 7.3) */
        connectionValues: z.record(z.string(), z.string()).optional(),
        samples: z.array(z.object({ input: z.record(z.string(), z.unknown()) })).min(1),
      }),
      output: z.object({ accepted: z.boolean(), findings: z.array(z.object({ severity: z.string(), message: z.string() })) }),
    } as const satisfies RouteContract;
    
  • Step 1: 契约 + handler

handler 挨着 listToolsRoute,守卫 guard('tool','govern')(文件内工厂现成):

/** 导入自定义工具 —— 与第一方同一条门:两审(静态 + 样例)过了才入目录 */
app.post(registerToolRoute.path, async (c) => {
  const denied = await governTool(c.get('account'));
  if (denied) return c.json(denied, denyStatus(denied));
  const parsed = registerToolRoute.body.safeParse(await c.req.json());
  if (!parsed.success) {
    const body: ApiError = { code: 'validation-failed', message: '工具契约不合 schema', detail: parsed.error.issues };
    return c.json(body, 400);
  }
  const verdict = await container.toolbox.register({
    contract: parsed.data.contract,
    samples: parsed.data.samples.map((s) => ({ input: s.input, expectShape: { type: 'object' } })),
    ...(parsed.data.connectionValues === undefined ? {} : { connectionValues: parsed.data.connectionValues }),
  });
  return c.json({
    accepted: verdict.accepted,
    findings: verdict.staticFindings.map((finding) => ({ severity: finding.severity, message: finding.message })),
  });
});

(governTool 若不存在,按文件内 guard('tool', 'govern') 补一行; toolbox.register 的入参/回参字段名以 apps/api/src/modules/toolbox/contract.ts 实际为准,写前 grep。)

  • Step 2: curl 验证
curl -s -b $CLAUDE_JOB_DIR/tmp/ch.cookie -X POST http://localhost:8787/api/gov/tools \
  -H 'content-type: application/json' -d '{
  "contract": { "name": "erp.stock-read", "version": "1.0.0",
    "description": "读取 ERP 现货库存:PN → 库存量与库龄", "effect": "read",
    "input": { "type": "object", "required": ["pn"], "properties": { "pn": { "type": "string" } } },
    "output": { "type": "object", "properties": { "qty": { "type": "number" } } },
    "connectionKeys": ["endpoint"] },
  "connectionValues": { "endpoint": "replay://erp" },
  "samples": [{ "input": { "pn": "LM358DR" } }] }'

Expected: {"accepted":true,...};随后 GET /api/gov/tools 目录多一件;坏 schema 400;无权 403。

  • Step 3: api:check + 提交
pnpm api:check --update && git add api-report
git add packages/contracts apps/api/src/app/routes/governance-ops.ts
git commit -m "MVP底座⑤:自定义工具导入路由 —— 与第一方同门两审"

Task 6: 工具箱一页四段(卡片化 + LLM/人类/接入呈现)

Files:

  • Modify: apps/web/src/features/gov-tool/internal/panel.tsx(整页重写)
  • Modify: apps/web/src/shared/gov/gov-route.ts(tools TABS:catalog 能力目录 保持单签,页内分段)
  • Modify: docs/debt.md(MCP 连接:仅呈现)

Interfaces:

  • Consumes: useGovTools(功能工具)、useGovLlm(LLM 实例)、registerToolRoute(Task 5)。

  • Produces: ToolsHome({ tab, nav }: HomeProps) 四段卡片;ToolPage 详情保持。

  • Step 1: 重写 ToolsHome

四段,全部用既有类(g-shelf/g-tile 网格 + Sec 分段 + Prop/Field 表单):

export function ToolsHome({ nav }: HomeProps) {
  const tools = useGovTools();
  const llm = useGovLlm();
  return (
    <QueryGate query={tools}>
      <>
        <Sec ico="wrench" title="功能工具" extra="读类自由调用 · 写类登记意向,放行后落地">
          <div className="g-shelf">
            {(tools.data?.tools ?? []).map((t) => (
              <button key={t.name} className={'g-tile t-' + (t.effect === 'write' ? 'warn' : 'ok')}
                onClick={() => nav.push({ k: 'tool', id: t.name })}>
                <span className="g-tile-h">
                  <span className="g-tile-i"><Ico name="wrench" s={16} /></span>
                  <span className="g-tile-t"><b>{t.name}</b><em>v{t.version}</em></span>
                  <St tone={t.effect === 'write' ? 'warn' : 'ok'} ico={t.effect === 'write' ? 'pen-line' : 'circle-check'}>
                    {t.effect === 'write' ? '写' : '读'}
                  </St>
                </span>
                <span className="g-tile-m"><Chip tone="mute">{t.description.slice(0, 40)}</Chip></span>
                <span className="g-tile-f">
                  <Chip ico="key">{t.connectionKeys.length === 0 ? '无连接' : `连接 ${t.connectionKeys.join('/')}`}</Chip>
                </span>
              </button>
            ))}
          </div>
        </Sec>

        <Sec ico="cpu" title="LLM 工具" extra="调模型经模型网关一缝;活动只见逻辑名,不触厂商与密钥">
          <div className="g-shelf">
            {(llm.data?.instances ?? []).map((m) => (
              <button key={m.id} className={'g-tile t-' + (m.enabled ? 'ok' : 'mute')}
                onClick={() => nav.jump('system', 'llm', { k: 'llm', id: m.id })}>
                <span className="g-tile-h">
                  <span className="g-tile-i"><Ico name="cpu" s={16} /></span>
                  <span className="g-tile-t"><b>{m.name}</b><em>{m.provider} · {m.model}</em></span>
                  <St tone={m.apiKeyBound ? 'ok' : 'danger'} ico={m.apiKeyBound ? 'circle-check' : 'alert-circle'}>
                    {m.apiKeyBound ? '已绑密钥' : '未绑密钥'}
                  </St>
                </span>
                <span className="g-tile-f">
                  {m.systemUse && <Chip tone="brand">系统用途</Chip>}
                  <span className="r">{m.enabled ? '启用中' : '已停用'}</span>
                </span>
              </button>
            ))}
          </div>
        </Sec>

        <Sec ico="hand" title="人类工具" extra="调人经工单两模式与对话卡族;打扰上限在契约授权边界">
          <div className="g-shelf">
            {HUMAN_TOOLS.map((h) => (
              <div key={h.name} className="g-tile t-collab">
                <span className="g-tile-h">
                  <span className="g-tile-i"><Ico name={h.ico} s={16} /></span>
                  <span className="g-tile-t"><b>{h.name}</b><em>{h.mode}</em></span>
                  <St tone="ok" ico="circle-check">内置</St>
                </span>
                <span className="g-tile-m"><Chip tone="mute">{h.desc}</Chip></span>
              </div>
            ))}
          </div>
        </Sec>

        <Sec ico="plus" title="接入" extra="自定义工具走两审同门;MCP 工具经 ToolInvoker 一缝接入">
          <ImportToolCard />
          <McpCard />
        </Sec>
      </>
    </QueryGate>
  );
}

HUMAN_TOOLS 静态目录(五形态,语义出自 packages/contracts/src/card.ts):

const HUMAN_TOOLS = [
  { name: '执行工单', mode: 'work-order · execution', ico: 'clipboard-check', desc: '作业指导书 + 空表单,请人把一步做完;落地失败卡是它的系统预置形态' },
  { name: '验收工单', mode: 'work-order · acceptance', ico: 'shield-check', desc: '请人验收产出:通过 / 驳回重做 / 驳回迭代' },
  { name: '决策卡', mode: 'chat-card · decision', ico: 'git-branch', desc: '分支条件选择表,含自定义收尾项 —— 决策是对话区的选择题' },
  { name: '补全卡', mode: 'chat-card · elicitation', ico: 'pen-line', desc: '就地要几个小信息;「没法填」旁路与提交两条路必走其一' },
  { name: '评价卡', mode: 'chat-card · rating', ico: 'star', desc: '任务评价:评分 1–5 + 改进建议,回流度量与蒸馏' },
] as const;

ImportToolCard:Card + Prop/Field 表单(名称/说明/效应 Seg/入参 schema textarea(JSON)/连接键 ListEdit/连接值(逐键 input)/样例入参 textarea)→ AskBand 确认(影响:两审过了即入目录、可入白名单;审不过给 findings)→ call(registerToolRoute) → 失效 ['gov','tools'],失败把 findings 列成 Note tone="warn"。 McpCard:呈现卡(St mute「未连接」),正文一句话:「连接 MCP 服务器后,其工具经两审入目录、按白名单授给活动 —— 接入走 ToolInvoker 一缝,底座就绪后开放。」按钮 disabled。

  • Step 2: debt 登记

docs/debt.md 追加:「工具箱 · MCP 连接(2026-07-27):仅呈现卡。真接入需 MCP 客户端 + 工具目录同步(经 ToolInvoker 一缝、两审入目录),当前无运行时 —— 卡上如实标注未连接,连接按钮不可点。」

  • Step 3: 浏览器验收 + 五门 + 提交

/design → 工具箱:四段齐;导入 erp.stock-read(若 Task 5 已注册过,换名 erp.po-read)走完确认带 → 功能工具段多一张卡。五门全绿。

git add apps/web/src/features/gov-tool apps/web/src/shared/gov/gov-route.ts docs/debt.md
git commit -m "MVP底座⑥:工具箱四段卡片 —— 功能/LLM/人类/接入,导入走两审同门"

Task 7: 活动库卡片化(六槽位卡)

Files:

  • Modify: apps/web/src/features/gov-activity/internal/panel.tsx(LibraryHome 列表→卡片网格)

Interfaces:

  • Consumes: useGovActivities(activityId/name/category/paradigm/enabled/packageVersions/assetVersions)。

  • Produces: LibraryHome 卡网格;ActPage 详情保持。

  • Step 1: LibraryHome 重写为 g-shelf 卡网格

每活动一张 g-tile:范式章(标准活动=ok / 案例活动=collab)+ 岗位/分类 chips + 版本徽 + 槽位状态—— packageVersions 里最新版即契约版;实现是否留白,以「活动分类 统一报价 + 当前无资产增量」呈现为 St tone="warn" ico="square-pen" 「契约就绪 · 待实现」(判定:assetVersions.length === 0 或全部 effective === false; 若字段实际形状不同,以 GovActivitiesOutput 为准 grep 后调整):

<button key={a.activityId} className={'g-tile t-' + (a.paradigm === 'flow' ? 'ok' : 'warn')}
  onClick={() => nav.push({ k: 'act', id: a.activityId })}>
  <span className="g-tile-h">
    <span className="g-tile-i"><Ico name={a.paradigm === 'flow' ? 'list-checks' : 'rotate-ccw'} s={16} /></span>
    <span className="g-tile-t"><b>{a.name}</b><em>{a.activityId}</em></span>
    <Chip tone={a.paradigm === 'flow' ? 'ok' : 'warn'}>{ACTIVITY_TYPE_LABEL[a.paradigm]}</Chip>
  </span>
  <span className="g-tile-m">
    <Chip ico="boxes">{a.category}</Chip>
    <Chip ico="git-commit">包 v{a.packageVersions[a.packageVersions.length - 1]?.version ?? '—'}</Chip>
  </span>
  <span className="g-tile-f">
    <St tone="warn" ico="square-pen">契约就绪 · 待实现</St>
    <span className="r">{a.enabled ? '已上架' : '停用'}</span>
  </span>
</button>
  • Step 2: 验收 + 提交

/design → 活动库:六张槽位卡(A1/A2/A3/A4/A6/A7,范式两色、全部「契约就绪 · 待实现」;A5 是审批点,不在库中);点卡进详情不回退。五门全绿。

git add apps/web/src/features/gov-activity
git commit -m "MVP底座⑦:活动库卡片化 —— 六槽位卡,契约就绪待实现"

Task 8: 数据域卡片化(四域大卡)

Files:

  • Modify: apps/web/src/features/gov-data/internal/panel.tsx(DataHome 域分组列表→四域大卡)

Interfaces:

  • Consumes: useGovObjects / useGovLedger(列/主键)、domainShort/sortDomains(Task 1)。

  • Produces: DataHome 四域卡;LedgersHome/ObjPage 不动(运营台列表为主,按需求 1 保留)。

  • Step 1: DataHome 重写

一域一张大卡(Card),卡头 = 短名 + 域键 mono + 对象数;卡体 = 对象 chips 网格(g-entries 或 chips 流式), 点对象 chip → nav.push({ k: 'obj', id })(设计侧详情仍只见结构):

export function DataHome({ nav }: HomeProps) {
  const q = useGovObjects();
  const objects = q.data?.objects ?? [];
  const domains = sortDomains([...new Set(objects.map((o) => o.domain))]);
  return (
    <QueryGate query={q}>
      <>
        <Note>数据按经营对象四分:<b>项目</b>(每一单的流转单据)· <b>客户</b> · <b>情报</b> · <b>供应商</b>(沉淀资产)。这里改结构;行数据在运营中心 · 台账看。</Note>
        {domains.map((d) => (
          <Card key={d} ico="database" title={domainShort(d)}
            extra={<span className="g-mono">{d} · {objects.filter((o) => o.domain === d).length} 对象</span>}>
            <div className="g-entries">
              {objects.filter((o) => o.domain === d).map((o) => (
                <Entry key={o.object} ico="table" title={o.object}
                  desc={`${d}.${o.object}`}
                  onClick={() => nav.push({ k: 'obj', id: `${d}.${o.object}` })} />
              ))}
            </div>
          </Card>
        ))}
        {objects.length === 0 && <Clear>还没有物化的对象 —— 槽位安装时按声明物化,种子跑完就出现在这里。</Clear>}
      </>
    </QueryGate>
  );
}

(Entry 的 desc 想带列数/主键需逐对象再查 —— 设计首屏不发 19 个查询,列数留给点开后的 ObjPage。)

  • Step 2: 验收 + 提交

/design → 数据域:四张大卡(项目 12 · 客户 1 · 情报 3 · 供应商 2);运营台台账页签同步变四域短名(项目/客户/情报/供应商 —— Task 1 字典已生效)。五门全绿。

git add apps/web/src/features/gov-data
git commit -m "MVP底座⑧:数据域四域大卡 —— 项目/客户/情报/供应商"

Task 9: 收口 —— 全链走查 · 文档对账 · 五门

Files:

  • Modify: docs/产品设计.md(附录追记:四域划分决策 + 设计台卡片/运营台列表原则)

  • Modify: docs/debt.md(对账)

  • Step 1: 走查清单(浏览器,Chaihao)

  1. /design 蓝图:链上四数 = 流程包 1 · 活动 6 · 工具 ≥3(2 内置 + 导入的)· 数据域 4;待补清单为空(全闭合、全安装、有调起);
  2. 流程包卡 → 画布对照 docs/MVP示例工作流.html FIG(双终点/三网关/琥珀审批点/两卡点/边标签;G3 被审批点吸收是唯一预期差异);属性卡点 A5 见审批人 PM 与驳回去向 A4;
  3. 活动库七卡 / 工具箱四段 / 数据域四卡;
  4. /ops 台账页签 = 项目/客户/情报/供应商,各域对象行数 0(新库);
  5. 起单(登记触发)→ 工作流建单、A1 任务出现;任务如实报未实现(不装作跑过)—— 截图留档;
  6. 调起规则页两条(事件 + 登记)。
  • Step 2: 文档

产品设计.md 附录补一行决策:「数据四域(2026-07-27):项目=流转单据,客户/情报/供应商=沉淀资产;设计台卡片式、运营台列表式为交互基调」。debt.md 确认含:MCP 仅呈现、槽位未实现(逐活动开发清单 A1→A7)。

  • Step 3: 五门 + 提交
pnpm gen:tiers && pnpm typecheck && pnpm lint && pnpm depcruise && pnpm api:check && pnpm test
git add docs && git commit -m "MVP底座⑨:收口 —— 走查通过,四域决策入档"

自审记录

  • 规格覆盖:需求 1→Task 6/7/8(+流程包已卡片);需求 2→Task 1/2/4(四域物化与呈现);需求 3→Task 3/4(槽位契约 + 1:1 拓扑与手绘图;A5 审批点、G3 吸收为唯一预期差异);需求 4→Task 5/6(功能/LLM/人类三段 + 导入实装 + MCP 呈现,MCP 无运行时为已声明偏差记 debt);需求 5→Global Constraints(工具按名声明、槽位无实现、占位入口不可执行语义)+ Task 3。
  • 占位扫描:重复结构以「版图数据表 + 单个完整样例」承载,表即内容;两处「以实际 schema 为准 grep 后调整」是形状核对动作,不是留白。
  • 类型一致性:槽位 id 六处(Task 3 清单 / Task 4 白名单与节点 activityId / Task 7 呈现)统一用 rfq-intake / alt-selection / sourcing-inquiry / cost-pricing / quote-negotiation / intel-archive(A5 审批点无 activityId);declarationsFor 签名 Task 2 定义、Task 3 消费;registerToolRoute Task 5 定义、Task 6 消费。
  • 形状已核:contractInputSchema={data,messageAllowed}、outputPortSchema={name,payload,description?}、authorization={discretion,preauthorizedActions,humanCallBudget}、resources={tools,skills,models,roles,budget{steps,tokens,activeMinutes,humanCalls}} —— Task 3 样例即按此写,A2–A7 展开照抄此形。Node 24,Object.groupBy 可用。
  • 已知取舍(执行者与评审须知):① A5 = 内置审批点(用户确认):驳回走 reject 声明 reopen A4,G3「审批结果」被审批点吸收 —— 图与 MVP 差一颗菱形,语义同构;定价审批表落审批机制不入台账(对象 18);② 槽位 flow 骨架的步骤是 human 执行工单标题级占位,入口 JS 直接抛「未实现」—— 运行不会假装成功;③ 整库重建,旧 quote_* 域与多版本包全部退场,activities/quote-* 目录留仓作参考实现、不再安装。