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-TMP-REPO/docs/superpowers/plans/2026-07-27-mvp-foundation.md
T

976 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 整段替换两个用例的期望:
```ts
it('已知域给 2-3 字人读名', () => {
expect(domainShort('project')).toBe('项目');
expect(domainShort('customer')).toBe('客户');
expect(domainShort('intel')).toBe('情报');
expect(domainShort('supplier')).toBe('供应商');
});
```
`sortDomains` 用例期望改为:
```ts
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` 的字典与序换成:
```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。
```bash
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:
```ts
export const MVP_OBJECTS: readonly ObjectDeclaration[]; // 19 个,即版图字段全表
export function declarationsFor(objects: readonly `${string}.${string}`[]): ObjectDeclaration[];
```
`declarationsFor(['project.intake_record', ...])` 供槽位清单按名取声明子集。
- [ ] **Step 1: 写失败测试**
```ts
/**
* 四域十九对象 —— 声明即物化的输入。守三件事:数量与域别齐、主键齐、按名可取。
*/
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 个照表机械展开:
```ts
/**
* 四域十八对象(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` 归零。
```bash
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:
```ts
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: 写失败测试**
```ts
/**
* 六个活动槽位 —— 契约就绪、实现留白。守:范式/岗位/出口与 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 个按版图槽位表机械展开):
```ts
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 + 提交**
```bash
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: 写失败测试(拓扑即契约)**
```ts
/**
* 流程包 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`**
按「版图 · 节点表 + 边表」逐行转录。骨架:
```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:
```ts
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: 整库重建 + 种子**
```bash
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)**
```bash
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`。
```bash
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:
```ts
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')`(文件内工厂现成):
```ts
/** 导入自定义工具 —— 与第一方同一条门:两审(静态 + 样例)过了才入目录 */
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 验证**
```bash
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 + 提交**
```bash
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` 表单):
```tsx
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`):
```tsx
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`)走完确认带 → 功能工具段多一张卡。五门全绿。
```bash
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 后调整):
```tsx
<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 是审批点,不在库中);点卡进详情不回退。五门全绿。
```bash
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 })`(设计侧详情仍只见结构):
```tsx
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 字典已生效)。五门全绿。
```bash
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: 五门 + 提交**
```bash
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-*` 目录留仓作参考实现、不再安装。