Commit Graph
130 Commits
Author SHA1 Message Date
xcsandClaude Fable 5 8c5f3a7736 feat(activity-ops): 启动表单两格按芯片类型联动 —— 候选带分组键,先填的那格是锚
CI / ci (push) Failing after 10s
契约:launchOption 加 group/groupLabel(x-options.group 从字典表现读人读名)、
launchParam 加 linkGroup(x-link 结组);api-report 已随 --update 入库。
前端 launch-link.ts 纯函数管三条口径:锚摆全量、其余按锚的分组键收窄
(未标类型殿后一组,新增照走);锚粘着 —— 清空只解除收窄不换锚,人改得了主意。
服务端不据此立墙:combo 本就收候选之外的值,类型一致归 S1 收步判据
sub_eval_part_distinct_chip_type_id = 1(数得出来的门)。
W05 清单两格挂上 x-link: chip-type;文档同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-31 18:57:29 +08:00
xcsandClaude Fable 5 c4219a7b7d feat(w05+platform): 确认闸条件档 gateWhen —— 类型不一致不许确认;量高改可缩;卡间互换动画
2026-08-31 用户三连:

① 「提示两颗料不是同一个芯片类型…就不应该能写入并继续下一步」——
  缺格闸只数必填空不空,管不了跨行一致性。平台加**确认闸条件档**:配方 card 声明
  gateWhen(S-FEEL,只读基座读数)+ gateWhenSay,受理回传前在 submitGate 现算
  (rows/missing/tally/distinct 全套读数按声明表现读),为假 400、拒回用发卡人那句话;
  装包静态审与动作级 when 同一套审;评估不出的条件不成墙(坏表达式装包就拦)。
  W05 钉 parts_profile_distinct_chip_type_id <= 1 —— 钉 profile 本表(草稿现读零滞后;
  钉 sub_eval.part 那份要等配方 tick,人存完就点确认会被误拦),profile.chip_type_id
  加 distinct 声明。e2e 案例 2 的空手探针从此吃的就是这道闸。

② 「页面底部没东西还能向下滑一截」—— ViewContainer 量高从
  max(documentElement.scrollHeight, body) 改 body 优先:documentElement 以 iframe 自身
  高度为下限,内容缩回后它缩不回去,量高**只涨不缩**(参数编辑态把卡背拉长过一次,
  底部从此留一截空白滚动)。body(margin 0)才是内容自己的高。

③ 「两张卡片中间做出可替换的动画」—— 单点单向信号换成**对向双信号点**(左右互穿,
  走在卡后、只在缝里现身)+ 中央 ⇄ 徽轻呼吸 —— 这界面是判「可否替换」的,动画说的
  就是这件事。reduced-motion 全静。

测:契约 gateWhen 解析 + runner 透传(变异红)+ conditionGateOf 3 例(含坏表达式不成墙)
+ steps/activity 锚;2531 全仓四门绿;2.12.0 装库核过(gateWhen 入库/swapfx);
:8787 已重建重启。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-31 17:43:08 +08:00
xcsandClaude Fable 5 5a7bb9b58f fix(w05): 数据工单三病一改 —— 档案卡上桌、类型守卫、prune 跳过已落地
2026-08-31 用户实测三连 + 「设计只是头上加了动画,操作感没变」:

① 类型「自己给上了」:data-by 行缺键落回外层同名字段(debt I3),查名 pill 摆出
  案卷锚(原 PN)的类型 —— 这一行根本没有的值。守卫认 selectCurrent 按行作用域写的
  option[selected]:真选上才摆名,没选摆「待选择」。守卫必须单层 :has ——
  :has() 里嵌 :has() 是非法选择器,整条被浏览器静默丢弃(第一版栽过,预览截图逮回)。

② 「品牌…点保存根本不会保存」:块① 行源是 sub_eval.part,那张案卷表没有主数据列,
  renderTemplate 只认行作用域 —— 保存真落草稿,显示与孪生件永远空白(四步重写后的
  潜伏缺陷:e2e 走 API、浏览器走查在 swap 版上,两条验证线都照不到,真人一填才现形)。
  行源改 mine:parts.profile 本身(S1 ④ prune:true 恒有两行),行里天生有全部列。

