feat(w05): 差异评估活动包骨架与清单 —— 六张主数据追加声明、三张自持单据、两张 DMN、五条样例

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:28:17 +08:00
co-authored by Claude Fable 5
parent f52f0168df
commit 13183b9287
9 changed files with 937 additions and 0 deletions
@@ -0,0 +1,128 @@
/**
* ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。
* 案例活动没有 `run`,自测不跑回合;测的是清单本身与授权面对齐,结构照 E04 / I01。
*
* 本文件目前只写三组(清单本身合规 / 台账声明 / 活动入口);
* 「案例模型与清单的对齐」「报告模版:编辑面与只读面各就各位」两组留给 Task 4/5 追加 ——
* `src/case-model.ts` 与 `src/views/report.ts` 眼下还是占位,写了那两组只会红。
*/
import { describe, expect, it } from 'vitest';
import { activityManifestSchema } from '@contracts';
import { manifest, D, RULE_VERSION } from '../src/manifest.ts';
import activity from '../src/index.ts';
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);
describe('清单本身合规', () => {
it('过 @contracts 的 schema', () => {
expect(() => activityManifestSchema.parse(manifest)).not.toThrow();
});
it('没有静态监听(等外部回音一律落在执行工单上,listen 在本仓不可用)', () => {
expect(manifest.declarations.listeners).toEqual([]);
});
it('L1/L2 判据都给了谓词;L4 三条不给(裁量项);L5 恰一条', () => {
for (const c of manifest.contract.acceptanceCriteria) {
if (c.level === 'L1' || c.level === 'L2') expect(c.predicate, c.key).toBeDefined();
if (c.level === 'L4') expect(c.predicate, c.key).toBeUndefined();
}
expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L4')).toHaveLength(3);
expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L5')).toHaveLength(1);
});
it('机检谓词只读基座读数与记账旗:不出现活动自报的汇总列名', () => {
const forbidden = ['relation_total', 'candidate_total', 'recommended_total', 'not_recommended_total'];
for (const c of manifest.contract.acceptanceCriteria) {
for (const name of forbidden) expect(c.predicate ?? '', `${c.key} 读了自报数 ${name}`).not.toContain(name);
}
});
it('三件必交付物都有台账地址;人环预算严格大于「基线 8 + 两轮审核循环 4」', () => {
expect(manifest.contract.requiredDeliverables).toHaveLength(3);
for (const d of manifest.contract.requiredDeliverables) expect(d.ledger, d.key).toBeDefined();
expect(manifest.contract.authorization.humanCallBudget).toBeGreaterThan(12);
expect(manifest.contract.resources.budget.humanCalls).toBeGreaterThan(12);
});
it('起单只必填 4 项(客户 / 需求 / 原 PN / 候选数组);datasheet 是选填文件', () => {
expect(manifest.contract.input.data['required']).toEqual(['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns']);
const properties = manifest.contract.input.data['properties'] as Record<string, { type?: string; format?: string }>;
expect(properties['candidateInternalPns']?.type).toBe('array');
expect(properties['datasheets']?.format).toBe('file');
});
it('五条样例:第一条满载走数据工单那条路,名字 ≤14 字,形状各不相同', () => {
expect(manifest.samples).toHaveLength(5);
for (const s of manifest.samples) expect(s.name.length, s.name).toBeLessThanOrEqual(14);
expect(new Set(manifest.samples.map((s) => JSON.stringify(s.input))).size).toBe(5);
const first = manifest.samples[0]?.input as { candidateInternalPns?: string[] };
expect(first.candidateInternalPns).toContain('XL2596S-5.0');
const newCustomer = manifest.samples.find((s) => (s.input as { customerName?: string }).customerName !== undefined);
expect(newCustomer).toBeDefined();
});
it('五张视图模版都声明了,剧本每一步引用的 view 都在其中', () => {
expect(new Set(manifest.assets.viewTemplates.map((v) => v.name))).toEqual(new Set(VIEWS));
for (const step of manifest.assets.businessModel.steps as ReadonlyArray<{ view?: string }>) {
if (step.view !== undefined) expect(VIEWS, step.view).toContain(step.view);
}
});
});
describe('台账声明:授权面与《统一方案》口径', () => {
it('P-08 一张表:主键 internal_pn,参数集是它的一个必填字段,没有 parts.parameter 子表', () => {
const p08 = byAddress('parts', 'profile');
expect(p08?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['internal_pn']);
expect(p08?.fields.find((f) => f.name === 'normalized_parameter_set')?.required).toBe(true);
expect(byAddress('parts', 'parameter')).toBeUndefined();
});
it('P-11 主记录的原 PN / 候选 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门)', () => {
const rel = byAddress('substitution', 'relation');
for (const name of ['original_internal_pn', 'candidate_internal_pn']) {
expect(rel?.fields.find((f) => f.name === name)?.refersTo, name).toBe('parts.profile.internal_pn');
}
});
it('客户批准状态**不在声明里**:本活动没有它的写权(只读边界)', () => {
const rel = byAddress('substitution', 'relation');
expect(rel?.fields.map((f) => f.name)).not.toContain('customer_approval_status');
});
it('UNKNOWN 必有去向:unknown_disposition 钉在 difference_status = unknown 的行上;四态是 ASCII tally', () => {
const item = byAddress('substitution', 'difference_item');
expect(item?.fields.find((f) => f.name === 'unknown_disposition')?.requiredWhen).toEqual({ column: 'difference_status', value: 'unknown' });
expect(item?.fields.find((f) => f.name === 'difference_status')?.tally).toEqual(['match', 'mismatch', 'unknown', 'conditional']);
expect(item?.fields.find((f) => f.name === 'substitution_relation_id')?.distinct).toBe(true);
});
it('P-01 回写五项钉在 current_stage = gate03 那一刻(放行前不拦,放行门 L2 数得出来)', () => {
const p01 = byAddress('demand', 'request');
for (const name of ['risk_level', 'verification_requirement', 'output_ref', 'next_action', 'current_activity']) {
expect(p01?.fields.find((f) => f.name === name)?.requiredWhen, name).toEqual({ column: 'current_stage', value: 'gate03' });
}
});
it('自持单据都带 caseKey eval_ref 且在主键里;放弃候选必须写原因', () => {
for (const object of ['pn_check', 'input_gap', 'verdict']) {
const t = byAddress(D, object);
expect(t?.kind, object).toBe('record');
expect(t?.caseKey, object).toBe('eval_ref');
expect(t?.fields.find((f) => f.name === 'eval_ref')?.primaryKey, object).toBe(true);
}
expect(byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'abandon_reason')?.requiredWhen).toEqual({ column: 'exists_status', value: 'abandoned' });
});
it('规则版本常量随包冻结', () => {
expect(RULE_VERSION).toBe('R-2026.07');
expect(manifest.assets.businessModel.dmn.tables.map((t) => t.id)).toEqual(['d01-difference-status', 'd02-recommendation']);
});
});
describe('活动入口', () => {
it('没有 run —— 行为全在清单里', () => {
expect(activity.run).toBeUndefined();
});
});
@@ -0,0 +1 @@
{ "name": "@activities/part-substitution-assessment", "version": "0.1.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts" } }
@@ -0,0 +1,13 @@
/** 占位:真正的案例模型见 Task 4(整份替换本文件)。这里只放一份 schema 过得去的最小件。 */
export const CMMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL">
<case id="part-substitution-assessment">
<caseFileModel/>
<casePlanModel id="plan" name="差异评估" autoComplete="true">
<humanTask id="S0_bind" name="占位"/>
</casePlanModel>
</case>
</definitions>`;
export const NARRATIVE = '(占位:案例说明书见 Task 4)';
export const ASSESSMENT_STEPS: Array<{ item: string; intent: string; guide: string; writes: string[]; reads: string[]; tools: string[]; view?: string; doneWhen?: string }> = [];
export const ASSESSMENT_PHASES: Array<{ name: string; items: string[] }> = [];
@@ -0,0 +1,8 @@
/** 清单与案例模型共用的常量 —— 单独成文件,免得 manifest ↔ case-model 循环 import 撞 TDZ。 */
export const ACTIVITY_ID_TEXT = 'part-substitution-assessment';
/** 活动自持单据的域名 */
export const D = 'sub_eval';
/** GOV 差异规则版本 —— 随包冻结,随每条结论落账(rule_version) */
export const RULE_VERSION = 'R-2026.07';
export const TOOL_COMPARE = 'part-substitution-assessment.compare-parameters';
export const TOOL_RECOMMEND = 'part-substitution-assessment.recommend';
@@ -0,0 +1,11 @@
/**
* ACT-WORK-05 · 差异评估 —— 活动包代码入口(ADR-004:默认导出 defineActivity 的返回值)。
*
* 这个活动没有 `run`。案例活动的推进由 Loop 驱动,行为全部写在清单里:
* 案例模型 + 说明书 + 步骤剧本 + 三条技能 + 台账声明 + 五张 HTML 报告模版。
* 这个文件之所以还存在,是因为清单的 `entry` 指着它 ——
* 它是**被静态审扫描的产物文本**(查 import 与全局引用,证明活动包零环境能力)。
*/
import { defineActivity } from '@sdk';
export default defineActivity({ name: 'part-substitution-assessment' });
@@ -0,0 +1,558 @@
/**
* 活动包清单 —— ACT-WORK-05 差异评估(案例活动)。
*
* 业务出处:`docs/SS02-ACT-WORK-05-html/`(活动流程与工单说明 / 一键启动入参说明 /
* 《SS02-SS03-SS04_Demo台账字段细化与统一方案》/ 四份目标件 / 全流程分支图)。
* 骨架照 E04 / I01 已验证的样子:双级入参 → 活动自持单据 + 共享主数据 → decide 循环做中间审核 →
* 终局放行。三处与前两个活动刻意不同,写在这里免得下一个读者当成疏漏:
* ① **主数据是六张(P-08/P-10/P-01/P-11 主+子/P-06)**,物理表由平台基线
* (`apps/api/src/app/seed-mvp-data.ts`)物化,这里只**追加**本活动要用的列与标记
* (required / tally / refersTo / requiredWhen 是活动的授权面;声明合并是纯 add-column)。
* ② **数据工单**(PN 不在库时让人从零建整条主数据)= 零输入执行工单 + 工作区的件
* (`mine:parts.profile` / `mine:parts.document` 可加行),确认落草稿层、放行并入正式表
* (守铁律②;文档里「确认即入库」在本平台落成「无审核环节、放行并入」)。一张表一张单。
* ③ 逐项四态与推荐等级**由两枚纯工具算**(规则同时以 D01/D02 两张 DMN 随包冻结当文档),
* 模型只负责对照表、证据与影响说明 —— 硬约束 MISMATCH 不可能被措辞改判。
*/
import type { ActivityManifest, DmnModel } from '@contracts';
import { activityIdSchema } from '@contracts';
import { ASSESSMENT_SKILLS } from './skills.ts';
import { CMMN_XML, NARRATIVE, ASSESSMENT_PHASES, ASSESSMENT_STEPS } from './case-model.ts';
import { P08_MASTER_CREATE, P10_DOCUMENT_CREATE, EVAL_INPUT_LIST, DIFFERENCE_REVIEW, LANDING_CONFIRM } from './views/report.ts';
import { ACTIVITY_ID_TEXT, D, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts';
export { D, RULE_VERSION, TOOL_COMPARE, TOOL_RECOMMEND } from './constants.ts';
const ACTIVITY_ID = activityIdSchema.parse(ACTIVITY_ID_TEXT);
/**
* 两张判定表 —— **文档形态**(求值器在 `apps/api/src/app/tools/first-party.ts`,与这里同一份规则):
* D01 逐参数四态、D02 推荐等级。都是 FIRST:规则从上往下第一条命中即出结论,顺序就是优先级。
*/
export const D01_DMN: DmnModel['tables'][number] = {
id: 'd01-difference-status',
label: '差异四态 D01(R-2026.07)',
hitPolicy: 'FIRST',
inputs: [
{ name: 'compare_result', expression: 'compare_result' },
{ name: 'constraint_type', expression: 'constraint_type' },
{ name: 'evidence_present', expression: 'evidence_present' },
],
outputs: [{ name: 'difference_status' }, { name: 'risk_level' }],
rules: [
{ id: 'd01-u', when: ['"unknown"', '-', '-'], then: ['unknown', 'medium'], annotation: '一侧未披露 → UNKNOWN,挂验证任务' },
{ id: 'd01-m0', when: ['"equal","covers"', '"hard"', 'false'], then: ['conditional', 'medium'], annotation: '硬约束缺证据不判 MATCH' },
{ id: 'd01-m', when: ['"equal","covers"', '-', '-'], then: ['match', 'low'], annotation: '一致或候选范围盖住原范围' },
{ id: 'd01-c', when: ['"configurable","partial"', '-', '-'], then: ['conditional', 'medium'], annotation: '可调配成 / 部分重叠 → 条件' },
{ id: 'd01-x', when: ['"narrower","differs"', '"hard"', '-'], then: ['mismatch', 'high'], annotation: '硬约束不一致' },
{ id: 'd01-s', when: ['"narrower","differs"', '"soft"', '-'], then: ['conditional', 'medium'], annotation: '软约束不一致 → 评估影响' },
],
};
export const D02_DMN: DmnModel['tables'][number] = {
id: 'd02-recommendation',
label: '推荐等级 D02(R-2026.07)',
hitPolicy: 'FIRST',
inputs: [
{ name: 'hard_mismatch_total', expression: 'hard_mismatch_total' },
{ name: 'unknown_total', expression: 'unknown_total' },
{ name: 'configurable_total', expression: 'configurable_total' },
{ name: 'conditional_total', expression: 'conditional_total' },
],
outputs: [{ name: 'recommendation_level' }],
rules: [
{ id: 'd02-n', when: ['> 0', '-', '-', '-'], then: ['not_recommended'], annotation: '硬约束失败直接不推荐' },
{ id: 'd02-u', when: ['0', '> 0', '-', '-'], then: ['conditional'], annotation: '有 UNKNOWN → 等验证闭环' },
{ id: 'd02-a', when: ['0', '0', '> 0', '-'], then: ['alternate'], annotation: '只靠改外围配得成 → 备选' },
{ id: 'd02-c', when: ['0', '0', '0', '> 0'], then: ['conditional'], annotation: '其余条件项 → 条件推荐' },
{ id: 'd02-r', when: ['0', '0', '0', '0'], then: ['recommended'], annotation: '全 MATCH → 推荐' },
],
};
export const ASSESSMENT_DMN: DmnModel = {
drd: [
{ decision: 'd01-difference-status', requires: [], inputs: ['compare_result', 'constraint_type', 'evidence_present'] },
{ decision: 'd02-recommendation', requires: [], inputs: ['hard_mismatch_total', 'unknown_total', 'configurable_total', 'conditional_total'] },
],
tables: [D01_DMN, D02_DMN],
};
/** 字段声明的短写(与 E04 同一套写法:显式 primaryKey / masked,其余按需) */
type F = { name: string; type: 'text' | 'number' | 'integer' | 'boolean' | 'date' | 'timestamp' | 'json'; label: string; primaryKey?: boolean; required?: boolean; expect?: boolean; tally?: string[]; auto?: boolean; distinct?: boolean; refersTo?: string; requiredWhen?: { column: string; value: string } };
const col = (f: F) => ({ primaryKey: false, masked: false, ...f });
export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
packageVersion: '1.0.0',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
},
/* ── 契约七件 ────────────────────────────────────────────────── */
contract: {
activityId: ACTIVITY_ID,
version: '1.0.0',
input: {
/*
* 一键启动只收 4 项(文档《一键启动入参说明》极简原则):客户 / 客户需求 / 原 PN / 候选 PN 数组。
* 应用场景、BOM 位、替代类型、规则版本、负责人期限**不是入参** —— 启动后在执行工单 01 里补。
* datasheet 选填文件:传了 AI 在数据工单件上预填,没传人全填(文档 0c:同为合法路径)。
* `customerName` 只在客户不在库(分支 0a)时用:S0 发数据录入卡建档,名字先摆上去让人核。
*/
data: {
type: 'object',
required: ['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns'],
properties: {
customerCode: { type: 'string', title: '客户编码(L-02 客户主台账)', examples: ['CU-0142'] },
customerName: { type: 'string', title: '客户名称(仅客户不在库时填,启动后先建档)', examples: ['深圳恒星电源有限公司'] },
requestDescription: {
type: 'string',
title: '客户需求(原始描述 —— 据此新建 P-01 需求主记录 · 版本 V1)',
examples: ['工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料'],
},
originalInternalPn: { type: 'string', title: '原内部料号(P-08)', examples: ['LM2596S-5.0'] },
candidateInternalPns: {
type: 'array',
title: '候选内部料号(数组,≥1;不在库的启动后经数据工单创建)',
items: { type: 'string' },
examples: [['XL2596S-5.0', 'LM2596S-ADJ']],
},
datasheets: {
type: 'array',
format: 'file',
title: '候选 datasheet(选填;传了 AI 在数据工单里预填,没传人全填)',
items: {
type: 'object',
properties: {
name: { type: 'string', title: '文件名' },
contentType: { type: 'string', title: '类型' },
size: { type: 'integer', title: '字节数' },
ref: { type: 'string', title: '存档号' },
text: { type: 'string', title: '读出来的文本' },
note: { type: 'string', title: '读取说明' },
},
},
},
},
},
messageAllowed: true,
messageHint: 'FAE 可在此说明客户的应用边界、验证条件或本轮评估的特殊要求',
},
output: {
/* 单一必发口:本活动终点是 ACT-GATE-03 评审放行;下游 ACT-WORK-08 尚未实现,按「契约先立」声明 */
mandatory: [
{
name: 'assessment-released',
description: '差异评估结论已落账(P-11 替代关系主记录 + 差异明细、P-01 回写)—— 供 ACT-WORK-08 证据归集消费;当前无消费者,载荷 = 已落地事实',
payload: {
type: 'object',
required: ['evalRef', 'requestId', 'relationTotal'],
properties: {
evalRef: { type: 'string', title: '评估案号' },
requestId: { type: 'string', title: '需求编号(P-01)' },
relationTotal: { type: 'integer', title: '落账的替代关系条数' },
recommendedTotal: { type: 'integer', title: '推荐' },
conditionalTotal: { type: 'integer', title: '条件推荐' },
alternateTotal: { type: 'integer', title: '备选' },
notRecommendedTotal: { type: 'integer', title: '不推荐' },
},
},
},
],
selective: [],
},
requiredDeliverables: [
{ key: 'eval-input', label: '评估输入清单', ledger: `${D}.input_gap`, ledgers: [`${D}.input_gap`, `${D}.pn_check`, `${D}.verdict`], editableAtRelease: [] },
{ key: 'difference-items', label: '差异评估结论(P-11 差异明细)', ledger: 'substitution.difference_item', ledgers: ['substitution.difference_item', 'substitution.relation'], editableAtRelease: [] },
{ key: 'landing', label: '拟落账内容(P-11 主记录 + P-01 回写)', ledger: 'substitution.relation', ledgers: ['substitution.relation', 'demand.request'], editableAtRelease: [] },
],
/*
* 验收判据 —— L1/L2 只读基座读数;L4 三条是文档验收工单 01/02 里留给人的裁量;
* 「推荐等级与明细一致」「硬约束不判 MATCH」机器已由工具守住,这里仍留 L4 让签字的人过目。
*/
acceptanceCriteria: [
{
key: 'input-confirmed',
label: '评估输入已经 PW 补齐确认:执行工单 01 已交回,缺口行的补充内容都有,补不了的留了 UNKNOWN',
level: 'L1',
predicate: `input_confirmed_total > 0 and ${D}_input_gap_rows > 0 and ${D}_input_gap_missing = 0`,
},
{
key: 'review-approved',
label: '差异评估结论已经 FAE/RI 审核通过:验收工单 01 已过,不处在驳回待改状态',
level: 'L1',
predicate: 'review_approved_total > 0 and substitution_difference_item_rows > 0 and substitution_difference_item_missing = 0',
},
{
key: 'every-candidate-assessed',
label: '每个未放弃的候选都有一条替代关系记录(N 候选 = N 条 P-11,并行独立落账)',
level: 'L2',
predicate: `substitution_relation_rows > 0 and substitution_relation_rows + ${D}_pn_check_exists_status_abandoned + 1 = ${D}_pn_check_rows`,
},
{
key: 'every-item-has-status',
label: '关键参数差异均有明确状态:每条明细都是 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 之一',
level: 'L2',
predicate:
'substitution_difference_item_rows > 0 and substitution_difference_item_difference_status_match + substitution_difference_item_difference_status_mismatch + ' +
'substitution_difference_item_difference_status_unknown + substitution_difference_item_difference_status_conditional = substitution_difference_item_rows',
},
{
key: 'unknown-has-disposition',
label: 'UNKNOWN 项均有去向:每个 UNKNOWN 都挂了验证任务或补来源动作,影响说明与证据引用逐条齐全',
level: 'L2',
predicate: 'substitution_difference_item_missing = 0',
},
{
key: 'judged-by-rule',
label: '四态与推荐等级由规则算出:逐参数比对工具与推荐定级工具都真调过',
level: 'L2',
predicate: `tool_part_substitution_assessment_compare_parameters_calls > 0 and tool_part_substitution_assessment_recommend_calls > 0`,
},
{
key: 'analysis-status-confirmed',
label: '分析状态与客户批准状态分离:每条 P-11 只写分析层 analysis_status = FAE 确认,规则版本已锁定随结论落账',
level: 'L2',
predicate: 'substitution_relation_analysis_status_fae_confirmed = substitution_relation_rows and substitution_relation_missing = 0',
},
{
key: 'p01-writeback-complete',
label: '回写 P-01 字段完整:风险等级、验证要求、输出链接、下一步动作、当前阶段五项齐全',
level: 'L2',
predicate: 'demand_request_rows > 0 and demand_request_missing = 0',
},
{ key: 'evidence-credible', label: '证据链可信:每条结论都能追溯到 P-08 / P-10 / P-06 的具体记录,或本单数据工单的创建记录(人工录入)', level: 'L4' },
{ key: 'hard-not-auto-match', label: '硬约束项未被自动判 MATCH:封装 / 工艺 / 电气边界等硬约束的 MATCH 都有证据支持', level: 'L4' },
{ key: 'recommendation-consistent', label: '推荐等级与差异明细一致:存在 UNKNOWN / CONDITIONAL 的候选没有给「推荐」', level: 'L4' },
/* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */
{ key: 'gate03-release', label: 'ACT-GATE-03 评审放行:签下去 P-11 落账、P-01 回写,结论供下游消费', level: 'L5' },
],
revisableAtRelease: [],
prohibitions: [
{ key: 'no-fabricated-parameter', statement: 'UNKNOWN 是一等公民:查不到的参数值必须显式写「未披露」并记去向,不得伪造、不得用估计值或空串冒充' },
{ key: 'no-override-hard-mismatch', statement: '硬约束 MISMATCH 不得被任何措辞改判 MATCH;要改只能补证据后重比' },
{ key: 'no-customer-approval-write', statement: '客户批准状态由独立审批流维护,本活动不写 customer_approval_status(没有这一列的写权)' },
{ key: 'no-commercial-judgement', statement: '本活动只答技术可替代性:不做数量、价格、交期、采购判断' },
{ key: 'no-rebind', statement: '启动绑定的 4 项(客户 / 需求 / 原 PN / 候选数组)工单内不可改;绑错只能退回触发方重新启动' },
{ key: 'no-external-write', statement: '放行前不得触达外部系统 —— 触达世界的动作集中在放行那一刻' },
],
authorization: {
discretion: [],
preauthorizedActions: [],
/*
* 基线:数据工单 ≤2 + 候选放弃/创建决策 ≤2 + 新客户录入卡 1 + 执行单 01 + 审核决策 1 + 放行 1 = 8;
* 审核循环预留两轮(+4)= 12。cap 必须严格大于最后一次拍板时的实际次数(E04 实测栽过),留到 16。
*/
humanCallBudget: 16,
},
resources: {
tools: [{ name: TOOL_COMPARE }, { name: TOOL_RECOMMEND }],
skills: ASSESSMENT_SKILLS.map((s) => s.id),
models: ['deepseek-v4-flash'],
roles: ['FAE/RI'],
budget: { steps: 90, tokens: 600_000, activeMinutes: 120, humanCalls: 16 },
},
},
/* ── 声明 ────────────────────────────────────────────────────── */
declarations: {
models: ['deepseek-v4-flash'],
tools: [{ name: TOOL_COMPARE }, { name: TOOL_RECOMMEND }],
ledger: [
/* L-02 客户主台账:只在分支 0a(客户不在库)由数据录入卡建一行 —— 录入卡写的列都得声明 */
{
domain: 'customer',
object: 'profile',
kind: 'master',
label: '客户档案',
fields: [
col({ name: 'customer_code', type: 'text', label: '客户编码', primaryKey: true }),
col({ name: 'name', type: 'text', label: '客户名称', required: true }),
col({ name: 'industry', type: 'text', label: '所属行业', expect: true }),
col({ name: 'region', type: 'text', label: '区域', expect: true }),
],
},
/* P-08 芯片主数据:数据工单建整条;参数集是它的一个字段(一张表,不建子表) */
{
domain: 'parts',
object: 'profile',
kind: 'master',
label: '芯片主数据(P-08)',
fields: [
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }),
col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }),
col({ name: 'manufacturer_name', type: 'text', label: '制造商名称', expect: true }),
col({ name: 'specification_description', type: 'text', label: '规格描述', required: true }),
col({ name: 'normalized_parameter_set', type: 'text', label: '标准化参数集(一行一参数:参数名 | 值 | 单位 | hard/soft | 说明)', required: true }),
col({ name: 'package', type: 'text', label: '封装', required: true }),
col({ name: 'process_technology', type: 'text', label: '工艺', expect: true }),
col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }),
col({ name: 'eol_pcn_status', type: 'text', label: 'EOL/PCN 状态', expect: true }),
col({ name: 'datasheet_ref', type: 'text', label: 'Datasheet 引用', expect: true }),
col({ name: 'datasheet_version', type: 'text', label: 'Datasheet 版本', expect: true }),
col({ name: 'application_scenarios', type: 'text', label: '适配应用场景', expect: true }),
],
},
/* P-10 原厂技术资料库:数据工单建;评估时只读做证据回链 */
{
domain: 'parts',
object: 'document',
kind: 'master',
label: '原厂技术资料库(P-10)',
fields: [
col({ name: 'internal_pn', type: 'text', label: '关联内部料号', primaryKey: true }),
col({ name: 'doc_seq', type: 'text', label: '资料序号(同一料号下 01/02…)', primaryKey: true }),
col({ name: 'document_type', type: 'text', label: '资料类型', required: true, tally: ['datasheet', 'app_note', 'reference_design', 'white_paper', 'sample_doc', 'commitment_letter', 'other'] }),
col({ name: 'brand_name', type: 'text', label: '品牌名称', expect: true }),
col({ name: 'document_title', type: 'text', label: '资料标题', required: true }),
col({ name: 'document_version', type: 'text', label: '资料版本', required: true }),
col({ name: 'document_ref', type: 'text', label: '资料文件链接', required: true }),
col({ name: 'citation_locator', type: 'text', label: '证据定位(页码/章节/表格)', expect: true }),
col({ name: 'document_status', type: 'text', label: '资料状态', required: true, tally: ['valid', 'expired', 'pending', 'void'] }),
],
},
/* P-01 需求主记录:S0 新建 V1,放行时回写五项(current_stage = gate03 那一刻五项必填) */
{
domain: 'demand',
object: 'request',
kind: 'master',
label: '客户需求主记录(P-01)',
fields: [
col({ name: 'request_id', type: 'text', label: '需求编号', primaryKey: true }),
col({ name: 'customer_code', type: 'text', label: '客户编码', required: true, refersTo: 'customer.profile.customer_code' }),
col({ name: 'requirement_version', type: 'text', label: '需求版本', required: true }),
col({ name: 'request_type', type: 'text', label: '需求类型', required: true }),
col({ name: 'request_description', type: 'text', label: '需求描述', required: true }),
col({ name: 'input_snapshot_ref', type: 'text', label: '输入快照', required: true }),
col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true }),
col({ name: 'bom_position', type: 'text', label: 'BOM 位', expect: true }),
col({ name: 'application_scenario', type: 'text', label: '应用场景', expect: true }),
col({ name: 'technical_constraints', type: 'text', label: '技术约束' }),
col({ name: 'risk_level', type: 'text', label: '风险等级', tally: ['low', 'medium', 'high', 'critical'], requiredWhen: { column: 'current_stage', value: 'gate03' } }),
col({ name: 'verification_requirement', type: 'text', label: '验证/复测/确认要求', requiredWhen: { column: 'current_stage', value: 'gate03' } }),
col({ name: 'request_status', type: 'text', label: '需求状态', required: true, tally: ['pending', 'responded', 'won', 'abandoned', 'closed'] }),
col({ name: 'current_activity', type: 'text', label: '当前 Activity', requiredWhen: { column: 'current_stage', value: 'gate03' } }),
col({ name: 'current_stage', type: 'text', label: '当前阶段' }),
col({ name: 'next_action', type: 'text', label: '下一步动作', requiredWhen: { column: 'current_stage', value: 'gate03' } }),
col({ name: 'output_ref', type: 'text', label: '标准输出链接', requiredWhen: { column: 'current_stage', value: 'gate03' } }),
],
},
/* P-11 替代关系主记录:一候选一条;两枚 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门) */
{
domain: 'substitution',
object: 'relation',
kind: 'master',
label: '替代关系主记录(P-11)',
fields: [
col({ name: 'substitution_relation_id', type: 'text', label: '替代关系ID', primaryKey: true }),
col({ name: 'eval_ref', type: 'text', label: '评估案号', required: true }),
col({ name: 'request_id', type: 'text', label: '关联需求编号', required: true }),
col({ name: 'bom_position', type: 'text', label: '关联 BOM 位', expect: true }),
col({ name: 'original_internal_pn', type: 'text', label: '原内部料号', required: true, refersTo: 'parts.profile.internal_pn' }),
col({ name: 'candidate_internal_pn', type: 'text', label: '候选内部料号', required: true, refersTo: 'parts.profile.internal_pn' }),
col({ name: 'substitution_type', type: 'text', label: '替代类型', required: true, tally: ['pin_to_pin', 'functional', 'parametric', 'other'] }),
col({ name: 'recommendation_level', type: 'text', label: '推荐等级', required: true, tally: ['recommended', 'conditional', 'alternate', 'not_recommended'] }),
col({ name: 'recommendation_reason', type: 'text', label: '推荐理由', required: true }),
col({ name: 'difference_summary', type: 'text', label: '差异摘要', required: true }),
col({ name: 'risk_level', type: 'text', label: '替代风险等级', required: true, tally: ['low', 'medium', 'high', 'critical'] }),
col({ name: 'verification_requirement', type: 'text', label: '验证要求', requiredWhen: { column: 'recommendation_level', value: 'conditional' } }),
col({ name: 'validation_id', type: 'text', label: '关联验证ID' }),
col({ name: 'analysis_status', type: 'text', label: '分析状态', required: true, tally: ['ai_draft', 'fae_confirmed', 'pending_customer', 'customer_approved', 'rejected', 'void'] }),
col({ name: 'analysis_report_ref', type: 'text', label: '替代分析报告', expect: true }),
col({ name: 'confidence_score', type: 'number', label: '证据可信度', expect: true }),
col({ name: 'rule_version', type: 'text', label: '差异规则版本', required: true }),
],
},
/* P-11 差异明细子表:一条参数一条记录;relation id 上 distinct 让基座数「几条关系有明细」 */
{
domain: 'substitution',
object: 'difference_item',
kind: 'master',
label: '差异明细(P-11 子表)',
fields: [
col({ name: 'substitution_relation_id', type: 'text', label: '替代关系ID', primaryKey: true, distinct: true, refersTo: 'substitution.relation.substitution_relation_id' }),
col({ name: 'difference_item_id', type: 'text', label: '差异明细ID', primaryKey: true }),
col({ name: 'parameter_name', type: 'text', label: '标准参数名', required: true }),
col({ name: 'original_value', type: 'text', label: '原物料参数值', required: true }),
col({ name: 'candidate_value', type: 'text', label: '候选物料参数值', required: true }),
col({ name: 'parameter_unit', type: 'text', label: '参数单位', expect: true }),
col({ name: 'constraint_type', type: 'text', label: '约束类型', required: true, tally: ['hard', 'soft'] }),
col({ name: 'compare_result', type: 'text', label: '比对结果(工具出参)', expect: true }),
col({ name: 'difference_status', type: 'text', label: '差异状态', required: true, tally: ['match', 'mismatch', 'unknown', 'conditional'] }),
col({ name: 'impact_statement', type: 'text', label: '影响说明(MATCH 写「无」)', required: true }),
col({ name: 'unknown_disposition', type: 'text', label: 'UNKNOWN 去向(验证任务号 / 补来源动作)', requiredWhen: { column: 'difference_status', value: 'unknown' } }),
col({ name: 'risk_level', type: 'text', label: '风险等级', required: true, tally: ['low', 'medium', 'high'] }),
col({ name: 'evidence_ref', type: 'text', label: '差异证据引用', required: true }),
col({ name: 'confidence', type: 'number', label: 'AI 置信度', expect: true }),
col({ name: 'rule_version', type: 'text', label: '差异规则版本', required: true }),
],
},
/* P-06 验证记录:事实类 —— 本活动只写「验证要求」(状态 not_started),不经数据工单建 */
{
domain: 'validation',
object: 'record',
kind: 'master',
label: '芯片验证记录(P-06)',
fields: [
col({ name: 'validation_id', type: 'text', label: '验证ID', primaryKey: true }),
col({ name: 'request_id', type: 'text', label: '关联需求编号', required: true }),
col({ name: 'validation_internal_pn', type: 'text', label: '验证内部料号', required: true, refersTo: 'parts.profile.internal_pn' }),
col({ name: 'validation_type', type: 'text', label: '验证类型', required: true, tally: ['pin_to_pin', 'functional', 'reliability', 'certification', 'retest'] }),
col({ name: 'validation_status', type: 'text', label: '验证状态', required: true, tally: ['not_started', 'in_progress', 'passed', 'failed', 'conditional', 'pending_evidence'] }),
col({ name: 'test_condition', type: 'text', label: '测试条件', expect: true }),
col({ name: 'next_validation_action', type: 'text', label: '后续验证动作', required: true }),
col({ name: 'evidence_ref', type: 'text', label: '验证证据引用', expect: true }),
],
},
/* ── 活动自持单据(record,caseKey eval_ref)── */
{
domain: D,
object: 'pn_check',
kind: 'record',
caseKey: 'eval_ref',
label: '料号存在性核对',
fields: [
col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }),
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }),
col({ name: 'role', type: 'text', label: '角色', required: true, tally: ['original', 'candidate'] }),
col({ name: 'exists_status', type: 'text', label: '在库状态', required: true, tally: ['found', 'absent', 'created', 'abandoned'] }),
col({ name: 'abandon_reason', type: 'text', label: '放弃原因', requiredWhen: { column: 'exists_status', value: 'abandoned' } }),
col({ name: 'note', type: 'text', label: '备注' }),
],
},
{
domain: D,
object: 'input_gap',
kind: 'record',
caseKey: 'eval_ref',
label: '评估输入清单',
fields: [
col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }),
col({ name: 'item_key', type: 'text', label: '输入项键', primaryKey: true }),
col({ name: 'input_item', type: 'text', label: '输入项', required: true }),
col({ name: 'source_ledger', type: 'text', label: '来源台账', expect: true }),
col({ name: 'status', type: 'text', label: '状态', required: true, tally: ['gap', 'default', 'ok'] }),
col({ name: 'detail', type: 'text', label: '绑定值 / 缺口说明', expect: true }),
col({ name: 'supplement', type: 'text', label: '补充内容', requiredWhen: { column: 'status', value: 'gap' } }),
col({ name: 'origin', type: 'text', label: '来源(ai/human-added)' }),
],
},
{
domain: D,
object: 'verdict',
kind: 'record',
caseKey: 'eval_ref',
label: '本次评估结论',
fields: [
col({ name: 'eval_ref', type: 'text', label: '评估案号', primaryKey: true }),
col({ name: 'request_id', type: 'text', label: '需求编号' }),
col({ name: 'rule_version', type: 'text', label: '规则版本' }),
col({ name: 'original_internal_pn', type: 'text', label: '原内部料号' }),
col({ name: 'candidate_total', type: 'integer', label: '候选数' }),
col({ name: 'input_confirmed_total', type: 'integer', label: '评估输入已确认' }),
col({ name: 'review_approved_total', type: 'integer', label: '差异结论已审核通过' }),
col({ name: 'landing_prepared_total', type: 'integer', label: '拟落账已备好' }),
col({ name: 'relation_total', type: 'integer', label: '替代关系条数' }),
col({ name: 'recommended_total', type: 'integer', label: '推荐' }),
col({ name: 'conditional_total', type: 'integer', label: '条件推荐' }),
col({ name: 'alternate_total', type: 'integer', label: '备选' }),
col({ name: 'not_recommended_total', type: 'integer', label: '不推荐' }),
col({ name: 'summary', type: 'text', label: '本次评估结论' }),
],
},
],
listeners: [],
harness: {
memoryWindow: 24,
stallAfter: 'PT4H',
nudge: [{ after: 'PT2H', action: 'nudge' }],
retry: { times: 2, backoff: 'PT30S' },
},
},
/* ── 资产初始版 ──────────────────────────────────────────────── */
assets: {
businessModel: { cmmnXml: CMMN_XML, narrative: NARRATIVE, dmn: ASSESSMENT_DMN, steps: ASSESSMENT_STEPS, phases: ASSESSMENT_PHASES },
skills: ASSESSMENT_SKILLS,
/*
* 五个局面各一张:
* · `p08-master-create` —— S0d 摆,数据工单(P-08),件上可加行、可编辑;
* · `p10-document-create` —— S0d 摆,数据工单(P-10),同上;
* · `eval-input-list` —— S1 摆,评估输入清单:绑定区只读 + 缺口表可编辑;
* · `difference-review` —— S2/S3/S4 摆,差异明细 + 各候选推荐等级,只读;
* · `landing-confirm` —— S5/T_release 摆,拟写 P-11 / 拟回写 P-01,只读;放行后即归档件。
*/
viewTemplates: [
{ name: 'p08-master-create', html: P08_MASTER_CREATE },
{ name: 'p10-document-create', html: P10_DOCUMENT_CREATE },
{ name: 'eval-input-list', html: EVAL_INPUT_LIST },
{ name: 'difference-review', html: DIFFERENCE_REVIEW },
{ name: 'landing-confirm', html: LANDING_CONFIRM },
],
cardSchemas: {},
distillHooks: [{ on: 'terminal', prompt: '归纳本次差异评估:哪些候选、各自推荐等级与关键依据、UNKNOWN 挂了什么验证任务、审核是否被驳回过(为什么)、数据工单建了哪些主数据。' }],
},
/*
* ── 样例集:五条形状真不同(常备案例即验收案例)──
* 第一条满载且走「候选缺库 → 数据工单」那条最长的路(测试台落地即预填的是它);样例名 ≤14 字。
* 库里只播了 LM2596S-5.0 / LM2596S-ADJ 两颗(见 seed-mvp-data.ts),XL2596S-5.0 / CN-DCDC-32V / LM2596S-3.3 刻意不在库。
*/
samples: [
{
name: '候选缺库·派数据工单',
input: {
customerCode: 'CU-0142',
requestDescription: '工业电源项目在用 LM2596S-5.0(TI,12V→5V),原厂 PCN 转 NRND,客户要求验证替代料;应用 12V→5V、工业温域、输入上限 40V',
originalInternalPn: 'LM2596S-5.0',
candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ'],
},
expect: { ports: ['assessment-released'] },
},
{
name: '都在库·常路',
input: {
customerCode: 'CU-0142',
requestDescription: 'LM2596S-5.0 转 NRND,客户先评估同族可调版 LM2596S-ADJ 能否替代(愿意改外围);应用 12V→5V、工业温域',
originalInternalPn: 'LM2596S-5.0',
candidateInternalPns: ['LM2596S-ADJ'],
},
expect: { ports: ['assessment-released'] },
},
{
name: '原PN缺库·硬阻塞',
input: {
customerCode: 'CU-0142',
requestDescription: '客户另一块板在用 LM2596S-3.3(3.3V 固定版),同批 PCN,要求评估 LM2596S-ADJ 替代;应用 12V→3.3V',
originalInternalPn: 'LM2596S-3.3',
candidateInternalPns: ['LM2596S-ADJ'],
},
expect: { ports: ['assessment-released'] },
},
{
name: '三候选·含硬约束不推荐',
input: {
customerCode: 'CU-0142',
requestDescription: '工业电源 12V→5V,输入上限 40V,工业温域;三个候选:XL2596S-5.0、LM2596S-ADJ、国产 CN-DCDC-32V(输入上限只有 32V)',
originalInternalPn: 'LM2596S-5.0',
candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ', 'CN-DCDC-32V'],
},
expect: { ports: ['assessment-released'] },
},
{
name: '新客户·先建档',
input: {
customerCode: 'CU-9001',
customerName: '深圳恒星电源有限公司',
requestDescription: '新客户口述:工业电源 12V→5V 在用 LM2596S-5.0,想评估 LM2596S-ADJ',
originalInternalPn: 'LM2596S-5.0',
candidateInternalPns: ['LM2596S-ADJ'],
},
expect: { ports: ['assessment-released'] },
},
],
entry: { file: 'index.js', checksum: 'dev-entry-part-substitution-assessment' },
};
@@ -0,0 +1,55 @@
/**
* ACT-WORK-05 差异评估的技能 —— 载方法,不载权力。
* 剧本写「这一步做什么、写哪几列」,技能写「为什么这么判、拿不准时按哪条走」。
*/
import { skillIdSchema } from '@contracts';
import type { Skill } from '@contracts';
export const ASSESSMENT_SKILLS: Skill[] = [
{
id: skillIdSchema.parse('substitution.normalize-parameters'),
label: '把多来源资料整成可比对的标准参数对照表',
applicability: '输入确认之后、逐项比对之前:原 PN 与每个候选的参数集要摆成同名同单位的一对一。',
method: [
'参数集在 `parts.profile.normalized_parameter_set` 里,一行一参数:`参数名 | 值 | 单位 | hard/soft | 说明`。',
'对照表按**原 PN 的参数名**为骨架:原 PN 有的参数,候选没有对应行 → 候选值写「未披露」(它会成为 UNKNOWN);',
'候选多出来的参数不比(比对只回答「能不能替」,多出来的能力不构成差异)。',
'单位以原 PN 为准:候选单位不同先换算(mV→V、kHz→MHz),换算不了就不改值、在说明里写明 —— 不许静默改数。',
'约束类型以原 PN 那一行的 hard/soft 为准;原 PN 没标的按 GOV 口径:封装 / 工艺 / 电气边界(电压 / 电流 / 温度 / 阈值)一律 hard,其余 soft。',
'**缺参数不得伪造**:查不到的写「未披露」,不写估计值、不写「约」。',
].join('\n'),
crystallized: [],
knowledgeRefs: ['标准化参数集行语法', '硬/软约束口径'],
version: '1.0.0',
},
{
id: skillIdSchema.parse('substitution.difference-judgement'),
label: '逐项四态由工具算,证据与影响由你写',
applicability: '对照表齐了,要出 MATCH / MISMATCH / UNKNOWN / CONDITIONAL 时。',
method: [
'四态**不许自己判**:调 `part-substitution-assessment.compare-parameters`,把每个候选的全部参数对一次性传进去(candidates 数组,一次一批)。',
'每对参数都带 `evidenceRef`(值从哪份记录来:「P-08」「P-10 Rev H §6.5」「数据工单 <单号> · 确认人 <人>」)——硬约束没有证据引用,工具不判 MATCH。',
'客户在应用场景里给了边界(如「输入上限 40V」)就写进那一对的 `requirement`(`>= 40`),候选不满足直接 MISMATCH。',
'工具出参照抄进 `substitution.difference_item`:difference_status / compare_result / risk_level 一字不改;',
'`impact_statement` 你写:MATCH 写「无」,其余说清对应用 / 验证 / 报价的影响;UNKNOWN 必须写 `unknown_disposition`(挂验证任务号或补来源动作),不许悬空。',
'推荐等级同样由工具定(`part-substitution-assessment.recommend`,传每个候选的 counts),`recommendation_reason` 照工具的 reason 扩写,不许把不推荐改成推荐。',
].join('\n'),
crystallized: [],
knowledgeRefs: ['四态口径', '推荐等级四档'],
version: '1.0.0',
},
{
id: skillIdSchema.parse('substitution.landing'),
label: '落账只写分析层,客户批准不碰',
applicability: '审核通过之后拟 P-11 / 回写 P-01 时。',
method: [
'`analysis_status` 从 `ai_draft` 推到 `fae_confirmed`;`customer_approval_status` 由独立审批流维护 —— 本活动**没有这一列的写权**,写了会被拒。',
'P-01 回写五项:risk_level(取各候选里最高的)、verification_requirement(把所有验证任务号列全)、output_ref(替代分析报告号)、next_action(「提交 ACT-GATE-03 评审放行」)、current_stage 写 `gate03`。',
'规则版本随结论落账:每条 P-11 主记录与差异明细都写 rule_version(工具出参的 ruleVersion),中途规则更新不影响进行中的评估。',
'被打回重做时改的是**台账本身**(理由 / 依据 / 去向),不是把同一份原样再递一遍。',
].join('\n'),
crystallized: [],
knowledgeRefs: ['analysis_status 与 customer_approval_status 分离'],
version: '1.0.0',
},
];
@@ -0,0 +1,158 @@
/**
* ACT-WORK-05 差异评估 · 视觉底 —— 逐字节抄自 I01(`brand-content-toolkit/src/views/prd-style.ts`)。
*
* 本活动的四份目标件(`docs/SS02-ACT-WORK-05-html/*.html`)与 E04 / I01 的目标件是同一套
* 设计语言产出的(卡壳 / 卡头渐变 / 判据框 / 表格排版全同);`activities/*` 只准 import
* `@sdk` / `@contracts`,包之间没有代码通道,只能各自持有一份。改这份不要改口径:
* 数据驱动的开关(三档占位 / 缺项红带 / 状态徽 / 编辑闸)在 `report.ts` 的 `BIND_CSS`。
*/
export const PRD_CSS = `
.proto-root {
--ink:#1f2329; --ink-2:#41464e; --ink-3:#646a73; --ink-4:#8f959e;
--line:#dee0e3; --line-soft:#e8eaed; --soft:#f8f9fb;
--brand:#3370ff; --brand-deep:#245bdb; --brand-soft:#e1eaff; --brand-wash:#f0f4ff;
--human:#a9711e; --human-soft:#f6ecd8; --human-line:#e6d3a8;
--ledger:#b23a2e; --ledger-soft:#f7e4e0; --ledger-line:#ecc5bd;
--ok:#278717; --ok-soft:#e8f8e4;
--danger:#d83931; --danger-soft:#feeae9;
--warn-accent:#a9711e; --warn-soft:#f6ecd8;
--mono: "SF Mono", Menlo, Consolas, monospace;
--t-fast: .16s;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--ink);
font-size: 14px;
line-height: 1.7;
}
.proto-root, .proto-root * { box-sizing: border-box; }
/* ==================== 卡壳(嵌进工作区,不自带浮起——外层已经是一张卡)==================== */
.proto-root .jc-report { background: #fff; overflow: hidden; }
.proto-root .jc-rp-head { display: flex; align-items: center; gap: 14px; padding: 18px 22px; border-bottom: 2px solid var(--ink); }
.proto-root .jc-report.tone-human .jc-rp-head { background: linear-gradient(120deg, #fffaf1 0%, #fff 55%, #fbf6ea 100%); }
.proto-root .jc-report.tone-ledger .jc-rp-head { background: linear-gradient(120deg, #fdf3f1 0%, #fff 55%, #fbf2f0 100%); }
.proto-root .jc-rp-badge {
flex: none; width: 44px; height: 44px; border-radius: 10px;
display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 13px;
}
.proto-root .jc-report.tone-human .jc-rp-badge { background: var(--human); box-shadow: 0 6px 16px rgba(169,113,30,.25); }
.proto-root .jc-report.tone-ledger .jc-rp-badge { background: var(--ledger); box-shadow: 0 6px 16px rgba(178,58,46,.25); }
.proto-root .jc-rp-tit { flex: 1; min-width: 0; }
.proto-root .jc-rp-tit em { font-style: normal; font-size: 11px; letter-spacing: .08em; color: var(--ink-4); font-weight: 700; }
.proto-root .jc-rp-tit h3 { margin: 2px 0 0; font-size: 19px; font-weight: 800; line-height: 1.3; }
.proto-root .jc-rp-sub { font-size: 12px; color: var(--ink-3); margin-top: 3px; }
.proto-root .jc-rp-side { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex: none; }
/* 目标件 side 第二行:步骤(静态文案),同目标件 .step 的字号字色 */
.proto-root .jc-rp-step { font-size: 12px; color: var(--ink-3); }
/* 关联任务号:全长 UUID 如实给,排版上截尾 —— 截断是排版,不是数据 */
.proto-root .jc-rp-taskid {
display: inline-block; max-width: 150px; overflow: hidden; text-overflow: ellipsis;
white-space: nowrap; vertical-align: bottom; font-family: var(--mono); font-size: 11px;
}
.proto-root .jc-rp-tag { font-size: 11px; font-weight: 800; padding: 2px 10px; border-radius: 999px; border: 1px solid; }
.proto-root .jc-report.tone-human .jc-rp-tag { color: var(--human); background: var(--human-soft); border-color: var(--human-line); }
.proto-root .jc-report.tone-ledger .jc-rp-tag { color: var(--ledger); background: var(--ledger-soft); border-color: var(--ledger-line); }
.proto-root .jc-rp-wait { font-size: 12px; color: var(--ink-3); }
.proto-root .jc-rp-wait.warn { color: var(--danger); font-weight: 700; }
.proto-root .jc-rp-bd { padding: 20px 22px; }
/* 【做什么】/【交回什么】这类分区小标——括号label + 破折号说明,同一行 */
.proto-root .jc-rp-part { font-size: 11px; font-weight: 800; letter-spacing: .12em; color: var(--ink-4); margin: 20px 0 8px; }
.proto-root .jc-rp-part:first-child { margin-top: 0; }
.proto-root .jc-rp-part b { color: var(--ink-3); }
/* AI 已完成 / 需要你 —— 说明框,左侧色条按 tone 走 */
.proto-root .jc-derive { border: 1px solid var(--line); border-left: 3px solid; border-radius: 8px; background: var(--soft); padding: 14px 16px; margin-bottom: 16px; }
.proto-root .jc-report.tone-human .jc-derive { border-left-color: var(--human); }
.proto-root .jc-report.tone-ledger .jc-derive { border-left-color: var(--ledger); }
.proto-root .jc-derive p { margin: 0; font-size: 13px; color: var(--ink-2); }
.proto-root .jc-derive p + p { margin-top: 6px; }
.proto-root .jc-derive b { color: var(--ink); }
.proto-root .jc-derive .done { color: var(--ok); font-weight: 700; }
.proto-root .jc-report.tone-human .jc-derive .need { color: var(--human); font-weight: 700; }
.proto-root .jc-report.tone-ledger .jc-derive .need { color: var(--ledger); font-weight: 700; }
/* 验收判据框——目标件里两份验收工单同一色(赭红),不随 tone 变 */
.proto-root .jc-criteria { border: 1px solid var(--ledger-line); border-radius: 10px; background: var(--ledger-soft); padding: 13px 16px; margin-bottom: 18px; }
.proto-root .jc-criteria-t { font-size: 11px; font-weight: 800; letter-spacing: .08em; color: var(--ledger); margin-bottom: 7px; }
.proto-root .jc-criteria ul { margin: 0; padding-left: 18px; font-size: 12.5px; color: var(--ink-2); }
.proto-root .jc-criteria li { margin: 4px 0; }
.proto-root .jc-criteria b { color: var(--ink); }
/* 表格基础排版(交互态的显隐开关在 BIND_CSS) */
/*
* **表宽超出就得能横着滑**(2026-08-23 用户:「表格已经显示不全了,不能左右滑动」)。
* 从前这里是 overflow: hidden —— 那是为了让圆角裁住表头,可它同时把超出的列
* **直接裁掉且滑不动**:列一多(或某列为了放下下拉框加宽)就看不全,人还没有任何办法。
* quote-* 各包一直是 overflow-x: auto,这两个新包抄版式时把这一条抄丢了。
* (注释里不写反引号 —— 整段 CSS 是模版字符串,一个反引号就把它截断,本轮踩第三次。)
* 竖向仍 hidden:件是整篇铺开跟着外层滚的,表格自己不该再套一层竖滚动条。
*/
.proto-root .jc-tbl-wrap { border: 1px solid var(--line-soft); border-radius: 10px; overflow-x: auto; overflow-y: hidden; margin-bottom: 4px; }
/*
* 资料名 = 可点开的那一格(2026-08-23)。**看得出能点**才会有人点:
* 品牌蓝 + 下划虚线 + 手型;悬停实线。点了去哪由外层接(件里一行脚本也没有)。
*/
.proto-root .jc-fileln { color: var(--brand); border-bottom: 1px dashed var(--brand-soft); cursor: pointer; transition: all var(--t-fast); }
.proto-root .jc-fileln:hover { border-bottom-style: solid; background: var(--brand-wash); }
/* 空标题不该长得像能点开 —— 那一格根本没有资料 */
.proto-root .jc-fileln:empty { border-bottom: 0; cursor: default; background: none; }
/* 格式徽:摆的是这份资料交出去时的样子(md / html / docx / xlsx) */
.proto-root .jc-fmt { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 19px;
padding: 1px 7px; border-radius: 5px; font-size: 10px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
background: var(--soft); color: var(--ink-3); border: 1px solid var(--line); }
.proto-root .jc-fmt.v-docx { background: #eaf1ff; color: #2b5fd9; border-color: #c3d6ff; }
.proto-root .jc-fmt.v-xlsx { background: #e9f7ee; color: #1f7a45; border-color: #bfe6cd; }
.proto-root .jc-fmt.v-html { background: #fdf0e6; color: #b25e15; border-color: #f3d3b4; }
.proto-root .jc-fmt.v-md { background: #f2f3f5; color: #5a6068; border-color: #dfe1e5; }
.proto-root .jc-tbl-in { width: 100%; border-collapse: collapse; font-size: 13px; }
.proto-root .jc-tbl-in th { text-align: left; background: var(--soft); font-size: 11px; letter-spacing: .04em; font-weight: 800; color: var(--ink-3); padding: 9px 11px; border-bottom: 1px solid var(--line); }
.proto-root .jc-tbl-in th.req::after { content: " *"; color: var(--danger); font-size: 11px; }
.proto-root .jc-tbl-in th.sug::after { content: " 建议"; color: var(--warn-accent); font-size: 10px; font-weight: 600; }
.proto-root .jc-tbl-in th.opt::after { content: " 选填"; color: var(--ink-4); font-size: 10px; font-weight: 600; }
.proto-root .jc-tbl-in td { padding: 9px 11px; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
.proto-root .jc-tbl-in tr:last-child td { border-bottom: 0; }
.proto-root .jc-tbl-in td.jc-rn { width: 38px; white-space: nowrap; font-family: var(--mono); font-size: 11px; color: var(--ink-4); text-align: center; position: relative; }
.proto-root .jc-vcell { display: block; font-size: 12.5px; color: var(--ink); line-height: 1.5; overflow-wrap: anywhere; }
.proto-root .jc-vcell.mono { font-family: var(--mono); font-size: 11.5px; color: var(--ink-2); }
.proto-root .jc-vcell.cust { font-weight: 700; }
.proto-root .jc-vcell.dim { color: var(--ink-4); text-decoration: line-through; }
.proto-root .jc-diffnew { font-weight: 800; color: var(--ok); }
.proto-root .jc-cell, .proto-root select.jc-cell, .proto-root textarea.jc-cell {
width: 100%; min-width: 60px; border: 1px solid var(--line); border-radius: 6px;
padding: 6px 8px; font: inherit; font-size: 12.5px; color: var(--ink); background: #fff;
}
.proto-root .jc-cell.mono { font-family: var(--mono); font-size: 11.5px; }
.proto-root .jc-cell:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-wash); outline: none; }
.proto-root .jc-rowdel {
width: 22px; height: 22px; margin: 0 auto; cursor: pointer;
border: 1px solid var(--line); border-radius: 6px; padding: 0;
font: inherit; font-size: 14px; line-height: 1; color: var(--ink-3); background: #fff;
transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
}
.proto-root .jc-rowdel:hover { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); }
.proto-root .jc-addrow em { font-style: normal; font-size: 11.5px; color: var(--ink-4); }
.proto-root .jc-addrow button {
cursor: pointer; border: 1px solid var(--brand-soft); border-radius: 7px; padding: 5px 12px;
font: inherit; font-size: 12px; font-weight: 700; color: var(--brand-deep); background: var(--brand-wash);
}
.proto-root .jc-addrow button:hover { background: var(--brand-soft); }
/* 本工单只承载内容,提交/审批请在对话区——目标件三份文件都有这一条 */
.proto-root .jc-flow { margin-top: 16px; padding: 9px 12px; border-radius: 8px; background: var(--soft); border: 1px dashed var(--line); font-size: 12px; color: var(--ink-3); }
.proto-root .jc-flow b { color: var(--ink); }
.proto-root .jc-rp-foot { padding: 13px 22px; border-top: 1px solid var(--line-soft); font-size: 11.5px; color: var(--ink-4); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
.proto-root .jc-rp-foot span:last-child { font-family: var(--mono); }
`;
@@ -0,0 +1,5 @@
export const P08_MASTER_CREATE = '<div class="proto-root"></div>';
export const P10_DOCUMENT_CREATE = '<div class="proto-root"></div>';
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>';