diff --git a/activities/part-substitution-assessment/__tests__/activity.test.ts b/activities/part-substitution-assessment/__tests__/activity.test.ts
index 21590d3..90f9f14 100644
--- a/activities/part-substitution-assessment/__tests__/activity.test.ts
+++ b/activities/part-substitution-assessment/__tests__/activity.test.ts
@@ -2,12 +2,11 @@
* 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 { createMockHost } from '@sdk';
import { manifest, D, RULE_VERSION } from '../src/manifest.ts';
import activity from '../src/index.ts';
@@ -15,6 +14,7 @@ const ITEMS = ['S0_bind', 'S0d_master_data', 'S1_input_confirm', 'S2_normalize',
const NO_DONE_WHEN = new Set(['S0d_master_data', 'S1_input_confirm', 'S4_review', 'T_release']);
const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm'];
const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object);
+const viewHtml = (name: string) => manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? '';
describe('清单本身合规', () => {
it('过 @contracts 的 schema', () => {
@@ -177,8 +177,69 @@ describe('案例模型与清单的对齐', () => {
});
});
+describe('报告模版:编辑面与只读面各就各位', () => {
+ it('两张数据工单件可加行可编辑:data-edit / data-edit-add 与主键 data-col 都在', () => {
+ const p08 = viewHtml('p08-master-create');
+ expect(p08).toContain('data-edit="parts.profile"');
+ expect(p08).toContain('data-edit-add="parts.profile"');
+ expect(p08).toContain('data-col="internal_pn"');
+ expect(p08).toContain('data-col="normalized_parameter_set"');
+ const p10 = viewHtml('p10-document-create');
+ expect(p10).toContain('data-edit="parts.document"');
+ expect(p10).toContain('data-col="doc_seq"');
+ });
+
+ it('评估输入清单:缺口表可编辑(supplement),绑定区只读', () => {
+ const html = viewHtml('eval-input-list');
+ expect(html).toContain(`data-edit="${D}.input_gap"`);
+ expect(html).toContain('data-col="supplement"');
+ expect(html).toContain(`data-rows="mine:${D}.pn_check"`);
+ });
+
+ it('差异审核与落账确认只读:没有任何 data-edit / data-col', () => {
+ for (const name of ['difference-review', 'landing-confirm']) {
+ expect(viewHtml(name), name).not.toContain('data-edit=');
+ expect(viewHtml(name), name).not.toContain('data-col=');
+ }
+ });
+
+ it('每个可编辑格子的 data-col 都在对应台账对象的声明里', () => {
+ const cases: Array<[string, string, string]> = [
+ ['p08-master-create', 'parts', 'profile'],
+ ['p10-document-create', 'parts', 'document'],
+ ['eval-input-list', D, 'input_gap'],
+ ];
+ for (const [view, domain, object] of cases) {
+ const cols = new Set([...viewHtml(view).matchAll(/data-col="([^"]+)"/g)].map((m) => m[1] ?? ''));
+ const declared = new Set(byAddress(domain, object)?.fields.map((f) => f.name));
+ for (const c of cols) expect(declared.has(c), `${view}: ${c}`).toBe(true);
+ }
+ });
+
+ it('枚举列只用 data-class-of 投影,不摆 ASCII 原值', () => {
+ expect(viewHtml('difference-review')).toContain('data-class-of="difference_status"');
+ expect(viewHtml('difference-review')).toContain('data-class-of="recommendation_level"');
+ expect(viewHtml('difference-review')).not.toContain('data-slot="difference_status"');
+ });
+
+ it('宿主拒推含脚本的模版', async () => {
+ const host = createMockHost({ contract: CONTRACT });
+ await expect(host.base.interaction.pushHtml('
')).rejects.toThrow(/无脚本执行权/);
+ });
+});
+
describe('活动入口', () => {
it('没有 run —— 行为全在清单里', () => {
expect(activity.run).toBeUndefined();
});
});
+
+const CONTRACT = {
+ activityId: 'part-substitution-assessment',
+ version: '1.0.0',
+ data: {},
+ message: undefined,
+ outputPorts: { mandatory: ['assessment-released'], selective: [] },
+ prohibitions: [],
+ discretion: [],
+} as const;
diff --git a/activities/part-substitution-assessment/src/views/report.ts b/activities/part-substitution-assessment/src/views/report.ts
index e03127b..32d3828 100644
--- a/activities/part-substitution-assessment/src/views/report.ts
+++ b/activities/part-substitution-assessment/src/views/report.ts
@@ -1,5 +1,712 @@
-export const P08_MASTER_CREATE = '';
-export const P10_DOCUMENT_CREATE = '';
-export const EVAL_INPUT_LIST = '';
-export const DIFFERENCE_REVIEW = '';
-export const LANDING_CONFIRM = '';
+/**
+ * ACT-WORK-05 差异评估的 HTML 报告模版 —— **活动自己持有的资产**,随包版本冻结。
+ *
+ * 五个局面各一张:
+ * · `p08-master-create` —— S0d 摆,数据工单(P-08 芯片主数据),可加行、可编辑;
+ * · `p10-document-create` —— S0d 摆,数据工单(P-10 原厂技术资料),可加行、可编辑;
+ * · `eval-input-list` —— S0/S1 摆,评估输入清单:绑定区只读 + 缺口表可编辑;
+ * · `difference-review` —— S2/S3/S4 摆,各候选推荐等级 + 差异明细,**只读**(通过/驳回在对话区决策卡);
+ * · `landing-confirm` —— S5/T_release 摆,拟写 P-11 / 拟回写 P-01,**只读**,放行后即归档件。
+ *
+ * 版面语言逐字量自 `docs/SS02-ACT-WORK-05-html/` 四份目标件;取值三纪律:顶层只取汇总列与单值列,
+ * 逐条走 `mine:域.对象`,列名逐字照 `declarations.ledger` 抄。**模版不算不判**。
+ */
+import { PRD_CSS } from './prd-style.ts';
+import { D } from '../constants.ts';
+
+/* ====================
+ * 绑定期补的样式(原样复制自 E04 `activities/crm-relationship-coverage/src/views/report.ts`
+ * 第 25–211 行的 `BIND_CSS`,共 187 行 —— 不是 I01 那份(裁决:见任务交底)。
+ * 原因:本活动的件里有「只读格与可改格同表混排」(枚举列是