③ 「改回同一类型,下面还是两个类型」:②③ 自投影为绕「已落地行撤不掉」的死锁
  (2026-08-30 e2e 案例 2)特意不带 prune,没落地的陈旧行也跟着积在屏上。
  治在平台:draftDrop 加配方专用旗 skipLanded(跳过并出声「N 行已随数据工单入库、
  这一笔不清它」,不炸;模型那一路的硬拒一个字不变),②③ 恢复 prune:true ——
  死锁与积行两头都收,debt「prune 与确认即入库的架构性冲突」销账。
  quiet 拍仍不撤行(F3),清理落在每次确认工单那一拍。

④ 设计:块① 从 11 列横滑表推翻成电路板工作台 —— 深色板底上两张可就地编辑的
  芯片档案卡(SOIC 封装 SVG + 角色/在库徽 + 类型行 + 双栏主数据格),编辑态整卡
  变表单;三级占位(必填红/建议橘)上卡与参数值表;参数值表所属料号列加角色徽。
  嵌套的角色/在库查表全只读(嵌套行里有 data-col 会被 collect 并成一条,测钉死)。

契约:loop-engine port draftDrop +skipLanded/+landed(纯新增,CONTRACT.md 已记,
api-report 已刷)。测:107 包内(三锚变异验证)+ auto-runner skipLanded(变异验证)
+ 2520 全仓;2.7.0 装库 SQL 核过(档案卡宿主/守卫/胶囊/S1 四笔 prune)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-31 14:12:10 +08:00
xcsandClaude Opus 5 45cec82984 fix(w05): 终审两条 Critical + 地基缺陷的另一半 + 文档与欠账收口
**C1 新建芯片类型会改写既有字典行(共享主数据被静默污染)**
链条逐行复核成立:件上种子行漏挂 data-col → barred 与 isSeq 双保护同时失效
(isSeq 那条是因为 Task 7b 把 type_id 从 integer 改成了 text)→ 落进「全表一致」,
而 H35 的自投影恰好保证这张表只有一行 → 人新建的类型被认成既有 type_id=1 →
确认时随范围放行把既有字典行改名并落进正式表,屏上只说「改了 1 行」。
三条各自成立的裁定叠出来的静默污染。
修法:种子行改成 disabled 的真实 <input data-col=type_id>(desk.ts 零改动),
格子里摆「由系统生成」——不能一声不吭地拿掉,那样人会以为这张档少了一格。
测试不止断言结果:另加两条用**真实 resolveRowKey** 重放病灶/修法两种 cells 形状,
直接证明因果机制。平台根治(全表一致应排除 auto 列)记 debt。

**C2 两个启动参数都选填 → 留空则整单永久无解**
gate_reason 说「在下面把两格补上」,而块① 没有加一行、也不在 gateLedgers ——
一句指向空气的指路。改为必填;第一档改口为唯一真无解的那条路径(两格填了同一个号)。

**I1 地基缺陷只修了一半:recipe.let 同样按对象键序求值**
与 fields 是一模一样的假设,同样住 jsonb 列、同样被 PG 重排;今天不炸只因 W05 那五条
互不引用,而 ADR-014 是给后续所有活动用的。新增 orderLets/letReferences
(把 {in:X} 视为依赖 —— 语义与 fields 不同,故单独实现),auto-runner 与静态审同步接入。

**I2 审核卡按「重做」照样收步推进** —— 坐实为真实平台缺陷(「卡答过了就收步」快道不看裁决),
并证明活动侧加选项救不了、chosenGuidance 那条也救不了(那是喂模型的提示词,
而这条快道存在的目的就是跳过模型)。按裁定只留复现测试,平台未改。

**I4** 同一堵墙撞第二次即发兜底执行工单交人 + 立监听(头一次软化留痕),
不再空转到 exhausted 而人无处可改。

