feat(w05): 五张报告模版 —— 两张数据工单件、评估输入清单、差异审核件、落账确认件(目标件平移)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
xcs
2026-08-26 23:45:17 +08:00
co-authored by Claude Fable 5
parent f7fe8e3836
commit f9dbdc8c9c
2 changed files with 776 additions and 8 deletions
@@ -2,12 +2,11 @@
* ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。 * ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。
* 案例活动没有 `run`,自测不跑回合;测的是清单本身与授权面对齐,结构照 E04 / I01。 * 案例活动没有 `run`,自测不跑回合;测的是清单本身与授权面对齐,结构照 E04 / I01。
* *
* 本文件目前只写三组(清单本身合规 / 台账声明 / 活动入口); * 五组:清单本身合规 / 台账声明 / 案例模型与清单的对齐 / 报告模版:编辑面与只读面各就各位 / 活动入口。
* 「案例模型与清单的对齐」「报告模版:编辑面与只读面各就各位」两组留给 Task 4/5 追加 ——
* `src/case-model.ts` 与 `src/views/report.ts` 眼下还是占位,写了那两组只会红。
*/ */
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { activityManifestSchema } from '@contracts'; import { activityManifestSchema } from '@contracts';
import { createMockHost } from '@sdk';
import { manifest, D, RULE_VERSION } from '../src/manifest.ts'; import { manifest, D, RULE_VERSION } from '../src/manifest.ts';
import activity from '../src/index.ts'; import activity from '../src/index.ts';
@@ -15,6 +14,7 @@ const ITEMS = ['S0_bind', 'S0d_master_data', 'S1_input_confirm', 'S2_normalize',
const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']); const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']);
const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm']; const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm'];
const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object); const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object);
const viewHtml = (name: string) => manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? '';
describe('清单本身合规', () => { describe('清单本身合规', () => {
it('过 @contracts 的 schema', () => { it('过 @contracts 的 schema', () => {
@@ -177,8 +177,69 @@ describe('案例模型与清单的对齐', () => {
}); });
}); });
describe('报告模版:编辑面与只读面各就各位', () => {
it('两张数据工单件可加行可编辑:data-edit / data-edit-add 与主键 data-col 都在', () => {
const p08 = viewHtml('p08-master-create');
expect(p08).toContain('data-edit="parts.profile"');
expect(p08).toContain('data-edit-add="parts.profile"');
expect(p08).toContain('data-col="internal_pn"');
expect(p08).toContain('data-col="normalized_parameter_set"');
const p10 = viewHtml('p10-document-create');
expect(p10).toContain('data-edit="parts.document"');
expect(p10).toContain('data-col="doc_seq"');
});
it('评估输入清单:缺口表可编辑(supplement),绑定区只读', () => {
const html = viewHtml('eval-input-list');
expect(html).toContain(`data-edit="${D}.input_gap"`);
expect(html).toContain('data-col="supplement"');
expect(html).toContain(`data-rows="mine:${D}.pn_check"`);
});
it('差异审核与落账确认只读:没有任何 data-edit / data-col', () => {
for (const name of ['difference-review', 'landing-confirm']) {
expect(viewHtml(name), name).not.toContain('data-edit=');
expect(viewHtml(name), name).not.toContain('data-col=');
}
});
it('每个可编辑格子的 data-col 都在对应台账对象的声明里', () => {
const cases: Array<[string, string, string]> = [
['p08-master-create', 'parts', 'profile'],
['p10-document-create', 'parts', 'document'],
['eval-input-list', D, 'input_gap'],
];
for (const [view, domain, object] of cases) {
const cols = new Set([...viewHtml(view).matchAll(/data-col="([^"]+)"/g)].map((m) => m[1] ?? ''));
const declared = new Set(byAddress(domain, object)?.fields.map((f) => f.name));
for (const c of cols) expect(declared.has(c), `${view}: ${c}`).toBe(true);
}
});
it('枚举列只用 data-class-of 投影,不摆 ASCII 原值', () => {
expect(viewHtml('difference-review')).toContain('data-class-of="difference_status"');
expect(viewHtml('difference-review')).toContain('data-class-of="recommendation_level"');
expect(viewHtml('difference-review')).not.toContain('data-slot="difference_status"');
});
it('宿主拒推含脚本的模版', async () => {
const host = createMockHost({ contract: CONTRACT });
await expect(host.base.interaction.pushHtml('<div><script>x()</script></div>')).rejects.toThrow(/无脚本执行权/);
});
});
describe('活动入口', () => { describe('活动入口', () => {
it('没有 run —— 行为全在清单里', () => { it('没有 run —— 行为全在清单里', () => {
expect(activity.run).toBeUndefined(); expect(activity.run).toBeUndefined();
}); });
}); });
const CONTRACT = {
activityId: 'part-substitution-assessment',
version: '1.0.0',
data: {},
message: undefined,
outputPorts: { mandatory: ['assessment-released'], selective: [] },
prohibitions: [],
discretion: [],
} as const;
@@ -1,5 +1,712 @@
export const P08_MASTER_CREATE = '<div class="proto-root"></div>'; /**
export const P10_DOCUMENT_CREATE = '<div class="proto-root"></div>'; * ACT-WORK-05 差异评估的 HTML 报告模版 —— **活动自己持有的资产**,随包版本冻结。
export const EVAL_INPUT_LIST = '<div class="proto-root"></div>'; *
export const DIFFERENCE_REVIEW = '<div class="proto-root"></div>'; * 五个局面各一张:
export const LANDING_CONFIRM = '<div class="proto-root"></div>'; * · `p08-master-create` —— S0d 摆,数据工单(P-08 芯片主数据),可加行、可编辑;
* · `p10-document-create` —— S0d 摆,数据工单(P-10 原厂技术资料),可加行、可编辑;
* · `eval-input-list` —— S0/S1 摆,评估输入清单:绑定区只读 + 缺口表可编辑;
* · `difference-review` —— S2/S3/S4 摆,各候选推荐等级 + 差异明细,**只读**(通过/驳回在对话区决策卡);
* · `landing-confirm` —— S5/T_release 摆,拟写 P-11 / 拟回写 P-01,**只读**,放行后即归档件。
*
* 版面语言逐字量自 `docs/SS02-ACT-WORK-05-html/` 四份目标件;取值三纪律:顶层只取汇总列与单值列,
* 逐条走 `mine:域.对象`,列名逐字照 `declarations.ledger` 抄。**模版不算不判**。
*/
import { PRD_CSS } from './prd-style.ts';
import { D } from '../constants.ts';
/* ====================
* 绑定期补的样式(原样复制自 E04 `activities/crm-relationship-coverage/src/views/report.ts`
* 第 25–211 行的 `BIND_CSS`,共 187 行 —— 不是 I01 那份(裁决:见任务交底)。
* 原因:本活动的件里有「只读格与可改格同表混排」(枚举列是 <select class="jc-cell"> 挨着
* <span class="jc-tag" data-class-of>),E04 那份带 `:has(+ .jc-cell)` 守卫 ——
* 只藏「后面真跟着孪生件」的那一份只读格;I01 没有这个守卫,照抄会让混排里的只读字
* 一进编辑态就整格消失(E04 2026-08-21 实测过的坑)。E04 那份还带增删行、行级红条等
* 本活动也用得上的样式。数据驱动的开关全在这里:三档占位 / 缺项红带 / 编辑闸 / 增删行 / 状态徽基座。
* ==================== */
const BIND_CSS = `
/* 空占位三级(全局口径):必填红 / 建议填橘 / 选填灰 —— 值真的空才轮到 */
.proto-root .jc-tbl-in td.req > .jc-vcell:empty::after,
.proto-root .jc-tbl-in td.sug > .jc-vcell:empty::after,
.proto-root .jc-tbl-in td.opt > .jc-vcell:empty::after {
display: inline-flex; align-items: center; font-style: normal; font-size: 10px; font-weight: 700;
line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed;
}
.proto-root .jc-tbl-in td.req > .jc-vcell:empty::after { content: "必填"; color: var(--danger); background: var(--danger-soft); border-color: #f3b6b3; }
.proto-root .jc-tbl-in td.sug > .jc-vcell:empty::after { content: "建议填"; color: var(--warn-accent); background: var(--warn-soft); border-color: #f0cf9a; }
.proto-root .jc-tbl-in td.opt > .jc-vcell:empty::after { content: "选填"; color: var(--ink-4); background: var(--soft); border-color: var(--line); }
/* 归档件 / 只读件里空格子就是「没有」,不再摆「你还能填」的胶囊 */
.proto-root .jc-tbl-in td.dimempty > .jc-vcell:empty::after { content: "—"; color: var(--ink-4); border: 0; background: none; padding: 0; }
/* 缺必填的整行浅红 + 行首红条 —— 由 :has() 认 */
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty) td { background: #fffbfb; }
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty):hover td { background: #fff5f5; }
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty) td.jc-rn { color: var(--danger); font-weight: 800; }
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty) td.jc-rn::after {
content: ""; position: absolute; left: 0; top: 4px; bottom: 4px; width: 3px; border-radius: 0 3px 3px 0; background: var(--danger);
}
/* 序号由 CSS 计数器排 */
.proto-root .jc-tbl-in tbody { counter-reset: rn; }
.proto-root .jc-tbl-in td.jc-rn::before { counter-increment: rn; content: counter(rn); }
/*
* 就地改:可改格子渲了只读与输入两份,默认锁着,表上加 on 才换孪生件。
*
* **只换真有孪生件的那一份**(2026-08-21 用户送来实测截图查出来的坑):从前认
* 「表上有 on 就把这张表里所有 .jc-vcell 全藏起来」,而 coverage-receipt 混排
* 只读列(计划动作/对象里的联系人姓名——承继自计划,声明明写"改不了")与可改列
* (完成状态等),只读列压根没有 .jc-cell 孪生件可换 —— 一开编辑,那格字直接消失,
* 要退出编辑才重新看得见。**只藏「后面紧跟着一个孪生件」的那一份**:
* 没有孪生件的 .jc-vcell 不属于这道开关,编辑态里也照旧显示。
*/
.proto-root .jc-cell { display: none; }
.proto-root .jc-tbl-in.on td > .jc-vcell:has(+ .jc-cell) { display: none; }
.proto-root .jc-tbl-in.on td > .jc-cell { display: block; }
/* 编辑态下必填空格子:红框 + 整行浅红(由 :placeholder-shown / :invalid 认) */
.proto-root .jc-tbl-in.on td.req > select.jc-cell:invalid,
.proto-root .jc-tbl-in.on td.req > .jc-cell:placeholder-shown { border-color: #f3b6b3; background: #fffafa; }
.proto-root .jc-tbl-in.on tbody tr:has(td.req > .jc-cell:placeholder-shown),
.proto-root .jc-tbl-in.on tbody tr:has(td.req > select.jc-cell:invalid) { background: #fffbfb; }
/* 铺满工作区,卡壳不再自带外边框(外层容器已经是一张卡) */
.jc-stage { max-width: none; padding: 0 0 4px; }
/* 那道闸 —— 默认「已锁定 + 编辑」,开了变成「取消 / 保存」;只在可改屏现身 */
.proto-root .jc-editgate { display: none; }
html.editable .proto-root .jc-editgate {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
margin: 14px 0 10px; padding: 9px 12px; border-radius: 9px;
background: var(--soft); border: 1px solid var(--line-soft);
font-size: 12px; color: var(--ink-3);
}
.proto-root .jc-editgate-t { flex: 1 1 auto; min-width: 0; }
.proto-root .jc-editgate-t b { color: var(--ink-2); font-weight: 700; }
.proto-root .jc-egbtn {
flex: none; display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
border: 1px solid var(--line); border-radius: 7px; padding: 6px 13px;
font: inherit; font-size: 12px; font-weight: 700; color: var(--ink-2); background: #fff;
transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
}
.proto-root .jc-egbtn:hover { border-color: var(--brand); color: var(--brand); }
.proto-root .jc-egbtn.save { color: #fff; background: var(--human); border-color: var(--human); }
.proto-root .jc-egbtn.save:hover { filter: brightness(1.06); color: #fff; }
.proto-root .jc-editgate .jc-egbtn.ghost, .proto-root .jc-editgate .jc-egbtn.save { display: none; }
.proto-root .jc-editgate.on { background: var(--human-soft); border-color: var(--human-line); color: #7c4a12; }
.proto-root .jc-editgate.on .jc-egbtn.open { display: none; }
.proto-root .jc-editgate.on .jc-egbtn.ghost, .proto-root .jc-editgate.on .jc-egbtn.save { display: inline-flex; }
.proto-root .jc-editgate.on .jc-editgate-t { visibility: hidden; }
/* 缺必填红带 —— 只在可改屏出,一格不缺自动收(v-0 那一档 display:none) */
.proto-root .jc-gapbar { display: none; }
html.editable .proto-root .jc-gapbar {
display: flex; align-items: flex-start; gap: 8px;
margin: 0 0 10px; padding: 9px 12px; border-radius: 9px;
background: #fff5f5; border: 1px solid #f3b6b3; color: var(--danger);
font-size: 12px; line-height: 1.65;
}
.proto-root .jc-gapbar b { font-weight: 800; }
.proto-root .jc-gapbar span { color: #9a3b36; }
html.editable .proto-root .jc-gapbar.v-0 { display: none; }
/* 那条「怎么改」的提示只在开了闸之后说 */
.proto-root .jc-editbar { display: none; }
.proto-root .jc-editgate.on + .jc-editbar {
display: flex; align-items: flex-start; gap: 8px;
margin: 0 0 10px; padding: 9px 12px; border-radius: 9px;
background: var(--brand-wash); border: 1px dashed var(--brand-soft); color: var(--ink-3);
font-size: 12px; line-height: 1.6;
}
.proto-root .jc-editbar b { color: var(--ink-2); }
/* 增删行 */
.proto-root .jc-rowdel { display: none; }
.proto-root .jc-tbl-in.on td.jc-rn::before { content: none; }
.proto-root .jc-tbl-in.on .jc-rowdel { display: block; }
.proto-root .jc-tbl-in.on tr.jc-row-gone td { background: #fff6f6; opacity: .55; }
.proto-root .jc-tbl-in.on tr.jc-row-gone .jc-cell { text-decoration: line-through; color: var(--ink-3); }
.proto-root .jc-tbl-in.on tr.jc-row-gone .jc-rowdel { border-color: var(--danger); color: var(--danger); background: #fff; }
.proto-root .jc-tbl-in.on tr.jc-row-new td { background: #f2fbf7; }
.proto-root .jc-addrow { display: none; }
html.editable .proto-root .jc-tbl-wrap:has(table.on) + .jc-addrow {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
margin: -2px 0 12px; padding: 7px 12px; border-radius: 9px;
background: var(--soft); border: 1px dashed var(--line);
}
/*
* 统一的枚举标签(候选状态 / 优先级 / 完成状态 / 关键人变动 共用同一套机制):
* 值是机器码,人读名与颜色一律由 data-class-of 投影出的 .v-xxx 决定,不写死在取值那一侧。
* 三组值域(pending/covered/excluded、high/medium/low、done/partial/skipped、是/无变动)各自的
* .v-* 类名不会互相碰撞,同一个 .jc-tag 基座够用,不必分三套徽章系统。
*/
.proto-root .jc-tag { font-weight: 800; }
.proto-root .jc-tag::after { content: "待定"; color: var(--ink-4); font-weight: 600; }
.proto-root .jc-tag.v-pending::after { content: "待覆盖"; }
.proto-root .jc-tag.v-pending { color: var(--warn-accent); }
.proto-root .jc-tag.v-covered::after { content: "已覆盖"; }
.proto-root .jc-tag.v-covered { color: var(--ok); }
.proto-root .jc-tag.v-excluded::after { content: "已排除"; }
.proto-root .jc-tag.v-excluded { color: var(--ink-3); }
.proto-root .jc-tag.v-high::after { content: "高"; }
.proto-root .jc-tag.v-high { color: var(--danger); font-family: var(--mono); }
.proto-root .jc-tag.v-medium::after { content: "中"; }
.proto-root .jc-tag.v-medium { color: var(--ink-2); font-family: var(--mono); }
.proto-root .jc-tag.v-low::after { content: "低"; }
.proto-root .jc-tag.v-low { color: var(--ink-4); font-family: var(--mono); }
.proto-root .jc-tag.v-done::after { content: "完成"; }
.proto-root .jc-tag.v-done { color: var(--ok); }
.proto-root .jc-tag.v-partial::after { content: "部分完成"; }
.proto-root .jc-tag.v-partial { color: var(--warn-accent); }
.proto-root .jc-tag.v-skipped::after { content: "未执行"; }
.proto-root .jc-tag.v-skipped { color: var(--ink-3); }
/*
* **值域不能用中文**(2026-08-21 用户送来实测截图查出来的坑):classToken()
* 只放行 ASCII 字母数字与横杠(见 view-render.ts 的注释——属性位插值唯一开放
* 的白名单,宁可没有颜色也不给一个错的钩子),中文字符整段被消毒成空字符串,
* data-class-of="key_person_change" 因此永远长不出 v-是/v-无变动 这两个
* class —— 徽章只会落到默认的「待定」,人怎么选、选了多少次都一样,像是保存没生效。
* 机器值改成 ASCII(changed/unchanged),中文只留在这里的 ::after 与
* option 文字里(人看到的字一个没少)。
*/
.proto-root .jc-tag.v-changed::after { content: "是"; }
.proto-root .jc-tag.v-changed { color: var(--danger); }
.proto-root .jc-tag.v-unchanged::after { content: "无变动"; }
.proto-root .jc-tag.v-unchanged { color: var(--ink-4); }
/* 关键人标记(boolean 列):值是 true/false,人读字由这里出 —— 与其余枚举徽同一机制 */
.proto-root .jc-tag.v-true::after { content: "是"; }
.proto-root .jc-tag.v-true { color: var(--ok); }
.proto-root .jc-tag.v-false::after { content: "否"; }
.proto-root .jc-tag.v-false { color: var(--ink-4); }
/*
* **覆盖深度同一坑的另一半**(2026-08-21 用户:「编辑的下拉框是中文,显示是英文」)——
* depth 本身不含中文(说明书写死 ASCII deep/standard/light,不撞 classToken 那道墙),
* 但只读那一份从前是 .jc-vcell 直接回显原值,人看到的是英文单词,跟下拉选项(深度/标准/
* 轻度)对不上。换成同一套 data-class-of + .jc-tag 机制,人读的字上下一致。
*/
/*
* **BT 层级同一坑的第三半**(2026-08-23 用户:「BT 层级下拉框显示不全,
* 都看不到具体选了哪个」)。两处一起错:
* · 列宽只有 80px,而选项文字是「BT-01 · 采购/供应链」—— 下拉被挤扁,选了什么看不见
* (覆盖深度那列因为选项只有两个字,一直没暴露);
* · 只读那一份是 data-slot 直显,存的是 ASCII bt-02,人看到的就是 bt-02 ——
* 与编辑态下拉里写的「BT-02 · 技术」对不上。覆盖深度当初专门修过这个坑,
* BT 层级漏了。现在两列同一套:值仍是 ASCII(台账与 recompute-score 的
* btLevel 入参认它),人读的字由这里出。
*/
.proto-root .jc-tag.v-bt-01::after { content: "BT-01 · 采购/供应链"; }
.proto-root .jc-tag.v-bt-02::after { content: "BT-02 · 技术"; }
.proto-root .jc-tag.v-bt-03::after { content: "BT-03 · 高层"; }
.proto-root .jc-tag.v-bt-01, .proto-root .jc-tag.v-bt-02, .proto-root .jc-tag.v-bt-03 { color: var(--ink-2); font-family: var(--mono); font-size: 11.5px; }
.proto-root .jc-tag.v-deep::after { content: "深度"; }
.proto-root .jc-tag.v-standard::after { content: "标准"; }
.proto-root .jc-tag.v-light::after { content: "轻度"; }
/* 状态徽承继自引擎产物,不进编辑态的显隐开关——on 时也照旧摆着,当「现在是什么」的参照 */
.proto-root .jc-tbl-in.on td > .jc-tag { display: inline-flex; align-items: center; margin-top: 4px; }
/*
* 「本轮触发 / 本轮说明」这类**值还没写下就整行不摆**的说明行:
* 行里所有取值位(b/span/i)都空着 → 整行藏掉 —— 「本轮说明:」后面跟着一片空白
* 比不摆更糟(像是丢了数据)。值一落(S1 写 verdict),行自己现身。
*/
.proto-root .jc-note-line:not(:has(b:not(:empty), span:not(:empty), i:not(:empty))) { display: none; }
`;
/* ==================== 本活动的枚举徽标与参数集格子 ==================== */
const EXTRA_CSS = `
/* 参数集一行一参数:只读那一份按行换行显示 */
.proto-root .jc-pre { white-space: pre-line; font-family: var(--mono); font-size: 12px; }
.proto-root textarea.jc-cell { min-height: 120px; font-family: var(--mono); font-size: 12px; }
/* 徽标按列作用域取人读名(同一个 v-conditional 在推荐等级与四态里是两个词) */
.proto-root .jc-tag.t-role.v-original::after { content: "原 PN"; }
.proto-root .jc-tag.t-role.v-candidate::after { content: "候选"; }
.proto-root .jc-tag.t-exist.v-found::after { content: "在库"; }
.proto-root .jc-tag.t-exist.v-absent::after { content: "不在库"; }
.proto-root .jc-tag.t-exist.v-created::after { content: "本单新建"; }
.proto-root .jc-tag.t-exist.v-abandoned::after { content: "已放弃"; }
.proto-root .jc-tag.t-exist.v-absent { color: var(--danger); background: var(--danger-soft); }
.proto-root .jc-tag.t-exist.v-created { color: var(--ok); background: var(--ok-soft); }
.proto-root .jc-tag.t-gap.v-gap::after { content: "待补*"; }
.proto-root .jc-tag.t-gap.v-default::after { content: "默认·可改"; }
.proto-root .jc-tag.t-gap.v-ok::after { content: "齐备"; }
.proto-root .jc-tag.t-gap.v-gap { color: var(--danger); background: var(--danger-soft); }
.proto-root .jc-tag.t-gap.v-ok { color: var(--ok); background: var(--ok-soft); }
.proto-root .jc-tag.t-life.v-active::after { content: "Active"; }
.proto-root .jc-tag.t-life.v-nrnd::after { content: "NRND"; }
.proto-root .jc-tag.t-life.v-eol::after { content: "EOL"; }
.proto-root .jc-tag.t-life.v-discontinued::after { content: "停产"; }
.proto-root .jc-tag.t-life.v-risk::after { content: "风险"; }
.proto-root .jc-tag.t-doc.v-datasheet::after { content: "Datasheet"; }
.proto-root .jc-tag.t-doc.v-app_note::after { content: "应用笔记"; }
.proto-root .jc-tag.t-doc.v-reference_design::after { content: "参考设计"; }
.proto-root .jc-tag.t-doc.v-white_paper::after { content: "白皮书"; }
.proto-root .jc-tag.t-doc.v-sample_doc::after { content: "样片资料"; }
.proto-root .jc-tag.t-doc.v-commitment_letter::after { content: "承诺函"; }
.proto-root .jc-tag.t-doc.v-other::after { content: "其他"; }
.proto-root .jc-tag.t-docst.v-valid::after { content: "有效"; }
.proto-root .jc-tag.t-docst.v-expired::after { content: "过期"; }
.proto-root .jc-tag.t-docst.v-pending::after { content: "待核实"; }
.proto-root .jc-tag.t-docst.v-void::after { content: "作废"; }
.proto-root .jc-tag.t-sub.v-pin_to_pin::after { content: "Pin-to-Pin"; }
.proto-root .jc-tag.t-sub.v-functional::after { content: "功能替代"; }
.proto-root .jc-tag.t-sub.v-parametric::after { content: "参数替代"; }
.proto-root .jc-tag.t-sub.v-other::after { content: "其他"; }
.proto-root .jc-tag.t-rec.v-recommended::after { content: "推荐"; }
.proto-root .jc-tag.t-rec.v-conditional::after { content: "条件推荐"; }
.proto-root .jc-tag.t-rec.v-alternate::after { content: "备选"; }
.proto-root .jc-tag.t-rec.v-not_recommended::after { content: "不推荐"; }
.proto-root .jc-tag.t-rec.v-recommended { color: var(--ok); background: var(--ok-soft); }
.proto-root .jc-tag.t-rec.v-conditional { color: var(--warn-accent); background: var(--warn-soft); }
.proto-root .jc-tag.t-rec.v-not_recommended { color: var(--danger); background: var(--danger-soft); }
.proto-root .jc-tag.t-anal.v-ai_draft::after { content: "AI 草稿"; }
.proto-root .jc-tag.t-anal.v-fae_confirmed::after { content: "FAE 确认"; }
.proto-root .jc-tag.t-anal.v-pending_customer::after { content: "待客户验证"; }
.proto-root .jc-tag.t-anal.v-customer_approved::after { content: "客户已批准"; }
.proto-root .jc-tag.t-anal.v-rejected::after { content: "不批准"; }
.proto-root .jc-tag.t-anal.v-void::after { content: "作废"; }
.proto-root .jc-tag.t-anal.v-fae_confirmed { color: var(--ok); background: var(--ok-soft); }
.proto-root .jc-tag.t-diff.v-match::after { content: "MATCH"; }
.proto-root .jc-tag.t-diff.v-mismatch::after { content: "MISMATCH"; }
.proto-root .jc-tag.t-diff.v-unknown::after { content: "UNKNOWN"; }
.proto-root .jc-tag.t-diff.v-conditional::after { content: "CONDITIONAL"; }
.proto-root .jc-tag.t-diff.v-match { color: var(--ok); background: var(--ok-soft); }
.proto-root .jc-tag.t-diff.v-mismatch { color: var(--danger); background: var(--danger-soft); }
.proto-root .jc-tag.t-diff.v-unknown { color: var(--ink-3); background: var(--soft); }
.proto-root .jc-tag.t-diff.v-conditional { color: var(--warn-accent); background: var(--warn-soft); }
.proto-root .jc-tag.t-con.v-hard::after { content: "硬约束"; }
.proto-root .jc-tag.t-con.v-soft::after { content: "软约束"; }
.proto-root .jc-tag.t-con.v-hard { color: var(--danger); background: var(--danger-soft); }
.proto-root .jc-tag.t-risk.v-low::after { content: "低"; }
.proto-root .jc-tag.t-risk.v-medium::after { content: "中"; }
.proto-root .jc-tag.t-risk.v-high::after { content: "高"; }
.proto-root .jc-tag.t-risk.v-critical::after { content: "关键"; }
.proto-root .jc-tag.t-risk.v-low { color: var(--ok); background: var(--ok-soft); }
.proto-root .jc-tag.t-risk.v-medium { color: var(--warn-accent); background: var(--warn-soft); }
.proto-root .jc-tag.t-risk.v-high, .proto-root .jc-tag.t-risk.v-critical { color: var(--danger); background: var(--danger-soft); }
/* 绑定区:四项锁定的灰底只读行 */
.proto-root .jc-bound { display: grid; grid-template-columns: 140px 1fr; gap: 6px 14px; padding: 10px 14px; margin: 8px 0 14px; background: var(--soft); border: 1px solid var(--line-soft); border-radius: 9px; font-size: 13px; }
.proto-root .jc-bound b { color: var(--ink-3); font-weight: 600; }
`;
/** 三层壳照目标件(收件人 / 关联任务两格由引擎注入;目标件的「最迟交回」与单号本仓没有真数据源,不摆) */
const doc = (input: { tone: 'human' | 'ledger'; badge: string; tag: string; kicker: string; title: string; step: string; state: string; body: string; foot: string }): string =>
`<style>${PRD_CSS}${BIND_CSS}${EXTRA_CSS}</style>
<div class="proto-root"><div class="app-shell"><div class="jc-stage">
<article class="jc-report tone-${input.tone}">
<header class="jc-rp-head">
<div class="jc-rp-badge">${input.badge}</div>
<div class="jc-rp-tit">
<em>${input.kicker}</em>
<h3>${input.title}</h3>
<div class="jc-rp-sub">收件人:<span data-slot="case:owner"></span> · 关联任务:<span class="jc-rp-taskid" data-slot="case:taskId" data-blank></span></div>
</div>
<div class="jc-rp-side">
<span class="jc-rp-tag">${input.tag}</span>
<span class="jc-rp-step">${input.step}</span>
${input.state}
</div>
</header>
<div class="jc-rp-bd">${input.body}</div>
<footer class="jc-rp-foot">${input.foot}</footer>
</article>
</div></div></div>`;
const part = (label: string, aside = ''): string => `<p class="jc-rp-part"><b>【${label}】</b>${aside === '' ? '' : ` —— ${aside}`}</p>`;
const criteria = (title: string, items: readonly string[]): string =>
`<div class="jc-criteria"><div class="jc-criteria-t">${title}</div><ul>${items.map((i) => `<li>${i}</li>`).join('')}</ul></div>`;
const flow = (text: string): string => `<div class="jc-flow">${text}</div>`;
/** 页脚右侧「工单类型:× · <案号>」 */
const sign = (note: string, tag: string): string => `<span>${note}</span><span>${tag} · <span data-slot="eval_ref" data-blank></span></span>`;
const editGate = (address: string, label: string): string => `<div class="jc-editgate" data-edit-for="${address}">
<span class="jc-editgate-t">${label}</span>
<button type="button" class="jc-egbtn open" data-edit-open="${address}">编辑</button>
<button type="button" class="jc-egbtn ghost" data-edit-cancel="${address}">取消</button>
<button type="button" class="jc-egbtn save" data-edit-save="${address}">保存</button>
</div>`;
const gapbar = (object: string, what: string): string => `<div class="jc-gapbar" data-class-of="${object}_missing">
<span><b>还差 <span data-slot="${object}_missing"></span> 行${what}没填</b> —— <span data-slot="${object}_missing_keys"></span>。按「编辑」开表,红框那几格就是要填的。补齐了才确认得了。</span>
</div>`;
const tag = (scope: string, col: string): string => `<span class="jc-tag ${scope}" data-class-of="${col}"></span>`;
const cell = (col: string, cls = ''): string => `<span class="jc-vcell${cls === '' ? '' : ` ${cls}`}" data-slot="${col}" data-col="${col}" data-blank></span>`;
const ro = (col: string, cls = ''): string => `<span class="jc-vcell${cls === '' ? '' : ` ${cls}`}" data-slot="${col}" data-blank></span>`;
const select = (col: string, options: ReadonlyArray<readonly [string, string]>, required = false): string =>
`<select class="jc-cell" data-col="${col}"${required ? ' required' : ''}><option value=""></option>${options.map(([v, l]) => `<option value="${v}">${l}</option>`).join('')}</select>`;
/* ==================== 局面 0 · 数据工单 P-08 ==================== */
const P08_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th class="req" style="min-width:140px">内部料号</th>
<th class="req" style="min-width:100px">品牌</th>
<th class="sug" style="min-width:120px">制造商</th>
<th class="req" style="min-width:220px">规格描述</th>
<th class="req" style="min-width:110px">封装</th>
<th class="sug" style="min-width:90px">工艺</th>
<th class="req" style="min-width:120px">生命周期</th>
<th class="sug" style="min-width:150px">EOL / PCN</th>
<th class="sug" style="min-width:200px">Datasheet 引用</th>
<th class="sug" style="min-width:110px">Datasheet 版本</th>
<th class="sug" style="min-width:140px">适配场景</th>
<th class="req" style="min-width:320px">标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)</th>
</tr></thead>
<tbody data-rows="mine:parts.profile" data-edit="parts.profile"><tr>
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="parts.profile" title="删一行 = 该记录不建">×</button></td>
<td class="req" data-h="内部料号">${cell('internal_pn', 'mono')}</td>
<td class="req" data-h="品牌">${cell('brand_name')}</td>
<td class="sug" data-h="制造商">${cell('manufacturer_name')}</td>
<td class="req" data-h="规格描述">${cell('specification_description')}</td>
<td class="req" data-h="封装">${cell('package', 'mono')}</td>
<td class="sug" data-h="工艺">${cell('process_technology')}</td>
<td class="req" data-h="生命周期">${select('lifecycle_status', [['active', 'Active'], ['nrnd', 'NRND'], ['eol', 'EOL'], ['discontinued', '停产'], ['risk', '风险']], true)}${tag('t-life', 'lifecycle_status')}</td>
<td class="sug" data-h="EOL / PCN">${cell('eol_pcn_status')}</td>
<td class="sug" data-h="Datasheet 引用">${cell('datasheet_ref', 'mono')}</td>
<td class="sug" data-h="Datasheet 版本">${cell('datasheet_version')}</td>
<td class="sug" data-h="适配场景">${cell('application_scenarios')}</td>
<td class="req" data-h="标准化参数集"><span class="jc-vcell jc-pre" data-slot="normalized_parameter_set" data-blank></span><textarea class="jc-cell" data-col="normalized_parameter_set" placeholder="输入电压范围 | 4.5~40 | V | hard | 客户上限 40V&#10;输出电压 | 5.0 | V | hard | 固定版&#10;使能阈值 | 未披露 | V | hard | 原厂未给"></textarea></td>
</tr></tbody>
</table></div>`;
export const P08_MASTER_CREATE = doc({
tone: 'human',
badge: '数据',
tag: '数据工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-08)',
title: '芯片主数据创建',
step: '步骤 0.5 · 等待数据',
state: `<span class="jc-rp-wait warn">待建 <b data-slot="parts_profile_missing"></b> / <b data-slot="parts_profile_rows"></b> 行</span>`,
body:
part('做什么', '批量创建 P-08 主数据记录') +
`<div class="jc-derive">
<p><span class="need">模板定位</span>:本单是「数据工单」的实例 —— <b>一单一类(可多条)</b>:目标台账 = <b>芯片主数据(P-08)</b>;
<b>字段即表头、每行一条新记录</b>;标准化参数集是这条记录的一个字段(一行一参数)。</p>
<p><span class="need">触发原因</span>:系统按内部料号查芯片主数据<b>查无记录</b> —— 是「完全没有」,不是字段缺失(那归执行工单)。</p>
<p><span class="done">AI 预填</span>:上传了 datasheet 的料号 AI 已按资料预填,标着值的格子请核对;没上传的料号全部手填,同为合法路径。</p>
<p><span class="need">纪律</span>:必填列全齐才允许确认;确认后这批记录进本单草稿,放行时并入正式库并留痕(确认人 = 数据责任人);只建主数据,验证记录等事实类不在此建。</p>
</div>` +
part('创建什么', '主数据记录(每行一条,*为必填列)') +
gapbar('parts_profile', '必填') +
editGate('parts.profile', `<b>待建 <span data-slot="parts_profile_rows"></span> 条记录</b>,点「编辑」核对预填、补齐空缺;「+ 加一行」= 再建一条`) +
`<div class="jc-editbar"><span>必填列(*)全齐才允许确认;原厂未披露的参数值请直接写「未披露」,它会进入 UNKNOWN 并挂验证任务;删一行 = 该记录不建。改动只落任务草稿层,<b>保存不等于放行</b>。</span></div>` +
P08_TABLE +
`<div class="jc-addrow">
<button type="button" class="jc-egbtn ghost" data-edit-add="parts.profile">+ 加一行(再建一条记录)</button>
<em>新行的内部料号必须写(它是这一行的钥匙);其余必填列全部填写后才允许确认。</em>
</div>` +
flow('<b>提交 / 确认:</b>本工单只承载内容;确认请在对话区的【执行卡】中进行。确认后活动自动从「等待数据」唤醒。'),
foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 芯片主数据创建(P-08)', '工单类型:数据'),
});
/* ==================== 局面 0′ · 数据工单 P-10 ==================== */
const P10_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th class="req" style="min-width:140px">关联料号</th>
<th class="req" style="width:70px;min-width:70px">序号</th>
<th class="req" style="min-width:120px">资料类型</th>
<th class="sug" style="min-width:90px">品牌</th>
<th class="req" style="min-width:220px">资料标题</th>
<th class="req" style="min-width:110px">版本</th>
<th class="req" style="min-width:220px">文件链接</th>
<th class="sug" style="min-width:180px">证据定位</th>
<th class="req" style="min-width:100px">状态</th>
</tr></thead>
<tbody data-rows="mine:parts.document" data-edit="parts.document"><tr>
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="parts.document" title="删一行 = 该资料不建">×</button></td>
<td class="req" data-h="关联料号">${cell('internal_pn', 'mono')}</td>
<td class="req" data-h="序号">${cell('doc_seq', 'mono')}</td>
<td class="req" data-h="资料类型">${select('document_type', [['datasheet', 'Datasheet'], ['app_note', '应用笔记'], ['reference_design', '参考设计'], ['white_paper', '白皮书'], ['sample_doc', '样片资料'], ['commitment_letter', '承诺函'], ['other', '其他']], true)}${tag('t-doc', 'document_type')}</td>
<td class="sug" data-h="品牌">${cell('brand_name')}</td>
<td class="req" data-h="资料标题">${cell('document_title')}</td>
<td class="req" data-h="版本">${cell('document_version')}</td>
<td class="req" data-h="文件链接">${cell('document_ref', 'mono')}</td>
<td class="sug" data-h="证据定位">${cell('citation_locator')}</td>
<td class="req" data-h="状态">${select('document_status', [['valid', '有效'], ['expired', '过期'], ['pending', '待核实'], ['void', '作废']], true)}${tag('t-docst', 'document_status')}</td>
</tr></tbody>
</table></div>`;
export const P10_DOCUMENT_CREATE = doc({
tone: 'human',
badge: '数据',
tag: '数据工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 数据工单(P-10)',
title: '原厂技术资料创建',
step: '步骤 0.5 · 等待数据',
state: `<span class="jc-rp-wait warn">待建 <b data-slot="parts_document_missing"></b> / <b data-slot="parts_document_rows"></b> 行</span>`,
body:
part('做什么', '为本单新建的料号登记原厂资料(P-10)') +
`<div class="jc-derive">
<p><span class="need">触发原因</span>:按内部料号查原厂技术资料库<b>查无记录</b>。一份资料一行;同一料号多份资料序号 01/02… 自己续。</p>
<p><span class="need">为什么要它</span>:差异明细的证据引用要指到具体资料的版本与页码 —— 没有这一行,结论就追溯不到来源。</p>
</div>` +
part('创建什么', '资料记录(每行一条,*为必填列)') +
gapbar('parts_document', '必填') +
editGate('parts.document', `<b>待建 <span data-slot="parts_document_rows"></span> 条资料记录</b>,点「编辑」补齐`) +
`<div class="jc-editbar"><span>资料类型、标题、版本、文件链接、状态为必填;证据定位写页码或章节。改动只落任务草稿层,<b>保存不等于放行</b>。</span></div>` +
P10_TABLE +
`<div class="jc-addrow">
<button type="button" class="jc-egbtn ghost" data-edit-add="parts.document">+ 加一行(再登记一份资料)</button>
<em>关联料号与序号是这一行的钥匙,新行都要写;必填列全齐才允许确认。</em>
</div>` +
flow('<b>提交 / 确认:</b>本工单只承载内容;确认请在对话区的【执行卡】中进行。'),
foot: sign('SS02 · ACT-WORK-05 差异评估 · 数据工单 · 原厂技术资料创建(P-10)', '工单类型:数据'),
});
/* ==================== 局面 1 · 评估输入清单(S0/S1)==================== */
const PN_CHECK_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:160px">料号</th>
<th style="width:90px;min-width:90px">角色</th>
<th style="width:110px;min-width:110px">在库状态</th>
<th style="min-width:200px">备注 / 放弃原因</th>
</tr></thead>
<tbody data-rows="mine:${D}.pn_check"><tr>
<td class="jc-rn"></td>
<td data-h="料号">${ro('internal_pn', 'mono')}</td>
<td data-h="角色">${tag('t-role', 'role')}</td>
<td data-h="在库状态">${tag('t-exist', 'exists_status')}</td>
<td class="dimempty" data-h="备注"><span class="jc-vcell" data-slot="note" data-blank></span> <span class="jc-vcell" data-slot="abandon_reason" data-blank></span></td>
</tr></tbody>
</table></div>`;
const GAP_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="width:120px;min-width:120px">键</th>
<th class="req" style="min-width:170px">输入项</th>
<th class="sug" style="width:80px;min-width:80px">来源</th>
<th style="width:100px;min-width:100px">状态</th>
<th class="sug" style="min-width:220px">绑定值 / 缺口说明</th>
<th class="req" style="min-width:260px">补充内容</th>
</tr></thead>
<tbody data-rows="mine:${D}.input_gap" data-edit="${D}.input_gap"><tr>
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="${D}.input_gap" title="删一行 = 该输入项不适用">×</button></td>
<td data-h="键">${cell('item_key', 'mono')}</td>
<td class="req" data-h="输入项">${cell('input_item')}</td>
<td class="sug" data-h="来源">${cell('source_ledger', 'mono')}</td>
<td data-h="状态">${select('status', [['gap', '待补'], ['default', '默认·可改'], ['ok', '齐备']])}${tag('t-gap', 'status')}</td>
<td class="sug" data-h="绑定值 / 缺口说明">${cell('detail')}</td>
<td class="req" data-h="补充内容"><span class="jc-vcell" data-slot="supplement" data-blank></span><textarea class="jc-cell" data-col="supplement" placeholder="资料链接 / 任务编号 / 具体值;补不了写「留 UNKNOWN」或「挂验证任务」"></textarea></td>
</tr></tbody>
</table></div>`;
export const EVAL_INPUT_LIST = doc({
tone: 'human',
badge: '执行',
tag: '执行工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1',
title: '评估输入与资料补齐确认',
step: '步骤 1/5 · 建立评估上下文',
state: `<span class="jc-rp-wait warn">待补 <b data-slot="${D}_input_gap_missing"></b> 项</span>`,
body:
part('做什么', '启动 4 项已绑定,其余信息在工单里补齐') +
`<div class="jc-derive">
<p><span class="done">AI 已完成</span>:系统已按启动传入的 <b>4 项</b>完成绑定 —— 客户、客户需求(新建 P-01 · 版本 V1)、原 PN、候选 PN(数组,逐个评估)—— 并生成输入快照。</p>
<p><span class="need">需要你补齐</span>:启动时未收的信息(应用场景、BOM 位)<b>直接在下表补</b>;默认值(替代类型 / 规则版本 / 负责人期限)可确认可改。本单缺口只覆盖「<b>有而不全</b>」;「完全没有」(料号不在库)已由数据工单创建。未补齐的缺口项将按 <b>UNKNOWN</b> 进入差异评估。</p>
</div>` +
part('已绑定', '启动时冻结,工单内不可改 —— 绑错请退回启动人重新启动') +
`<div class="jc-bound">
<b>客户</b><span data-slot="customerCode" data-blank></span>
<b>客户需求</b><span><span class="mono" data-slot="request_id" data-blank></span> · V1 · <span data-slot="requestDescription" data-blank></span></span>
<b>原内部料号</b><span class="mono" data-slot="originalInternalPn" data-blank></span>
<b>规则版本</b><span class="mono" data-slot="rule_version" data-blank></span>
</div>` +
PN_CHECK_TABLE +
part('交回什么', '评估输入清单(已绑定项只读核对,缺口项补齐,*为必填)') +
gapbar(`${D}_input_gap`, '补充内容') +
editGate(`${D}.input_gap`, `<b>还有 <span data-slot="${D}_input_gap_missing"></span> 项待补 / 待确认</b>,点「编辑」在「补充内容」列填入`) +
`<div class="jc-editbar"><span>加一行 = 补充 AI 未列出的资料项(新行的「输入项」与「补充内容」必填);删一行 = 该输入项不适用;补不了的写「留 UNKNOWN」或「挂验证任务」—— 那也是一个答案。改动只落任务草稿层,<b>保存不等于放行</b>。</span></div>` +
GAP_TABLE +
`<div class="jc-addrow">
<button type="button" class="jc-egbtn ghost" data-edit-add="${D}.input_gap">+ 加一行(补充资料项)</button>
<em>键写一个英文短名(如 thermal_note);「绑定值 / 缺口说明」栏写清这项资料是什么、AI 为什么拿不到。</em>
</div>` +
flow('<b>审批 / 提交:</b>本工单只承载内容;「提交」请在对话区的【执行卡】中进行。<b>提交后去向:</b>进入步骤 2 · AI 读取与标准化,仍缺失的项将按 UNKNOWN 进入差异评估。'),
foot: sign('SS02 · ACT-WORK-05 差异评估 · 执行工单 1/1 · 评估输入与资料补齐确认', '工单类型:执行'),
});
/* ==================== 局面 2 · 差异评估结论审核(S2/S3/S4,只读)==================== */
const RELATION_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:150px">候选料号</th>
<th style="width:100px;min-width:100px">替代类型</th>
<th style="width:100px;min-width:100px">推荐等级</th>
<th style="min-width:260px">推荐理由</th>
<th style="min-width:200px">差异摘要</th>
<th style="width:70px;min-width:70px">风险</th>
<th style="min-width:180px">验证要求</th>
<th style="width:80px;min-width:80px">置信度</th>
</tr></thead>
<tbody data-rows="mine:substitution.relation"><tr>
<td class="jc-rn"></td>
<td data-h="候选料号">${ro('candidate_internal_pn', 'mono')}</td>
<td data-h="替代类型">${tag('t-sub', 'substitution_type')}</td>
<td data-h="推荐等级">${tag('t-rec', 'recommendation_level')}</td>
<td class="dimempty" data-h="推荐理由">${ro('recommendation_reason')}</td>
<td class="dimempty" data-h="差异摘要">${ro('difference_summary')}</td>
<td data-h="风险">${tag('t-risk', 'risk_level')}</td>
<td class="dimempty" data-h="验证要求">${ro('verification_requirement')}</td>
<td class="dimempty" data-h="置信度">${ro('confidence_score', 'mono')}</td>
</tr></tbody>
</table></div>`;
const ITEM_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:130px">关系</th>
<th style="min-width:110px">参数项</th>
<th style="min-width:90px">原 PN 值</th>
<th style="min-width:90px">候选 PN 值</th>
<th style="width:60px;min-width:60px">单位</th>
<th style="width:80px;min-width:80px">约束类型</th>
<th style="width:110px;min-width:110px">结论</th>
<th style="width:60px;min-width:60px">风险</th>
<th style="min-width:160px">证据引用</th>
<th style="min-width:200px">影响说明</th>
<th style="min-width:160px">UNKNOWN 去向</th>
<th style="width:70px;min-width:70px">置信度</th>
</tr></thead>
<tbody data-rows="mine:substitution.difference_item"><tr>
<td class="jc-rn"></td>
<td data-h="关系">${ro('substitution_relation_id', 'mono')}</td>
<td data-h="参数项"><b class="cust" data-slot="parameter_name" data-blank></b></td>
<td data-h="原 PN 值">${ro('original_value', 'mono')}</td>
<td data-h="候选 PN 值">${ro('candidate_value', 'mono')}</td>
<td class="dimempty" data-h="单位">${ro('parameter_unit')}</td>
<td data-h="约束类型">${tag('t-con', 'constraint_type')}</td>
<td data-h="结论">${tag('t-diff', 'difference_status')}</td>
<td data-h="风险">${tag('t-risk', 'risk_level')}</td>
<td class="dimempty" data-h="证据引用">${ro('evidence_ref', 'mono')}</td>
<td class="dimempty" data-h="影响说明">${ro('impact_statement')}</td>
<td class="dimempty" data-h="UNKNOWN 去向">${ro('unknown_disposition')}</td>
<td class="dimempty" data-h="置信度">${ro('confidence', 'mono')}</td>
</tr></tbody>
</table></div>`;
export const DIFFERENCE_REVIEW = doc({
tone: 'ledger',
badge: '验收',
tag: '验收工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2',
title: '差异评估结论审核',
step: '步骤 3~4/5 · 逐项差异评估 + FAE/RI 审核',
state: `<span class="jc-rp-wait">共 <b data-slot="substitution_relation_rows"></b> 个候选 · <b data-slot="substitution_difference_item_rows"></b> 条比对</span>`,
body:
part('AI 结论说明', '结论是怎么算出来的') +
`<div class="jc-derive">
<p><b>AI 已完成的计算</b>:按 GOV 评估规则(锁定版本 <b class="mono" data-slot="rule_version" data-blank></b>),对原 PN <b class="mono" data-slot="original_internal_pn" data-blank></b>
与每个候选逐项比对,四态结论 —— MATCH <b data-slot="substitution_difference_item_difference_status_match"></b> /
MISMATCH <b data-slot="substitution_difference_item_difference_status_mismatch"></b> /
UNKNOWN <b data-slot="substitution_difference_item_difference_status_unknown"></b> /
CONDITIONAL <b data-slot="substitution_difference_item_difference_status_conditional"></b>,并给出风险等级、证据引用与置信度。
差异明细 = <b>P-11 差异明细子表</b>(一条参数一条记录);当前为 AI 草稿,<b>审核通过前不落账</b>。本单数据工单创建的主数据,其参数证据标注「人工录入」。</p>
</div>` +
part('验收判据', '你据此判断「通过 / 驳回」') +
criteria('判断这份差异评估结论是否可信,请逐项核对', [
'<b>关键参数差异均有明确状态</b>:逐项结论无漏项,每个参数都有四态之一的判定;',
'<b>UNKNOWN 项均有去向</b>:每个 UNKNOWN 都挂了验证任务或补来源动作,不允许悬空;',
'<b>证据链完整</b>:每项结论可追溯至 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入);',
'<b>硬约束项未被自动判 MATCH</b>:封装 / 工艺等硬约束未经证据支持不得判 MATCH。',
]) +
part('各候选推荐等级', '只读展示 · 每个候选一条 P-11 主记录草稿') +
RELATION_TABLE +
part('AI 产出的差异明细', '只读展示 · P-11 差异明细子表草稿') +
ITEM_TABLE +
flow('<b>审批:</b>本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。审核意见与补充条件写入 P-06(验证要求);<b>通过后去向:</b>进入步骤 5 · 评审放行与落账确认。'),
foot: sign('SS02 · ACT-WORK-05 差异评估 · 验收工单 1/2 · 差异评估结论审核', '工单类型:验收'),
});
/* ==================== 局面 3 · 评审放行与落账确认(S5/T_release,只读)==================== */
const P11_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:150px">替代关系ID</th>
<th style="width:100px;min-width:100px">替代类型</th>
<th style="min-width:130px">原内部料号</th>
<th style="min-width:130px">候选内部料号</th>
<th style="width:100px;min-width:100px">推荐等级</th>
<th style="min-width:220px">推荐理由</th>
<th style="min-width:180px">差异摘要</th>
<th style="min-width:120px">替代分析报告</th>
<th style="width:100px;min-width:100px">分析状态</th>
<th style="width:120px;min-width:120px">客户批准状态</th>
<th style="width:70px;min-width:70px">风险</th>
<th style="min-width:180px">验证要求</th>
<th style="width:80px;min-width:80px">证据可信度</th>
<th style="width:100px;min-width:100px">规则版本</th>
</tr></thead>
<tbody data-rows="mine:substitution.relation"><tr>
<td class="jc-rn"></td>
<td data-h="替代关系ID">${ro('substitution_relation_id', 'mono')}</td>
<td data-h="替代类型">${tag('t-sub', 'substitution_type')}</td>
<td data-h="原内部料号">${ro('original_internal_pn', 'mono')}</td>
<td data-h="候选内部料号">${ro('candidate_internal_pn', 'mono')}</td>
<td data-h="推荐等级">${tag('t-rec', 'recommendation_level')}</td>
<td class="dimempty" data-h="推荐理由">${ro('recommendation_reason')}</td>
<td class="dimempty" data-h="差异摘要">${ro('difference_summary')}</td>
<td class="dimempty" data-h="替代分析报告">${ro('analysis_report_ref', 'mono')}</td>
<td data-h="分析状态">${tag('t-anal', 'analysis_status')}</td>
<td data-h="客户批准状态"><span class="jc-vcell">未提交 <i class="jc-sub">由审批流程维护,本活动不产生</i></span></td>
<td data-h="风险">${tag('t-risk', 'risk_level')}</td>
<td class="dimempty" data-h="验证要求">${ro('verification_requirement')}</td>
<td class="dimempty" data-h="证据可信度">${ro('confidence_score', 'mono')}</td>
<td data-h="规则版本">${ro('rule_version', 'mono')}</td>
</tr></tbody>
</table></div>`;
const P01_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:150px">需求编号</th>
<th style="width:80px;min-width:80px">风险等级</th>
<th style="min-width:220px">验证要求</th>
<th style="min-width:130px">输出链接</th>
<th style="min-width:180px">下一步动作</th>
<th style="width:100px;min-width:100px">当前阶段</th>
<th style="width:100px;min-width:100px">需求状态</th>
</tr></thead>
<tbody data-rows="mine:demand.request"><tr>
<td class="jc-rn"></td>
<td data-h="需求编号">${ro('request_id', 'mono')}</td>
<td data-h="风险等级">${tag('t-risk', 'risk_level')}</td>
<td class="dimempty" data-h="验证要求">${ro('verification_requirement')}</td>
<td class="dimempty" data-h="输出链接">${ro('output_ref', 'mono')}</td>
<td class="dimempty" data-h="下一步动作">${ro('next_action')}</td>
<td class="dimempty" data-h="当前阶段">${ro('current_stage', 'mono')}</td>
<td class="dimempty" data-h="需求状态">${ro('request_status', 'mono')}</td>
</tr></tbody>
</table></div>`;
export const LANDING_CONFIRM = doc({
tone: 'ledger',
badge: '验收',
tag: '验收工单',
kicker: 'SS02 · ACT-WORK-05 差异评估 · 验收工单 2/2',
title: '评审放行与落账确认',
step: '步骤 5/5 · 提交 ACT-GATE-03',
state: `<span class="jc-rp-wait">拟落账 <b data-slot="substitution_relation_rows"></b> 条 P-11</span>`,
body:
part('AI 结论说明', '本单确认什么') +
`<div class="jc-derive">
<p><b>本活动以 ACT-GATE-03 评审放行为终点。</b>本单确认拟落账内容:评估结论层写 <b>P-11 替代关系主记录</b>(每个候选一条)与差异明细子表,需求状态回写 <b>P-01</b>(风险等级 / 验证要求 / 输出链接 / 下一步动作 / 当前阶段)。
<b>客户批准不属于本活动</b> —— 客户批准状态由审批流程维护,本单只写分析层(分析状态 = FAE 确认)。</p>
</div>` +
part('验收判据', '你据此判断「通过 / 驳回」') +
criteria('放行与落账前,请逐项核对', [
'<b>推荐等级与差异明细一致</b>:存在 UNKNOWN / CONDITIONAL 的候选,推荐等级不得给「推荐」;',
'<b>分析状态与客户批准状态分离</b>:本单只写分析层(FAE 确认),客户批准状态保持「未提交」;',
'<b>GOV 规则版本已锁定</b>:规则版本随结论一并落账;',
'<b>回写 P-01 字段完整</b>:风险等级、验证要求、输出链接、下一步动作、当前阶段五项齐全。',
]) +
part('拟写 P-11 · 替代关系主记录', '只读展示') +
P11_TABLE +
part('拟回写 P-01 · 需求状态', '只读展示') +
P01_TABLE +
flow('<b>审批:</b>本工单只承载内容;「通过 / 驳回」请在对话区的【验收卡】中进行。<b>通过后去向:</b>P-11 落账、P-01 回写、放行;结论供 ACT-WORK-08 证据归集消费。'),
foot: sign('SS02 · ACT-WORK-05 差异评估 · 验收工单 2/2 · 评审放行与落账确认', '工单类型:验收'),
});