Commit Graph
98 Commits
Author SHA1 Message Date
xcsandClaude Fable 5 fa27cef60e 数据工单只建名单上的料:拼出来的料号与在库的料号写下来都当场拒(W05 1.4.1)
用户在新一单上看到件上多出一行要人补的料号。查实:模型在数据工单里给一颗**不存在的**
XL2596S-ADJ 铺了空骨架 —— 库里没有、入参里也没有,是把两个候选的名字拼出来的;
件上于是多出一行、红带点名它缺品牌。存在性核对本身判对了(ADJ found、XL absent),
错在「判对了却多写了一行」—— 事实在模型手里,能判对也能写错。

两道门,都在写入那一刻:
- `parts.profile.internal_pn refersTo sub_eval.pn_check.internal_pn` —— 只许建核对表里核过的料号,
  凭空拼出来的当场拒(参数/资料两张子表的 internal_pn 本就引用 profile,随之一并拦住)。
- 契约新增对象声明 `createOnly`(平台侧,~25 行):标了的表本单只建不改,键已在正式表当场拒并指路。
  它守的是另一形(在库的料被当成新料重建)。只管 Agent 的写路径,人在件上的保存不受此限。
纯函数 `createOnlyViolation` 可测(复合主键按全部键列比);活动侧三张 parts 表标上;剧本补一句。
当前在跑那一单的草稿里拼出来的那行已手工撤掉。2100 测 / typecheck / lint / depcruise 绿;api-report 已刷。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 13:28:25 +08:00
xcsandClaude Fable 5 51c87af583 数据工单有了自己的面孔;件上四处样式对齐;缺库不再问人;5173 反代改回本机
用户在工作台上连报六条(2026-08-27):
- 对话区把数据工单摆成「执行工单」→ 契约 ExecutionWorkOrder.variant:'data'(引擎侧与执行工单无异,
  只换面孔),归一层从卡上带下来,web 新增 DataForm(同一套办法、另一副面孔:色/图标/「数据工单」/「确认写入」),
  办结卡与治理台人类工具目录同步;活动的两张数据工单卡骨架标 variant:"data"。
- 表显示不全不能右滑 → 目标件把 overflow-x:auto 写在 .tbl-wrap,平台包装是 .jc-tbl-wrap,谁都没管到;补一条。
- 表头只留中文 → 三张数据工单表去英文列名与 .thd 小字。
- 编辑态内部料号「只读字 + 输入框」并排 → 目标件 #tbl1 .jc-vcell{display:inline}(id 特异度)盖过
  table.on 的隐藏规则,表 1 每格其实都并排,PN 那格恰好有字;用 id 特异度覆盖回去。
- input 与 select 没对齐 → 徽标从前包 .ro 编辑态留着、压在下拉框头上把那格顶低 13px,改成随只读格一起藏;
  input/select 钉同一高度。
- 第一步「派数据工单还是放弃候选」不该问人 → 决策卡退役(覆盖规格「候选 ≥2 可放弃」那一条,用户定),
  查无记录一律自动派;剧本/说明书/测试/e2e 同步(e2e 新增 expectAsks.max 断言「不该问的没问」)。
顺带:5173 的 /api 反代默认指向一条公网隧道(150.158.83.131:46100 → nps → npc → 本机 :8787),
浏览器每次请求先出公网再绕回来;默认改回本机,要远端的显式给 API_PROXY_TARGET。

静态渲染实证:900px 宽的框里横向能滑 200px;编辑态同一行 3 input + 1 select 全部 32px 同一 y;
只读 PN 格 display:none。活动包 1.4.0;2097 测 / typecheck / lint / depcruise 绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 12:36:13 +08:00
xcsandClaude Fable 5 3171d8a864 合并 feat/org-directory:成员目录(Keycloak 读写面穿透)+ 治理台成员/组织/权限三页 + 广播桥回弹修复 + 稳定版脚本 + W05 目标件
CI / ci (push) Failing after 11s
分支四条(2026-08-26 做的,一直留在工作树没入库,今天补提交):
① feat(org) 成员目录 —— KeycloakDirectory 端口与适配器、org-directory 契约/路由、sync-roster CLI、
   personName 姓在前、Staff.kind 按账号名现判;前端员工屏重做成成员 / 组织 / 权限三页签;
② fix(app) 广播桥「转发进来的不再 NOTIFY 回去」—— 两台后端互桥回弹 19 万条广播那次的修复,代码此前没入库;
③ chore stable.sh + .stable/ 入 ignore + vite preview 宿主白名单;
④ docs(w05) 目标件五份 HTML 与三份说明、实施计划、三张图。

冲突六处,全在 org 后端「姓在前」上 —— 主支(0c029b6,shanqin)与分支同日各做了一版:
主支拼「姓 名」留空格,分支 personName 汉字直拼(「柴浩」)、非汉字留空格(「Admin Amy」),
且分支那版是超集(服务账号识别、名册同步走同一把尺)。取分支版:port.ts / claims.ts /
keycloak-token-verifier.ts / keycloak-admin-roster.ts 及其测试按分支;主支那条 claims 测试
(「亲 删」→ 姓在前)保留,期望从「删 亲」改为「删亲」。api-report 重生成。

五道门禁绿:2096 测试通过、23 跳过(无模型密钥 / 无真信箱的惯常项)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-27 11:49:05 +08:00
xcsandClaude Fable 5 8556432dba feat(org): 成员目录 —— Keycloak 读写面穿透、治理台成员/组织/权限三页、姓在前 personName、Staff.kind
后端:`KeycloakDirectory` 端口(users / groups / roles 读写;写直接穿透 Keycloak —— 这里建的人立刻能登录、
删的人立刻消失,停用与收权之后 logout 踢掉会话)、`keycloak-directory.ts` + `keycloak-admin-http.ts` 适配器、
org-directory 契约与 /api/gov 路由、`sync-roster` CLI(ops 路由够不着时整册照过来),
容器按 provisioning 服务账号有无决定注不注入(没配就如实说不可用)。
`personName` 姓在前:汉字直拼「柴浩」、否则留一个空格「Admin Amy」,token 与名册同步两条路同一把尺;
`staffKindOf` 按 Keycloak 固定前缀 service-account- 认服务账号,员工那一屏不摆它(`Staff.kind` 不落库,按账号名现判)。
前端:员工那一屏重做成成员 / 组织 / 权限三页签(member-page 下钻),页签栏右端一句话说清这一格管什么。
契约变更:contracts org.ts / api/http.ts / api/org-directory.ts;api-report 已刷新;org CONTRACT.md 与 ADR-012 同步。
2026-08-26 做的,一直留在工作树没入库。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-27 11:44:36 +08:00
xcsandClaude Fable 5 bd86dd6088 docs(w05): 差异评估的目标件五份 HTML + 三份说明 + 实施计划;三张图
活动包 views/target-style.ts / report.ts 与两份 W05 文档都指着 docs/SS02-ACT-WORK-05-html ——
模版「逐字节搬自目标件」的那份目标件此前没入库。docs/img/cy 三张 png 现无引用,随批入库。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-27 11:44:35 +08:00
xcsandClaude Fable 5 6b7bf82df3 chore: 稳定版实例 stable.sh(.stable/ 入 ignore)+ vite preview 宿主白名单
./stable.sh 起一份稳定版(:8790 / :5180)托 .build/stable 的静态件;vite preview 有宿主校验,
经域名 / 反代进来会被拦 —— 放哪些宿主由起它的人说(PREVIEW_ALLOWED_HOSTS),开发那台 vite 不读它。
脚本只列环境变量名,密钥仍只从环境读。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-27 11:44:35 +08:00
xcsandClaude Fable 5 d67f744f59 fix(app): 广播桥 —— 转发进来的不再 NOTIFY 回去,治两台后端互桥回弹的广播风暴
「自己发的自己不收」只挡一跳:收到别人的就 publish 到本地,而桥自己也订阅着这个 broadcaster,
那条转发被当成「自己推的」再 NOTIFY 一遍(from 换成自己)→ 对面不是它发的、再转发……两台接力
就是永动机。实测两台后端连同一个库,一次 POST /api/gov/process/validate 引出 45 秒 19 万条广播,
工作台三把查询被打成连续重取(用户看到的是「疯狂轮询三个接口」)。
转发进来的那条打记号(WeakSet,publish 同步派发),「本地 → 库」那一侧认出记号就不发;
`bridgeBroadcaster` 第三参 `self` 可注入(同进程两座桥才模拟得出两台机器)。
2 测:转发不再 NOTIFY 出去 · 两台互桥一条广播各到一次就停(假频道封顶 50 条,让用例失败而不是撑爆进程)。
2026-08-26 修的,代码一直留在工作树没入库;CLAUDE.md 快照已记。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-27 11:44:35 +08:00
xcsandClaude Fable 5 6a69acfc69 合并 feat/act-work-05:第三个活动 ACT-WORK-05 差异评估(part-substitution-assessment@1.3.6)+ 脚本 e2e 逼出的平台侧改动
主支这边(d133d6c,PR #1/#2)是活动模块 L2/L3 前端(实例详情页、启动参数抽屉、禁用规则重定、
二次确认气泡、沙箱占位页)、工作台撤顶栏、姓名展示改姓在前(token claims 与名册同步);
分支这边 21 条:差异评估活动包(九个计划项、4 域 7 张主数据表、五张目标件平移的模版、
两枚纯函数工具、五案例 e2e 五例全 released 被拒 0)与平台侧 gateLedgers / answeredCardFact /
emptyGate / 早收只记观察 / 工具入参 null 归一 / 拒回不摆 S-FEEL 函数名,CLAUDE.md 快照跟上。

两边改的文件零重叠,自动合并无冲突。合并树上补三笔本该随各自提交入库的东西:
① api-report 两份快照 —— `org/port.d.ts`(主支加 given_name / family_name 时没刷)与
  `api/workbench.d.ts`(分支加 gateLedgers 时刷出来的文件没一起提交);
② pnpm-lock 补 `activities/part-substitution-assessment` 的 importer 条目(新 workspace 包,
  与当年 mail 模块同一回事;`--frozen-lockfile --offline` 装过核对一致);
③ org CONTRACT.md 补一笔 `VerifiedClaims` 增两个可选散件的影响半径。

五道门禁绿:2040 测试通过、23 跳过(无模型密钥 / 无真信箱的惯常项)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-27 11:37:29 +08:00
xcsandClaude Fable 5 6f7513aa58 W05 收口:五例在收紧断言下全 released · 全被拒 0(活动 1.2.9 → 1.3.6)
第 7–12 轮逼出七条(T–Z),两条是脚本自己的、五条是活动的,其中一条上结构:
- W 参数名写成英文 key,与库里中文行不是同一行 → 27 条里 14 条 UNKNOWN。措辞两遍 +
  乘法对账都拦不住单颗料那一路(5 = 5×1 恒成立),改自指 refersTo:param_name
  只能用台账里已经在用的名字,一颗料时也咬得住。
