diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts new file mode 100644 index 0000000..089437b --- /dev/null +++ b/activities/part-substitution-assessment/__tests__/activity.test.ts @@ -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; + 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(); + }); +}); diff --git a/activities/part-substitution-assessment/package.json b/activities/part-substitution-assessment/package.json new file mode 100644 index 0000000..362ed54 --- /dev/null +++ b/activities/part-substitution-assessment/package.json @@ -0,0 +1 @@ +{ "name": "@activities/part-substitution-assessment", "version": "0.1.0", "private": true, "type": "module", "exports": { ".": "./src/index.ts" } } diff --git a/activities/part-substitution-assessment/src/case-model.ts b/activities/part-substitution-assessment/src/case-model.ts new file mode 100644 index 0000000..a12f581 --- /dev/null +++ b/activities/part-substitution-assessment/src/case-model.ts @@ -0,0 +1,13 @@ +/** 占位:真正的案例模型见 Task 4(整份替换本文件)。这里只放一份 schema 过得去的最小件。 */ +export const CMMN_XML = ` + + + + + + + +`; +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[] }> = []; diff --git a/activities/part-substitution-assessment/src/constants.ts b/activities/part-substitution-assessment/src/constants.ts new file mode 100644 index 0000000..1febf6a --- /dev/null +++ b/activities/part-substitution-assessment/src/constants.ts @@ -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'; diff --git a/activities/part-substitution-assessment/src/index.ts b/activities/part-substitution-assessment/src/index.ts new file mode 100644 index 0000000..71c42a2 --- /dev/null +++ b/activities/part-substitution-assessment/src/index.ts @@ -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' }); diff --git a/activities/part-substitution-assessment/src/manifest.ts b/activities/part-substitution-assessment/src/manifest.ts new file mode 100644 index 0000000..1dad0f8 --- /dev/null +++ b/activities/part-substitution-assessment/src/manifest.ts @@ -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' }, +}; diff --git a/activities/part-substitution-assessment/src/skills.ts b/activities/part-substitution-assessment/src/skills.ts new file mode 100644 index 0000000..f271e7f --- /dev/null +++ b/activities/part-substitution-assessment/src/skills.ts @@ -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', + }, +]; diff --git a/activities/part-substitution-assessment/src/views/prd-style.ts b/activities/part-substitution-assessment/src/views/prd-style.ts new file mode 100644 index 0000000..4f252b3 --- /dev/null +++ b/activities/part-substitution-assessment/src/views/prd-style.ts @@ -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); } +`; diff --git a/activities/part-substitution-assessment/src/views/report.ts b/activities/part-substitution-assessment/src/views/report.ts new file mode 100644 index 0000000..e03127b --- /dev/null +++ b/activities/part-substitution-assessment/src/views/report.ts @@ -0,0 +1,5 @@ +export const P08_MASTER_CREATE = '
'; +export const P10_DOCUMENT_CREATE = '
'; +export const EVAL_INPUT_LIST = '
'; +export const DIFFERENCE_REVIEW = '
'; +export const LANDING_CONFIRM = '
';