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
This commit is contained in:
xcs
2026-08-25 01:23:30 +08:00
co-authored by Claude Fable 5
parent 92dd729c36
commit f3397a60ef
91 changed files with 3919 additions and 350 deletions
+3 -7
View File
@@ -28,6 +28,7 @@ import { call } from '@web-shared/typed-fetch.ts';
import { useChatWidth } from '@web-shared/useChatWidth.ts';
import { EmptyState } from '@web-shared/desk/module-kit.tsx';
import { queryKeys } from './query.ts';
import { Btn } from '@web-shared/Btn.tsx';
type ModuleKey = 'tasks' | 'approvals' | 'messages' | 'minutes' | 'report';
@@ -165,15 +166,10 @@ export function AppShell() {
<span className="jc-topbar-title">{NAV_MODULES.find((m) => m.key === module)?.label ?? ''}</span>
<div className="jc-topbar-user">
<span className="jc-topbar-name">{me}</span>
<button
type="button"
className="jc-topbar-logout"
disabled={logout.isPending}
onClick={() => logout.mutate()}
>
<Btn className="jc-topbar-logout" busy={logout.isPending} onClick={() => logout.mutate()}>
<Ico name="log-out" s={14} />
退出登录
</button>
</Btn>
</div>
</header>
+3 -2
View File
@@ -36,6 +36,7 @@ import { ArchiveHome } from '@features/gov-archive';
import { TestbedHome } from '@features/gov-testbed';
import { CockpitHome } from '@features/gov-memory';
import { ChanPage, LlmPage, ModelsHome, SystemHome } from '@features/gov-system';
import { Btn } from '@web-shared/Btn.tsx';
/* ── URL 编解码:`#/<surface>/<zone>/<tab>[/<k>:<id>…]` ────────────────── */
@@ -203,10 +204,10 @@ export function GovShell({ surface }: { surface: Surface }) {
<Ico name="list-checks" s={13} />
工作台
</a>
<button className="g-tbtn" onClick={() => logout.mutate()} disabled={logout.isPending}>
<Btn className="g-tbtn" onClick={() => logout.mutate()} busy={logout.isPending}>
<Ico name="log-out" s={13} />
登出
</button>
</Btn>
</div>
</header>
+5
View File
@@ -62,6 +62,11 @@ if (govSurface !== undefined) {
await import('./styles/proto.vendor.css');
await import('./styles/app.css');
}
/*
* 键感与「在办」那一层三面共用,**装在最后** —— 同名规则以它为准。
* (2026-08-24 用户:「点击了都不知道点没点上」;见 styles/press.css 头注)
*/
await import('./styles/press.css');
/**
* 鉴权壳(ADR-010)—— 会话是**服务端**的事:401 即落登录页。
+10
View File
@@ -2907,6 +2907,16 @@
.proto-root .jc-tnav-btn:disabled { opacity: .35; cursor: default; }
.proto-root .jc-tnav-n { min-width: 34px; text-align: center; font-size: 11px; font-weight: 800; color: var(--ink-2); }
/* 两组翻页同时在场时(多张卡各自多页)各带一个字,单独在场时不摆 —— 一个数不必解释自己 */
/*
* 保存之后的红字 —— **有行没落下就摆在件的上方**(2026-08-24 用户:件上新增的
* 客户推到下一步就不见了)。它不是错误提示:别的格子确实存下了,只有没钥匙的那几行没落 ——
* 所以用「提醒」的分量,点名缺哪几列,人补上再存一次。
*/
.proto-root .jc-savewarn { display: flex; align-items: flex-start; gap: 7px; margin: 0 0 10px;
padding: 9px 12px; border: 1px solid #f3b6b3; border-radius: 9px; background: #fff5f5;
color: var(--danger); font-size: 12.5px; line-height: 1.6; }
.proto-root .jc-savewarn svg { flex: none; margin-top: 2px; }
.proto-root .jc-tnav-mk { font-style: normal; font-weight: 700; color: var(--ink-3); margin-right: 3px; }
.proto-root .jc-tnav-btn.back { width: auto; padding: 0 14px 0 10px; font-size: 12.5px; font-weight: 700; line-height: 1; white-space: nowrap; }
.proto-root .jc-tnav-btn svg { flex: none; display: block; }
+54
View File
@@ -245,6 +245,60 @@
哪几张只是一单一份的凭证。而「该不该管它、谁有权改它」全看性质:
主数据要治理(谁能改、依据、去重、时效),单据只需查得到、派生根本不该手写。
所以分区要**看得见**:一条带色的区头 + 一句说清这一类是什么。 */
/* 试跑台:「连主数据一起落真库」那一格 —— 破例要看得见自己在破例 */
.tb-land { display: flex; align-items: flex-start; gap: 9px; padding: 10px 12px; cursor: pointer;
border: 1px dashed var(--g-line, #dfe1e5); border-radius: 10px; background: #fbfbfc; transition: all .16s; }
.tb-land:hover { border-color: var(--g-brand-soft, #b6d0ff); }
.tb-land.on { border-style: solid; border-color: #f3b6b3; background: #fff7f6; }
.tb-land input { margin-top: 2px; flex: none; }
.tb-land b { display: block; font-size: 12.5px; font-weight: 700; color: var(--g-ink, #1f2329); }
.tb-land.on b { color: #c9372c; }
.tb-land em { display: block; margin-top: 2px; font-style: normal; font-size: 11.5px; line-height: 1.65; color: var(--g-ink-3, #646a73); }
/* ══ 数据域:按活动分组(2026-08-24 重写)══════════════════════════════
一屏回答的是「我这个活动写了哪几张表、现在各有几行」——
所以行数是主角:字号最大、放行末端;性质降成一枚小徽,地址与主键退成灰字。 */
.g-actdata { display: flex; flex-direction: column; gap: 10px; padding: 14px 16px 13px;
background: #fff; border: 1px solid var(--g-line); border-radius: 12px; box-shadow: var(--g-shadow, 0 1px 2px rgba(31,35,41,.05)); }
.g-actdata + .g-actdata { margin-top: 14px; }
.g-actdata-h { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.g-actdata-h > svg { color: var(--g-brand, #3370ff); flex: none; }
.g-actdata-h b { font-size: 14.5px; font-weight: 800; color: var(--g-ink, #1f2329); }
.g-actdata-h .n { margin-left: auto; font-size: 12px; font-weight: 700; color: var(--g-ink-3, #646a73); }
.g-actdata-h .n em { font-style: normal; color: var(--g-brand, #3370ff); }
.g-actdata-h p { flex-basis: 100%; margin: 2px 0 0; font-size: 12px; line-height: 1.7; color: var(--g-ink-3, #646a73); }
.g-actdata-tables { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 8px; }
.g-tbl { display: grid; grid-template-columns: 1fr auto; grid-template-areas: 't n' 'a n';
gap: 2px 12px; width: 100%; padding: 9px 12px; text-align: left; font: inherit; cursor: pointer;
background: var(--g-soft, #f7f8fa); border: 1px solid transparent; border-radius: 9px; transition: all .16s; }
.g-tbl:hover { background: #fff; border-color: var(--g-brand-soft, #b6d0ff); box-shadow: 0 2px 8px rgba(51,112,255,.10); }
.g-tbl-t { grid-area: t; display: flex; align-items: center; gap: 6px; min-width: 0; }
.g-tbl-t b { font-size: 13px; font-weight: 700; color: var(--g-ink, #1f2329); }
.g-tbl-k { font-style: normal; font-size: 10px; font-weight: 800; padding: 1px 6px; border-radius: 4px;
background: #eef1f5; color: #5a6068; }
.g-tbl-k.t-m { background: #eaf1ff; color: #2b5fd9; }
.g-tbl-k.t-r { background: #f2f3f5; color: #5a6068; }
.g-tbl-k.t-d { background: #fdf0e6; color: #b25e15; }
.g-tbl-a { grid-area: a; font-size: 11px; color: var(--g-ink-4, #8f959e); overflow-wrap: anywhere; }
.g-tbl-a em { font-style: normal; }
/* 行数:这一屏的主角 */
.g-tbl-n { grid-area: n; align-self: center; text-align: right; font-size: 11.5px; color: var(--g-ink-3, #646a73); white-space: nowrap; }
.g-tbl-n b { font-size: 17px; font-weight: 800; color: var(--g-ink, #1f2329); margin-right: 2px; }
.g-tbl-n .none { font-style: normal; color: var(--g-ink-4, #8f959e); }
.g-tbl-n .draft { display: block; font-style: normal; font-size: 10.5px; font-weight: 700; color: var(--g-brand, #3370ff); }
.g-actdata-runs { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; padding-top: 2px; }
.g-actdata-runs > em { font-style: normal; font-size: 11px; font-weight: 700; color: var(--g-ink-4, #8f959e); }
.g-run { display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px; border-radius: 999px;
font-size: 11px; font-weight: 700; background: #f2f3f5; color: #5a6068; }
.g-run i { font-style: normal; font-weight: 600; opacity: .72; }
.g-run.t-ok { background: #e9f7ee; color: #1f7a45; }
.g-run.t-live { background: #eaf1ff; color: #2b5fd9; }
.g-run.t-wait { background: #fdf0e6; color: #b25e15; }
.g-run.t-warn { background: #fff1f0; color: #c9372c; }
.g-actdata.t-u { background: #fbfbfc; }
.g-kind { display: flex; flex-direction: column; gap: 12px; }
.g-kind + .g-kind { margin-top: 22px; }
.g-kind-h { display: grid; grid-template-columns: auto auto auto 1fr; align-items: center; gap: 8px;
+136
View File
@@ -0,0 +1,136 @@
/* ==========================================================================
按下去要有反应,在办事要说出来 —— 三面共用的一层(2026-08-24)
起因是用户一句话:「点击了都不知道点没点上」。查下来是两件事各缺一半:
① **按下那一刻没有键感** —— 治理台整套 `g-*` 控件一条 `:active` 都没有,
工作台也只有零星几个。点下去屏上纹丝不动,人只能靠「后来发生了什么」倒推。
② **等回音那一段完全静默** —— 请求在路上的几百毫秒里,按钮长得和没点过一模一样。
这一段恰恰是人最想要个准信的时候(慢的接口要好几秒)。
所以这一层只做两件事:**键感**(`:active`)与**在办**(`aria-busy`)。
两者都只动 transform / background,不改布局、不换 DOM —— 谁都不必为它改结构。
装载在两份基座之后(main.tsx),所以同名规则以它为准;
台面自己已经写过的 `:active` 特异度更高,原样保留(它们本就是同一个手感)。
========================================================================== */
/* --------------------------------------------------------------------------
一 · 键感:按下就下沉
小控件缩 .97 手感最准;整行整卡那种大件同样缩会「晃」,只给极轻的一点。
-------------------------------------------------------------------------- */
button:not(:disabled):not([aria-busy='true']):active,
summary:active,
[role='button']:not([aria-disabled='true']):active {
transform: scale(.97);
}
/*
* 按下要**立刻**到位。不少控件把 transform 一起塞进了 0.16s 的过渡里
* (`transition: all var(--t-fast)`)—— 而一次点击常常比这还短:
* 手抬起来的时候键帽才刚下去一半,看着就像「没反应」。
* 按下这一刻不过渡;松开后 `:active` 不再命中,回弹仍走控件自己的过渡(软的)。
* 两档特异度是为了压过工作台那批 `.proto-root .x`(0,2,0)的基础规则。
*/
.proto-root button:not(:disabled):active,
.proto-root [role='button']:not([aria-disabled='true']):active,
button:not(:disabled):active,
[role='button']:not([aria-disabled='true']):active {
transition-duration: 0s;
}
/* 大件(整行清单 / 整张卡 / 整条规则):幅度收到几乎看不见,只留一点「陷下去」 */
.g-nrow:active,
.g-entry:active,
.g-tile:active,
.g-wf:active,
.g-rule:active,
.g-fcard:active,
.g-mcard:active,
.proto-root .jc-dv-row:active,
.proto-root .task-item:active,
.proto-root .jc-branch:active {
transform: scale(.995);
}
/* 键盘走到哪儿要看得见 —— 鼠标点击不出这个圈(:focus-visible 的本分) */
button:focus-visible,
a:focus-visible,
summary:focus-visible,
[role='button']:focus-visible {
outline: 2px solid var(--brand, #3370ff);
outline-offset: 2px;
}
/* --------------------------------------------------------------------------
二 · 在办:脚下走一道不定长进度条
**为什么是 `::after` 而不是 background-image**:第一版把这道条画进按钮自己的
background,在真页面上量出来是**画不出来的** —— `.tbtn` 这类控件的皮肤用
`background:` 简写上色,特异度(0,2,0 起)压过 `[aria-busy]`(0,1,0),
`background-image` 当场被顶回 none(动画倒是在跑,条却不存在)。
`::after` 这一层没人跟它抢:全仓的按钮类没有一条定义过 `::before/::after`(审过)。
同理**不给 `<select>` 挂 aria-busy** —— 替换元素画不出伪元素,它那一行才是该点亮的东西。
条只动 transform,`currentColor` 取字色:白字蓝底的主钮出白条,深字白底的次钮出深条。
不给 `disabled`:那会把按钮画成灰的,而它此刻正在**做**这件事,不是做不了。
真做不了的照旧 `disabled`(见 shared/Btn.tsx 的注)。
-------------------------------------------------------------------------- */
[aria-busy='true'] {
position: relative;
cursor: progress;
}
[aria-busy='true']::after {
content: '';
position: absolute;
/* 左右各让出一点、离底边抬起 2px —— 贴着边画会与控件自己的边框糊成一条,看着只是「框粗了」 */
left: 6px;
right: 6px;
bottom: 2px;
height: 3px;
border-radius: 999px;
background: currentColor;
opacity: .85;
pointer-events: none;
animation: jc-busy-run 1.1s cubic-bezier(.45, 0, .55, 1) infinite;
}
/* 从左抽出、再从左收走 —— 不定长进度条的经典两拍,全程只有 transform 在动 */
@keyframes jc-busy-run {
0% { transform: scaleX(0); transform-origin: left; }
45% { transform: scaleX(1); transform-origin: left; }
55% { transform: scaleX(1); transform-origin: right; }
100% { transform: scaleX(0); transform-origin: right; }
}
/* 在办的时候不再摆「可点」的手感:悬停不上色、按下不下沉(点了也不算数) */
[aria-busy='true']:hover,
[aria-busy='true']:active {
transform: none;
}
/* --------------------------------------------------------------------------
三 · 关掉动效的人:条不跑了,但**仍然要看得出在办事** ——
停在中间摆一道静态的杠,别让这类用户回到「什么都没有」那个起点。
-------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
button:active,
summary:active,
[role='button']:active,
.g-nrow:active,
.g-entry:active,
.g-tile:active,
.g-wf:active,
.g-rule:active {
transform: none;
}
/* 条不跑了,但**仍然要看得出在办事** —— 摆一道静态的杠,别让这类用户回到「什么都没有」那个起点 */
[aria-busy='true']::after {
animation: none;
transform: scaleX(1);
}
}