- V/Z 数据工单那一项的两个方向:建完第一颗就收掉(再给第二颗发卡撞「已办完」)、
  铺完骨架就标 created 直接 finish(撞「从没发过卡」)—— 顺序写死:骨架 → 发卡 →
  拍结束 → 人交回 → 才标 created;absent 没清零不许 finish。
- T P-10 也要「发卡前骨架先有行」;X 第二颗缺库也要先问。
- 脚本:U 一例翻车不拖垮整套;Y 按闸的指示「加一行」;断言收紧 —— 点名的行整例跑完
  仍没出现 = 失败,决策卡按「问的是哪件事」数(第 9 轮逮到一次假绿)。

流程上两条纪律(用户当场纠正):只跑受影响的那一例,改了平台共享面才全跑;先说计划再动手。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 11:13:53 +08:00
xcsandClaude Opus 5 182d3c39b5 CLAUDE.md:脚本 e2e 那八条入快照 —— 两条线逮到的缺陷不重合
记死三条可推广的:闸看的表与件上摆的表不是同一张,闸就等于没有;
拒回喂指针没用,得喂每拍都在的事实;零行 = 零缺会同时骗过闸与哨兵。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 09:32:03 +08:00
xcsandClaude Opus 5 df18882322 脚本 e2e 逼出的八条:闸看错了表 · 拒回喂指针 · 零行骗过闸与哨兵 · 早收不该算撞墙
凭证到位后 `w05` 五案例连跑六轮,逼出八条**浏览器走查逮不到**的缺陷 ——
脚本每张卡都先空手交一次、每个岔路口都按序代答,人手点的时候永远不会去试
「空着交上去会怎样」。末轮:跑到的四例全部 released,三例被拒 0
(第五例停在大模型账户余额不足,不是代码问题)。

平台侧(六条):
- L 数据工单的提交闸没牙 —— 卡没绑表就回落到必交付物,而数据工单要人补的是
  `parts.*` 主数据,不在交付物名单里:闸盯着一堆无关的表、一格不缺,空手交卷一路放行。
  契约新增 `ExecutionWorkOrder.gateLedgers`(这张卡该拦哪几张表,发卡的人说了算),
  `gateAddressesOf` 变三级取一:绑的表 > 卡自报 > 交付物。
- M `finish` 撞 doneWhen 为假的拒回只谈「行与列」,而缺的是一次**工具调用**;
  现在摆出判据原文 + 那几个名字此刻的读数,`tool_*_calls` 读到 0 的单独点名(`unmetWhy`)。
- N 人点了决策卡,模型每拍再发一张,连拒五拍熔断。把「他选的是 approve」写进拒回**没用** ——
  下一拍它又从头翻一次会滚走的窗口。改结构:`answeredCardFact` 把
  「这张卡已答过 → 他选的是什么」做成每拍都在的事实行。
- O 模型传 `requirement: null`,工具 `null.trim()` 当场崩(边界上一律经 `text()` 归一)。
- P 拒回里的 `count=(没写过)` 是 S-FEEL 函数名被当成台账名 —— 假线索模型会去追。
- Q/S 零行 = 零缺,闸与哨兵一起放过空手交卷的数据工单(`emptyGate`:卡自报的表一行都没有
  = 没完成);「早收」头一次只落观察、第二次起才算被拒(与「重发已答过的卡」同一把尺)。

活动侧(1.2.7 → 1.2.9):三张执行工单各自声明 `gateLedgers`;说明书③补「发卡前骨架必须先有行」、
⑤补这张卡的前提(一颗 absent 都不剩);哨兵 `parts_profile_rows >= created` 逐颗对账;
S0_bind 写明发录入卡那一拍别抢写需求行。