debt +4(C1 根治 / modules→app / thead-after-tbody / data-by 落回外层),
并补写此前被两处引用却从不存在的「refersTo 传递闭包」条目。
文档:ADR-014:201「按声明顺序求值」改对(照它写配方的人会重犯本轮的地基缺陷)、
陈旧注释四处、spec 四处分叉逐条判断处置、端到端测试记录标为历史、
CLAUDE.md 纠正「新建类型端到端通了」那句(它没通过真人路)并更新版本与文档表。

包版本 2.1.9(已装,SQL 直查库内 version/required/gate_reason/视图 HTML 四项核实)。
pnpm gates 退出码 0(2476 通过 / 34 跳过);E04 92/92、I01 55/55 无回归。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-30 16:09:18 +08:00
xcsandClaude Opus 5 716f17ee2a chore(w05): 两枚工具与两张判定表退役,技能戳删除 —— 规则只许有一处真源
四态与推荐等级现在活在 S2 配方的 when 链里(spec §3.5)。
再留一份 DMN 或一枚工具,就是同一件事两处各存一份,改一处就分叉而没人看得出以哪份为准。
技能戳是给模型看的,而这个活动全程没有 agent。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 21:20:56 +08:00
xcsandClaude Opus 5 40daa6c9f7 fix(w05): 评审回来的一条 Critical、一条 Important、两条 Minor
Critical:S1 数据工单的 gateLedgers 带着 parts.chip_type,而这张件没有任何手段
写它(选类型走 data-options 下拉现读,不是编辑表)—— mine:parts.chip_type 永远
是空数组,desk.ts 的 emptyGate 永远判「这一单还一行都没有」,确认钮永久按不下去,
三张真被编辑的表填得再全也一样。删掉 parts.chip_type,只留真被这张件写过的三张;
注释写明这是永久决定,不是等 Task 7b 补了新建类型的编辑表就该还原(主路是从下拉
挑已有类型,那时这张表天然零行,「零行=没干完」这条闸的前提对它压根不成立)。
新增 apps/api 侧回归探针(调用生产 emptyGate,不抄实现):证明旧配置确实会永久
卡死、新配置在同一份数据下真的放行。

Important:BIND_COMMON 的序号计数器规则只认 .tbl-wrap,而三张表的外壳是
.jc-tbl-wrap(继承自已删除的旧 P08_MASTER_CREATE 同一个坑)—— 两个 class 对不上,
「#」列在只读态与编辑态下逐行空白。补上 .jc-tbl-wrap,钉一条结构测试。

Minor:①block③ 的注释「只两栏可改」没说全 —— internal_pn/param_name 也走 cell()
(主键格必挂 data-col 是既定约定,行的钥匙),补全这句话;②data-class-of="gate_reason"
在中文自由文本上永远算不出 v-token,是源码里躺着的一个永远不起作用的属性——删掉,
显隐只留 :has(:empty) 一处,brief 给的那条断言同步改成断真实机制。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 19:11:03 +08:00
xcsandClaude Opus 5 7b8d3b559b feat(seed): 芯片类型字典两张表 —— 约束类型从「每颗料各标一次」搬到「按类型标一次」
两颗料是不是用同一套参数名、同一套软硬约束,从此是结构保证而不是措辞叮嘱:
字段清单挂在类型上,一颗料关联一个类型。此前靠 parts.parameter.param_name 的
自指引用墙硬咬(措辞拦了三次没拦住才砌的),那堵墙可以退休了。
另播一颗 LDO(AMS1117-3.3)—— 它是「两颗料类型不一致 → 硬拦」那条路的唯一素材。

同批修复两条因此变为过时的既有断言(均在 seed-mvp-data.test.ts,非本任务新增):
「样例料号」测试原本枚举 parts.profile 恰好两行,新增 AMS1117-3.3 后改为三行;
「标准化参数集」测试原本断言每行 parts.parameter 都带 constraint_type,该列已
搬到 parts.type_param 字典,断言相应改为 toBeUndefined()。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 11:46:27 +08:00
xcsandClaude Opus 5 c3fed573ee fix(loop-engine): 收尾复审二审 —— 兜底卡自己会抛 / 清单兜底不看面孔 / 空转写解闸
复审逐条核实上一波(C2/C3/M1/M2)修法本身引入的三处破坏,全部属实并修死:

- I1 二审:配方办不下去时发的那张兜底卡(deps.cards.issue)从前裸调,调人预算
  触顶抛 HumanCallBudgetExceededError 会精确复现 C3 那种无限热循环(推进器每
  2 秒重来、不烧预算、不换状态、界面纹丝不动)。包进 try/catch,失败留痕、
  状态退回 awaiting,不让它抛出去。配了变异验证:拿掉 try/catch 后新用例
  真的变红(runTurn 抛出预算触顶错误),还原后转绿。
- C2 二审:decisionCardFace 的 checklist 兜底从前不看 reviewFace —— 一步只声明
  acceptanceChecklist 而 cardVariant 不是 review(T_release 那种形状)时,配方发
  的普通 decide 卡也会被兜上清单,前端不渲染勾选区、回传门却要求 checked ⊇
  checklist,卡按不下去。改成只在 reviewFace 为真时才退回剧本声明的清单。
- M1/M2 二审:project 动作里 localExisting/localUnkeyed 触发的观察痕与真写
  (localWrote/localDropped)共用 detail.auto + detail.ledger 这两个键,而
  raiseAcceptance 的 revised 判据只认这两个键在不在 —— 一颗料 skipExisting、
  或一行主键算不出来,这条"什么都没动"的痕照样能解开重做闸,人打回重做、
  配方原样重跑一遍,闸却开了。auto-runner 补 detail.changed(真写了/真撤了
  才为真),revised 判据对配方那一路改认这个标记。

另两笔记账:
- save-wake.test.ts 的用例标题写着"验收态就是这一种(没有卡在等人)"是错的,
  pendingCards 把未办结工单也算在内,验收态恰恰有一张开着的验收工单——只改
  标题措辞,不动断言与实现。
- docs/debt.md 补两笔:保存后唤醒的收窄几乎不生效(pendingCards 口径过宽);
  模型路径的写同样会 un-land、只补了配方那一半(独立起一条,排在 W05 重写前)。

pnpm gates 退出码 0(2410 passed / 23 skipped);
git diff --stat b995148...HEAD -- activities/ 为空,九个活动包一字未动。

报告:.superpowers/sdd/2026-08-29-deterministic-steps-auto-recipe/closing-fix-report.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 09:21:02 +08:00
xcsandClaude Opus 5 642bdcef3b fix(loop-engine): 配方少写的行要出声,撤掉的行要点名
**M1**:主键算不出来那一行从前是静默 `continue` —— 既不计数也不留痕。
表里少了行,人和判据都查不出为什么。现在这一笔结束时点明少了几行、主键列是哪几个。

**M2**:`prune` 撤行从前只说「撤掉 M 行」。2026-08-02 为人工撤行立过
「撤要说出来、要点名哪几行」的规矩(不说清,下一拍的读者会把人刚删的写回去),
配方这一侧跟上:点名撤了哪几行,封顶 8 个,**并去掉单号那一段** ——
人正在这一单里看,单号是脚下的地面,不是区分行的信息(2026-08-25 同一条口径,
基座那一侧是 `rowLabel`)。为此 `TaskDataGateway.writable` 补一个可选的 `caseKey`。

3 测(少写出声 / 撤行点名 / 名单不带单号)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 08:43:53 +08:00
xcsandClaude Opus 5 588d5c2dd4 fix: 配方只认 liveStep;保存不再无端叫醒任务;prune 不在人填的时候撤行
**I6 · 配方只认 `liveStep`**:`current` 有一条兜底(`liveStep ?? 在场的第一个有剧本的项`),
它不过滤 `settledIds`。当初这只影响提示词内容;配方接进来之后它驱动**副作用** ——
再写一遍台账、再 prune 一遍,carded 为假时甚至再发一张卡。兜底继续只服务提示词。
同处 `settledIds` 改取 sweep 之后的 `stateRuntime`(`liveStepOf` 吃的是 sweep 之后的
`stateEnabled`,两边得同一份局面)。
**注**:这一形状(既已收掉又在场)我没能在当前 lifecycle 里构造出可达来路 ——
`publishEnabled` 把 Completed/Terminated 挡在 entries 与 inProgress 之外,
`reenter` 又是就地改状态、不留旧实例。用例用的是 `live-step.ts` 注释描述的那种局面
(同 id 两份实例),守的是不变式;详见最终报告。

