Archived
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:
@@ -11,14 +11,31 @@
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
*/
|
||||
import { Card, Chip, Clear, Entry, NList, NRow, Note, PageHead, QueryGate, Sec } from '@web-shared/gov/primitives.tsx';
|
||||
import { Card, Chip, Clear, NList, NRow, Note, PageHead, QueryGate, Sec } from '@web-shared/gov/primitives.tsx';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import { useGovLedger, useGovObjects } from '@web-shared/gov/gov-api.ts';
|
||||
import { domainShort, sortDomains } from '@web-shared/gov/domains.ts';
|
||||
import { useGovDataByActivity, useGovLedger, useGovObjects } from '@web-shared/gov/gov-api.ts';
|
||||
import { domainShort } from '@web-shared/gov/domains.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
/**
|
||||
* 记录浏览的格子文本 —— JSON 列(决策链地图 / 互动时间线)从前被 String() 成
|
||||
* `[object Object]`(2026-08-24 用户报):对象与数组原样 stringify 摆出来,
|
||||
* 超长截断进 title(截了才加省略号,悬停看全文)。这是取值,不是判断。
|
||||
*/
|
||||
const cellText = (v: unknown): string => {
|
||||
if (v === null || v === undefined) return '';
|
||||
if (typeof v === 'object') {
|
||||
try {
|
||||
return JSON.stringify(v);
|
||||
} catch {
|
||||
return String(v);
|
||||
}
|
||||
}
|
||||
return String(v);
|
||||
};
|
||||
|
||||
/* ==================== 设计侧:数据域(定义面) ==================== */
|
||||
|
||||
/**
|
||||
@@ -26,103 +43,151 @@ export type HomeProps = { tab: string; nav: Nav };
|
||||
* 行数与行浏览入口不在这里 —— 那在运营中心的台账。
|
||||
*/
|
||||
/**
|
||||
* 三类的呈现次序与说辞 —— **按「要不要管它」排**,不按字母。
|
||||
* **按活动看**(2026-08-24 用户定:「按活动数据来分类,要让每个活动执行完成后
|
||||
* 台账在这里看到变化」)。
|
||||
*
|
||||
* 主数据摆最前:一屏里真正需要治理的就是它(谁能改、依据是什么、多久算过期)。
|
||||
* 单据量最大但不需要「管」—— 查得到、回放得出就够了,所以摆中间。
|
||||
* 派生摆最后,并且明写「不该手写」:手写的派生值会和它的来源打架,而没人会发现。
|
||||
* 从前这一屏按**性质**分组(主数据/单据/派生)—— 那是「治理这张表」的人的分法,
|
||||
* 答的是「这个对象定义全不全」。可人跑完一单来这儿,问的是另一件事:
|
||||
* **我这个活动写了哪几张表,现在各有几行,刚才那一单落下了什么**。
|
||||
* 按活动分组、把行数摆在最显眼处,那个问题才答得了;性质降成一枚徽,
|
||||
* 它仍要紧(单据与主数据的治理方式不同),但不该再当分组的轴。
|
||||
*
|
||||
* `unknown` 是**库里有表、却没有哪个在装的活动声明它**——多半是退役活动留下的空壳。
|
||||
* 它不是一种性质,是一种状况,所以单列一档、说清楚,不混进上面三类假装正常。
|
||||
* 三个数各说各的,不许混成一个:
|
||||
* · **正式** —— 过了放行门的事实,这张表现在真有几行;
|
||||
* · **草稿** —— 还在某一单草稿层的行(在办中,放行才进正式表);
|
||||
* · **未物化** —— 活动声明了、一次都没写过,所以库里还没这张表。
|
||||
* 它与「有表但空着」是两件事,不拿 0 混过去。
|
||||
*/
|
||||
const KIND_ORDER = ['master', 'record', 'derived', 'unknown'] as const;
|
||||
type KindKey = (typeof KIND_ORDER)[number];
|
||||
const KIND_BADGE: Readonly<Record<string, { label: string; tone: string }>> = {
|
||||
master: { label: '主数据', tone: 'm' },
|
||||
record: { label: '单据', tone: 'r' },
|
||||
derived: { label: '派生', tone: 'd' },
|
||||
};
|
||||
|
||||
const KIND_VIEW: Readonly<Record<KindKey, { title: string; ico: string; desc: string; tone: string }>> = {
|
||||
master: {
|
||||
title: '主数据',
|
||||
ico: 'library',
|
||||
tone: 'm',
|
||||
desc: '跨单积累的经营资产 —— 会被反复更新,下一单读它做判断。治理都在这一类上:谁能改、依据是什么、怎么去重、多久算过期。',
|
||||
},
|
||||
record: {
|
||||
title: '单据',
|
||||
ico: 'file-text',
|
||||
tone: 'r',
|
||||
desc: '一单一份的过程与结论 —— 放行即定稿、只增不改。它不需要「管理」,需要的是查得到、回放得出;主键第一列就是那一单的标识。',
|
||||
},
|
||||
derived: {
|
||||
title: '派生',
|
||||
ico: 'trending-up',
|
||||
tone: 'd',
|
||||
desc: '能由单据与主数据算出来的指标 —— 本不该手写:手写的派生值会和它的来源打架,而没人会发现。标出来,至少让它可被重算校验。',
|
||||
},
|
||||
unknown: {
|
||||
title: '无人声明',
|
||||
ico: 'help-circle',
|
||||
tone: 'u',
|
||||
desc: '库里有这张表,却没有哪个在装的活动声明它 —— 多半是退役活动留下的空壳。台账「只增不破坏」,清空壳表是一次性的人工决定。',
|
||||
},
|
||||
/** 一单此刻是什么样 —— 终态优先(它才是结论),没终态就说运行态 */
|
||||
function runWord(run: { runtime: string; terminal?: string | undefined }): { text: string; tone: string } {
|
||||
if (run.terminal === 'released') return { text: '已放行', tone: 'ok' };
|
||||
if (run.terminal !== undefined) return { text: run.terminal, tone: 'warn' };
|
||||
if (run.runtime === 'running') return { text: 'AI 推进中', tone: 'live' };
|
||||
return { text: '等人办', tone: 'wait' };
|
||||
}
|
||||
|
||||
const atMinute = (iso: string): string => {
|
||||
const d = new Date(iso);
|
||||
return Number.isNaN(d.getTime()) ? '' : `${d.getMonth() + 1}-${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
export function DataHome({ nav }: HomeProps) {
|
||||
const q = useGovObjects();
|
||||
const objects = q.data?.objects ?? [];
|
||||
const q = useGovDataByActivity();
|
||||
const activities = q.data?.activities ?? [];
|
||||
const unclaimed = q.data?.unclaimed ?? [];
|
||||
// 一张表都没声明的活动不占地方 —— 那多半是还没写到台账的槽位
|
||||
const withTables = activities.filter((a) => a.tables.length > 0);
|
||||
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<>
|
||||
{KIND_ORDER.map((kind) => {
|
||||
const inKind = objects.filter((o) => (o.kind ?? 'unknown') === kind);
|
||||
if (inKind.length === 0) return null;
|
||||
const view = KIND_VIEW[kind];
|
||||
const domains = sortDomains([...new Set(inKind.map((o) => o.domain))]);
|
||||
{withTables.map((a) => {
|
||||
const formal = a.tables.reduce((n, t) => n + (t.rows ?? 0), 0);
|
||||
const drafting = a.tables.reduce((n, t) => n + t.drafts, 0);
|
||||
return (
|
||||
<section key={kind} className={'g-kind t-' + view.tone}>
|
||||
<div className="g-kind-h">
|
||||
<Ico name={view.ico} s={15} />
|
||||
<b>{view.title}</b>
|
||||
<span className="n">{inKind.length} 张表</span>
|
||||
<p>{view.desc}</p>
|
||||
<section key={a.activityId} className="g-actdata">
|
||||
<div className="g-actdata-h">
|
||||
<Ico name="list-checks" s={15} />
|
||||
<b>{a.name}</b>
|
||||
<span className="g-mono">
|
||||
{a.activityId}
|
||||
{a.version === undefined ? '' : `@${a.version}`}
|
||||
</span>
|
||||
<span className="n">
|
||||
{a.tables.length} 张表 · 正式 {formal} 行
|
||||
{drafting > 0 && <em> · 在办 {drafting} 行</em>}
|
||||
</span>
|
||||
</div>
|
||||
{domains.map((d) => {
|
||||
const inDomain = inKind.filter((o) => o.domain === d);
|
||||
return (
|
||||
<Card
|
||||
key={d}
|
||||
ico="database"
|
||||
title={domainShort(d)}
|
||||
extra={
|
||||
<span className="g-mono">
|
||||
{d} · {inDomain.length} 对象
|
||||
|
||||
<div className="g-actdata-tables">
|
||||
{a.tables.map((t) => {
|
||||
const badge = KIND_BADGE[t.kind ?? ''];
|
||||
return (
|
||||
<button
|
||||
key={`${t.domain}.${t.object}`}
|
||||
type="button"
|
||||
className="g-tbl"
|
||||
onClick={() => nav.push({ k: 'obj', id: `${t.domain}.${t.object}` })}
|
||||
>
|
||||
<span className="g-tbl-t">
|
||||
<b>{t.label ?? t.object}</b>
|
||||
{badge !== undefined && <i className={'g-tbl-k t-' + badge.tone}>{badge.label}</i>}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="g-entries">
|
||||
{inDomain.map((o) => (
|
||||
<Entry
|
||||
key={o.object}
|
||||
ico="table"
|
||||
// 人读名优先;没登记 label 才退回对象键 —— 治理台不该拿 `intake_record` 给人看
|
||||
title={o.label ?? o.object}
|
||||
/* 单据把**按什么分单**摆出来 —— 那是它和主数据最要紧的差别 */
|
||||
desc={
|
||||
o.caseKey === undefined
|
||||
? `${o.domain}.${o.object}`
|
||||
: `${o.domain}.${o.object} · 按 ${o.caseKey} 分单`
|
||||
}
|
||||
onClick={() => nav.push({ k: 'obj', id: `${o.domain}.${o.object}` })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<span className="g-tbl-a g-mono">
|
||||
{t.domain}.{t.object}
|
||||
{t.primaryKey.length > 0 && <em> · 主键 {t.primaryKey.join(' + ')}</em>}
|
||||
</span>
|
||||
<span className="g-tbl-n">
|
||||
{/* 「没物化」不写 0 —— 声明了没写过,与写过又空了不是一回事 */}
|
||||
{t.rows === undefined ? (
|
||||
<em className="none">还没建表</em>
|
||||
) : (
|
||||
<>
|
||||
<b>{t.rows}</b> 行
|
||||
</>
|
||||
)}
|
||||
{t.drafts > 0 && <i className="draft">在办 {t.drafts} 行</i>}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{a.recent.length > 0 && (
|
||||
<div className="g-actdata-runs">
|
||||
<em>最近的单</em>
|
||||
{a.recent.map((run) => {
|
||||
const word = runWord(run);
|
||||
return (
|
||||
<span key={run.id} className={'g-run t-' + word.tone}>
|
||||
{word.text}
|
||||
<i>{atMinute(run.at)}</i>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
{objects.length === 0 && (
|
||||
<Clear>还没有物化的对象 —— 活动第一次写入时,系统按它的声明把表建出来,然后出现在这里。</Clear>
|
||||
|
||||
{unclaimed.length > 0 && (
|
||||
<section className="g-actdata t-u">
|
||||
<div className="g-actdata-h">
|
||||
<Ico name="help-circle" s={15} />
|
||||
<b>无人声明</b>
|
||||
<span className="n">{unclaimed.length} 张表</span>
|
||||
<p>库里有这张表,却没有哪个在装的活动声明它 —— 多半是退役活动留下的空壳。台账「只增不破坏」,清空壳表是一次性的人工决定。</p>
|
||||
</div>
|
||||
<div className="g-actdata-tables">
|
||||
{unclaimed.map((o) => (
|
||||
<button
|
||||
key={`${o.domain}.${o.object}`}
|
||||
type="button"
|
||||
className="g-tbl"
|
||||
onClick={() => nav.push({ k: 'obj', id: `${o.domain}.${o.object}` })}
|
||||
>
|
||||
<span className="g-tbl-t">
|
||||
<b>{o.label ?? o.object}</b>
|
||||
</span>
|
||||
<span className="g-tbl-a g-mono">
|
||||
{o.domain}.{o.object}
|
||||
</span>
|
||||
<span className="g-tbl-n">{o.rows === undefined ? <em className="none">读不到</em> : <><b>{o.rows}</b> 行</>}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{withTables.length === 0 && unclaimed.length === 0 && (
|
||||
<Clear>还没有活动声明台账 —— 装一个活动包,它声明的表就出现在这里;第一次写入时表才真建出来。</Clear>
|
||||
)}
|
||||
</>
|
||||
</QueryGate>
|
||||
@@ -238,11 +303,15 @@ export function ObjPage({ id }: { id: string }) {
|
||||
<tbody>
|
||||
{(page.data?.rows ?? []).map((row, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map((c) => (
|
||||
<td key={c.name} className="g-mono">
|
||||
{String(row[c.name] ?? '')}
|
||||
</td>
|
||||
))}
|
||||
{columns.map((c) => {
|
||||
const full = cellText(row[c.name]);
|
||||
const long = full.length > 120;
|
||||
return (
|
||||
<td key={c.name} className="g-mono" title={long ? full : undefined}>
|
||||
{long ? `${full.slice(0, 120)}…` : full}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user