e2e 脚本加 `JCAI_E2E_BEARER` 直供旁路(服务账号 client_credentials 仍是正路)。
门禁 typecheck / lint / depcruise / test(2091)全绿;api:check 的差异仍只在 org/* 与 api/*
(工作树里先前那批鉴权改动),card.d.ts 是本轮的纯新增字段。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 09:31:35 +08:00
xcsandClaude Opus 5 e235207e4a CLAUDE.md:第三个活动 ACT-WORK-05 入库,现状快照与文档地图跟上
快照记三条值得记住的:「机检只读基座数」第六次现形(这次是存在性不是计数)、
模型会压拍(第四次,压过之后只有说明书还在)、措辞治不了的结构治但要先分清
「已经说过」与「从没说过」。文档地图补两份:端到端测试记录与 W05 字段说明。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 04:30:29 +08:00
xcsandClaude Opus 5 eb80876554 走查逼出的六条:发卡前清单要有行 · 在库要拿得出依据 · 数据工单只建缺的那颗 · 录入卡自带 basis
ACT-WORK-05 差异评估的浏览器全流程走查(六轮六条路径,全部 released),
逐条治了实跑里现形的缺陷。活动包 1.2.2 → 1.2.6:

- H(1.2.3):模型压拍把执行工单发了、清单一行没写,人对着空表 ——
  「先写清单再发卡」只在剧本里,压过拍就只有说明书还在;规矩搬进说明书。
- I(1.2.4,结构治):原 PN 根本不在库,模型两颗都判「在库」,硬阻塞整条路被跳过。
  剧本已写到头 → pn_check 加 found_pn_ref(refersTo + requiredWhen):
  判在库就必须填一个基座查得到的料号,查无此值当场拒、填不出则收不了步。
  这是「机检只读基座数」第六次现形,前五次都是计数,这次是存在性。
- J(1.2.5):数据工单派给了在库的那颗,缺的那颗没人建 ——
  剧本把「建哪几颗」和「要不要问人」写在同一串分岔里,从没写过「found 的不建」;拆成两问。
- K(1.2.6):数据录入卡的参考信息栏摆着引擎内部家具(九个计划项全名)——
  entryCard.basis 契约里可选,活动没声明,基座就兜了内部事实;卡骨架补三行人话。

包内自测 56 → 61 条;门禁 typecheck / lint / depcruise / test(2071)全绿
(api:check 的差异全在 org/* 与 api/*,是工作树里先前那批鉴权改动,本轮未添)。

docs/ACT-WORK-05-端到端测试记录.md:六轮走查的逐轮结论与 A–K 十一条发现总表。
docs/debt.md:四条平台欠账(disable 无路可走 · 前一项不收后一项的件推不出来 ·
拒回数不出缺的是工具调用 · 零行台账的提交闸无牙)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 04:29:40 +08:00
xcsandClaude Opus 5 7ea08fb16a ACT-WORK-05:S3 写盘前先自检工具调用数,堵死漏调 recommend 死循环
实测(R2,两候选)在 S3_assess 熔断:doneWhen 有一半读
tool_..._recommend_calls > 0,而模型只调了 compare-parameters、没调
recommend 就直接写盘、自己拍了推荐等级(数据面看着对,机制上是它自己
定的,绕过了"能算的不问 LLM"这条设计意图)。doneWhen 永远不真、finish
必被拒;拒回文案只说"是不是该写的行/列漏了",把模型的注意力导向行列,
于是连续5拍死循环重写同样的18行明细、2行关系,直至熔断——拒回指错方向
比不给指引更糟这条设计律的又一次现形。

不动 doneWhen(那半句工具读数正是"机检只读基座数"的落点)。改动收在
guide 两处:第二拍结尾点出"推荐等级只能由工具算,不调它这一步收不了步";
第三拍开头加一道自检——写盘前先看两个基座读数是否都>0,recommend 还是
0 就回去调它,不要往下写更不要自拍推荐等级。

包内测试钉死:第三拍段落里必须同时出现两个工具读数名与"自检/先看/必须>0"
这类先决语,第二拍段落里必须点出"推荐等级只能由它算"与"收不了步"。
版本号 1.2.1 → 1.2.2。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 03:25:08 +08:00
xcsandClaude Opus 5 c121349dea ACT-WORK-05:P-08 交回后先 finish 再发 P-10 卡,顺序写死进 guide
实测逮到:人交回 P-08 那张卡(表1+表2)后,模型下一拍直接发了 P-10
的卡,却没先 finish S0d_p08_master——引擎挑工作区画面按"当下还没干完
的第一步"取 view,S0d_p08_master 还 Active 时画面就停在 p08-master-create
不动,人拿到 P-10 的卡却看不见资料表一格,提交闸必然拦死。

两处 guide 补齐先后顺序:S0d_p08_master 交回段落写死"finish 在先、
发卡在后"两步动作串,并点出不收步的后果(画面停在哪);S0d_p10_document
开头加兜底句,若进来时上一项还开着,先补 finish 再发卡。

包内测试钉死这段顺序表述不会被静默删掉或改软。版本号 1.2.0 → 1.2.1。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 02:54:09 +08:00
xcsandClaude Opus 5 c6b02dfd39 ACT-WORK-05:标准化参数集改子表 parts.parameter,与目标件页面结构对齐
用户核对实跑页面后指出:P-08 数据工单做出来的页面结构必须与目标件
《数据工单-01-芯片主数据创建》一致——目标件本就是两张表(表 1 主记录 /
表 2 标准化参数子表),而实现把参数集塞成了表 1 的一个文本列,违反
《SS02-SS03-SS04_Demo台账字段细化与统一方案》§六「重复明细用子表承载,
不在单一文本字段堆叠」。

五处联动改齐:
- 平台基线新增 parts.parameter 主数据表(内部料号+参数名复合主键),
  两颗样例料的参数集从 normalized_parameter_set 文本列拆成 7 行子表数据
  (数值原样搬,一个字未改);parts.profile 物理列保留、不再赋值。
- 活动清单:parts.profile 撤掉该字段声明,新增 parts.parameter 声明
  (internal_pn 引用墙、param_value/constraint_type 必填);版本号
  1.1.1 → 1.2.0。
- 案例模型:S0d_p08_master/S1_input_confirm/S2_normalize 的读写面与
  guide 全部改指向新子表;顺带修正候选计数措辞歧义(实测样例0一张该发
  的决策卡被跳过——"候选absent且候选数≥2"被模型读成"不在库的候选≥2颗",
  改成先分别数"候选总数"与"不在库的候选数"两个数、显式说清楚判据认哪个)。
- caseFileModel 补 parts_parameter_missing 读数,两条哨兵(s_p08_created /
  s_pns_resolved)一并要求这一列不缺必填。
- 报告模版补齐表 2(逐块搬自目标件),主键列挂 data-col,约束类型走
  data-class-of 徽标。
- e2e 套件加性支持"一张卡填两张表"(stage.extraTables),buildEdits
  的 rowKey 泛化支持复合主键数组形态(不破坏既有八套件)。

验证:vitest 413 passed(活动包+平台种子测试全绿)、typecheck/lint/
depcruise 三绿、activities:install 零静态审发现、SQL 核对
parts.parameter 主键与行数(两颗料各7行)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 02:19:39 +08:00
xcsandClaude Fable 5 02ef572470 fix(w05): 评估输入清单两条 Critical 回归 —— 主键丢钥匙、必填填不了
定向复审发现上一轮模版重做(00133c2)新引入两条回归,都出在 sub_eval.input_gap 的缺口表:

- item_key 那格丢了 data-col:一单 11 行,item_key 逐行不同,服务端 resolveRowKey 既在件上
  找不到它、又不是「全表一致」的案号型列(那条路只对 caseKey 成立),人保存任何一行都会
  「认不出它是哪一行」。补回 <span data-col="item_key">,并把「加一行」提示补上「键必须填」。
- status 声明 required:true,又给了「加一行」,却被降成只读徽标——人新加的行永远没有格子
  填这一列,_missing 恒正、提交闸永远拦着(墙不配路)。改回 E04 执行回执同款的混排:
  <select class="jc-cell" data-col="status"> 挨着 <span data-class-of="status"> 徽标,
  只读徽标不裹 .jc-vcell(它没有孪生件),编辑态天然不被 :has(+ .jc-cell) 那条规则藏起来,
  不用动 BIND_CSS。

自查确认另两张可编辑件(parts.profile 的 internal_pn、parts.document 的 internal_pn+doc_seq)
的主键 data-col 本就都在,不是这次回归的范围。

补两条回归网(新 describe「可编辑件的主键与必填列」):①三张可编辑件每个非 caseKey 的主键列
都有 data-col;②每个 required:true 且落在可编辑件上的列都有 data-col。两条从
manifest.declarations.ledger 现读,不是照抄字面值,以后同类回归会自动变红。

P08_CSS 的「空行」复审点:重新用 hexdump 核对源文件 `ACT-WORK-05-数据工单-01-芯片主数据
创建.html` 第 86-89 行(两个 <style> 标签接缝),字节层面确认原文没有空行
(`...border-color:var(--line)}\n.tbl-gap{...}\n</style>\n<style>\n/* ===== 视觉补充段...`,
中间没有任何空行或隐藏字符);target-style.ts 的 P08_CSS 与该区间逐字节相同(Python diff
再次核验,长度 7531 完全一致)。没有改动这段内容——加一行会让它偏离已验证正确的源文件,
详见报告里贴的十六进制证据。

升版本号 1.1.0 → 1.1.1;pnpm activities:install 已验证装包成功。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 01:43:06 +08:00
xcsandClaude Fable 5 00133c2ca0 fix(w05): 五张模版整份重做 —— 逐块搬自四份目标件,不再照 E04/I01 那套版面语言做
用户实跑截图指出:芯片主数据创建的模版样式与给的目标件对不上。病根是 brief 一边说
「版面语言逐字量自目标件」,一边让照 I01 的 prd-style.ts 当视觉底 —— 两句互相矛盾,
《HTML报告生成规范》§7 写的是「搬」不是「照着做」。

- 新增 src/views/target-style.ts:四份目标件的 <style> 块逐字节机械抽取(P08 两段拼接、
  EXEC/ACC1/ACC2 各一段),抽取用 sed 按行号截取、diff 校验零差异;P10 无独立目标件,
  复用 P08 那份壳(规格里数据工单是同一模板按台账实例化)。删掉 src/views/prd-style.ts
  (I01 的视觉底,搬完之后没有任何模版再引用它)。
- 整份重写 src/views/report.ts:<div class="page"><div class="card">…</div></div> 外壳
  逐字节搬自目标件(不再套 .proto-root/.app-shell/.jc-stage 那套 E04/I01 自造的壳),
  内联 SVG 图标(.hic/.zic)原样搬;数据面(哪张表可编辑、哪几列可改、只读件零 data-edit)
  与上一版审查通过的绑定面保持不变。
- 修两处实跑逼出的坑(并入这次重做,未另开一轮):
  ① classToken(展示层消毒,吃掉下划线)与 slug(基座读数消毒,吃掉连字符)是两套不同规则,
     手写 .v-pin_to_pin 这类选择器永远匹配不上,件上显示「待定」——改用 vtok()/badgeCss()
     从 tally 值算选择器,不再手写;
  ② requiredWhen 是按行不按列,缺口表补充内容从前用静态 class="req" 把每一行都标必填,
     改成 <td data-class-of="status"> 按本行 status 值投影,只在 v-gap 那一档出红胶囊。
- 途中发现旧的 CSS 说明性注释里写着字面的「<script>」,会被 host.ts 的
  /<\s*script\b/i 检测判定为「含脚本」在装包时被拒 —— 顺手改写措辞避开(纯文档性质,
  不影响任何绑定或判断)。
- 升版本号 1.0.1 → 1.1.0(版面重做 + 两处呈现层修复是内容变更);pnpm activities:install
  已验证:ACT-WORK-05 · 差异评估(part-substitution-assessment@1.1.0)装包成功。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 01:15:44 +08:00
xcsandClaude Fable 5 c3ab236f70 fix(w05): s_bound 哨兵只在真有料号不在库时开门;全包清掉 disable 教学;version 1.0.1
浏览器全流程走查(样例 1「都在库·常路」)熔断,根因两条:①disable 在本引擎里不可达
(Disabled 只能从 Enabled 来,普通计划项一直停在 Available,disable 恒被拒);②s_bound
在每条路上都开(只看 demand_request_rows/sub_eval_pn_check_rows 两个恒真的读数),都在库
时把两个数据工单项照样推进「当下可为」,逼模型去 disable 它们、连拒五拍。

修:s_bound 的 onPart/ifPart 都改盯 sub_eval_pn_check_exists_status_absent > 0 ——
都在库时门不开,两项静静停在 Available、不进「当下可为」、根本用不上 disable;有缺库时
门照常开。S0_bind 的 guide 与其余各处教 disable 的句子全部清掉,改写清楚「都在库你什么
都不用做」,并明写「任何时候都不要对计划项用 disable」。s_p08_created 未动(都在库时
parts_profile_rows 本就是 0,它本来就不开)。

steps.test.ts 补两条回归测试:s_bound 条件文本含 sub_eval_pn_check_exists_status_absent
> 0(退回旧写法会红);NARRATIVE 与全部 ASSESSMENT_STEPS[].guide 里不出现「disable + 计划
项 id」这种祈使形(正则只逮教模型真去调用的句子,不误伤「不要用 disable」这类禁止句)。

manifest.ts 的 identity.packageVersion 1.0.0 → 1.0.1(安装恒新增,不升版本改动装不进去);
pnpm activities:install 已重装,静态审无提示(零发现,清单行未追加 findings 后缀)。

48/48 测试(steps.test.ts 18 + activity.test.ts 30)与 typecheck 全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 00:33:27 +08:00
xcsandClaude Fable 5 148d16a1b7 test(w05): e2e 套件 —— 五案例(候选缺库 / 常路 / 原 PN 硬阻塞 / 三候选含不推荐 / 新客户建档)
套件代码就位(SUITES.w05 + buildEdits/expectGateBlocks 加性扩展 stageEdits/fillWhere +
main() 套件名单加入 w05);node --check 与 pnpm lint 全绿,五案例结构(sample 0..4、三条
stage 正则、decideSeq/decide 取值集合、stageEdits 地址与 stages 地址对齐)已用一次性脚本
静态核对。缺 KEYCLOAK_E2E_CLIENT_SECRET,套件本身待凭证到位后跑。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 00:23:48 +08:00
xcsandClaude Fable 5 e1b04f01aa feat(w05): 差异评估活动登记进构建与安装通道;装包时顺带注册纯工具
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 00:13:26 +08:00
xcsandClaude Fable 5 7d743635eb docs(w05): 数据域字段说明 —— ACT-WORK-05 差异评估十张台账
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 00:10:28 +08:00
xcsandClaude Fable 5 ec3de1f19e fix(w05): landing-confirm 的 request_status / current_stage 改徽标 —— 放行签字页不再摆 ASCII 机器码
审查发现 P01_TABLE 的需求状态(tally 列)与当前阶段仍用 ro() 直显原值,把 responded/gate03
这类机器码摆在了放行签字页上(本仓栽过两次同款:BT 层级、key_person_change)。改用 tag() +
data-class-of 投影,EXTRA_CSS 补 t-reqstat(五档)与 t-stage(assessing/gate03 两档,取值域
由本活动写死)人读名;台账写入值不变,requiredWhen 机检仍读原值。测试组「枚举列只用
data-class-of 投影」扩到 landing-confirm 的四个枚举列(含既有的 analysis_status /
recommendation_level),把这次的回归钉死。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 00:05:24 +08:00
xcsandClaude Fable 5 0629a7f054 fix(w05): S0d 拆成 P-08/P-10 两个计划项各自绑 view;补齐 fresh() 三面旗;候选全放弃防死锁
Task 4 审查发现 S0d_master_data 固定单一 view 导致 P-10 那份件在引擎里永不可达
(剧本声明的 view 恒压过模型的 say.view),按裁决拆成 S0d_p08_master / S0d_p10_document
两个计划项,各自绑 p08-master-create / p10-document-create;caseFileModel 补
parts_profile_rows、新增哨兵 s_p08_created 串起两步的入场前提;说明书三处 itemId、
S0_bind 的 disable 提示、ASSESSMENT_PHASES 第一段随之更新。steps.test.ts 的 fresh()
补齐 input_confirmed_total / review_approved_total / landing_prepared_total 三面旗
(此前因未绑定名比较求值为假才没暴露)。S0d_p08_master 剧本加「至少留一颗候选」规则,
防止候选全被放弃导致 s_pns_resolved 永远不成立的死锁。46/46 测试与 typecheck 全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 00:01:39 +08:00
xcsandClaude Fable 5 f9dbdc8c9c feat(w05): 五张报告模版 —— 两张数据工单件、评估输入清单、差异审核件、落账确认件(目标件平移)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-26 23:45:17 +08:00
xcsandClaude Fable 5 f7fe8e3836 feat(w05): 差异评估案例模型 —— CMMN 八项七哨兵、说明书五卡骨架、八条剧本、五段进度
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-26 23:37:16 +08:00
xcsandClaude Fable 5 13183b9287 feat(w05): 差异评估活动包骨架与清单 —— 六张主数据追加声明、三张自持单据、两张 DMN、五条样例
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-26 23:28:17 +08:00
xcsandClaude Fable 5 f52f0168df feat(w05): 差异评估两枚纯工具 —— 逐参数四态比对与推荐定级(R-2026.07)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-26 23:17:43 +08:00
xcsandClaude Fable 5 63034f5ab2 feat(w05): 主数据基线加料件/需求/替代/验证四域六表(P-08/P-10/P-01/P-11/P-06)与样例料号
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-26 23:10:08 +08:00
xcsandClaude Fable 5 a681ffe9f7 合并 feat/keycloak-auth:统一鉴权中心后端(ADR-012)+ 邮件收发模块(ADR-013)+ CI/CD 入库
CI / ci (push) Failing after 10s
主支这边(f01286a)是前端接 Keycloak(keycloak-js / PKCE / Bearer / 60 秒票据)、
活动模块运营态模版、数据域与工作台改版、/me 个人中心;分支这边 66 条:后端资源服务器
与角色→端、邮件收发(IMAP/SMTP · 企业微信邮件 API)、CI/CD 四件与两份文档、
pnpm-lock 补 mail 模块 importer。

冲突两处(packages/contracts/src/org.ts · api/workbench.ts):前端对着后端文档先行补的
principalSchema / meRoute / authTicketRoute(注释里自己写着「apps/api 仍是旧后端,契约暂留」)
缺 RouteContract 必填的 access,SURFACES/surfaceSchema 也已在 api/http.ts —— 取分支(后端)
这一份;前端代码对它原样编译通过(staff.email / roles 本就在 staffSchema 里)。
pnpm-lock 自动合并,--frozen-lockfile 核对一致。

五道门禁绿:1945 测试通过、23 跳过(无模型密钥 / 无真信箱的惯常项);
合并树与先前验证过的那棵树(9e9ba38)逐字节一致。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01712YeSo5fTfkCSPZHBiniq
2026-08-26 14:34:19 +08:00
xcsandClaude Fable 5 de63c275cf pnpm-lock:补 mail 模块的 importer 条目 —— 登记为 workspace 包时漏了,每次 pnpm install 都会把它写回来
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01712YeSo5fTfkCSPZHBiniq
2026-08-26 14:34:19 +08:00
xcsandClaude Fable 5 f52edd5383 CI/CD 四件与两份文档入库 —— debt.md 早已写「已接」而文件一直没跟踪
.gitea/workflows/ci.yml · tools/ci.mjs · tools/cd.mjs · docs/部署.md:推主干跑五道门禁 +
从空库起的集成测试 + 三份构建,全绿后部署到 runner 那台机(机密一律 secrets.*,机器相关
一律 vars.*,文件里不写死)。debt.md 在 9848e2f 就点名了这几个文件,远端却从未有过 .gitea。

docs/keycloak-backend-guide.md:ADR-012 引为出处的后端接入手册(只有 clientId 与「有/没有」,
无密钥)。docs/数据域字段说明-E04-I01.md:设计中心数据域那一屏的字段说明。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01712YeSo5fTfkCSPZHBiniq
2026-08-26 14:34:19 +08:00
xcs 0f1e53d114 文档:两份前端文档按真环境核对 —— clientId 实测为 task-dispatcher,示例换成实测响应
设计阶段写的两份对外文档,Task 12 只按契约核过,没按真环境核过。这次跑通之后逐条对:
① clientId 写的是「建议 api-gateway」——实际这个 realm 里 api-gateway 的 mapper 对我们不适用,
   本部署是 task-dispatcher(两个前端 client 都已挂好指向它的 audience mapper,实测确认);
② e2e 账号写的是 bpos-e2e,实际用的是 task-dispatcher 的服务账号,且它只有 service+user
   两个角色 → 只进得了工作台,要跑测试台得另配;
③ 「ends 为空」原本像个假想边界,实际 service 角色就是——没配进「角色→端」表的角色一律不加端;
④ §8 的响应示例从「按契约构造」换成实测响应(/api/me 的 JIT 建档结果、403 的真 message、
   aud 不对时的 401、票据的真形状);
⑤ 两处「核对状态」改写:前置已就位、整条链真环境验过,并写明仍未实测的那一件——用真人 token 走一遍。
2026-08-26 10:55:58 +08:00
xcsandClaude Fable 5 f70de44338 终审残余两处:换 address 就是换信箱;错误话的出口也剥 NUL
复审的两条范围外观察,都落在这一轮刚动过的那两段里,一并收掉:

① 收侧判据看不见 address —— `imapSettingsOf` 用的是 `login ?? address`,而 login **常常空着**,
   这时候真正决定「登到哪个信箱」的就是 address。上一刀把发信三键排除掉之后,
   换 address 判据为假:旧游标的 UID 说的是原来那个箱子,而 UID 是按信箱编号的。
   判据改认**实际登录名**:login 填了就比 login(这时 address 只是发信落款,换它不动收信),
   空着就比 address。wecom 不参与 —— 它的 address 由探测回填,比它会在回填那一刻误清游标。

② 治毒信的那道门自己会被同一个字符毒死 —— 降级存档把解析失败的原因写进正文与
   `last_poll_error`,而那串原因常常来自那封信自己的字节。`errorText` 出口一律剥 NUL:
   所有落库的错误话都从这一个出口出去,分散到每个调用点去剥,总会漏掉下一个新调用点。

两条测试都验过 RED→GREEN(拿掉修复各自变红,旁边的既有用例不受影响)。

顺带:`mail/CONTRACT.md` 两处点名的 `createFakeMailProvider` 全仓不存在(未定义、未导出、无调用方)——
契约文档摆一个不存在的出口,照它接线的人会去 import 一个没有的东西;改成实情。
debt 补 ⑳-㉔(IMAP 每轮重登录与腾讯频率限制 / wecom -1 无退避 / pollDue 每 tick 刷日志 /
notes 不设上限 / claimForResend 推 created_at 使该列不再是创建时刻)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 04:07:08 +08:00
xcsandClaude Fable 5 58b1111e7e 终审修复轮补测:翻页上限与 lowestUid 各钉一条(P1-1 / P1-2 原本只改了实现没有测试)
- wecom 翻页上限:服务端一直回 has_more:1 时不再死循环 —— 到顶交出手上这些、more=true、游标原样退回。
  单请求 30s 超时护不住它:每一页都是成功返回,超时永远不触发。
- imap 回填锚点的取最小抽成纯函数 `lowestUid`(原先内联在 withImap 里,测不着):
  测里拿二十万个 UID 反证 `Math.min(...uids)` 真的抛 RangeError,而 reduce 那一版稳。
- 顺手清掉 mail-service-poll.test.ts 里最后一处指向「修复报告」的注释(语境会丢,P1-6 同款)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 03:49:57 +08:00
xcsandClaude Fable 5 816389a185 邮件收发终审修复轮:token 按凭证指纹、毒信有退路、只改发信设置不丢信、重发先抢、列表不搬原件、save 不盖运行列、重锚出声、脏游标不炸页、线程口径归一
全分支终审(跨任务视角)逮出的十条 P0 + 八条 P1,一次修完。每条都配了测试。

P0-1 企业微信 token 缓存改按**凭证指纹**(sha256(corpId,corpSecret),secret 不进 Map 的键):
  按 account.id 存有两条真实串号路径 —— 草稿探测共用 'draft'(探完 A 公司再探 B 公司会命中 A 的
  token,回的是 A 的应用邮箱地址,凭证填错的 B 也显示成功);已存账号换了凭证 id 不变,
  下一轮轮询继续拿旧 token 拉旧 corp 的收件箱,分发给新账号绑的通道(窗口 ≤ 7200s)。
P0-2 一封「毒信」不许卡死整条路:①domain 的 buildInboundRecord 把落 text/jsonb 的字段一律剥 NUL
  (PG 拒收它,MIME 解出二进制垃圾时真会出现);②单封失败先落**降级骨架**(原件原样存、
  主题从原始字节扒、正文写明失败原因)→ 落成了就推进游标并把「第 N 封(providerRef)解析失败,
  已降级存档」写进 last_poll_error;骨架也落不下才停在这一封,那时 error **点名是哪一封**。
  降级行不自动分发(正文是失败原因,不是客户的话)。
P0-3 cursorReset 只比**收侧**键(imapHost/imapPort/imapTls/login/folder;corpId/apiBase):
  从前比全部 settings,把 465 改成 587 也会清游标 → IMAP 重锚 → 上次游标到重锚之间的来信永远不拉。
P0-4 复用旧行重发**先抢**(OutboundStore.claimForResend,一条条件更新;真 store 与内存件同款):
  两个后端连同一个库,两台同时重放同一次放行都判「超时可复用」→ 两封真邮件,破掉「同键不发两封」。
P0-5 列表与只要元信息的读**不取 raw**(一封上限 16MB × 200 行);rawAvailable 由 `raw is not null`
  投影,不靠取回字节再判;只有「下载原件」走全列的 get()。InboundRecord 新增 rawAvailable。
P0-6 账号 save() 只写**配置列**:运行列(cursor/last_polled_at/last_poll_error/last_inbound_at/
  polling_until)只经 savePoll/claimPolling/releasePolling 改 —— 整行 upsert 会把另一台正持着的租约
  与刚推进的游标盖回读那一刻的快照(08-24 loop_state 整份覆盖同款指纹)。清游标改为显式单写那一列。
P0-7 UIDVALIDITY 变化不再静默:provider 回 note,用例落进 last_poll_error(「已重锚在当下;
  期间来信未拉取」)—— 丢了一段信而账号页一片绿是界面撒谎。
P0-8 parseCursor 连取值范围一起查(非负安全整数 + 界内:UID ≤ 0xFFFFFFFF、秒 ≤ Date 上限)——
  1e13 正是安全整数,乘 1000 出界,一行脏游标能让 listAccounts 整页 500。debt ⑨ 划线销账。
P0-9 线程口径归一:threadIdOf 改成**取根**(references[0] ?? inReplyTo,ADR-013 决策 3),
  mail-wiring 调它、不再内联一份。原测试改断言,不删。
P0-10 文档说老实话:mail.send 工具路发出的信**不登记 outbound_ref**(recordOutbound 的唯一调用方
  channel.sendMail 在 src 里零调用方)—— API 文档 §4 与 ADR-013 决策 6 各补一段,根治记 debt。

P1:imap anchor 的 Math.min(...uids) 改 reduce(十万级 UID 会栈溢出)· wecom 翻页加页数上限
(服务端一直回 has_more:1 会死循环)· MAIL_TOOL 导出并对**真契约**断言(原 v1/v2 是手抄夹具、
自证,且 v1 版本号写错)· reset-runs 清运行数据时把邮箱账号的游标与拉取状态一并清(留着游标而信没了,
wecom 十分钟重叠窗会把最近的信当新信再投一次)· due.test 的 leaseMsFor(30) 断言改写死 300_000(原为自证)·
cursor.test 删掉指向评审报告的注释 · 设计稿稿头标注三处已被 ADR-013 取代 · ADR-013 补记 wecom
compose_send 不带 inReplyTo/references(它发出的信在对方那里不串线程)。

契约变更(§12.4):port.ts 的 InboundStore 加 getMeta、OutboundStore 加 claimForResend、
savePoll 的 cursor 允许显式 null、MailProvider.pull 回参可带 note —— 实现方只有本模块 adapter 与
__tests__/fakes.ts 的内存件(两边语义一致,已同改),CONTRACT.md 已同步,api-report 已刷新。

测试:1922 → 1942(mail 模块 102 → 113,含真 PG 的 save 不盖运行列 / 不搬原件 / 两条真连接抢同一行)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 03:47:48 +08:00
xcsandClaude Fable 5 18ca6dacfe 文档修复:补齐入站线程取根、企业微信游标整窗语义、发信幂等第三支(未超时 sending 不发)—— controller 评审逐条核实后落笔
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:57:44 +08:00
xcsandClaude Fable 5 e862a5279b 文档:ADR-013 邮件收发、API 文档与前端对接说明(两形账号 / 四个动作 / 投递状态 / 腾讯侧前置)、总表 15 条、欠账十五笔、CLAUDE 快照
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:38:12 +08:00
xcsandClaude Fable 5 e0fc16db33 mail.send 工具契约 2.0.0(to 数组 / html / 附件 / inReplyTo / dryRun;保留 v1 的 body 与 queued);契约快照刷新,五道门禁绿
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:23:24 +08:00
xcsandClaude Fable 5 b6ec37a231 mail 接线两处收紧:入站线程取根不取最近一跳(否则跑一个来回①级归属就落空);附件没给 uploadRef 当场判 invalid,不静默丢掉
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:20:08 +08:00
xcsandClaude Fable 5 ab30ce56c5 计划修:入站线程取根不取最近一跳,附件缺 uploadRef 不静默丢
评审逐跳走了一遍:我方出站登记的 threadId 恒是这一串的首封,而归属解析①级是精确等值。
入站若取 In-Reply-To(最近一跳),跑一个来回就漂 —— 我方 M1 → 客户回 M1(命中)→ 我方再回
(实际 M2、仍记在 M1 名下)→ 客户回 M2 → 库里从没记过 M2 ⇒ ①级落空,退到③地址+时间窗。
取 references[0](根)才稳定;客户端只给 In-Reply-To 时退回它。

同批:app 层丢掉没给 uploadRef 的附件却照回 queued:true —— 模块自己那一层是硬的(抛 MailSendInvalidError),
app 层比它松,活动说「带上 q.pdf」而信发了附件没了、没人吭声。翻成 invalid。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:16:41 +08:00
xcsandClaude Fable 5 2d1558b99e 邮件路由 15 条:账号 CRUD / 探测 / 立即拉取 / 试发 / 入站出站 / 附件原件 / 重新投递;领域错误翻状态码,厂商失败在 body 里说
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:13:16 +08:00
xcsandClaude Fable 5 7b79d542a8 修复轮 1(wecom pull):游标推进与 more 改由同一判据决定,治单页 has_more:0 也被 limit 截断时丢信
评审逐字节比对确认 wecom-provider.ts 与 brief 转录一致——问题在计划本身:`exhausted` 只说明
页翻完了,而 LIST_PAGE(100)与 opts.limit 无关,最后一页可能同时 has_more:0 且新信多过本轮预算,
此时按 exhausted 推进游标会把 wanted 截掉的那几封永久丢弃(重叠窗只兜 10 分钟)。

改法:先算 more = !exhausted || fresh.length > wanted.length,再据同一个 more 决定 cursor 是否
推进到窗口上沿——游标与 more 从此不会分裂成两个判据。补两条测试:①单页 has_more:0 但新信
多过 limit 时游标原样退回(改前 RED:游标被错误推到 endTime,改后 GREEN);②-1 连续繁忙到
边界时 get_email_alias 恰被调 4 次(首次 + BUSY_RETRIES 3 次),此前已隐性通过,补断言钉死。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:09:06 +08:00
xcsandClaude Fable 5 d64f0b5ffd 修复轮 1(评审):mail 用例测试补钉三处伪覆盖 —— 附件不重存/中途失败/分发不因单个失败断链
评审判 Needs fixes:三条断言此前恒真,改坏实现照样绿。①重复拉到里撞 providerRef 的
那封改带附件,才验得出「守卫在存附件之前生效」;②拉取抛错改为两轮 pollNow ——
第一轮先用 afterCalls 让 pull() 真正成功推进游标,第二轮才炸,不再是「零封被处理,
游标没动只因为它压根没机会动」;③分发失败的订阅方 boom 从最后一个挪到中间(ok →
boom → skip),证「继续往下投」而不是恰好排在末尾。send 测试补 references 断言。
fakes.ts 的 failPullWith 加 afterCalls 支持第 N 次调用起报错。逐条用临时注入的
实现缺陷验证测试确实会红(验证后已还原,mail-service.ts 净差异为零)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:06:42 +08:00
xcsandClaude Fable 5 0c6e8e2c1b app 装配 mail:channel 作首个订阅方、出站经 mail.send、mail.send 工具真发信(dryRun 只校验)、轮询工作者;上传件存取抽出共用
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:05:50 +08:00
xcsandClaude Fable 5 58358210e2 计划再修:游标推进与 more 必须同一个判据 —— 「页翻完」不等于「新信吃完」
上一次修的是「多页时截断不许推进游标」,但漏了一形:最后一页可能同时 has_more:0
且新信多过本轮预算(LIST_PAGE 是 100,与 perTick 的 50 无关)—— 那时 exhausted 为真、
游标照推,而 wanted 截掉的那几封从此永久丢(重叠窗只兜 10 分钟)。

评审的话说得准:more 的判据是对的,cursor 的不是,两者本该是同一个。
改成 `const more = !exhausted || fresh.length > wanted.length; cursor: more ? cursor : 推进`。
补两条测试:单页 has_more:0 但新信超预算(游标不许动)、-1 连击到边界不再重试。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 02:05:39 +08:00
xcsandClaude Fable 5 5bd92f2e93 mail adapter:企业微信邮件 API provider(token 缓存与刷新 / 翻页 / 已知 mail_id 不再读 / EML 编码兜底 / compose_send)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:50:11 +08:00
xcsandClaude Fable 5 7afc40987b mail adapter:IMAP/SMTP provider(只读打开 / UID 游标 / n:* 过滤 / UIDVALIDITY 重锚 / nodemailer 发信)+ env 门控真信箱 IT
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:47:24 +08:00
xcsandClaude Fable 5 f04c64926b 计划修死企业微信那条路的游标丢信:截断时不许把游标推到窗口上沿,且滤重要在截断之前
Task 9 的评审点过「批末无条件覆盖游标、没有单调性护栏」,当时两个适配器还没落地、判不了实情。
照原计划的 wecom pull 读下来是两个真缺陷,都只在积压超过一轮预算(50 封)时现形:

① 被 limit 截断时仍把游标推到窗口上沿 —— 没读到的那几封从此跳过去了;
   重叠窗只兜 10 分钟,兜不住一次积压(停机重启、首次回填)。
② 滤重(known)发生在截断**之后** —— 一个窗口里若前 50 条恰好都已入库,这一轮交出 0 封,
   而引擎见空批即收工、游标又没动,下一轮还是这 0 封,永远吃不到后面的新信。

改法:滤重提到分页循环里(每页先滤再数够不够),游标只在把窗口翻完时才推进,
截断时原样退回 —— 已入库的下一轮被 known 滤掉,每轮照旧往前吃一批,不卡也不丢。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:42:10 +08:00
xcsandClaude Fable 5 40e592ae94 mail 用例测试:发信留痕 / 厂商失败不抛 / 幂等键 / 附件来源 / from 对账号
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:39:06 +08:00
xcsandClaude Fable 5 fba82c71dc mail 用例测试:拉取 / 去重 / 逐封游标 / 失败停位 / 分发三态 / 重新投递 / 附件原件
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:38:01 +08:00
xcsandClaude Fable 5 c5cb779390 修复轮 1:邮件 GET 改归 ops,不撑大两端共用读路由名单
全仓测试 route-access.test.ts 钉着一份有意枚举、随 Keycloak spec 审过的名单:哪些读路由被两端共用。
之前把邮件 7 条 GET 标成 design|ops,名单从八条长到十四条。裁决改路由、不改名单:邮箱那一屏落在
运营中心,设计端没有消费方;入站正文装的是客户来往内容,ADR-012 已把「设计端能读客户主数据」记为
一处有意承担的代价,不再无故加宽。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:36:12 +08:00
xcsandClaude Fable 5 43bacffd90 计划裁决:邮件路由的读也归运营中心,不去撑大「两端共用读路由」那份名单
全仓测试 route-access.test.ts 钉着一份有意枚举、随 Keycloak spec 审过的名单:哪些读路由被两个端共用。
计划原把邮件的 7 条 GET 标成 design|ops,名单就从八条长到十四条。裁决改路由、不改名单:
①邮箱那一屏按本计划自己的前端说明落在运营中心,设计端没有消费方(测试台读的是通道,不是邮箱账号);
②入站正文装的是客户来往内容 —— ADR-012 已把「设计端能读客户主数据」记为一处有意承担的代价,不再无故加宽;
③要往那份名单里加,得有真实消费方撑着。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:34:24 +08:00
xcsandClaude Fable 5 78e36ab73c 修复轮1:凭证解不开时明文经 SyntaxError 泄进 last_poll_error;同键在飞的那封会被当失败行重发;发成功不清上一次的错误话;卡死的 dispatching 给人手一条路
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:29:24 +08:00
xcsandClaude Fable 5 79fd25e1c1 mail 测试夹具:内存 store 排序与真 store 严格同序,补两处差异注释
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:11:48 +08:00
xcsandClaude Fable 5 1328aeb142 mail 用例:账号建改删与探测(草稿探测可借库里密文;wecom 探到地址回填);收发用例同份落地待测
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 01:08:48 +08:00
xcsandClaude Fable 5 b9a4af9a2a mail adapter:postal-mime 解析器(GBK 字头 / 群组地址 / References 拆串 / 附件字节)+ 测试夹具
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:59:40 +08:00
xcsandClaude Fable 5 d65579c0d8 修复轮1:mail 账号列表排序键还原为 name(与 channel/llm 惯例一致),测试改断言集合而非顺序
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:53:28 +08:00
xcsandClaude Fable 5 5f1e64b4d4 迁移 0033:邮箱账号 / 入站 / 投递 / 出站四张表;mail store 与跨进程租约(两连接对抢 20 轮每轮一个赢)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:49:24 +08:00
xcsandClaude Fable 5 8d7e56aa92 mail domain:发信校验与选账号(from 不静默回落)、分发状态机、账号记录与视图(密文进不了视图)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:28:02 +08:00
xcsandClaude Fable 5 6748ef01b7 mail domain:解析结果 → 入站记录 / 视图,附件与原件按上限分存留注,厂商原话脱敏三形
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:26:08 +08:00
xcsandClaude Fable 5 58d6d778ea 计划勘误:cursor 测试的 wecom epoch 字面量错了整一年(1756116000 是 2025-08-25)
实现 Task 4 时逼出来的:那条断言期望 2026-08-25T10:00:00Z,而字面量落在 2025 —— 整套夹具都锚在
2026-08-25T10:00:00Z,改输入而不是改期望,才不会留下唯一一个锚在别的年份的夹具。
代码侧已在 83f65b2 修正,这里把计划文本对齐。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:14:53 +08:00
xcsandClaude Fable 5 83f65b2785 mail 模块骨架:契约 / 端口 / 领域错误 / 游标两形 / 到期与租约(ADR-013)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-26 00:11:14 +08:00
xcsandClaude Fable 5 7c6e399b57 邮件路由契约 15 条:账号 CRUD / 探测 / 立即拉取 / 试发 / 入站出站列表 / 附件与原件 / 重新投递
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-25 23:59:57 +08:00
xcsandClaude Fable 5 411fecc8c6 邮件契约修复:地址校验保持严格 z.email(),改测试夹具而非放宽生产 schema
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-25 23:51:57 +08:00
xcsandClaude Fable 5 404a32fa0a 邮件契约:账号入参按 kind 分两形、视图只回 secretBound、入站 / 出站 / 探测 schema
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-25 23:45:37 +08:00
xcsandClaude Fable 5 26f07f3487 mail 模块登记为 L1;批准 imapflow / nodemailer / postal-mime 三个依赖(设计稿 §2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-25 23:34:17 +08:00
xcsandClaude Fable 5 649cec1197 邮件收发模块实施计划:17 个任务(契约 / 迁移与 store / domain / 用例 / 两条 provider / app 装配 / 路由 / 工具契约 2.0.0 / 文档)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-25 23:21:52 +08:00
xcsandClaude Fable 5 ebc6f778df 邮件收发模块设计稿:两条接入路(IMAP/SMTP · 企业微信邮件 API)、订阅分发、出站留痕、跨进程租约
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
2026-08-25 22:36:58 +08:00
xcsandClaude Opus 5 bb22545bdf 路由表字面重复也掀桌子;死信没收件人时出声;票据横向范围与「权限只到端」写进 ADR
**I1:字面路由重复注册仍是静默覆盖 —— 与参数侧的掀桌子不对称。**
两条**字面**契约同方法同路径撞上时,后写覆盖先写,命中谁全看 `Object.values(contracts)`
的顺序。参数模版撞了会抛(理由是「命中谁只看声明顺序 = 静默判错权限」),而字面这一半
没设防:同一个病、同一个后果。把守卫从 59 处收成 1 处之后,注册表是**唯一**的授权
事实源,它的自检不该有半边缺口。`size` 一并从 `routes.length` 改成
`literal.size + patterns.length`(重复被拒之后两者相等,但语义上它说的是「表里有几条」)。
`lookup` 上补一句:它只认 HTTP_METHODS 那五个,**归一是调用方的职责**;测里钉了
`lookup('HEAD', …)` 仍是 undefined —— 将来有人把中间件那行归一挪走,这条分工还在。

**顺手两条:**
- 死信通知的收件人为空时不出声(`deadLetters.report`),而 container 的 governanceStaff
  对同一件事会往 stderr 喊一句 —— 后者的理由(把「出事了」无声丢掉最糟)在前者同样成立。
- ADR-010 状态行补「已被 ADR-012 取代」:ADR-012 说了取代,但先翻到 ADR-010 的人看不到。

**两条不修,写进文档:**
- 票据的**横向范围**进 ADR-012 与 debt:票据里没有用途,60 秒内它等于持票人的
  **全部读权限** —— 凭一枚为下载附件取的票据,同一分钟里读得了整张客户主数据。
  不是提权(那些读权限他本来就有),风险在留痕面(URL 走浏览器历史 / 反代日志 / Referer)。
  收敛做法记进 debt:签发时把用途绑进票据(scope ∈ {stream, file}),或中间件对认票据的
  路径设白名单。
- 「权限只到端」的影响面进 ADR-012 的「后果」:design 现在同时意味着装活动、跑测试台、
  传文件、探大模型;design|ops 意味着 `GET /api/gov/data/:domain/:object` 能翻整张客户
  主数据(**从前要 data:read 才行**)。半年后有人问「为什么设计的人看得到客户手机号」,
  ADR 里得有这句话在。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 19:42:48 +08:00
xcsandClaude Opus 5 5facf6b745 名册同步补三道下限保护;同名让位;停用判定走在 JIT 写之前
**C2:一次同步能把所有人停用,而停用之后没有界面能救回来。**
「本地有、远端无 → 停」是有意的,但没有下限:①`listUsers()` 因任何原因回了空数组
(realm 配错但存在、代理 200 空体、view-users 被改窄)→ 全表 disabled;②更可能的是
**Admin 的 `GET /users` 不返回服务账号用户**(手册 §14.5 为此单列一条接口)——
e2e 与试跑的 AI 账号是首登 JIT 建的,本地在册、远端查无此人,**第一次点「同步名册」
就把它停了,再同步多少次都回不来**。而恢复动作 `POST /api/gov/staff/sync` 要 ops,
ops 要一个 active 的 principal:全员停用之后这条路自己把自己锁死,只剩去库里手改 state。

三道闸(两个触发面分别被后两条挡住,第一条是最后的网):
- `diffRoster`:`remote` 为空时不产出「缺席即停」—— 远端一个人都没有,更像取数出了问题;
- `diffRoster`:`testAccounts` 豁免「缺席即停」(新入参,必填 —— 只有装配层知道哪几个是
  服务账号),判据用既有的 `isTestAccount`(不分大小写、忽略空白);
- `syncRoster`:落 setState 之前用**同步后的活跃员工角色集**(新纯函数 `activeRolesAfter`)
  跑一遍 `assertNotLockedOut`,不过就整体拒绝、一条都不写;路由映射 409(与
  setRoleAccess 那两条同款,`syncStaffRoute` 从前没有这个 catch)。
  `AccessLockoutError` / `assertNotLockedOut` 收可选 message —— 两个来路拒绝的理由不同,
  给人的下一步也不同(这条指向「先核对 provisioning 指的是不是同一个 realm」)。

**I3:JIT 与同步撞 `account` 唯一索引 → 500,且此人从此登不进。**
Keycloak 里删了人再建一个同名的(或用户名被让渡),新 sub 走 insert 就撞
`staff_account_key`,`server.ts` 的 onError 把 PG 原文原样回给调用方。
处置选的是**先查后让位**(不是捕异常):走到 insert 就说明按 sub 找不到他,本地那条同名的
必然是另一个 sub 的 stale 行 —— 把它的账号让位成 `账号#retired-<旧 sub 前 8 位>` 再插,
留一条 stderr;不删那一行(它身上可能还挂着在途工作)。`StaffRepository.findByAccount`
为此回到端口上(退役的是 OrgService 上的那个同名出口,不是仓储这一口)。
同步逐条落库改为**单条失败不停在半路**:失败的人点名进 `RosterSyncOutcome.failed`
(契约变更,api-report 已刷新),三个计数改成**真落下的条数** —— 半途失败的同步
不许报成一排绿数字。

顺手两条:
- `authenticate` 的 JIT 写挪到停用判定之后 —— 一个已停用的账号只要还攥着有效 token,
  从前每 60 秒就能驱动一次 updateIdentity 写库;「第一次见到的人要建档」原样保留。
- `keycloak-admin-roster` 自己又写了一遍 dedup+sort+滤噪声 → 改用 `normalizeRoles`。
  两处对「同一次归一」的理解一分叉,`identityChanged` 的逐字比对就会天天判「变了」。

测:roster-diff +8(两道下限保护 + activeRolesAfter 四种局面)、org-service +5
(整体拒绝且一条 setState 都没落 · 远端空着不停人 · 服务账号缺席不停 · 单条失败点名且
不停在半路 · 停用账号驱动不了 JIT 写)、同名让位 1(内存仓储照搬了唯一索引这条约束)。
文档:org/CONTRACT.md 不变量与影响半径、API 文档 §0(HEAD / 409)· §3(三道保护 + failed)· §7。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 19:42:19 +08:00
xcsandClaude Opus 5 5964a80b28 鉴权中间件:HEAD 按 GET 判 —— 不归一等于「只要登录就能跑任何 GET 处理器」
Hono 在 #dispatch 里把 HEAD 改写成 GET 再分发,而 `c.req.method` 读的是请求行上的
原始方法,仍是 'HEAD';路由注册表只装 HTTP_METHODS 那五个,于是 lookup('HEAD', …)
**必然 miss** → 落进 `?? 'any'` 兜底 → 一个 `ends: []` 的人(Keycloak 里还没配角色的
新员工)照样能执行三个端的全部 GET 处理器。不绕过认证(无凭据仍 401),够不到
POST/PUT/DELETE,响应体被框架丢掉 —— 但它绕过**端授权**:响应头照抄过去
(desk 那两处 content-disposition 带着资料文件名),而 GET 处理器不是都没副作用
(`GET /api/reports/:period` 一路走到 report-service 的 store.save)。

改:查表之前先归一方法;**票据那一路的 GET 判断用同一个归一后的 method**,
否则 HEAD 拿不到票据那条路,两条路行为不一致。

测(auth-middleware.test.ts +3):HEAD 到 ops 路由持只有 workbench 的 token → 403
(与 GET 同款)· HEAD 到自己端内的路由 → 200 · HEAD 上的票据与 GET 同款。
反向验证:撤掉归一那一行,前两条当场红(200 / 401),装回即绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 19:41:43 +08:00
xcs 253c6c3afa 文档:补 412 precondition-failed 状态码说明,修正 Keycloak 前置编号
评审逐个核六条会返 412 的路由(流程包写回/启用、活动包安装两审、决议登记、
汇报提交、入站通道),发现 §0「状态码怎么读」表漏了 412 —— 这六条路由全在本文
自己的 §6 总表里,漏了会让照文档接的人撞上一个查不到含义的状态码。补一行说清
412 不是权限/参数问题,是「此刻办不了」。
另修一处编号错误:示例来源注明里把 e2e 服务账号 client(bpos-e2e)前置写成了
「前置 1」,按 spec 编号应为「前置 4」(前置 1 是本服务 client api-gateway)——
前端对接说明那份原本就写对,这次只改 API 文档这一处。
2026-08-25 19:01:27 +08:00
xcs a4716c8632 文档:前端对接说明与 API 文档按真实契约核对(路由归属总表与 schema 均已核对一致,补记示例来源)
用契约(packages/contracts/src/api/*.ts + org.ts)重新生成路由归属总表,与 API 文档 §6 逐行比对
(方法/路径/access 三者):零差异,95 条契约路由与文档现状完全一致,不改路由表。
§7 的六个 schema(Staff/Position/Principal/RoleAccess/RosterSyncOutcome/InFlightWork/WorkGroup/
ApiError)逐个核对字段名与可选性,与 org.ts 一致,不改。前端对接说明 §2 的十个文件指针全部存在。
Keycloak 侧前置(serviceAccountsEnabled 的 client、audience mapper)本轮未就位,示例响应仍按契约
构造 —— 两份文档末尾补记这一状态,留给前置就位后换真实响应。
2026-08-25 18:51:13 +08:00
xcs 7978c6faca 文档修订:评审四处 —— 「约100」改实数59(governance-ops 45 + governance 14)、ADR 引用改实际文件名、CONTRACT.md depcruise 措辞改准
评审指出「约 100 处守卫」是把行数(每处两行)误当处数,实测切换前 governance-ops.ts 45 处
+ governance.ts 14 处 = 59,其余路由文件 0 处;ADR-012 与 CLAUDE.md 各改一处。
ADR-012「后果」段引用的《前端接入手册》仓内不存在,改成实际路径
docs/前端对接说明-统一鉴权中心.md。org/CONTRACT.md「depcruise 已反查确认」改成
「depcruise 全量校验通过」——这一轮跑的是全量校验,不是针对性反查,原措辞强于事实。
2026-08-25 18:39:45 +08:00
xcs aa18701130 文档:ADR-012 统一鉴权中心;编码规范鉴权定调改为资源服务器 + 权限只到端;org CONTRACT.md 重写;debt 鉴权段;CLAUDE.md 铁规矩 5/6 2026-08-25 18:32:23 +08:00
xcs 9848e2f735 欠账:CD 接上了,五处「信号全绿而事情没办成」各配了一道机检
入口守卫的字符串比较、install-activities 的 continue、savePackage 的 onConflictDoNothing、
drizzle 只认 max(created_at) 的水位线、/api/health 不传 status —— 五个会静默成功的机制各配机检。
另记六条还欠(无版本可见性 / 无 graceful shutdown / 换版空窗 / activities:install 的样例副作用 /
reset-runs 被打进生产 dist / 每版一份 node_modules)。
2026-08-25 18:23:13 +08:00
xcs 4dabac2f7d e2e:改用 Keycloak 服务账号 client_credentials 拿 token,4 分钟一换;断言进得了设计端与工作台 2026-08-25 18:20:44 +08:00
xcs fc5afa9600 route-coverage 补下界断言防「数不到路由也绿」;跳过告警点名哪道保险没跑
评审 Important:missing 为空有两种可能——真的不漏,或者根本没数到路由
(app.routes 形状变了、createServer 早退)。空集是任何集合的子集,不设
下界这条 fail-closed 测试会从「会红」退化成「永远绿」。补
registered.length > 50 的下界,反向验证过(临时调到 1000 确认真的报红)。

无库跳过时的 stderr 告警原来只说连不上库,没说清「这道保险这次没跑、
漏的是哪一类」——按评审裁决不改 skipIf 行为(仓库级惯例),只改措辞
点名到位。
2026-08-25 18:17:03 +08:00
xcs 61622f94d5 启动自检 issuer 不一致拒绝启动;路由覆盖 fail-closed;Keycloak 配置解析测试(ADR-012)
上一个任务把统一鉴权中心接上了,但缺配置照常起、issuer 配错也照常起 ——
每个请求都 401 还查不出原因。boot() 开头补两道保险:缺
KEYCLOAK_URL/REALM/CLIENT_ID 直接拒绝启动;配了就拉一次
.well-known/openid-configuration 核对 issuer,不一致同样拒绝启动
(话里带期望/实际/KC_HOSTNAME,照后端接入手册第 5 步)。

另加路由覆盖 it 测试:Hono 里每条注册路由都必须能在 appRouteRegistry()
里查到归属,查不到就是一个没人管的权限口子。真库跑通,96 条路由全部
有归属、0 条查不到——把上一个任务里人工核对的结论固化成测试。
2026-08-25 18:04:21 +08:00
xcsandClaude Opus 5 8dc71db9d8 评审两条 Important:迁移连带清掉指向身份的两张配置表;治理台收件人为空时出声
节点负责人(排在轮值之前)与触发器确认人都是无 FK 的裸 staff id,而 reset-runs 按设计
不碰配置表 —— 切换后留着就是「把单派给不存在的人」与「命中的单没人点头全进死信」。
governanceStaff 在全新安装(还没人以 admin 登录过)时是空集,而调用方是死信通知:
收件人为空原本会静默丢掉「出事了」这件事,现在留一条说清怎么办的话。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 17:57:21 +08:00
xcsandClaude Opus 5 dd31b91829 统一鉴权中心接入(ADR-012):org 重写为 Keycloak 身份 + 角色→端;中间件按路由归属统一判;口令/会话/岗位授权退役;迁移 0032
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 17:40:55 +08:00
xcs 81a185cd61 app:鉴权中间件 —— Bearer / 票据 → Principal → 按路由归属判 401/403(ADR-012)
鉴权链的核心件:查路由归属表拿 access(public 直接放行),取凭据(Authorization:
Bearer;GET 且没带头时认 ?ticket=),Bearer 走 org.authenticate、票据走
org.principalOf,按端判定后把 principal 放进上下文。401(没凭据/凭据坏了,该
重新登录)与 403(端不够/账号停用,重新登录没用)分得清楚,前端靠这个分辨。

http-env.ts 只加不删:account/session 是 ADR-010 旧物,现役 server.ts 与十来个
路由文件还在读,退役是下一个任务的事。这一轮也不接线到 server.ts —— 接上会让
现役 cookie 会话链断掉。

测试文件按简报逐字照用,唯一偏差是 get/post 两个测试辅助函数包了一层
Promise.resolve:本仓 Hono 4.12 的 app.request() 类型签名是
Response | Promise<Response>,直接赋给 Promise<Response> 类型标注过不了
typecheck;包一层不改变行为(await 对两者结果一致)。
2026-08-25 17:14:30 +08:00
xcs d66f7fad3a app:60 秒 HMAC 票据 —— 给 EventSource 与 <a href> 下载这两条带不了头的路(ADR-012) 2026-08-25 17:02:08 +08:00
xcsandClaude Opus 5 3a34362eec app:路由注册表 —— 评审反馈修复(三个 Important findings)
补充了三处测试密度不足的地方:

① has/lookup 区分(Finding 1):
   参数名换一个时,lookup 仍能匹配(参数段认任意值),has 说没有。
   两个接口的语义差异这次被真的逼出来。

② 字面优先级压力测试(Finding 2):
   注册 /api/gov/:zone 和 /api/gov/staff,两条都能匹配同一个路径时,
   验证字面优先于参数段这个优先级是否真的生效。

③ 建表自检防卫(Finding 3):
   两条参数段模版互撞时,命中谁只看声明顺序 —— 静默判错权限。
   添加 canCollide() 函数与入口自检,撞了就 build fail loud。
   验证:真实 95 条路由建得起来(no collision)。

测试统计:9 → 18 条(+9),全量 1757 → 1766(+9),门禁全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 16:56:15 +08:00
xcsandClaude Opus 5 2eef8b4544 app:路由注册表 —— 契约自带 access,一处 lookup 给中间件(ADR-012)
每条 RouteContract 都声明了自己的 access 权限;此处扫一遍契约命名空间,
构建一个 lookup(method, path) → access 的注册表。新路由无需另加守卫,
只需在契约里声明,就自动生效——遗漏即无门禁,一处事实源确保完整。

- 字面路径优先于参数段(:name 匹配任意非空段)
- 方法维度单独追踪
- 识别 RouteContract:method/path/access/output 都具备
- routesOf() 抽出命名空间内的路由
- buildRouteRegistry() 与 lookup/has API 给中间件与测试用

扫结果:95 条路由已覆盖

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 16:42:11 +08:00
xcs 4d55ac465d org adapter:Keycloak Admin API 拉名册(provisioning 服务账号,分页 100,只读 view-users)
用 master realm 的 provisioning 服务账号(client_credentials)取管理 token(闭包缓存、
提前 60 秒过期),分页拉全 realm 用户(默认 max=100,必须分页),逐人查
role-mappings/realm/composite 取实际生效的 realm 角色并滤噪声(复用 isNoiseRole)。
非 2xx 一律抛带状态码的错,不吞;secret 不落任何留痕。
2026-08-25 16:32:47 +08:00
xcs 43917b559a org adapter:改正 explainJoseError 注释里的继承关系描述(评审 Important)
「JWTExpired 是 JWTClaimValidationFailed 的子类」是错的 —— jose 5.10.0 里两者都
直接继承 JOSEError,implements 只是 TS 结构化类型声明,运行时不建立继承。
判在前的真实理由是让「已过期」这条更具体的话先命中,不是靠父子关系兜底 ——
错误的继承说法会给日后「简化掉」判断顺序的人一个不存在的依据。仅改注释,
两条 instanceof 分支的顺序与内容一字未动,10/10 测试原样通过。
2026-08-25 16:28:18 +08:00
xcs cb143d6413 org adapter:jose 验签 —— RS256 写死、校 iss/aud、30s 容差、错误译人话(ADR-012;新依赖 jose,用户已批)
四项校验顺序即安全要点:algorithms 写死 RS256(绝不读 token header 的 alg,否则
alg:none 可绕过)、issuer、audience、clockTolerance 30 秒;jose 的错误经
explainJoseError 翻成人话后包成 TokenRejectedError,排错省时间。测试用
createLocalJWKSet 造本地 JWKS 当 Keycloak 替身,不联网,验签逻辑本身走同一个
jwtVerify。zod 的 .optional() 与本仓 exactOptionalPropertyTypes 有摩擦,
按 claims.ts::identityOf 已有的手法(undefined 就不带键)加了 toVerifiedClaims
做收窄,详见 task-3-report.md。
2026-08-25 16:21:17 +08:00
xcs 5108d7a580 org domain:claims 归一、角色→端与锁死不变量、名册差异、测试账号策略(ADR-012,纯函数)
为统一鉴权中心(Keycloak)落地铺地基:五个零 IO 的纯函数文件,后续 org 用例
重写(Task 8)直接复用。本任务只加不删 —— 旧的岗位授权 / 口令会话 / 停用检查
原样保留,现役 org-service 仍在用它们。

- claims.ts:token claims → Identity,滤掉三种 Keycloak 家具角色
- ends.ts:角色 → 端的并集,并守住「改完之后至少一人能进运营中心」这条锁死不变量
- roster-diff.ts:与 Keycloak 名册的插/改/停/复活差异(远端说了算)
- test-account.ts:测试账号判定与独立工作组构造(ADR-011 语义原样搬)
- in-flight.ts:停用前置检查要看的在途工作(与旧 disable-check.ts 并存一轮)

contract.ts / port.ts / index.ts / seed-data.ts 均为文件末尾追加,契约变更已
经 pnpm api:check --update 刷新快照。
2026-08-25 16:09:10 +08:00
xcsandClaude Opus 5 a5c8d4786d 契约:三个端与路由归属(access 必填)、Principal / 角色→端 / 名册同步 schema、退役路由标 @deprecated(ADR-012)
- packages/contracts/src/api/http.ts:新增 SURFACES / surfaceSchema / Surface / SURFACE_LABEL /
  RouteAccess;RouteContract.access 必填;API_ERROR_CODES 增 'gone'。
- packages/contracts/src/org.ts:staffSchema 补 email? / roles(realm 角色快照);新增
  principalSchema/Principal、roleAccessSchema/RoleAccess、rosterSyncOutcomeSchema/RosterSyncOutcome、
  inFlightWorkSchema/InFlightWork;旧岗位授权(grant* / governanceFunction*)与 sessionSchema/Session
  全部标 @deprecated 留一轮(web 编译不受影响,下个迭代删)。
- workbench.ts / governance.ts / ingest.ts:每条既有路由补 access(按所属端/两端共用/退役 public);
  新增 meRoute、authTicketRoute(access:'any')与 syncStaffRoute、setStaffPositionRoute、
  staffInFlightRoute、listRoleAccessRoute、setRoleAccessRoute、deleteRoleAccessRoute(access:'ops')。
- apps/api 的 org-service.ts 两处 staff.insert 补 roles: [](staffSchema 新必填字段,该文件下个
  迭代会整份替换,这里只是让本次 typecheck 过)。
- 新增 packages/contracts/src/__tests__/route-access.test.ts:95 条路由的 access 归属断言。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 15:54:20 +08:00
xcs 61f96d5ab2 gitignore:SDD 工作区不入库 2026-08-25 15:40:18 +08:00
xcs d788140548 工作成果里摆着别单的资料:「这一单产出的」认本单碰过的行
放行存档(archiveAssetFiles)与在办列单(assetFilesOf)取行都用 taskView(where:{}) ——
那是「正式版全表 + 本单草稿」的合并视图,而资料常落在主数据上,整表里躺着别单放行过的行。
抽 assetRowsOf 两处同用,touched 为必填入参;e2e 补份数断言。
顺手清两笔:wake.test.ts 的 as unknown as(lint 红)、wake 契约的 api-report 快照(api:check 红)。
2026-08-25 15:38:10 +08:00
xcsandClaude Fable 5 094386f02c 文档:前端对接说明与 API 文档(鉴权 / 人员 / 角色→端);计划加 Task 12 按契约核对两份文档
前端对接说明:要改哪些文件、/api/me 与端、401/403/410 处置、SSE 与下载的 60s 票据、
运营中心员工页的新接口、接入完成判据。API 文档:通则与状态码、每条新接口的出入参与示例、
退役八条的替代、全部 95 条契约路由的端归属总表、schema 附录、curl 实录。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 15:25:30 +08:00
xcsandClaude Fable 5 8ba4ab339e 实施计划:统一鉴权中心(Keycloak)后端接入 —— 11 个任务,每任务自带测试与门禁
按 spec 拆:契约归属 → org 纯函数 → jose 验签 / Admin 名册两个 adapter → 注册表 / 票据 /
中间件三件 app 小件 → 一次提交的大切换(org 重写 + app 重接线 + 迁移 0032)→ 启动自检与
路由覆盖 fail-closed → e2e 改服务账号 → ADR-012 与文档。执行前置:用户先处理工作树里未提交的改动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 15:15:56 +08:00
xcsandClaude Fable 5 98ce2966a2 设计:统一鉴权中心(Keycloak)后端接入 —— 资源服务器 + 角色→端 + 名册同步
推翻 ADR-010 的口令/会话/岗位授权:身份只认 Bearer JWT(jose 本地验签),
权限只到三个端(design/ops/workbench),角色→端一张表运营中心可改;
岗位本地 + 名册经 Admin API 同步;SSE 与下载走 60s 票据;退役契约留一轮回 410。
只做后端,web 不动。四问与三取舍已由用户裁决,记在 §2。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
2026-08-25 14:37:24 +08:00
xcsandClaude Fable 5 f3397a60ef E04 修到零被拒线,harness 补七刀:跨进程租约与广播桥、原子写、引用墙、对话区按序号排
活动侧(E04 2.1.0 → 2.1.9,I01 1.2.2):
- 覆盖闭合成为机检:基座新增 distinct 读数,「计划盖住的客户数 = 候选行数」钉进
  S3 收步 / S4 哨兵 / 放行 L2;人加的候选客户不再被整单无视
- 「关键人待识别」占位句式全链退役:查无联系人先发数据录入卡收人;契约新增 refersTo,
  计划/回执的 contact_code 只许引用真实存在的联系人(安装期核引用目标)
- 台账更新确认摆全落库面:客户档案/联系人档案两张落库预览(touched: 数据源,
  正式现值 + 草稿叠加),可补改、可加行(自增主键由基座续号);姓名升必填
- 更新明细:update_key 位置键 + 「同一处只留一条」L2;依据写人话不带机器键;
  标题旁不摆单号;判据与文案不夹字段名;行标识去掉单标识那一段
- 回执骨架 = 计划行数钉进 S3 收步与 S4 哨兵;重做分支不许删计划凑数
- 阶段刻度对齐文档 7 步;起单样例预填满

基座侧:
- 两个后端连同一个库:task-claim 跨进程租约(迁移 0030)+ Postgres LISTEN/NOTIFY
  广播桥;对话区按任务内记忆序号排,不按墙上时钟;并卡「刚说过话」按记忆序列判紧邻
- 多行 write 整批先验后写(dryRun),被拒 = 一行都没落
- 决策卡意见与本单草稿字面比对,归属以观察进轨迹(note-anchors)
- 括号栈补「补闭括号」两形(少一个 } / 行对象没关直接跟下一行)
- 件上加行:空着的自增主键直进续号,不从别的行借号;未落下的行点名回给前端
- 人答复落库那一刻就把 runtime 翻 running(task.wake),两块同帧变脸
- 试跑单可落真库(land_master,迁移 0031,默认开);数据域按活动分组;
  JSON 列不再是 [object Object];按钮键感 + 在办进度条 + 防抖
- 报价链 A1–A7 从系统卸载(只卸不删源码),种子只装 E04/I01

契约变更(api-report 已刷新):FieldDeclaration.distinct / refersTo,
objectDeclaration.file,landMaster,unkeyed,assetFiles,ledgerByActivityRoute。
1700 测 / typecheck / lint / depcruise / api:check 绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
2026-08-25 01:23:30 +08:00
xcsandClaude Opus 5 0aa2fc11b0 资料文件真产出来了;E04/I01 补到验收线,顺手填了几个「墙拦住却不给路」的坑
**资料文件全链**(用户:审核资料这一步要真产出 word/excel/md/html,前端能渲、点资料名能开)
接手时这一步一个文件都产不出来 —— S3 剧本白纸黑字写着「file_ref 本 MVP 阶段可留空」。
文件是台账那一行的投影,不另存一份:
· 声明 `file:{titleColumn,bodyColumn,formatColumn}`,它同时是授权面(没声明就点不开)
· 现造真字节 —— docx/xlsx 是自己打的 OOXML 包(zip-write:store+CRC32,零依赖)
· 读回来渲 —— 解的就是造出来那份字节,渲得出来才证明文件是真的
· 件上 `data-file-of` → 工作区二级界面(Word 纸面/Excel 网格/渲好的 md/沙箱 html)+ 下载原件
放行落地前把每一份冻进 `sys.task_asset_file`(迁移 0029):归档件点开的必须是签字那一刻
那一份,而资料落在主数据上、试跑单不落正式表 —— 不存这一份,收工后一个都点不开。
工作成果里「产出的资料」自成一摞。Markdown 富文本渲染顺带销账(仍零依赖)。

**Harness**
· 画面按「这一拍办完之后」在办的那一步挑 —— 从前拿拍首的局面决定拍尾的事:
  收掉 S4、发出 S5 的卡,而屏上还摆着 S4 的只读件,人无处可填(墙要配路第四次现形)
· 验收「重做」真能退回重做,且打回后台账没动就不许再提交(软硬两段,不锁死单子)
· 立监听的授权面是清单声明;发卡的授权面是调人预算,不是计划项
· 读工具同参重调有兜底;空转写身后还有行动就不废串;finish 的 doneWhen 按写完后的草稿算

**界面**
· 翻件与翻页各归各位(从前二选一,多卡多页时页翻不动);没得翻写「—」不写 0/0
· 工作成果详情能翻份;表宽超出能横滑(E04/I01 抄版式时把 overflow-x 抄丢了)
· 查看面灰底滚过一屏变白 —— 两条独立的路都治了(沙箱内不同色 / 弹性子项不长个)
· 测试台「取不到」不再当成「还活着」

**活动**
E04 关系覆盖、I01 品牌内容各自补到验收线:判据全读基座数、判据指向的东西摆上卡面、
样例满载且名字 ≤14 字;提交前要裁的判据搬进说明书(模型会压拍,压过之后只有说明书还在)。

验收:e2e i01 4/4、e04 3/3 全部 released,资料在办中渲得出、放行后存了档点得开;
门禁 typecheck/lint/depcruise/1628 测全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
2026-08-24 10:26:45 +08:00