**F1 · `afterSave` 只在有未答的卡时才 `wake`**:`wake` 对 awaiting-event / awaiting-human
一律拨成 running,推进器随即推一拍并**记一步预算**。而现有九个活动包一个 auto 都没有:
人每按一次保存就白烧一步、界面闪一次「AI 正在推进这单」;任务本来 awaiting-human
(等人裁决)时被叫醒,那一拍结束还会把它写成 awaiting-event —— 从「等人办事」降级成
「等外部回音」,而等的明明还是人。**这是界面撒谎。**「保存即铺格」的场景恰恰都有卡在场。
用既有的 `humanTool.pendingCards`,不新造。`wake` 失败不再 `.catch(() => false)` 静默 ——
它是「保存即铺格」的唯一触发器,吞掉等于人按了保存、格子没出现、系统一个字不说。

**F3 · `prune` 不在人填的时候撤行**:人在件上「加一行」加出来的行,配方的数据源里当然没有 ——
编辑中途把它悄悄撤掉是最不能接受的一种静默数据丢失。有卡在等人(quiet)那几拍只铺不撤;
同一张表既被 prune 又列在该步 `humanWrites` 里,装包报 warn;规范 §4.6 纪律补两条
(prune 的表不该是人能加行的表 · card 之后那几笔要等人交回之后那一拍才跑)。

I6 2 测(做过变异验证)· F1 5 测 · F3 2+1 测。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 08:42:06 +08:00
xcsandClaude Opus 5 198d12af1b fix(loop-engine): 配方失败给人一条真的路;幂等在「反复进入」下也成立
**I1 · 失败不再伪装成「等外部回音」**:从前配方办不下去只 `status:'awaiting'`
就走了 —— 映射成 `awaiting-event`,而推进器只捡 `running` 的单:卡已答完、
没立监听、没有时限,**再没有任何东西会叫醒它**。人在工作台上看到「等外部回音」,
而外面没有任何回音会来。现照熔断兜底卡的成例:发一张卡把办不下去那一笔的原话
摆给负责人 + 立人事件监听。走到那儿时这一单没有未答的卡(runTurn 开头
`cards.unanswered` 把住),所以不会堆卡。不补 narration —— 那句话 auto-runner
已落成系统观察进了对话区,而且停下来的是系统不是 AI(narration 是 AI 蓝)。

**I2 · 重投不许把已落地的行 un-land**:`DraftStore.put` 的 upsert 尾巴上有
`landed_at = null`,而配方每一拍都把同一张表重投一遍 —— 数据工单确认入库之后
那些行的「已落地」记号被悄悄擦掉,整单放行会再写一遍正式表、再抛一遍 data 事件。
新增两个**配方专用**旗(模型那一路一个字不变):`skipUnchanged`(值没变就不落笔)、
`skipExisting`(「只建不改」的表碰到已在库的键当「不用建」跳过并留痕,不抛 ——
那句拒回是写给模型看的,配方抛出去就是永久停单)。已在库的行也进 `kept`,
prune 不许撤它。

M4 一并做了:auto-runner 的假 `draftWrite` 从**覆盖**改成生产的**合并**语义
(`patch || excluded.patch`),并补上 landed / createOnly 两件生产真有的事 ——
「重投不会擦掉人填的格子」这条结论在覆盖语义下根本不成立,旧替身证明不了它。

I1 2 测、I2 5 测、M4 使既有 20 测在真语义下重跑。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 08:27:40 +08:00
xcsandClaude Opus 5 3190392a1d fix(app): 决策卡的人工验收清单也按 itemId 兜底 —— 配方发的审核卡从前一条待确认都没有
装配层从前只给 `variant` 兜底,`checklist` 没有。配方(auto-runner)发审核卡时
只递了 variant —— 那张卡于是穿着验收工单的衣服、allowCustom 也对,**清单是空的**:
回传门 `checkChatCardReply` 的 `checklistMissing` 在 checklist 缺席时**恒过**,
「确认通过」当场可按。2026-08-27 用户当面定的那道人工验收闸就此凭空没了,
而屏上还看不出来 —— 清单区是空的,不是红的。

