Files
JX-BPOS/docs/debt.md
T
ChaiHaoandClaude Opus 5 259192f736 人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的:

件上就地改
· 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名,
  于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 ——
  而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。
· **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。
  删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)——
  「没回传就是删了」会把一次不完整的回传变成一次删表。
· **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。
· 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。

判据与门
· 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来,
  人填好的目标价它照样判「客户未提供」的毛病由此消掉。
· 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。
· 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开,
  于是一单没打标就进了放行门。三个数全由基座现数。
· 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。
  **指向两扇打不开的门,比不指还糟。**

界面不撒谎
· 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。
· 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、
  往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。
· 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。
· 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。
· 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。

A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。
五道门禁全绿(971 passed)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
2026-08-02 13:41:44 +08:00

790 lines
57 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.
# 技术债登记簿
> 编码规范 §17:历史违规集中登记于此——旧路径 `warn`、新代码 `error`,清零后升级。
> 例外必须注明原因与预期清理时间;散落的 `eslint-disable` 不算例外,算违规。
| # | 债 | 原因 | 预期清理 | 状态 |
|---|---|---|---|---|
| M6-1 | 转人代办的答案**不经出参 Schema 校验** —— 归型失败原样交回,由活动自己面对 | 人已经把事做完了,校验不过就得把它判成「调用失败」;而人做完的事不会因为格式不合就没做过。宁可让活动拿到一个明摆着不合型的值,也不谎报「这一步没成」。是否补一道「不合型即驳回重答」,等有真活动踩到再定 | 有活动踩到 | 进行中 |
| M6-2 | 进程重启后恢复的任务,动作级 trace span 整段丢失(`beginAction` 依赖内存里的节点父 span,重启即空)| 观测尽力而为不掀翻业务的设计取舍;恢复路径要补父 span 得把节点 span 持久化或懒重建 | 观测面强化时 | 进行中 |
| 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 | ~~投递记录写入即止,尚无派发 worker~~ | 派发器已于 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`,两缝完整)与**开环版**(`JCAI_AGENT_OPEN_LOOP=1`:Mastra `Agent` + 工具总线,拍内自主连环调工具) | 端口 `AgentPort` 不变,usecase 未动。**开环的代价**:模型逐轮调用不经模型网关的录制回放(tool-calling 往返 `generate` 的 text-in/text-out 承载不了)—— 四元绑定与凭证隔离照旧走 `resolve`,**工具调用一缝完整**;要恢复逐轮录制须给 Mastra 供一个自实现的 AI SDK LanguageModel。开环待第一个真 loop 活动压测后再考虑转缺省 | 第一个真 loop 活动 | 进行中 |
| M5-1 | `process` 未用 Mastra workflow 编译器 —— **运行时切面已立**(2026-07-28:`FlowRuntime` 端口 + 自持实现整体迁入 `internal/runtime/`,usecase 成薄壳,八例 E2E 为等价性护栏),编译器实现仍欠 | 编码规范 §6.1;接入路线已定(端口化+双实现,回边 reopen 先 spike);冲突清单见 process/CONTRACT.md | 编译器随 spike 结论落地 | 进行中(切面已立) |
| M2-2 | pg-boss 已装未用 | 定时作业(监听时限扫描、周期触发器)随 M5 落地;当下派发器用 setInterval | M5 | 进行中 |
| M5-4 | AND 并行没有声明机制 —— 网关废除后(2026-07-28)多出边一律异或,画不出并行分叉 | 分支=条件出边、汇合=多入边异或已够 MVP;真要并行时以「并行组」声明加回(对应 Mastra `.parallel` 结构块),与 M5-1 编译器同批设计 | 需要并行的第一条流 | 进行中 |
| M5-5 | ~~含网关历史版本的运行时兼容三处~~ | 用户令库只留一条流一个版本(外科手术清空),在途与历史归零 —— schema 的 `kind: 'gateway'`、`joinReady`、汇合等待、`gateway-deprecated` 违规项已于次轮全部摘除 | 2026-07-28 | 已清理 |
## 节点分派 · 员工组轮值(2026-07-28 第五轮更新)
编排不带岗位(活动点与审批点同一口径)——「谁来干」是运营时分配:
工作流绑了员工组按组轮值(组员并集最闲者先),没绑组**全员轮值**;
分派结果记在实例 assignee 上,审批箱/裁决权/链条/治理列表都按它认人。
ADR-011 的「绑定先于策略」随岗位一起退场 —— 工作组快照的**成员集合**成了轮值池,
按岗位点名的绑定语义不复存在。still 欠:起单时选组、按人指派、认领池 —— 运营中心的
分派界面落地后,这里的默认轮值让位。
## 编排层 Mastra 化 · 阶段状态(2026-07-28)
四阶段路线(用户拍板:端口化 + 双实现;回边先 spike;Agent 放开工具循环):
| 阶段 | 内容 | 状态 |
|---|---|---|
| 0 | loop 三修(提交放行发验收卡 / 事件只记不跑 / 轨迹喂回叙事面) | ✅ 已落 |
| 1 | 抽 `FlowRuntime` 端口:执行面成可替换切面,usecase 成薄壳 | ✅ 已落(八例 E2E 为等价性护栏) |
| 2 | reopen 回边 spike:`.dowhile` 子图 vs 外部胶合新 run | ✅ 已定案:**路线 B**(`spike/mastra-reopen/FINDINGS.md`) |
| 3 | Mastra 编译器 = `FlowRuntime` 第二实现 | ✅ 编译器 + 胶合层已落(路线 B);**尚欠 `MastraRunner` 的真实现与接线开关** —— 见下 |
| 4 | Agent 开环:Mastra `Agent` + 工具总线 | ✅ 已落(`JCAI_AGENT_OPEN_LOOP=1` 开启,缺省仍单拍) |
**解耦本身已达成**(不依赖阶段 3):编排层对活动只有两个触点(`TaskGateway.start`
起活动 / 终态回报),执行面可整体替换而活动侧无感 —— 证据是
`new-package-flow.it.test.ts`:从零建一条新流(建包 → 链接已装载的活动 →
双判据激活 → 起单 → 走完),全程没碰活动一行代码。
**阶段 3 的剩余欠账**:`MastraRunner`(startRun / resumeRun 的真实现,需要
`Mastra` 实例 + `PostgresStore` 挂 `mastra` schema)与接线开关未做 ——
编译器与胶合层都已落且有单测,但**没有在真库上跑过一整条流**;
转正之前自持实现仍是唯一在跑的路径。真跑时要验的两条(spike 已点名):
跨进程 `createRun({ runId })` 取句柄再 resume、Postgres 下的快照并发写。
已绕过的语义冲突(详见 process/CONTRACT.md):回边 reopen(路线 B 解决)、
`.parallel()` 任一失败即整体失败(失败走向不进 Mastra)、三态聚合投影
(读实例表不读 run 状态)、`node_instance` 投影表照写(发号权在主工程)。
## 鉴权(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 同理:
先看自己出口的必填字段能不能由落地事实推出来。
> 后记(2026-07-28 稍后):占位骨架落地后整条流已在真库上八例端到端全绿;
> 且**网关已废除**(G1…G4 不再是节点,上表按当日图记述)—— 判据不变,只是「G1 判不了」
> 如今读作「A1 的条件出边判不了」:要 `needAlternative` 的还是那两条边,只是边挂在 A1 身上了。
## 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**,别指望闸提醒。
## 触发链路 · 邮件分类 Agent(2026-07-30)
用户定的触发逻辑五步:接收邮件 → **分类 Agent 识别消息类别** → 命中 RFQ 事件 →
(可选)系统消息到指定人工作台 → 工作台确认起单。第 ④⑤ 步(确认链路)本轮已落地
(`sys.trigger_pending` + 消息 `eventRef` + 幂等确认);第 ② 步的「分类」目前由
**通道抽键正则 + 触发器 S-FEEL 规则**代位 —— 判得了「主题带不带询价/RFQ」,判不了语义类别。
升级路径:通道入站归一后挂一个 LLM 分类工具(工具箱五件之 classify,经模型网关一缝),
产出 `messageCategory` 进事件载荷,触发器规则改判 `messageCategory = "rfq"`。
前置:模型实例已绑定(大模型区)、分类样例集与回放审。不阻塞现有 RFQ 流 —— 正则今天够用。
## 范式统一的欠账:占位活动没有模型就跑不动(2026-07-29)
用户定:**活动不再分 flow 和 loop,内部实现都是 Loop**。本轮照此退役:
`flow-engine` 模块整个删除、`paradigm` 字段从清单与两张表退场、
`activities/` 里四个 flow 活动(A1/A4/A6/A7)与六个占位槽位改写成案例模型(CMMN + 说明书)。
**直接后果,写在明处**:`turn-runner.runTurn` 每一拍都要 `agent.decide` ——
环境里没有模型 key(`JCAI_DEEPSEEK_API_KEY` 或等价的实例绑定),案例活动**一步也走不动**。
于是三套端到端整套跳过,并在文件头写明恢复条件:
| 套件 | 为什么跳 |
|---|---|
| `unified-quote-flow.it.test.ts` | 靠答验收工单走完整条流;工单从前由 flow 末步发,现在由 Agent 每拍决定 |
| `new-package-flow.it.test.ts` | 同上(新建流程包后走一遍) |
| `testbed-trace.it.test.ts` | 同上(测试台起单 → 真任务 → 全链 Trace) |
恢复要两件,缺一不可:①环境有可用模型;②走法改写成 loop 的口径 —— 按 Enabled 集推进,
而不是按步序答工单。**在此之前不许把断言改松让它变绿**:绿得不诚实比红着更坏。
顺带作废的旧欠账:「占位期六个槽位一律 flow 范式,真开发时装回 loop」——
已经没有 flow 可退回,这条不存在了。
## 活动与工具调用:实现前探明的账(2026-07-30)
写《docs/活动开发规范.md》时三路探查把现状钉死,发现的差距与断链**全部汇总在
规范第 12 章「待实现清单」**(11 条主账 + 一串小不一致),此处不重复,只记两条
必须先修的:
- **写类动作意向登记后永不执行**:`submitForRelease`/`precheck` 无调用方,活路径
绕过预检 → 意向永停 `drafted`,而 `land()` 只执行 `prechecked`。「所有写动作必须
经人类验收」目前只做到了「登记」,没做到「验收后执行」。
- **工具未命中转人**:产品定「声明了但系统没有的工具,调用时转人类执行卡 + 作业
指导书」;现状是运行时抛 `ToolNotFoundError`、安装时 `tool-contract-missing` 直接拒装。
实现活动与工具调用逻辑时,照规范第 12 章的顺序销账。
## ~~A2 执行单的可编辑表渲染不出输入框~~(2026-07-31 已销)
**结论:不是渲染 bug,是两件事叠在一起。**
① 我自己的排障失误 —— 用 `.closest('button,a,[role=button],li,…')` 找任务卡,匹配到了错误的祖先,
于是屏上一直是**上一个任务**的工单(把 `editing`/`order.id` 打到 DOM 上一读就现形:
`oid` 与 A2 的工单号对不上)。正确的是 `.closest('button.task-card')`。
**教训:排障先把状态摆到 DOM 上读,别靠一层层猜。**
② 真缺陷 —— A2/A3 派的执行单**只给列名不给行**(cols 4/6,rows 0)。
可编辑表没有行就没有格子:人打开看到一排表头加一片空白,一个字都填不了。
已在两个活动的说明书与剧本里写死:**两张表都必须带行**,没有的值给空字符串
(空格子界面自己标红「待填写」)。A2@1.4.0 / A3@1.1.0。
<details><summary>原记录</summary>
**现象**:MVP 工作流第二轮,A2 的执行工单载荷完全正确 —— `mode: execution`、两张 `t:"table"`
且 `editable: true`(已用 `/api/desk/tasks/:id` 核过接口出参),页签停在「待处理」、
`disabled` 为假(提交钮在)、两张表都渲染出来了,**但一个 `<input>` 都没有** ——
即 `WorkOrderReport` 里的 `editing` 为假。同一条代码路径在 A1 的执行单上是好的。
**已排除**:①载荷缺字段(接口出参已核)②`blocks` 没传到(ViewRegion:382 传的就是 `cur.blocks`)
③`disabled` 为真(提交钮在)④`useState` 初值只算一次 —— 已改成渲染期按 `order.id` 重算,**没解决**。
**下一步**:在 `work-order-report.tsx` 里把 `editing` / `order.id` / `order.blocks.length`
打到 DOM 上直接读一次,先确认它到底是不是假、以及 `order` 是不是 `cur`。
这条挡着 MVP 工作流跨过 A2,是三轮端到端的**第一个硬阻塞**。
</details>
## ⑨ A2 派的是一张没有表的泛化卡(2026-07-31,未销)
**现象**:MVP 第三轮,A2 起单后派出的执行工单标题是「需要你判断下一步」、**blocks 为 0** ——
而剧本 `ALTERNATIVE_STEPS.T_select` 明确要求卡面放两张可编辑表(初筛清单 / 替代对标表)。
上一轮(A2@1.1.0)它是发了两张表的(只是 rows 为 0,即 ⑧);这一轮连表都没有。
**「需要你判断下一步」是 `fillCard` 的兜底文案** —— 说明模型给的 `human` 行动卡面几乎是空的,
基座用当下局面把它补全了。也就是说 Agent **没照剧本组卡**。
**下一步**:
· 先看这一拍的模型出参(trace 里的 ModelCall),确认它到底给了什么;
· 若卡面确实空,问题在提示词 —— A2 的 guide 写得比 A1 长且全是「怎么组 blocks」的细节,
可能被单拍上下文里的其它段落淹掉;考虑把 blocks 的形状挪进 ACTION_ITEM_SCHEMA 的
description(schema 比说明书硬),或在 guide 开头就把 JSON 骨架给全。
· ⑧ 的修复(必须带行)因此**尚未验证** —— 它要等 A2 真的发出带表的卡才测得到。
## A1 视图与判据重做(2026-07-31 第三轮)—— 已销与余账
**已销**(六轮端到端逐轮逼出来的,每条都有实测账):
- 报告模版照 PRD `活动执行页/` 两份目标件重做:`INTAKE_OVERVIEW` = 来件原文 → 受理要素
→ 指标带 → 有效 PN 清单(必填/选填胶囊 + 缺必填整行标底)→ 本轮不报;
`INTAKE_RELEASE` = 要素 → 逐条记录(带状况标)→ 分级依据 → 分流去向 → 两类请求
→ 结论 → **这一签下去会发生什么**。`@sdk` 新增 `letter` / `factCards` / `actionList`
与列样式(`req`/`opt`/`num`/`pill`),`data-blank` 让空值真的留空(判断在样式里,不在填数里)。
- **`@sdk` 五个零调用方的拼装件已删**:`kvCard` / `tableCard` / `noteCard` /
`stagesCard` / `critList`(六个活动一个都没用过)。
- **说明书不再重写一遍逐步做法**:它与剧本 `guide` 曾各写一份六步,分叉过 ——
说明书里点名的 `S1_register` / `S2_ask_back` / `S3_normalize` 三个计划项案例模型里
根本不存在,模型照它报 itemId 一报一个被拒。现由测试钉死:说明书不许点名计划项。
- **机检判据不再两边自报**:新增基座读数 `域_对象_rows`(本单在那张表写下几行),
`lines-present` / `route-alt` / `route-rfq` 各有一边读它;`quote-intake.classify`
的出参带 `counts`,条数照抄不靠模型数。实测拦下的是:条数报 9 而表里躺着 10 行、
三类条数报 7A/2B 而判定器给的是 5A/3B —— 派单数与条数同源同错,`2 = 2` 顺利过关。
- **「问过 ≠ 答过」**:S2 的收步判据从 `asked_total`(卡发出去就为真)改成
`answered_total`(答案已写回台账)。此前人填完提交,引擎下一拍开头就把这一步收了,
模型没轮到把答案写回来 —— 那一格白填。
- **归档件不再被冲掉**:收口后引擎还会跑一拍,那一拍推固定模版名 `case-overview`、
退到「声明的第一张」,把人刚签过的放行页冲成阶段二的执行件。现按剧本 `view` 推、
活步收完不推。
**余账**:
| # | 事项 | 现状 | 目标 |
|---|---|---|---|
| A1-1 | 制表符 BOM 的拆列靠模型数格子 | 已在说明书里写死「每行格子数 = 表头列数」,第六轮九行全对;但这是提示词约束,换个 BOM 仍可能错位 | 基座把附件读成**结构化行**(表头 → 对象)一并交给活动,模型只做列的语义映射,不数制表符 |
| A1-2 | `preauthorizedActions` 仍写在六个活动清单里 | 全仓零消费者(§15 ⑨),但契约里是必填字段,删它是破坏性契约变更 | 三步弃用:先转可选、再从清单摘、最后从契约删 |
## Harness 可靠性:三处「门本身教模型撒谎」(2026-08-01,已销)
端到端连跑两条样例路径逼出来的,每一条都不是模型跑偏,是**门的形状不对**:
- **① 判据只接受「是」,模型就去改那个数。** `optional-asked` 原是 L2:
`missing_minor_total = 0 or asked_total > 0` —— 两个数都由活动自己写。
实测:一颗料只缺目标价,模型如实记 1,提交被连拒四次,第五拍把它**改回 0**,
判据当场变绿放行,台账从此写着「缺选填 0 条」。
**机检判据只许读基座数得出来的数**;读活动自报的数,它不是门,
是一句「改哪个数能过」的提示。这一条已降为 L4 交人,并把缺口数摆上放行页指标带。
- **② 裁量判据也被要求「为真」,模型就去改裁断。** 换成 L4 之后,它如实裁了
「未满足」并写了理由,提交照样被拒 —— 判、提、拒、再判……十二轮 21 拍 187k token,
最后一拍把裁断翻成「满足」才过去。**裁量的本义就是可以裁出「不满足」**,
而不满足恰恰最该交给人。提交门已改:机检(L1/L2)必须为真,裁量(L3/L4)**裁过即可**,
裁断连同理由随卡交给签字的人。
- **③ 熔断数不出「同一堵墙」。** 一拍一串行动,同一个理由拒回来时标题里带着串位
(「第 2 个」「第 5 个」),整条标题一比就成了 N 堵不同的墙;而叙事窗只有 20 条,
一次撞墙连带五六条留痕,至多装得下三四次。两处叠加的结果:一单被同一条判据拒回
**33 次**、40 万 token 烧到 `exhausted` 才停,期间界面上只是「进行中」一动不动。
已修:比对时削掉串位前缀;熔断另开 60 条的窗(只在引擎里数,不进提示词)。
顺带:计划项号认唯一前缀简写(`S4` → `S4_history`),歧义为零才认 ——
实测三轮里两次因为模型只写前半截白烧一拍。
**验收**:两条样例路径各三轮,全部 `released`;Itelma 10 拍 / 88-90k / 行动被拒 0,
样例1 9-11 拍 / 71-88k / 行动被拒 0-1;数据逐轮对得上(9 行 5A/3B/1C · 1 alt + 3 rfq)。
## 试跑日志吞主键(2026-08-01,已销)
存档那一屏只渲染 `patch`,`key` 整个不显示 —— 三行受理记录屏上只剩
「eau / lead_time」,看的人据此判定「PN 没有提取到」,而 PN 三条一条不少全在 key 里。
实时观测面(`activity-run.tsx`)一直是对的,只有存档面走岔。两处已合并到
`features/gov-testbed/internal/drafts.tsx` 一份实现。**没显示不等于没有,
但屏上看起来就是没有** —— 这也算「界面不撒谎」。
## 画面比数据早一拍 / 满屏时刻差八小时(2026-08-01,已销)
用户实测报「PN 没取到,还是 —;第二阶段过不去」。查下来两件都不是抽取的事:
- **帧推在行动之前**。`turn-runner` 每拍推画面那一下排在行动循环**之前**,
于是工作区那份件永远比数据早一拍:S2 那一拍把九条料写进草稿,而那一帧是写之前推的,
清单一行都没有;偏偏这一拍以「发卡问客户」收尾,整单挂在那儿等人 ——
人打开工作台看到一张空表,合理地判断「PN 没抽出来」。
已改成**推在行动之后**;提交那一下由 `raiseAcceptance` 自己摆放行页(`framed` 打招呼,回合末不再盖)。
测里钉了顺序断言(推帧时已写下几行,推早了就是 0)。
「第二阶段过不去」是同一件事的另一面:卡在等人答那张执行卡,答完即收步(已复跑验证,11 拍 released)。
- **满屏时刻是 UTC**。后端存 UTC ISO,前端 17 处 `at.slice(5,16).replace('T',' ')` 直接摆上去 ——
一封 08-01 04:18 到的信,工单写「派发 07-31 20:18」、气泡写「20:18」。
差八小时的时刻比没有时刻更坏:它看起来像一个可信的事实,人拿它核 SLA、跟客户对时间,一核一个错。
已抽 `@web-shared/at.ts`(atMinute / atClock / atFull)统一换掉;报告里的入站时刻走
`case:openedAt`(基座填,同样本地化)。
**顺手撞到的坑**:批量替换把 `FlowEditor` 里一处 `text.slice(0, 16)`(截标签长度,不是时刻)
也改成了时刻格式化 —— 已回退。正则扫时刻要认得出「哪些 slice 不是时刻」。
## 拆表交给基座 · 同一件事不许问第二遍(2026-08-01,已销 A1-1)
- **附件里的表由基座拆好**(`agent-runner` 的 `sheetRows` / `attachmentPart`)。
数制表符是能算的事,交给模型只会数错格子:实测同一行栽过三轮 ——
BOM 上白纸黑字写着「Антенна · 浙江嘉康 · (空) EAU」,拆出来成了
「集成电路 · (空) · 30000」:品类错、制造商丢、年用量凭空多一个。
现在 `attachments[].rows` 一行一个对象、键就是表头;列数对不齐就不认这张表
(半张读歪的表比没有更坏)。
**两处坑连着踩**:①先把 rows 塞进那段按 1800 字截断的作用域里 —— 三轮下来照错不误,
因为它压根没进提示词;②原文与拆好的行并排放,把这一段撑了一倍。
现在附件**单独成段、不参与截断**,拆出来就把原文换下来。
验:同一条 BOM 连跑三轮,九行数据**一模一样且全对**(此前每轮都不一样)。
- **同一件事不许问第二遍**。熔断管的是「连续被拒」,管不了这种:每一拍的行动都合法,
只是问的是同一件事。实测一单里同一个缺口连问三遍,四次调人的预算耗尽落 `exhausted`。
现在发卡前比一次:**人已经答过的同一张卡**直接拒,并把出路写进理由 ——
「他把格子留空交回来,就是对方给不了,那也是一个答案:如实记账、往下走」。
认的是标题(标点与全半角归一后比),**不做模糊匹配**:拦错比多问一遍更坏。
- **待处理那张可编辑表绑台账**(`blocks[].ledger` + `fields`):
发卡那一刻基座从草稿层把**整份清单**铺进去,人看到的是完整九行、逐格可改 ——
与 PRD 执行页一致。此前活动自己把行塞进回复里,两处死结:回复有长度上限,
铺长了半途截断整拍作废;那些行台账里已经有了,再抄一遍是拿 token 换一次抄错的机会
(抄的时候把 BOM 原样的 30 填了进去,而台账里已经是 30000)。
## 阶段2/阶段3 两张件对 PRD 逐块核到齐(2026-08-01)
按 `~/coding/JCAI_PRD/docs/活动执行页/` 两份目标件逐块比对(比对脚本按块名断言,不凭印象):
**阶段2 待处理 22/22 · 阶段3 工作成果 17/17**。这一轮补齐的三块:
- **归属签**「通道入站 → 冯兵 · BD」:两样都是基座知道的事实(触发来源、任务负责人),
由 `case:origin` / `case:owner` 填 —— 让活动自报等于让被指派的人自己说自己是谁。
两头有一头填不出就整枚不摆(`:has(b:empty)`)。
- **落款位置签**:写「这份件在流里是什么位置」(A1 受理归一 → 交下游),
**不写状态**。在办与归档是同一张模版,写「已放行」进了档案才成立,在办时就是谎。
- **成交迷你柱**:`market.price_history` 有 customer_code / quoted_at / unit_price 三列,
但**这张表一行数据都没有**(种子不写、CRM 那件工具是桩)。所以不画六根编出来的柱子,
留原型自己的**空轨**(虚线)并在旁写清为什么空 —— 空得有出处,才不是组件坏了。
真要画,先得有人往那张表里写成交(见余账)。
仍**有意不做**的两处,理由写在 `views/report.ts` 的文件头:
① G2 命中行高亮 —— 命中在编排层求值,这份件画个对勾就是替它宣布结果;
② 报告里的编辑态与按钮 —— 改数据走执行工单(那张表已绑台账、整份九行逐格可改)。
**余账**:`customer` 维度的成交台账没有写入方 —— A4 定价要取历史锚点、A1 要画成交柱,
两处都指着它。定了谁写(A6 议价成交回写?A7 归档?)再画那条柱子。
## 去向由分类表算出、验收页可就地改(2026-08-01,用户定的口径)
用户口径两句:**阶段3 要按验收目标件做,而且可编辑;最后走哪条边是分类表算出来的,
不是人编辑或选出来的**。照此改了三处:
- **出口谓词进契约**:`output.selective[].ports[].when`(S-FEEL,按序求值首个命中即出)。
A1 的三口写成 `type_c_total > 0` / `type_c_total = 0 and type_b_total > 0` /
`type_c_total = 0 and type_b_total = 0` —— 与报告上那张 G2 规则表逐条对应。
发放行卡时基座自己判,卡上只如实告诉人**走哪一口、凭哪一条**(`routedPort`),
不摆选择题;算不出来的组(每口都得写 `when`)才退回「人选一口」。
`revisableAtRelease` 里那两枚去向旗随之撤掉 —— **去向不是人该改的东西**。
- **验收页可就地改**:必交付物声明 `editableAtRelease: [列名…]`,基座在放行卡上铺一张
绑台账的表(整份行都在、逐格可改);人提交那一刻**基座写回草稿层**,AI 不经手。
执行卡上那张表同理(`blocks[].ledger` + `fields`)。
- **人手改的值与 AI 写的值过同一道归型门**。第一版直接调 `draftWrite` 绕过了
`coerceToColumn`,于是一句「客户已确认」落进 `double precision` 列,
到 `releaseWrite` 才被 PG 掀桌 —— **人签完字之后**,整单落 `blocked`(实测)。
现在归得了就归、空值不写、归不了**跳过那一格**并留痕(别的格照落):
人填错一格不该把他改对的另外八格一起丢掉。
**又踩了一次「链上最后一处丢值」**:`blocks` 契约声明了、submit 传了、release 转了,
到 `container.ts` 那个显式对象里被抹平 —— 编译不报、运行时静默丢,卡面 blocks 恒空。
`fields` 当年就是这么丢的,注释还在旁边。**装配层的显式对象是这条链的最后一关**,
加字段时那一处必须一起改。
## 交代不复读:一字不差拦不住换个说法(2026-08-01)
用户指着对话区那一段问「不觉得啰嗦吗」——连着五条「阶段结论」说的是同一件事:
「9颗料都打上标了:7颗A…」「9颗料都打标完了:5颗A…」「这单9颗料都打标完了…」
字面各不相同,读到的却是同一句被复读了五遍。原来的去重只认**一字不差**。
现改成看重合度:两句归一(去标点空白)后,短的那句八成以上的字出现在长的那句里就算同一句。
阈值不敢再低 —— 近义但不同的交代重合度远低于此,**拦错一句比多说一句更坏**。
## 件是投影,不是快照 —— 改完数据要重渲(2026-08-01,一半修完一半列账)
用户:「我编辑了表格,点了保存,实际表单变了,但是保存后 A、B、C 的图标没变;
我全修改成 A,按理说底下的流向应该是直通 A4 直接定价,也没有受影响。」
三层原因,前两层已修、第三层**未修**:
**① 保存压根没落库。** `onEdits` 只把值塞进前端缓冲,等提交那一下才带走。
新增 `POST /api/desk/tasks/:taskId/table`(复用 `applyBoundTableEdits`),保存即写回草稿层,
工单不办结、放行门的位置没动。
**② 落了库也不会重渲。** `putView` 只存渲染好的 HTML —— 那一帧就是快照。
补迁移 0028 存下**模版名**,加 `views.repush(taskId)`:按当下数据、用同一张模版再渲一遍。
验证:人把九条全改成 A,**类徽跟着全变 A** ✓。
**③ 汇总列没人重算 —— 这一条没修。**
分布条与结论横幅那条去向读的是 `verdict` 的 `type_a_total / type_b_total / type_c_total`,
那是**活动写的汇总数**;人改的是 `record.type_tag` 逐行。逐行变了,汇总列不会自己跟着变 ——
于是重渲之后仍是「类徽全 A、分布条 5/3/1、去向 to-alternative」,**自相矛盾**。
要治得给台账一个「派生列」维度(声明 `type_a_total = count(record where type_tag='A')`,
基座按声明现算),与 `required` → `域_对象_missing` 是同一个路子:
**能由数据算出来的,别让活动自报**。那是新能力,不是小修,故列在此。
在它落地之前,人在放行页改类标只改得动逐行那一层,汇总与去向仍以 AI 写的数为准。
顺带修了一处真缺口:`editableAtRelease` **原本不含 `type_tag`** ——
写回按这份名单摆格子,不在名单里的列压根不进载荷,所以人改了九格、一格也没落库。
三分类是 P-03 算的,但算得对不对该由人说了算:改一颗料的类就是推翻一次判定,那是他的职权。
## 判据的第三态,与「看」跟「改」分家(2026-08-01)
用户两问:「①『选填缺口经过人』这个未达标为啥还在?②报告里两个可编辑表格默认锁定,
右上角安置一个按键,编辑按钮点击了才进入编辑态,取消或保存。」
### ① 那条红叉不是「活没干完」,是 AI 办不到、交你定 —— 而卡上说不出这个区别
`acceptanceCriterionCheckSchema` 只有 `{label, level, pass}` —— **裁断与理由传不到卡面**。
于是「裁了但裁成不满足」与「压根没裁」长得一模一样:都是一个红叉「未达标」。
人看到一条永远不绿的项,既不知道 AI 有没有办过、也不知道它为什么办不到,只能凭感觉签。
而提交门早就允许「裁成不满足照样能提交」(裁量的本义就是可以裁出不满足),
更早的注释里也写着「裁成不满足的,连着理由一起进验收卡:那正是签字的人要看的东西」——
**那句话描述的行为从来没有实现过**,因为契约里没有那两个字段。
补齐:`settled`(裁过没有)+ `rationale`(凭什么)一路带到卡上;
裁量项在卡面变成**三态** —— 达标(绿)/ **AI 判为不满足 · 交你定**(琥珀,附理由)/ 未达标(红)。
两处判据表(对话区的卡、工单报告)统一走同一个组件,免得三态只在一处生效。
### ② 看与改是两件事:表格默认锁着,按「编辑」才开
从前进待处理那一屏,整份件就是编辑态:A1 那份 54 个输入框一起亮着 ——
人还没决定要改什么,屏上已经是一张待填的表。**件首先是给人看的。**
每张可改的表各带一道闸(`[data-edit-for="域.对象"]`):锁着 → 「编辑」;
开了 → 「取消 / 保存」。取消还原成按下「编辑」那一刻的值,保存才把这张表交给外层缓冲
(真正写回草稿层仍在提交那一下)。一屏两张表各管各的。
**iframe 里依然一行脚本都没有** —— 按钮由父文档代理(`allow-same-origin`)。
**踩了一个跨 realm 的经典陷阱**:handler 里写 `event.target instanceof Element` ——
`Element` 是**父文档**的构造器,而 target 来自 iframe 的 realm,原型链不同,
`instanceof` 恒为 false。症状:点「编辑」毫无反应,而 class、CSS、选择器逐个查都对,
监听也确实挂上了、事件也确实收到了 —— 每次都在 handler 第一行 return。
**认能力不认血统**:有 `closest` 就当它是元素。
顺带把监听的挂载改成幂等的:`srcDoc` 换文档的时机抓不准,与其判断「挂过没有」,
不如每次有机会(effect / onLoad / measure)就挂一遍 —— 同一个 (type, listener) 重复 add 无副作用。
从前那个 `bound` 去重 ref 恰恰让「第一次挂在了空白文档上」变成了**永远没有第二次机会**。
## Harness 的五条死胡同:门拦住了,却没给改正的手段(2026-08-01)
**账**(每一轮都是真起单、真跑、真放行,`e2e-a1.mjs` 连跑):
| 批次 | 版本 | released | 熔断 | 备注 |
|---|---|---|---|---|
| 基线 | v15 | 4/5 | 1 | 那一轮 13 次拒回、23 拍、234k token |
| c | v16(+drop) | 4/6 | 2 | |
| d | v17(+名单/Completed) | 6/8 | 1 | 另 1 轮是模型侧变慢(4 拍 605 秒),非缺陷 |
| e | v18(+同源/指路) | 7/8 | 1 | |
| f | v18(+JSON 名单) | 7/10 | 3 | **三次里两次是同一条**:`必填齐` |
| **g** | **v19(+required)** | **10/10** | **0** | 全程 0 拒回,10-11 拍 / 99-112k token |
连跑 A1 摸底:**5 轮里 4 轮干净、1 轮熔断**;再跑 6 轮又熔断 2 轮。
挨个看失败形态,三条全是同一个病 —— **判据正确地拦住了一件办错的事,而模型没有一个动作能把它办对**。
门本身没错,错在门后没有路。
### ① 写错一行没有退路 → 加 `drop`
`XTSD04GLGEAQG-I` 是客户明说本轮跳过的那颗料,被写进了有效清单。
数报得全对(9 有效 / 1 跳过),行却有 10 条,判据 `record_rows = line_total` 当场拦住。
而模型**改不动**:它唯一能改的是 `line_total`(改成 10,`skipped_total` 又不对)。
13 次拒回、23 拍、234k token 才熔断 —— 而它从写错那一刻起就已经无解。
行动型里只有 `write` 没有删:草稿层本来就是这一单的过程事实,过程里写错一行是常事。
加 `drop`(`ledger.draftDrop` → `DraftStore.drop`):**只撤本单草稿层那一笔**,
正式版一个字都动不了,也不抛事件 —— 不是第四种写,放行门的位置没变。
撤不到如实回 `false`(主键写法对不上是常见原因):静默成功会让模型以为撤掉了,
下一拍判据照旧不过,它又去撤一次。
### ② 拒回只给数字,不给「差在哪几行」→ 加主键名单
行数类判据拒回时只说「10 ≠ 9」。模型看得见对不上、看不见多的是哪一行,
于是只能猜:改数、重写、再提。三种失败形态全是这一类 ——
记录多一行、分类表 0 行、替代请求 1 张而 C 类 0 条。
基座数得出来的事实,基座给:`rowKeyList` 把每张表本单写下的行的**主键**摆进读数
(封顶 12 行,超出的如实说还有几行)。多的那个 PN 就在名单里对着 —— 一句话能办的事。
**不替模型判断谁多谁少**:名单是事实,判断留给它。
### ③ 拒回的话把模型指上死路 → 按状态分开说
「`S3_confirm` 标了人工激活,要先 start 再干活 —— 当下是 **Completed**」。
模型照做去 start,而 `Completed → Active` 是非法迁移,又被拒;回头再撞第一堵墙。
两堵墙对着撞,连拒五拍熔断。
**拒回的话是模型下一拍的行动指南;指错方向比不给指引更糟。**
现在收掉/停用的项说「这一步已经办完收掉了,挂到当下这一步去(可选:…)」并列出可为的项;
`start` 一个已收的项也改成无事发生(与「已在进行中」同款)——收掉的项不重新打开,但不必为按一下报错。
### 顺带:两处收步判据比验收判据松
- `S5` 只看自报的 `untyped_total = 0` —— 数归零就收,而验收比的是
`type_table_rows = record_rows`(基座数的行)。数对了表没写全,**收了步却过不了验收**,
模型得回头补写一张已经收掉的表(c3 那一单正死于此)。
- `S6` 只看「写过 `alt_request_total` 这个数」—— 记完账就收,而单还一张没开。
两处都改成与验收判据同源。**收步比验收松,松出来的那一截就是返工。**
S6 那条要额外挂一句 `untyped_total = 0` 当前置:两个 `_rows` 由基座铺 0,
而 `type_c_total` 要模型写过才有 —— 开局两边都是「没写过」,S-FEEL 里相等即真,
这一步会开局即收(包内自测那条「doneWhen 开局就为真」当场拦下)。
### ④ 名单给了,主键却抄不对 → 摆成能原样粘的 JSON
装上 `drop` 之后再跑,模型**用了**它 —— 撤 `{"rfq_ref":"RFQ-MD14.1","pn":"XTSD…"}`,
连撤三次都撤不到。而名单里那一行是 `RFQ MD14.1/XTSD…`:**单号里是空格,不是横杠**。
名单用 `/` 拼串,模型得自己猜第一段是哪一列;列名它猜对了,值却按更常见的写法转写了一次。
**主键是认行的钥匙,一个字符都不能差 —— 那就别让它转写。**
名单改摆 JSON(`{"rfq_ref":"RFQ MD14.1","pn":"XTSD…"}`),拒回里明说「原样粘进 keys 就行」。
### ⑤ 最后一条自报的机检判据 → 台账声明加 `required`,基座自己数
十轮里三轮死在同一条:**「必填齐(PN / 年用量)」[L1] `missing_key_total = 1`**,连拒五拍熔断。
这条是 L1 机检,读的却是**活动自己写的数**。人在 S3 卡上补了年用量、基座已经把值写回草稿层,
而那个汇总列还要模型自己重算 —— 它漏算一次,这条判据就永远绿不了,而数据本身完全合规。
(其中一轮它还想拿 `drop` 撤掉一行有效料来「解决」这条判据 —— 手段有了,可用错了地方,
因为门指的方向本来就是错的。)
这正是自己立过的那条纪律的漏网之鱼:
> 机检判据只许读基座数得出来的数;读活动自报的数,它就不是门,是一句「改哪个数能过」的提示。
台账字段声明缺一维:**没有「必填」**,所以基座数不出来。补 `fields[].required`(可选,
不建 NOT NULL —— 草稿层的本分就是过程中允许缺),基座据此出两个读数:
`域_对象_missing`(缺必填的行数)与 `_missing_keys`(缺在哪几行、缺哪一列)。
判据改读前者,S4 的入场前提也一并改。**模型改不动这个数,也不必改 —— 它只要把那一格补上。**
### 还删了一个孤儿
`intake-gaps` 模版:7 处 `view:` 引用没有一个指向它,声明着、渲染着、随包走,
而没有一步会推它 —— 缺口那件事早已由 `intake-overview` 的红胶囊与题头计数说清楚了。
## 对话区太密集:把「一拍一句」改成「一轮一句」(2026-08-01)
用户贴了一段对话区,五条气泡里四条长这样:
> 「我这就把审定结论落账,收掉这一步,往下走查历史价。」
> 「我去查这三颗料的历史价,拿到判据就写进分类表。」
> 「…我这就把标写进分类表和受理记录,收掉这一步。」
> 「…我这就把两张请求单和去向旗写好,收掉这一步。」
原话:「太密集了」。**那些不是给人的话,是工序播报** —— 写哪张表、收哪一步,
人既不用知道也帮不上忙;那一段本该由「AI 工作中」的办事条覆盖。
用户定的形状:**设定目标 →(分析现状 → 规划 → 执行)× N 轮 → 输出一个聊天气泡**,
气泡可以是仅结论,也可以是**结论 + 功能卡(录入 / 执行 / 验收)两部分**。
三处改到位:
1. **哪一拍才出声**(`turn-runner` 的 `handsOff`):一轮的收口 = 控制权要交出去 ——
发卡问人(ask)、派工单(human)、摆岔路(decide)、立监听等外部(listen)、
提交放行(submit)。别的拍照旧留痕进工作日志(`internal`),只是不进对话区。
按「打算做什么」判而不是「做成了什么」:交代必须赶在动手之前说出口,人要先看见
「我要去办什么」再看见卡。代价是模型说要发卡却被拒的那一拍会白说一句 ——
那种拍本来也该让人看见。
2. **模型知道这条规矩**(`agent-runner` 的提示词与 say 字段说明):
否则它照旧每拍写一句播报,只是被我们默默丢掉 —— 白烧 token,还写不好那一句总结。
现在明写:say 只在交出控制权那一拍才摆到人面前,写这一轮的结论与为什么现在需要他。
3. **结论与卡并成一个气泡**(`chat-projection`):它们本是同一轮的两半。
从前各占一条,每轮就是「一条话 + 一条只有标题加个按钮的卡」,而卡的标题多半是
那句话的缩写。顺带把收尾那三条引擎回声(裁决:pass / 落地收敛,发射输出口 / 终态)
收成一条 —— 裁决是人自己刚按下的,发了哪几个口在工作日志里。
**并卡的判断一度写错**:挡住并卡的条件写成 `tag !== 'conclusion'`,而交代本身常常
就是结论口吻(「判据我都裁过了,交你签」),模型给它标 conclusion 完全正当 ——
于是第一轮并对了、收口那一轮没并,同一屏两种样子。改成按**来路**认(只有 terminal
那一条不许认领卡),不按语气签认。
账:同一条 11 拍的 A1,对话区从 **9 条降到 5 条**(系统开场 1 + AI 结论带卡 2 +
办结回执 1 + 收尾 1);被压下的 8 条交代一条不少地留在工作日志里。
## 放行卡上那两条不该在的判据(2026-08-01)
用户看着放行卡的判据表:「最后的验收 L4 选题口不应该是收口验收的指标;
L5 当前卡片就是放行卡,也不该显示在这里 —— **这里是判据**。」
两条都对,而且是两种不同的错:
**① L4「去向判断合乎业务常理」—— 判据得能指向一个可做的动作。**
去向早已不是判断了:三个出口各写了 `when`,放行那一刻基座按序求值、首个命中即出。
再挂一条 L4 让模型裁「这个去向合不合理」,是让它给一个**自己没做的决定**盖章 ——
裁「满足」是废话,裁「不满足」也改不动任何东西(能改的是三类条数,而那已经由
route-alt / route-rfq 两条机检看着了)。这种判据只是一道要人按一下的手续。已删。
**② L5「BD 放行」—— 判据是签字前要核的东西,不是签字这件事自己。**
那张卡本身就是这道关。把它列进判据表,等于在放行卡上摆一条恒为「未达标」的项:
人一路核到底,最后一行永远是红的,而消掉它的动作就是按下同一张卡上的「确认通过」;
「n/m 达标」那个数也因此永远差一条,看着像还有活没干完。
**声明留着,呈现去掉** —— L5 说的是「这份交付物必须由人放行」,由 `T_release` 的
人工激活 + 终局唯放行落实,提交门与预检本来就把它排除在外;少的只是卡面这一行。
滤在基座组卡处(`engines.ts`),六个活动一起生效,不是 A1 单独打的补丁。
验收:端到端 11 拍 released、被拒 0;卡面九条(L1×3 / L2×4 / L3×1 / L4×1),
浏览器里数过 —— 没有 L5,也没有那条去向裁量。
## 一份报告里嵌着另一份报告(2026-08-01)
用户:「现在你用你自己的报告格式**嵌套了一层**我要的报告格式」。
工单报告组件(`WorkOrderReport`)自带一整套外壳 —— 题头、编号/所属/编制/派发带、
依据指标带、章节头(`Part`)、判据表、落款动作 —— 然后把活动推的那一屏
`<ViewContainer>` 塞进自己身体里的一个「数据与分析」章节。两套版面语言叠在一起,
外面那套还先声夺人。搬 PRD 目标件那几轮全在改里面那一层,而人一眼看见的是外面那层。
**分家口径(用户定)**:
| 区 | 放什么 |
|---|---|
| 工作区 · 待处理 | **只放那份件**,可就地改 |
| 工作区 · 工作成果 | **只放那份件**,不加编辑态 |
| 对话区的卡 | 作业指导书 · 依据 · 判据 · 填报表单 · **提交与裁决** |
就地改的做法:可改的格子挂 `data-col="台账列名"`、行容器挂 `data-edit="域.对象"`,
渲染时每格多渲一个 `<input class="jc-cell">` 孪生件,**只读那份与输入那份由样式表二选一**
(根节点带不带 `editing`)。容器是 `sandbox="allow-same-origin"` 的无脚本 iframe:
外层读得到也改得动里面的 DOM,开编辑就是加个 class,收值就是把 input 读出来 ——
**iframe 里始终一行脚本都没有**。为什么两份都渲:同一份件既在待处理给人改、
又在工作成果里定稿归档,**两处是同一份**;渲两套等于存两份,存两份就会分叉。
### 两个只在「真人点」那条路上现形的洞
跑测脚本一直好好的,浏览器里一走就露:
1. **监听挂在 `about:blank` 上**。`srcDoc` 是异步换文档的,渲染那一刻
`contentDocument` 还是空白页 —— 监听挂上去,srcdoc 一到就随旧文档一起没了。
症状:人在件上改了两格,提交时载荷里一个字都没有(和从前「表格改动只活在组件局部
state」是同一种病的第二种得法)。修:回调放 ref、在 `onLoad` 里再挂一次、
记住挂过哪一份文档。
2. **算出来的去向没交回去**。`routedPort`(基座按判据算出的那一口)在卡上只作展示,
`selectedPorts` 恒空;人按「确认通过」→ 放行落地判「选择组未选出口」→ 整单 `blocked`。
**台账已经合并进正式表了,单子却死在最后一步。**
试跑脚本自己带着 `port`,所以这条一直被盖着。修:有 `routedPort` 就把它当作选中的那一口
—— 人按下的「确认」,确认的正是基座算出来的那一口。
验收:浏览器里走完两道人环 —— 件上改两格(DAG 年用量 0→30000、UMW 制造商补上)→ 提交
→ 落草稿层 → 放行 → **released**,九行数据落进正式表;工作成果那一屏零输入框、零外壳。
## 阶段2 整个没发生:把「人的门」写成了「缺东西时的分支」(2026-08-01)
用户:「为啥步骤发生了变化?阶段2没有了?**我都没确认需求 它就让我验收了**」。
查下来不是步骤被删了,是这道门从一开始就**不是一道门**。「受理需求经过人」写在归一那一步的
收步条件里,是半句话:
```
doneWhen: line_total > 0 and missing_key_total = 0 and (missing_minor_total = 0 or answered_total > 0)
└────────── 只在缺东西的时候才叫人 ──────────┘
```
于是一封字段齐全的询价进来,这半句在**拆完那一刻**就为真 —— 引擎在下一拍开头把归一自己收了,
一张卡都没发,人下一次被叫到已经是终局的验收放行。缺字段那条路上看着「正常」,
所以前面几十轮跑测都没露出来:样例 BOM 恰好总缺那么两格。
**教训:判据是「这件事办完了没有」,不是「要不要办」。**
要不要经过人是模型结构的事;写进判据,就成了「不缺就不必经过人」。
而 PRD 的目标件早就把答案写在页脚了 —— `A1-询价受理需求.html` 的落款是
「AI 拟稿,你审定;可就地修改后再确认」,按钮是【修改】【确认受理记录】:
**它从来就是一张人签的页**,不是缺东西时才弹的追问。
处置:`S3_confirm` 独立成项,与放行同款 —— **人工激活 + 不给 doneWhen**
(触达世界的项一律不给 doneWhen,给了就是给引擎一把替人签字的钥匙);
S4 的入场前提改读 `confirmed_total` —— 一个只有「人把卡交回来」才产生的数。
规范 §7.1 立为纪律 3.5。
### 同一次跑测挖出的零条死锁:**没写过 ≠ 写了 0 行**
拿一封「全齐、无 C 类」的询价复跑,卡在了另一处:验收判据
`quote_intake_alt_request_rows = type_c_total`(没有 C 类 → 0 = 0,本该成立)
连拒五拍熔断。原因是行数读数**只数写过的表** —— 一行没写,那个键整个不存在,
而 S-FEEL 里未绑定的名字参与比较求值为假。数据完全合规,门却永远打不开。
已修:按 `declarations.ledger` 把每张**声明过**的表先铺成 0,再让实际行数盖上去。
声明面就是授权面,不在声明里的表本来也数不着。
验收:常路 11 拍 released / 被拒 0;全齐无 C 类那条 10 拍 released、去向 `to-sourcing`
(那 2 次被拒是模型真忘了给 B 类开询价请求,门拦住它并被它改对了 —— 门在做它该做的事)。
## 版面重做:块齐了不等于长得像(2026-08-01)
用户第三次指出「HTML 报告跟我给你的两份完全不一样」。我之前两轮的「25 块到位」
是**结构断言**(某个 class 在不在),而结构断言对**版面一点约束都没有** ——
块可以全在,而版面是另一套语言。截图并排一比,差的是这些:
| | PRD 目标件 | 我的(重做前) |
|---|---|---|
| 题头下 | 直接进内容 | 多一段灰色 summary |
| 元信息 | 没有 | 多一条「单号/客户码/主题」带 |
| 章节头 | 小号加粗字 + 细线 + 右端 mono 小签 | 彩色方块编号 + 大标题 |
| 指标带 | 没有 | 插了 5-7 格 |
| 字号 | 固定 11/12/18px | clamp 响应式,整体偏大偏松 |
**教训:视觉目标件不能用 DOM 断言验收,只能看。** 这一轮把 PRD 的 CSS 数值逐条抄过来
(题头 16/20/13、章节头 11px/800/.12em、正文 12px、表头 10.5px、落款 13/20/15),
`section()` 去掉编号参数(六个活动一并迁移),删掉 summary / meta / statsBand ——
数字改进**章节右签**(目标件就是这么摆的:数挨着它说明的那一段站着)。
顺带修掉截图里才看得见的两处:
- **刻度条格子里冒出数字**(`meter` 同时带了 `data-slot`,九格排着「0 0 0 … 1」)——
只保留 `data-class-of` 上色,格子不填字;
- **空成交轨像坏掉的进度条** —— 没数据就别画那一格,把「为什么没有」写在卡脚。
画一个空壳比不画更像故障。
### 后续:抄数值仍然不够,最后是**整份搬过来**(同日)
上面这一轮把 CSS 数值逐条抄了过来,用户看完仍然说不一样 ——「你直接把两份报告
copy 过来替换掉你的 UI,重新适配功能层」。于是不再用 `@sdk` 那套外壳复述:
`PRD_CSS` / `PRD_SPRITE` 是**机械抽取**的整段样式表与图标精灵(Python 抽,不手敲,
只把反引号换成 `'` 好活过 TS 模板串),标记逐块搬进 `report.ts`,只换掉四样东西
(写死的行→`data-rows`、JS 算的数→汇总列、JS 插的胶囊→`:empty::after`、
JS 拼的 class 与 `style="flex:N"`→`data-class-of`)。规范 §10.3.1 已立此法。
搬的过程中挖出**渲染器一个静默失色的洞**:`data-class-of` 从前只在**带 `data-slot`
的元素**上生效(两者写在同一条正则里)。于是分布条那种纯色块
(`<i data-class-of="type_tag"></i>`,格里不写字、只按值上色)整条不着色 ——
属性还留在成品上,一个 class 都没长出来,屏上是一排灰格子。
**而灰格子看着像「还没算出来」,不像「没上色」**,查了两遍才认出来;
老用例每一条都同时写了 `data-slot`,所以谁都没拦住它。
现在拆成两遍:取值填字是一件事,按值上色是另一件事,后者不以前者为条件(补了 4 条用例)。
同批做实的三件:
- **去向注入视图**(`case:routeKey/routePort/routeTo/routeWhy`)—— 判定表上命中哪一行
由基座按契约的出口谓词求值后注入,与放行时发口是**同一次判定**;报告不自己算、也不抄判据。
- **分布条按条数分宽** —— 原型是 JS 写 `style="flex:N"`;改由 `data-class-of` 把
**条数本身**投影成 `v-5`,样式表备一把 `v-1…v-20` 的梯子取宽。模版仍然不许算。
- **`contact_name` 落进 `quote_intake.verdict`** —— 联系人卡上那个人。它本在
`customer.contact`(主数据)里,可老客户不重写主数据、本单草稿里根本没有那一行;
顶层裸键又与 `customer.profile.name` 撞名(后写的赢)。
**一张写着公司名的联系人卡比空着更坏**,所以跟 `customer_name` 同一笔写下。