把 `unified-quote` 从起单一路手工答到底(这是逐个开发活动之前该做的验证),
一路撞出五处,按撞到的顺序:
① 七个活动派出七张一模一样的「请处理」
槽位骨架给的是 `card: {}`,工单标题/字段全落回默认。收卡的人不知道自己走到
哪一步、这一步要干什么,整条流没法手工走通。→ 卡面自述:标题就是步骤标签,
依据带「第几步 / 共几步」,并如实说「这一步的自动化还没开发,先由你来办」。
② 走到第 3 步就 `exhausted` 终态、工作流转 abnormal
六个槽位共用 `humanCalls: 3`,而 A1 声明了 6 个人类步 —— 活动**结构上永远走不完**。
→ `budgetFor(n)` 按各槽位自己的步数给。步/项清单提成 `STEPS` 常量,
预算与骨架共用同一个数组,数不上不会漂移。治理台申报值也同源(否则台上摆假数)。
③ 六步答完,任务卡在「等人,却没有任何东西等人答」
`flow-runner` 写死了 `released = lastStepIsAcceptance && lastVerdictPassed` ——
末步不是验收工单,活动就永远不 release。→ 声明的六步来自 MVP 作业指导书,
不篡改语义,在其后**追加**一步「收口验收」。
④ 只走了 A1(一个节点实例)却报 `completed` ← **最危险的一处**
状态聚合把「没有在跑的了」当成完成。界面上是一单已完成的报价,业务上一步没走。
→ 「完成」凭**到过终点节点**;收敛而未到终点 = 卡住 = `abnormal`,进驾驶舱异常卡。
`aggregateWorkflowState` 收第三个入参(终点 id 集),不传退回旧口径。
⑤ 异或网关一条边都选不中时,流无声停住
④ 让它至少报异常了,但只说「有问题」不说「哪儿」。→ 发 `route-dead-end` 事件:
哪个节点、各出边的条件、判据里当时有哪些键。只记键名不记值 —— 诊断够用,
载荷不进事件明文。实测这条痕直接指出「G1 要 needAlternative,而键里没有」。
顺带:改槽位内容必须升包版(0.1.0 → 0.4.0)。「安装恒为新增、版本并存」是硬不变量,
同版重装是 no-op、manifest 一字不动 —— 我第一次改完重播种,屏幕报「已装入」而库里还是旧的。
`installSlots` 现在先问库、如实报「已装」还是「已在库,未改动 —— 要换内容请升包版」。
**走查后的诚实状态**:A1 走得完、活动 released;流**过不了 G1**,因为出口的必填去向旗
产不出来。这不是缺陷 —— 出口载荷由台账落地事实推导,槽位没实现台账写入就没有事实,
凭空捏一个就是在系统内部 mock。于是 A1 的开发完成判据也具体了:必须让
`needAlternative` / `needSourcing` 成为落地事实,否则 G1 永远判不了。债务簿已记。
另记一条闸的盲点:api:check 只快照 index/contract/port.d.ts,而 index 是 re-export 名单 ——
给纯函数加形参它看不见(本次实测)。改签名要手动同步 CONTRACT.md,别指望闸提醒。
typecheck ✓ lint ✓ depcruise 374 模块 ✓ api:check ✓ 490 测试 ✓(均以退出码验)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
10 KiB
技术债登记簿
编码规范 §17:历史违规集中登记于此——旧路径
warn、新代码error,清零后升级。 例外必须注明原因与预期清理时间;散落的eslint-disable不算例外,算违规。
| # | 债 | 原因 | 预期清理 | 状态 |
|---|---|---|---|---|
| M0-1 | pnpm db:seed 只打印种子计划,未真正写库 |
种子数据(五岗位 / 五员工)已在 apps/api/src/app/seed.ts 冻结为常量并定义 OrgSeeder 端口;写入实现依赖 org 模块的 Drizzle schema,属 M1 |
M1 | 已清理(M1) |
| M0-2 | packages/{sfeel,activity-sdk,ui-kit} 仅有版本常量 |
M0 只立骨架,内容按里程碑序填(§15);@contracts 已于 M1 落首版 |
M3–M5 | 进行中 |
| M1-1 | container.ts 里两个占位端口:InFlightWorkProbe(空集)、TriggerMatcher(恒不命中) |
二者的真身在 task(L6,M4)与 trigger(L7,M5);MessageFallback 已于 M2 接真身 |
M4 / M5 | 进行中 |
| M1-2 | 条件求值 passthroughEvaluator 恒真 |
@sfeel 属 M3;M1 的监听只用类型 + 关联键过滤,不用内容条件 |
M3 | 进行中 |
| M1-3 | 派发器已于 M2 落地(advisory lock 串行 + 消费幂等);消费者(任务 / 触发器)随 M4 / M5 注册 | M2 | 已清理 | |
| M2-1 | 派发器的消费者表为空 —— 投递只标记已送达 | 任务消费者属 task(M4),触发器消费者属 trigger(M5);投递链路本身已可测 | M4 / M5 | 进行中 |
| M5-3 | 活动 .JS 产物是源码文本,未经 tsup 单文件 ESM 真编译 | 静态审扫描的正是产物文本(import 与全局引用),当前形态已可审;真编译在装载器落地时接入 | M6 前 | 进行中 |
| M5-2 | loop 的 Agent 回路直接经 ModelGateway,未用 Mastra Agent |
编码规范 §6.1 规定用 Mastra Agent;AgentPort 端口不变,换实现时 usecase 不动;当前实现仍过两缝(录制回放可见) |
M6 前 | 进行中 |
| M5-1 | process 未用 Mastra workflow 编译器,流转由自持确定性状态机 + sys.node_instance 承担 |
编码规范 §6.1 规定流程包编译为 Mastra 图;二者语义等价(挂起/恢复由节点实例表承担),先落可测的自持实现,Mastra 编译器作为可替换 adapter 后补 —— usecase 不动 | M5 收尾 | 进行中 |
| M2-2 | pg-boss 已装未用 | 定时作业(监听时限扫描、周期触发器)随 M5 落地;当下派发器用 setInterval | M5 | 进行中 |
鉴权(ADR-010 落地后的已知欠账)
- 登录失败无次数限制:可无限次试口令。单组织 ≤50 人、内网部署下暂缓; 接入外网前必须补(按账号 + 来源 IP 双维度退避)。
- 无口令强度校验、无首登强制改密:全员初始口令相同(
JCAI_SEED_PASSWORD), 谁都没改过就等于没有口令。治理台「员工」面需补:改密 / 重置 / 强制改密标记。 setPassword已在 org 出口就位但无路由:改密只能经播种或直接调用例,界面上没有入口。- 会话无「登出全部设备」:
revokeAllOf已实现(停用员工时调),但本人无从触发。
工作组(ADR-011 落地后的已知欠账)
没有治理台管理界面—— 2026-07-27 清账:运营中心 · 员工 · 工作组页签可建组 / 改组 (GET|PUT /api/gov/work-groups);实测改绑定后新起工作流的work_group快照即含新成员表。- 一人一组:
workGroupOf取该人名下稳定的第一条;起单时选哪个组这一步没做。 - 组成员离岗无提醒:绑定的人离岗时静默退回四策略,不告诉任何人。
工作成果 · 文件
- Markdown 只以原文纸面呈现:富文本渲染(GFM 表格 / 内联 SVG)要
react-markdown+remark-gfm+rehype-raw三个依赖,当前成果文件绝大多数是 HTML,先不引。 引入前不假装渲染过 —— FileViewer 里以<pre>出原文。 - 过程成果(deliverable)尚无来源:活动还不发布 deliverable,所以成果文件目前
只有活动推的那一屏。装配已接好(
desk.ts的results),活动一产出就自动进列表。
沟通记录(占位)
工作区第二个页签「沟通记录」目前是空占位。它要汇集这一单与外界的往来:
邮件线程、通道消息、原件存档 —— 数据在 channel 与 event 里已有,尚未做投影。
界面上明说「还没接」,不伪装成「暂无数据」。
2026-07-28 对账:这条曾只写在注释里没落到文案上 —— 注释写着「明说没接」,
正文却写「都会归到这里」,等于把没接说成了空,人会一直等。已改为「往来记录还没接通」,
并给空态加了第三色调 muted(通路没接,灰着不许诺;区别于 brand 的「等着就会有」)。
outbound_mail 至今只有 channel 建表与清库脚本碰过,任何界面都读不到 —— 所以文案也
不把人指去「消息」面。
工作区页签条装不下五个页签
五个四字页签 380px + 翻页囊 112px + 间距 30px = 522px,工作区仅 429px,差 94px。 当前先做成整条可横滚(不裁、不溢出卡片),最终形态待定,备选:
- 缩短页签文字(沟通 / 成果 / 详情 / 日志)—— 省 ~100px,但文案变短
- 翻页囊只留箭头、页码收进 title —— 省 ~50px,但看不见「第几件/共几件」
- 页签条换行两排 —— 全都放得下,但工作区正文少一行高
驾驶舱 · 死信卡(2026-07-27)
运营驾驶舱首屏收了「异常工作流 / 未闭合草稿 / 在途任务」三卡,没有死信卡。
死信目前只以治理者消息要素送出(deadLetters.report → message),没有一个按 kind 计数的
查询面 —— 要补这一卡,得给 system_message 加一条按 kind 聚合的治理路由。
在那之前,驾驶舱不摆一个查不出数的卡位。
工具箱 · MCP 连接(2026-07-28)
工具箱「接入」段有两件:导入自定义工具已实装(与第一方同门两审,端点调不通就不入目录); MCP 连接仅呈现。系统还没有 MCP 客户端 —— 接进来之后 MCP 的工具与自定义工具同等待遇: 经工具调用一缝去调、过两审入目录、按白名单授给活动。卡上如实标「未连接」,不摆点不动的按钮。
活动槽位未实现(2026-07-28)
六个槽位(rfq-intake / alt-selection / sourcing-inquiry / cost-pricing / quote-negotiation / intel-archive)只有契约面,入口产物是抛「未实现」的占位 —— 底座备齐后按 A1→A7 顺序逐个开发, 届时以新包版本经安装通道换入,槽位契约不动。
2026-07-28 更正:这条原写「起单后任务会停在 A1」,手工走查后发现说得不准 ——
A1 本身走得完(六步人工代办 + 收口验收放行,活动 released),卡的是它后面的 G1:
出口的必填去向旗产不出来,网关判不了。准确的诚实状态见下文「编排层 · 手工走查」。
编排层 · 走查已清项(2026-07-28)
一轮系统走查后已修:
- 卡片被压扁剪裁(
.g-page是滚动 flex 列,子块要flex-shrink: 0) - 数据对象只见键不见中文名(label 随物化落 PG 表/列注释,全链读回)
- LLM 工具错做成模型实例(改为五件大模型驱动的系统内通用工具)
- 元素盘点击落子落不下(原型里就坏:
create.move无 hover 目标 →create.rejected) - 卡点还原凭空造边(
e-A5-A4:审批点的驳回牌子被当成边;改为只认亲不新建) - 已发布包改图找不到路(编排工作区就地给「起草新版本并去编辑」)
- 活动契约面治理台看不到(列表出参 + 活动详情 + 导出依赖文件三处打通)
已验为正常、不需要改的:编辑器往返无损(16 边 14 节点、映射与驳回声明全留)、 边映射校验逐边点名、发布门硬(缺条件即 412)、调起规则四类齐、导出依赖文件内容完整。
编排层 · 手工走查(2026-07-28)
把 unified-quote 从起单一路手工答到底,发现并已修 5 项:
| 症状 | 根因 | 修法 |
|---|---|---|
| 流程链读作 A1→A7→A2→A6→A3 | toChain 按 layout.col 排,而 MVP 图是回形走线 |
新增 advanceOrder,按边拓扑排、排除回边 |
| 七个活动派出七张一样的「请处理」 | 槽位骨架 card: {},工单落回默认 |
卡面自述:标题 = 步骤标签,依据带步序 |
走到第 3 步就 exhausted 终态 |
六槽位共用 humanCalls: 3,A1 有 6 步 |
budgetFor(n) 按各自步数给 |
| 六步答完卡在「等人却没东西给人答」 | 末步全是 execution,而 released 只出自末步验收放行 |
骨架末尾追加一步「收口验收」 |
只走了 A1 却报 completed |
状态聚合把「没有在跑的了」当完成 | 「完成」凭到过终点;收敛未到终点 = abnormal |
当前诚实状态:A1 六步可人工代办、收口验收可放行、活动 released;但流过不了 G1 ——
出口 intake 的必填去向旗(needAlternative / needSourcing)产不出来。这不是缺陷:
出口载荷由台账落地事实推导(数组←落地行、标量←铺平事实),槽位没实现台账写入就没有事实,
让它凭空捏一个就是在系统内部 mock,规矩不许。现在这个状态会如实上报:
工作流 abnormal + 一条 route-dead-end 事件写明「G1 的两条出边要 needAlternative,
当时可用的键只有 body / customerCode / records / rfqRef / subject」。
于是 A1 的开发完成判据就具体了:rfq-intake 必须落 project.intake_record 等行、
并让 needAlternative / needSourcing 成为落地事实 —— 否则 G1 永远判不了。A2…A7 同理:
先看自己出口的必填字段能不能由落地事实推出来。
api:check 的盲点(2026-07-28)
契约面只收 index.d.ts / contract.d.ts / port.d.ts,而 index.d.ts 是 re-export 名单。
所以给纯函数加形参、改返回类型,这道闸看不见 —— 实测:aggregateWorkflowState 加了
第三个入参,api:check 报「公开契约无变化 ✔」。名单变化(增删导出)照样能拦住。
要补得让契约面也收 domain 的 .d.ts,代价是全部纯函数签名进快照、日常改动噪声变大;
在那之前:改纯函数签名时手动同步 CONTRACT.md,别指望闸提醒。