兜在装配层这一处(不是让每个调用方自己记得):模型路径将来漏传也一并保住。
判断抽成纯函数 `decisionCardFace` / `needsStepLookup`(照本仓 engines.ts 里
createOnlyViolation 那批纯函数的成例),9 测 —— 其中三条把挑出来的清单接到
真回传门上,证明闸真的因此有牙,不只是「函数回了个数组」。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 08:20:32 +08:00
xcsandClaude Opus 5 fcf1822233 fix(app): 配方的 analyze 走 traced 网关并回账用量 —— 一条问模型不花钱的路
装配层从前传的是裸 `deps.modelGateway`:`createModelGateway` 自己不做
`addConsumption`,计量全靠调用方给的 `onUsage` —— agent 那两条路都接了,
只有 analyze 漏了。后果两条:配方里 analyze 烧的 token 一个数都不计入活动预算
(预算这道闸对它形同虚设);时间轴上也不产生模型 span,「这一单问了几次模型」
看不见它。而 spec §5.2 白纸黑字写着「用量计入预算」。

现 `createAnalyzeGateway(tracedGateway, onAgentUsage)`,网关内**两次** generate
(含重问那一次)的用量各记一笔 —— 失败的调用不是免费的。记账不吞异常:
记不上账等于预算失效,宁可当场停下由配方留痕交人。

debt.md 里那半句「它烧 token、计预算」当时是假的,改老实。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 08:18:10 +08:00
xcsandClaude Opus 5 35ef0f1790 feat(desk): 件上保存之后叫醒任务 —— auto 步的配方才有机会按最新草稿铺格
从前只有两条回传路由调 wake;保存不是回传,于是「保存即铺格」永远等不到那一拍。
叫不醒不抬手:保存本身是成功的,不该因为叫不醒把 200 变成 500。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 06:32:45 +08:00
xcsandClaude Opus 5 fe8de5319c feat(loop-engine): analyze 动词 —— 系统组装入参、声明出参 schema、系统把结果落表
AI 在这里只是一个纯函数:不给工具、不给记忆、不多轮。
行数不由它决定 —— 出参是拿主键去 lookup 的一张查表,它漏一条那一格就是空,
required 立刻把它变成 _missing > 0,提交闸与放行门当场拦住。
出参不成形就地重问一次,再不成抛出去交人。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 03:54:14 +08:00
xcsandClaude Opus 5 b0a3cc3a55 feat(loop-engine): 配方执行器 —— project / prune / say,每一笔之前刷读数
三条硬规矩:每一笔之前刷读数(后一笔要数前一笔刚写下的行,不刷新就恒为 0);
值没变的写静默跳过(配方不是模型,原地打转不必被看见);
出错交人不重试不退回模型(配方抛错是包的 bug,退回模型就是让它去凑)。
主键算不出来的那一行先不写 —— 空主键写下去是一行永远对不上的垃圾。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
2026-08-29 03:41:34 +08:00
xcsandClaude Opus 5 c1137fb09a fix(ops): 实例时间线第 9 步的结果跟第 8 步走 —— 已放行的单不再永远停在 pending
放行那一步(T_release,人工激活)**没有自己的收步事件** —— 人签字后 `submit` 直接把整单落终态,
计划项本身从没被 finish 过。于是一单已 `released` 的详情里第 9 步永远是 `pending`,
而它前面拟落账那一步早就 `done`:同一张单上两句话互相打脸。
`followRes` 纯函数在服务端交出详情之前把第 9 步的 `res` 与 `at` 跟上第 8 步(2026-08-28 用户定,`at` 也跟);
**用时不编** —— 那一步没有自己的起止痕;两步有一步不在(别的活动步数不同)就什么都不动。
《API文档-活动模块》§ 时间线补一行口径。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-28 10:50:16 +08:00
xcsandClaude Fable 5 88f2149d12 差异比对三刀:客户约束不再一票否决 · 入参由基座从台账取 · 「下限~上限」一律读成范围
①**两颗一模一样的输入电压范围判成 MISMATCH**(用户送来 0bddd862):客户写「45V 以内」→ requirement `>= 45`,
而原 PN 与候选上限都是 40V;`judgePair` 只看候选过不过客户线、不看原 PN 自己过不过 ——
需求边界与在用料的差距记在了候选头上,硬约束 MISMATCH → 整颗不推荐。差异评估比的是「换上去会不会比现在差」:
候选与原 PN 相等 / 盖住、且客户线原 PN 也过不去 → 仍按比对结果判(MATCH,风险中),备注点名
「需求边界,请回头核对需求」;候选更窄且过不了线仍 MISMATCH(3 形有测)。
②**入参由基座从台账取,模型只报料号与客户约束**(用户:「这个做差异评估比对的 harness 肯定有问题」):
同一单痕里模型念出「输入上限 32V 低于客户 40V」—— 那是 S3 剧本示例的原话、不是这单的数;
工具入参只存哈希,喂了哪几对值事后查不到;写回时还把工具的 `equal` 改成 `less`。两头都靠模型抄、两头都没有基座核。
改三层:工具运行时把 `taskId / activityId` 交给处理器(`ToolHandler` 入参加两个可选键);
`compare-parameters` 给 `originalInternalPn + candidates[].candidateInternalPn(+ requirements)`,
参数行从 `parts.parameter`、封装 / 生命周期从 `parts.profile`、证据引用从 `parts.document` 由工具现读任务视图
(本单草稿也算),出参每项带原值与证据,写盘照抄;老形状(自带 pairs)照接,试运行走它;
container 用 `compareParametersWith(ledger)` 顶掉静态名单那份;≤800 字的工具入参整份进 `result` 痕(审得了才谈得上认真)。
**基座能读到的事实,不让模型转述给工具。**
③**「下限~上限」一律是范围**(用户:「4.5~40 / -40~+85 这种我怕模型搞不清楚」):实测解析器真有洞 ——
单位写进值里(`4.5V~40V` / `-40℃~+85℃`)被读成单个数,Unicode 减号、`…`、`≥45 / 40 以内` 读成文本。
分隔符认 ~ ~ - – — … 至 到 to、单位可夹在值里、单边界读成半开范围;出参每项 `readAs:{original,candidate}`
写明「范围 4.5~40 / 定值 5 / 下界 ≥ 45 / 文本」—— 比对依据可见,模型照它写影响、不自己重新解释值;
契约描述与 S3 / P-08 剧本明写读法(示例用中性数 1~10,不用真数据里的 4.5~40 —— 模型会把示例当这单的值)。
工具 1.0.0 → 1.3.0(注册一名一行、upsert 换版本,handler 在进程内按名绑,装完稳定版重建即生效);
W05 包 1.6.2 → 1.6.4:S3 第一拍只报料号与约束、示例只留占位形,第三拍 compare_result 照抄,S4 重比只改 requirements。
13 工具测 + 包内 89。在途单要人在审核卡上「重做」并写明意见才会重比。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-28 01:13:07 +08:00
xcsandClaude Fable 5 7b85a79323 harness 与工作台同日修:数据工单确认即入库 · 叫了人不再自动推进(两道结构闸)· 审核卡 = 终局验收卡结构 · 验收清单人点达标 · 迟到守卫不丢放行裁决 · 画面跟着这一拍发出去的卡走 · 成果详情只翻页 · 占位居中 · 进度条
human-tool / ledger / loop-engine / release / task / interaction 六模块与 app 层(desk 路由、engines、consumers、
data-order-landing、case-stamp、reset-runs、seed-activities、容器与 server 装配),web 的 desk-task
(ViewRegion / chat-bubbles / review-face)与 app.css、ui-kit 图标;契约 card / manifest;
迁移 0034_task_seq 与迁移日志;api-report 快照;CLAUDE.md 快照、debt、活动开发规范、HTML 报告规范同步。
逐条口径见 CLAUDE.md 快照(2026-08-27 下午至晚)。另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-27 23:20:16 +08:00
xcsandClaude Fable 5 0923fbfe4d feat(ops): 运营中心 · 活动模块接口落地 —— L1/L2/L3 十一条(全 ops)+ 设计端活动库两条改名让位
`apps/api/src/app/activity-ops/*`(service / store / instance-state / instance-steps / launch-params /
summaries / view-html / view-scope)+ `routes/activities.ts`;契约 `activity-ops.ts` / `api/activity-ops.ts`
(与队友按文档补的那版 43 个导出同名,多 INSTANCE_REASON_KINDS / STEP_RESULTS / launchOptionSchema);
迁移 0035_activity_ops;《API文档-活动模块》按契约回写。
设计端活动库 GET/POST `/api/gov/activities` 改名 `/api/gov/activity-packages`,`/api/gov/activities/*`
整个让给运营端(前端 import 路由对象,零改动);activity-registry CONTRACT.md 与路由总表同步。
2026-08-27 另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖,以合并提交为验证单位。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-27 23:20:15 +08:00
xcsandClaude Fable 5 75553371b0 审核通过与否读基座数;「重做」不再被指向 finish;审核卡摆成验收工单(W05 1.4.7)
用户两报:①「我上一步明确要求重做了,怎么会继续向下推进」②「验收工单弹出的是需要你定,明显不对」。

①病根两处合谋:人两次答 custom:重做一次,模型把一模一样的问法又发了一遍被闸拦下;而拒回的话
不分答案一律说「照这个选择把结论落进台账那面旗,再 finish」—— 对通过是对的,对重做是把模型往错路上推,
它照办写了 review_approved_total = 1,而 S4 的哨兵读的正是这面模型自写的旗(被叫的那一方自己记账,A6 那一课)。
- 基座新读数 decision_last / decision_<值>_total(按 card-reply 数);S4 哨兵改读 decision_last = "approve",
  人最后一次真点了通过才开门;静态审认这几个名字。
- 拒回 / 观察 / 事实行三处同一段 chosenGuidance 按答案分岔:custom: 不是通过 —— 按他写的改、换问法重发、
  不许写通过旗、不许 finish;选项值指回说明书那一档。
- S4 剧本驳回分支明写不许写旗、ask 开头必须写明这一轮改了什么(字面一样的卡会被吞)。
②平台的验收工单是终局,中间审核只能靠决策卡循环 —— 但不该穿「需要你定」的衣服:契约 decisionCard.variant:'review',
  PlanStep.cardVariant 扩到 'review',cards.decide 带 itemId,引擎按剧本决定;对话区摆成「工单 · 验收 / 审核」。
2106 测 / typecheck / lint / depcruise 绿;api-report 已刷;CONTRACT.md 同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 14:46:45 +08:00
xcsandClaude Fable 5 b98ec75a89 数据工单由剧本声明、引擎决定 —— 不靠模型抄卡骨架(W05 1.4.3)
用户:「右边 AI 聊天框里这个数据工单怎么又显示的执行工单?」查实:说明书骨架里
`"variant":"data"` 与 `gateLedgers` 并排写着,模型抄卡时 gateLedgers 抄到了、variant 丢了
(看着像元数据)。靠模型抄一个标记本来就靠不住。

改成与 `view` 同一条道理:这一步派的是什么单,写剧本时就定死 ——
- 契约 `PlanStep.cardVariant: 'data'`;活动在 S0d_p08_master / S0d_p10_document 两步上声明;
- `cards.issue` 入参多一项可选 `itemId`(turn-runner 从 human 动作带下来;引擎自发的验收/兜底卡没有项);
- 组装层按 itemId 查剧本,`stepVariant ?? 卡上写的` —— 模型写不写都一样。
在跑那一单的卡已就地补上 variant(payload),刷新即见「数据工单」。
2102 测 / typecheck / lint / depcruise 绿;api-report 已刷;两处 CONTRACT.md 同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
2026-08-27 13:51:14 +08:00
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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