diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index 136adfd6..da7e282d 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -96,6 +96,7 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -123,6 +124,10 @@ public class AihrPracticeSeedService { private static final List GROWTH_LEVELS = List.of("初级", "中级", "高级"); private static final Set SCENARIO_REVIEW_STATUSES = Set.of("草稿", "待业务审核", "已发布", "已下线"); private static final Set SCENARIO_RISK_LEVELS = Set.of("待评估", "常规", "高风险"); + private static final List PUBLICATION_PLACEHOLDER_MARKERS = List.of( + "待补", "待提供", "待完善", "待定", "占位内容", "占位文本", "占位符", "placeholder", "示例来源", "示例内容", + "待确认", "todo", "tbd" + ); private static final List MANDATORY_DUAL_REVIEW_TERMS = List.of( "事故", "消防", "火灾", "起火", "爆炸", "燃气", "漏电", "触电", "带电", "高空", "困人", "受伤", "人身伤害", "摔倒", "暴力", "肢体冲突", @@ -7808,10 +7813,45 @@ public class AihrPracticeSeedService { || scenario.rounds() == null || scenario.rounds().size() < 2) { throw new ServiceException("发布前请补齐业务来源/SOP、服务红线、训练目标、成功标准和至少两轮对练"); } + if (containsPublicationPlaceholder( + scenario.name(), scenario.scenarioType(), scenario.customerPersona(), scenario.openingText(), + scenario.explicitNeed(), scenario.hiddenNeed(), scenario.irritants(), scenario.delights(), + scenario.sopRefs(), scenario.successCriteria(), scenario.strategy(), scenario.goal(), + scenario.collaborationPositions(), scenario.curriculumVersion() + )) { + throw new ServiceException("发布前请移除待补、待确认或示例占位内容"); + } + if (scenario.rounds().stream().anyMatch(round -> round == null + || isBlank(round.customer()) || isBlank(round.coachHint()) + || containsPublicationPlaceholder(round.customer(), round.coachHint()))) { + throw new ServiceException("发布前请补齐每轮业主话术和教练提示,并移除占位内容"); + } if (scenario.rubric() == null || scenario.rubric().dimensions() == null || scenario.rubric().dimensions().size() != RUBRIC_DIMENSION_CODES.size()) { throw new ServiceException("发布前请配置完整的五维评分规则"); } + if (scenario.rubric().dimensions().stream().anyMatch(dimension -> dimension == null + || isBlank(dimension.name()) || isBlank(dimension.description()) + || containsPublicationPlaceholder(dimension.name(), dimension.description()))) { + throw new ServiceException("发布前请补齐五维评分说明,并移除占位内容"); + } + validateRubricDimensions(scenario.rubric().dimensions()); + } + + static boolean containsPublicationPlaceholder(String... values) { + if (values == null) { + return false; + } + for (String value : values) { + if (value == null || value.isBlank()) { + continue; + } + String normalized = value.trim().toLowerCase(Locale.ROOT); + if (PUBLICATION_PLACEHOLDER_MARKERS.stream().anyMatch(normalized::contains)) { + return true; + } + } + return false; } private boolean isKnownCurriculumCapability(String position, String growthLevel, String competencyCode) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java index c627a5d9..ffa867c6 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java @@ -533,6 +533,8 @@ public class AihrPracticeSeedServiceTest { assertTrue(source.contains("高风险场景需要另一位不同审核人复核")); assertTrue(source.contains("isBlank(scenario.irritants())")); assertTrue(source.contains("业务来源/SOP、服务红线")); + assertTrue(source.contains("containsPublicationPlaceholder")); + assertTrue(source.contains("发布前请移除待补、待确认或示例占位内容")); assertTrue(source.contains("growth_level")); assertTrue(source.contains("competency_code")); assertTrue(source.contains("risk_level")); @@ -612,6 +614,31 @@ public class AihrPracticeSeedServiceTest { assertTrue(jdbcTemplate.updateSql.isEmpty()); } + @Test + public void scenarioCannotPublishWithPlaceholderContent() throws Exception { + HighRiskScenarioJdbcTemplate jdbcTemplate = new HighRiskScenarioJdbcTemplate("常规"); + jdbcTemplate.sopRefs = "待补公司收费制度"; + AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), jdbcTemplate, null, null); + markScenarioTableReady(service); + + ServiceException error = assertThrows(ServiceException.class, () -> service.updateScenarioReviewStatus( + "placeholder-scene", new ScenarioReviewStatusRequest("已发布"), 101L, "reviewer-a")); + + assertEquals("发布前请移除待补、待确认或示例占位内容", error.getMessage()); + assertTrue(jdbcTemplate.updateSql.isEmpty()); + } + + @Test + public void publicationPlaceholderDetectionDoesNotRejectOrdinaryConfirmationLanguage() { + assertTrue(AihrPracticeSeedService.containsPublicationPlaceholder("待补正式制度")); + assertTrue(AihrPracticeSeedService.containsPublicationPlaceholder("收费依据(待确认)")); + assertTrue(AihrPracticeSeedService.containsPublicationPlaceholder("收费依据待确认")); + assertTrue(AihrPracticeSeedService.containsPublicationPlaceholder("TBD")); + assertFalse(AihrPracticeSeedService.containsPublicationPlaceholder("无依据时明确告知无法确认")); + assertFalse(AihrPracticeSeedService.containsPublicationPlaceholder("未经核实不得承诺减免")); + assertFalse(AihrPracticeSeedService.containsPublicationPlaceholder("车辆被占位后如何协调")); + } + @Test public void employeeScenarioReadDoesNotExposeContentReviewerAudit() throws Exception { HighRiskScenarioJdbcTemplate jdbcTemplate = new HighRiskScenarioJdbcTemplate("高风险"); @@ -2974,6 +3001,7 @@ public class AihrPracticeSeedServiceTest { private Long reviewerUserId; private String secondReviewer = ""; private Long secondReviewerUserId; + private String sopRefs = "收费管理SOP v1"; private String updateSql = ""; private HighRiskScenarioJdbcTemplate(String riskLevel) { @@ -3031,7 +3059,7 @@ public class AihrPracticeSeedServiceTest { row.put("hidden_need", "得到清晰答复"); row.put("irritants", "未经核实承诺减免"); row.put("delights", "给出责任人和反馈节点"); - row.put("sop_refs", "收费管理SOP v1"); + row.put("sop_refs", sopRefs); row.put("success_criteria", "解释依据并完成记录"); row.put("strategy", "先核实再答复"); row.put("goal", "训练收费争议沟通"); diff --git a/docs/README.md b/docs/README.md index e183022d..3f1c5dd1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,7 @@ | [MOBILE_APP_DEVICE_ACCEPTANCE.md](MOBILE_APP_DEVICE_ACCEPTANCE.md) | Android/iOS 自定义基座或签名包的真机验收清单 | | [帮道8月1日Android内测Go-No-Go-20260728.md](帮道8月1日Android内测Go-No-Go-20260728.md) | 更新至 2026-07-29 的当前候选 APK、版本冻结、生产依赖、真机与内容门禁 Go/No-Go 结论 | | [生活顾问调研转化清单-20260728.md](生活顾问调研转化清单-20260728.md) | 将仓库外原始访谈脱敏映射为待业务审核的 SOP、知识、题目和对练候选池 | +| [帮道8月1日生活顾问内容候选审核包-20260729.md](帮道8月1日生活顾问内容候选审核包-20260729.md) | 5 个脱敏场景与 30 道政策问题的待审候选包;明确 0 个正式场景、0 道正式来源标准答案及业务签认路径 | | [FIGMA_DESIGN_INVENTORY.md](FIGMA_DESIGN_INVENTORY.md) | Figma 页面节点索引、设计批次范围、取消项和全文件一致性审计结果 | | [FIGMA_DESIGN_SYSTEM_GUIDE.md](FIGMA_DESIGN_SYSTEM_GUIDE.md) | 员工端通用视觉元素、业务元素、状态映射和 LLM 组页规则 | | [FIGMA_IMPLEMENTATION_MATRIX.md](FIGMA_IMPLEMENTATION_MATRIX.md) | 70 张正式画板与代码/线上实现状态、二期边界和剩余动作 | diff --git a/docs/content-candidates/aug1-life-advisor-content-candidates-v0.1.json b/docs/content-candidates/aug1-life-advisor-content-candidates-v0.1.json new file mode 100644 index 00000000..7db5430e --- /dev/null +++ b/docs/content-candidates/aug1-life-advisor-content-candidates-v0.1.json @@ -0,0 +1,325 @@ +{ + "schemaVersion": "1.0", + "contentVersion": "aug1-life-advisor-candidate-v0.1", + "generatedDate": "2026-07-29", + "candidateStatus": "PENDING_BUSINESS_REVIEW", + "publishable": false, + "notice": "本文件仅是脱敏候选审核材料,不是正式制度、标准答案或可发布训练内容。", + "sourceRegistry": [ + { + "sourceId": "LC-INT-001", + "description": "仓库外保存的第 1 份生活顾问访谈,仅引用脱敏业务主题和行号", + "authority": "FIELD_INTERVIEW", + "formalPolicy": false + }, + { + "sourceId": "AUG1-PLAN", + "description": "帮道 8 月 1 日 Android 安装包测试版任务计划", + "authority": "PROJECT_PLAN", + "formalPolicy": false + } + ], + "scenarios": [ + { + "candidateId": "LC-AUG1-SC-001", + "candidateStatus": "PENDING_BUSINESS_REVIEW", + "publishable": false, + "sourceRefs": ["AUG1-PLAN:L206-L214", "LC-INT-001:L25-L26"], + "sourceSufficiency": "NEEDS_FORMAL_POLICY", + "proposedRiskLevel": "常规", + "requiredReviewers": ["客服负责人", "品质负责人"], + "reviewChecklist": ["补充报事报修受理制度及版本", "确认定位信息最小必要范围", "确认升级路径和反馈节点", "审核合成话术与评分标准"], + "scenarioDraft": { + "id": "lc-location-refusal-intake", + "name": "住户拒绝提供定位信息时的报修受理", + "position": "生活顾问", + "scenarioType": "报事报修受理", + "projectType": "住宅", + "growthLevel": "初级", + "competencyCode": "LG-REPAIR-INTAKE", + "collaborationPositions": "客服,保修", + "riskLevel": "待评估", + "curriculumVersion": "住宅五岗-v1", + "difficulty": 2, + "customerPersona": "担心隐私、不愿提供精确位置但希望尽快处理的住户", + "openingText": "我只想报修,不想在电话里说具体住址,你们先安排人来看。", + "explicitNeed": "完成报修并获得后续处理安排", + "hiddenNeed": "确认定位信息用途、减少不必要的信息暴露", + "irritants": "猜测房号、追问无关个人信息、以拒绝受理施压、承诺未经确认的到场时间", + "delights": "说明最小必要信息及用途,提供可选确认方式并给出受理编号和反馈节点", + "sopRefs": "待补:正式报事报修受理制度、隐私告知要求及有效版本", + "successCriteria": "说明必要信息用途;不索取无关隐私;给出替代确认方式;完成受理、责任转交和可核验反馈节点", + "strategy": "先承接诉求和隐私顾虑,再解释最小必要信息,提供受控替代方式并完成闭环", + "goal": "训练生活顾问在住户拒绝提供定位信息时兼顾隐私边界和报修闭环", + "enabled": false, + "rounds": [ + {"customer": "为什么一定要说到这么细?你们系统里不是能查到吗?", "coachHint": "解释本次受理所需信息及用途,不声称能读取未授权资料,并提供住户可接受的确认方式。", "emotion": 70, "trust": 58}, + {"customer": "如果我只提供公共区域位置,你们还能不能先登记?", "coachHint": "区分登记、派工和入户的必要信息,明确当前能完成的动作、责任人及下一次反馈节点。", "emotion": 58, "trust": 70} + ] + }, + "rubricDraft": { + "code": "rubric-lc-location-refusal-intake", + "scenarioCode": "lc-location-refusal-intake", + "name": "拒绝定位信息受理五维评分", + "version": "candidate-v0.1", + "enabled": false, + "description": "仅供业务审核,正式制度和边界确认后方可启用。", + "dimensions": [ + {"code": "task_completion", "name": "任务完成度", "weight": 0.4, "description": "完成必要信息说明、替代确认、受理记录和后续节点四项动作。"}, + {"code": "standardization", "name": "话术规范性", "weight": 0.25, "description": "不猜测身份位置,不索取无关隐私,不虚构系统权限或处理时限。"}, + {"code": "emotion", "name": "情绪管理能力", "weight": 0.2, "description": "承接隐私顾虑,保持中性表达,不以拒绝受理激化对话。"}, + {"code": "response_timeliness", "name": "响应时效", "weight": 0.1, "description": "在当前回合明确受理状态、责任转交和下一次可核验反馈节点。"}, + {"code": "marketing", "name": "增值转化潜力", "weight": 0.05, "description": "仅在解决报修主诉后介绍经批准的便民入口,不进行无依据推介。"} + ] + } + }, + { + "candidateId": "LC-AUG1-SC-002", + "candidateStatus": "PENDING_BUSINESS_REVIEW", + "publishable": false, + "sourceRefs": ["AUG1-PLAN:L206-L214", "LC-INT-001:L31-L40"], + "sourceSufficiency": "NEEDS_FORMAL_POLICY", + "proposedRiskLevel": "高风险", + "requiredReviewers": ["客服负责人", "财务负责人", "法务审核人"], + "reviewChecklist": ["补充收费依据和适用项目", "冻结优惠与分期审批权限", "确认争议登记及升级路径", "两名不同审核人复核"], + "scenarioDraft": { + "id": "lc-fee-dispute-explanation", + "name": "物业费明细与服务争议沟通", + "position": "生活顾问", + "scenarioType": "收费争议", + "projectType": "住宅", + "growthLevel": "中级", + "competencyCode": "LG-COMPLAINT-FEE-PARKING", + "collaborationPositions": "客服,财务", + "riskLevel": "待评估", + "curriculumVersion": "住宅五岗-v1", + "difficulty": 3, + "customerPersona": "质疑费用明细并把服务问题与缴费绑定的住户", + "openingText": "服务没做好,费用明细也说不清楚,我为什么要缴?", + "explicitNeed": "核对费用构成并处理服务争议", + "hiddenNeed": "获得可追溯的解释、整改责任人和复核入口", + "irritants": "争辩对错、擅自承诺减免或折扣、模糊收费依据、把整改作为缴费交换条件", + "delights": "区分费用核对与服务整改,提供正式依据、复核入口、责任人和反馈节点", + "sopRefs": "待补:正式收费制度、账单口径、优惠分期审批制度及有效版本", + "successCriteria": "准确复述争议;区分费用复核和服务整改;不越权承诺;完成双线记录并给出正式反馈节点", + "strategy": "先核对争议点,再将费用复核和服务整改拆分为两条可追踪链路", + "goal": "训练生活顾问在收费争议中说明依据、守住授权并完成双线闭环", + "enabled": false, + "rounds": [ + {"customer": "你先告诉我能减多少,不然别谈后面的事。", "coachHint": "不承诺减免,说明本人权限边界,同时分别登记费用复核和服务整改事项。", "emotion": 80, "trust": 48}, + {"customer": "你说登记了,我怎么知道不是又拖着不处理?", "coachHint": "提供两条事项的责任归属、记录凭证和下一次反馈节点,不保证未经确认的结果。", "emotion": 68, "trust": 63} + ] + }, + "rubricDraft": { + "code": "rubric-lc-fee-dispute-explanation", + "scenarioCode": "lc-fee-dispute-explanation", + "name": "收费争议五维评分", + "version": "candidate-v0.1", + "enabled": false, + "description": "仅供业务审核,收费制度、权限和复核路径确认后方可启用。", + "dimensions": [ + {"code": "task_completion", "name": "任务完成度", "weight": 0.4, "description": "完成争议复述、费用复核登记、整改登记及双线反馈节点。"}, + {"code": "standardization", "name": "话术规范性", "weight": 0.25, "description": "引用经审核依据,不擅自承诺减免、折扣、分期或争议结果。"}, + {"code": "emotion", "name": "情绪管理能力", "weight": 0.2, "description": "先承接服务不满,不争辩、不催压,持续区分事实与待复核事项。"}, + {"code": "response_timeliness", "name": "响应时效", "weight": 0.1, "description": "当场明确两条链路的当前状态、责任归属和下一次反馈节点。"}, + {"code": "marketing", "name": "增值转化潜力", "weight": 0.05, "description": "主诉未闭环前不营销;闭环后仅介绍经批准且与需求相关的服务。"} + ] + } + }, + { + "candidateId": "LC-AUG1-SC-003", + "candidateStatus": "PENDING_BUSINESS_REVIEW", + "publishable": false, + "sourceRefs": ["AUG1-PLAN:L206-L214", "LC-INT-001:L25-L26"], + "sourceSufficiency": "NEEDS_FORMAL_POLICY", + "proposedRiskLevel": "常规", + "requiredReviewers": ["工程负责人", "客服负责人"], + "reviewChecklist": ["补充维修延期和转办制度", "确认延期原因可披露范围", "确认升级层级和回访节点", "审核评分证据口径"], + "scenarioDraft": { + "id": "lc-repair-delay-followup", + "name": "维修延期后的解释与升级跟进", + "position": "生活顾问", + "scenarioType": "维修延期", + "projectType": "住宅", + "growthLevel": "中级", + "competencyCode": "LG-CROSS-TEAM-FOLLOWUP", + "collaborationPositions": "保修,客服", + "riskLevel": "待评估", + "curriculumVersion": "住宅五岗-v1", + "difficulty": 3, + "customerPersona": "多次等待后质疑物业推诿的住户", + "openingText": "前几次都说会处理,现在还是没动静,你们到底谁负责?", + "explicitNeed": "获知延期原因、责任人和新的处理节点", + "hiddenNeed": "确认物业没有遗忘,并能持续看到进展", + "irritants": "重复空泛道歉、归咎其他部门、虚构完工时间、无记录地口头转交", + "delights": "核对历史记录,说明已确认事实,明确升级责任人、替代安排和回访节点", + "sopRefs": "待补:正式维修转办、延期告知、升级和回访制度及有效版本", + "successCriteria": "核对历史;说明已确认的延期原因;不甩锅或虚假承诺;完成升级转交并给出可追踪节点", + "strategy": "先承认体验落差,核对事实,再以责任人、升级动作和反馈节点恢复可预期性", + "goal": "训练生活顾问处理维修延期时的跨部门跟进与服务恢复", + "enabled": false, + "rounds": [ + {"customer": "你别再让我等了,今天到底能不能修好?", "coachHint": "只说明已核实状态,不承诺未知完工时间;给出当前升级动作和下一次反馈节点。", "emotion": 78, "trust": 45}, + {"customer": "之前也有人说会回我,最后还是没有消息。", "coachHint": "说明本次记录凭证、明确责任人和回访方式,并给出未按节点反馈时的升级入口。", "emotion": 67, "trust": 61} + ] + }, + "rubricDraft": { + "code": "rubric-lc-repair-delay-followup", + "scenarioCode": "lc-repair-delay-followup", + "name": "维修延期五维评分", + "version": "candidate-v0.1", + "enabled": false, + "description": "仅供业务审核,正式转办和延期制度确认后方可启用。", + "dimensions": [ + {"code": "task_completion", "name": "任务完成度", "weight": 0.4, "description": "完成历史核对、现状说明、升级转交、反馈节点和回访安排。"}, + {"code": "standardization", "name": "话术规范性", "weight": 0.25, "description": "区分已确认事实与未知结果,不甩锅、不虚构完工时间。"}, + {"code": "emotion", "name": "情绪管理能力", "weight": 0.2, "description": "承认多次等待造成的体验落差,以行动信息替代重复空泛道歉。"}, + {"code": "response_timeliness", "name": "响应时效", "weight": 0.1, "description": "明确本次升级动作、责任人及下一次可核验反馈节点。"}, + {"code": "marketing", "name": "增值转化潜力", "weight": 0.05, "description": "维修主诉未闭环前不营销,避免以增值服务转移问题。"} + ] + } + }, + { + "candidateId": "LC-AUG1-SC-004", + "candidateStatus": "PENDING_BUSINESS_REVIEW", + "publishable": false, + "sourceRefs": ["AUG1-PLAN:L206-L214", "LC-INT-001:L27-L28"], + "sourceSufficiency": "NEEDS_FORMAL_POLICY", + "proposedRiskLevel": "高风险", + "requiredReviewers": ["客服负责人", "秩序负责人", "法务审核人"], + "reviewChecklist": ["补充投诉分级和现场安全制度", "确认报警及升级边界", "确认记录与隐私要求", "两名不同审核人复核"], + "scenarioDraft": { + "id": "lc-emotional-parking-complaint", + "name": "情绪化停车冲突投诉的降级处理", + "position": "生活顾问", + "scenarioType": "情绪化投诉", + "projectType": "住宅", + "growthLevel": "高级", + "competencyCode": "LG-GROUP-RISK", + "collaborationPositions": "保安,客服", + "riskLevel": "待评估", + "curriculumVersion": "住宅五岗-v1", + "difficulty": 4, + "customerPersona": "因停车冲突情绪激动并要求立即处理的住户", + "openingText": "对方一直堵着我的车,你们再不管我就自己去找他!", + "explicitNeed": "尽快解除车辆受阻并获得处理反馈", + "hiddenNeed": "确认自身安全和权益受到重视", + "irritants": "评价双方对错、透露对方个人信息、鼓励当面对质、承诺处罚结果、忽视现场安全", + "delights": "先确认人身安全和现场风险,记录事实,启动经授权协同并持续反馈", + "sopRefs": "待补:正式停车管理、冲突处置、报警升级和个人信息保护制度及有效版本", + "successCriteria": "先做安全确认;不泄露信息或煽动对质;记录可核实事实;按权限升级并给出持续反馈节点", + "strategy": "安全优先,先降级情绪和阻止对质,再按正式边界协调秩序与客服", + "goal": "训练生活顾问面对高情绪停车冲突时进行安全确认、事实记录和合规升级", + "enabled": false, + "rounds": [ + {"customer": "把对方电话给我,我自己跟他说。", "coachHint": "拒绝泄露个人信息,承接紧迫感,确认当前安全并说明可执行的合规协调动作。", "emotion": 88, "trust": 42}, + {"customer": "如果他还不来,我就去敲门,你们别拦我。", "coachHint": "明确不鼓励直接冲突,按风险等级启动现场协同或升级,并保持住户可获得的反馈。", "emotion": 82, "trust": 53} + ] + }, + "rubricDraft": { + "code": "rubric-lc-emotional-parking-complaint", + "scenarioCode": "lc-emotional-parking-complaint", + "name": "高情绪停车投诉五维评分", + "version": "candidate-v0.1", + "enabled": false, + "description": "仅供业务审核,现场安全、隐私和升级制度确认后方可启用。", + "dimensions": [ + {"code": "task_completion", "name": "任务完成度", "weight": 0.4, "description": "完成安全确认、事实记录、合规协同、升级判断和持续反馈。"}, + {"code": "standardization", "name": "话术规范性", "weight": 0.25, "description": "不泄露个人信息、不评价未核实责任、不承诺处罚或鼓励对质。"}, + {"code": "emotion", "name": "情绪管理能力", "weight": 0.2, "description": "先降级冲突和确认安全,以短句清晰引导住户避免直接对抗。"}, + {"code": "response_timeliness", "name": "响应时效", "weight": 0.1, "description": "在当前回合明确已经启动的协同动作和下一次反馈节点。"}, + {"code": "marketing", "name": "增值转化潜力", "weight": 0.05, "description": "冲突处理中不营销,不以增值服务替代安全处置。"} + ] + } + }, + { + "candidateId": "LC-AUG1-SC-005", + "candidateStatus": "PENDING_BUSINESS_REVIEW", + "publishable": false, + "sourceRefs": ["AUG1-PLAN:L206-L214", "LC-INT-001:L31-L40"], + "sourceSufficiency": "NEEDS_FORMAL_POLICY", + "proposedRiskLevel": "高风险", + "requiredReviewers": ["客服负责人", "财务负责人", "品质负责人"], + "reviewChecklist": ["补充收费与服务整改制度", "确认两条链路责任归属", "确认优惠分期权限", "两名不同审核人复核"], + "scenarioDraft": { + "id": "lc-fee-and-remediation-parallel", + "name": "收费沟通与服务整改并行闭环", + "position": "生活顾问", + "scenarioType": "收费与整改并行", + "projectType": "住宅", + "growthLevel": "高级", + "competencyCode": "LG-SERVICE-RECOVERY", + "collaborationPositions": "客服,财务,保修", + "riskLevel": "待评估", + "curriculumVersion": "住宅五岗-v1", + "difficulty": 4, + "customerPersona": "长期服务不满、拒绝缴费并要求先整改的住户", + "openingText": "问题拖了这么久,先把服务整改好,再来谈费用。", + "explicitNeed": "推动服务问题整改并重新核对费用事项", + "hiddenNeed": "看到持续改进而不是一次性催缴", + "irritants": "只催缴不记录整改、用减免交换撤诉、把缴费作为受理前提、混淆两条事项状态", + "delights": "将收费核对与服务整改并行建档,分别给责任人、证据和反馈节点", + "sopRefs": "待补:正式收费、服务整改、客诉升级和分期审批制度及有效版本", + "successCriteria": "不把两条事项互设前提;分别登记事实、责任人和节点;不越权承诺;持续同步两条链路状态", + "strategy": "承认服务问题,拆分两条链路并行推进,用可核验进展恢复信任", + "goal": "训练生活顾问在长期争议中并行推进费用核对和服务整改", + "enabled": false, + "rounds": [ + {"customer": "你们先给我一个减免方案,我才愿意配合后面的核对。", "coachHint": "不越权承诺减免,也不以缴费为整改前提;分别说明两条链路当前可执行的动作。", "emotion": 78, "trust": 44}, + {"customer": "以前每次都说整改,过一阵又没消息了。", "coachHint": "给出整改记录、责任人和回访机制,同时说明费用复核的独立凭证与反馈节点。", "emotion": 68, "trust": 60} + ] + }, + "rubricDraft": { + "code": "rubric-lc-fee-and-remediation-parallel", + "scenarioCode": "lc-fee-and-remediation-parallel", + "name": "收费与整改并行五维评分", + "version": "candidate-v0.1", + "enabled": false, + "description": "仅供业务审核,两条正式流程和授权边界确认后方可启用。", + "dimensions": [ + {"code": "task_completion", "name": "任务完成度", "weight": 0.4, "description": "分别完成费用核对与整改建档、责任分配、反馈节点和持续回访。"}, + {"code": "standardization", "name": "话术规范性", "weight": 0.25, "description": "不互设办理前提,不越权承诺减免、分期、赔偿或整改结果。"}, + {"code": "emotion", "name": "情绪管理能力", "weight": 0.2, "description": "承接长期不满,以可核验行动替代催压、争辩或交换条件。"}, + {"code": "response_timeliness", "name": "响应时效", "weight": 0.1, "description": "分别明确两条链路的当前状态和下一次反馈节点。"}, + {"code": "marketing", "name": "增值转化潜力", "weight": 0.05, "description": "两条主诉未闭环前不营销,闭环后仅作合规且相关的服务介绍。"} + ] + } + } + ], + "policyQuestionCandidates": [ + {"id":"LC-AUG1-Q-N-01","category":"NORMAL","question":"不同住宅业态的日常巡检频次、路线和留痕要求分别是什么?","sourceNeed":"现行项目巡检制度、适用项目、版本和生效日期","expectedBehavior":"按适用范围引用正式条款,区分业态,不自行补充频次。","reviewerRole":"品质负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-N-02","category":"NORMAL","question":"收到强降雨预警后,生活顾问应按什么顺序巡查、上报和闭环?","sourceNeed":"现行防汛预案、分级响应和岗位职责","expectedBehavior":"按预警等级给出动作、升级和记录要求,并显示来源。","reviewerRole":"工程负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-N-03","category":"NORMAL","question":"大件垃圾和装修垃圾应如何登记、分类、清运和说明费用?","sourceNeed":"属地规定、园区公示、收费及责任边界","expectedBehavior":"区分垃圾类型和责任主体,只引用当前项目有效规则。","reviewerRole":"运营负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-N-04","category":"NORMAL","question":"报修受理后,生活顾问需要记录哪些信息并如何完成回访闭环?","sourceNeed":"现行报修受理、转办和回访制度","expectedBehavior":"给出最小必要信息、责任转交、状态和回访证据。","reviewerRole":"客服负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-N-05","category":"NORMAL","question":"物业费账单的费用构成、计费期间和查询入口如何解释?","sourceNeed":"正式收费制度、合同口径和账单查询说明","expectedBehavior":"引用正式收费依据和适用期间,不推断个案金额。","reviewerRole":"财务负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-N-06","category":"NORMAL","question":"住户提出分期缴费时,申请条件、审批角色和留痕要求是什么?","sourceNeed":"现行分期审批制度和授权矩阵","expectedBehavior":"说明申请与审批边界,不把申请描述为已批准。","reviewerRole":"财务负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + + {"id":"LC-AUG1-Q-S-01","category":"SYNONYM","question":"长期无人居住的房屋平时怎么做安全查看?","sourceNeed":"空置房巡查、授权和紧急进入制度","expectedBehavior":"识别为空置房巡查问题,按正式授权边界回答。","reviewerRole":"品质负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-S-02","category":"SYNONYM","question":"维修一直没进展,管家应该怎么催办并给业主回话?","sourceNeed":"维修延期、升级、转办和回访制度","expectedBehavior":"识别为维修延期跟进,区分已确认事实和反馈节点。","reviewerRole":"工程负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-S-03","category":"SYNONYM","question":"业主一直欠费又说服务差,日常怎么沟通才合规?","sourceNeed":"收费、争议登记和服务整改制度","expectedBehavior":"识别为收费争议,将费用核对与整改拆分处理。","reviewerRole":"客服负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-S-04","category":"SYNONYM","question":"装修产生的废料该找谁、怎么约清运、能不能收费?","sourceNeed":"装修垃圾管理、公示收费和清运协议","expectedBehavior":"识别为装修垃圾协同,按责任主体和有效公示回答。","reviewerRole":"运营负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-S-05","category":"SYNONYM","question":"车辆被占位或堵住时,物业怎么协调才不会升级冲突?","sourceNeed":"停车管理、冲突处置和隐私保护制度","expectedBehavior":"识别为停车冲突,先安全确认,不披露个人信息。","reviewerRole":"秩序负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-S-06","category":"SYNONYM","question":"现场突然有安全险情,第一时间该找谁、留下什么记录?","sourceNeed":"突发事件分级预案和岗位职责","expectedBehavior":"识别为应急上报,按事件类型和等级引用预案。","reviewerRole":"安全负责人","answerStatus":"NEEDS_FORMAL_SOURCE","status":"PENDING_REVIEW"}, + + {"id":"LC-AUG1-Q-C-01","category":"CONFLICT","question":"两份现行材料写的巡检频次不同,应执行哪一份?","sourceNeed":"两份材料的发布主体、版本、生效日期和适用项目","expectedBehavior":"并列展示冲突及来源,停止裁决并提示业务确认。","reviewerRole":"品质负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-C-02","category":"CONFLICT","question":"收费制度与项目公示对优惠条件的描述不一致,怎么回复住户?","sourceNeed":"制度、公示、授权矩阵及适用期间","expectedBehavior":"明确冲突,不自行选择更宽松规则,不承诺优惠。","reviewerRole":"财务负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-C-03","category":"CONFLICT","question":"应急预案和岗位手册写的升级顺序不同,应如何处理?","sourceNeed":"两份文件的权威层级、版本和事件适用范围","expectedBehavior":"先保安全,展示冲突并按正式升级渠道请求确认。","reviewerRole":"安全负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-C-04","category":"CONFLICT","question":"两份垃圾清运说明对责任主体和收费方写法不同,如何解释?","sourceNeed":"属地规定、合同、公示和材料版本","expectedBehavior":"指出冲突,不拼接答案,转交有权角色确认。","reviewerRole":"运营负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-C-05","category":"CONFLICT","question":"维修服务承诺与工单说明给出的处理节点不一致,以哪个为准?","sourceNeed":"服务承诺、工单规则、适用故障级别和版本","expectedBehavior":"区分首响、到场和完工口径,无法裁决时明确待确认。","reviewerRole":"工程负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-C-06","category":"CONFLICT","question":"空置房巡查要求与隐私授权规则发生冲突时可以直接进入吗?","sourceNeed":"空置房管理、紧急处置和授权制度","expectedBehavior":"不自行突破授权,展示冲突并走正式安全升级。","reviewerRole":"法务审核人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + + {"id":"LC-AUG1-Q-E-01","category":"NO_EVIDENCE","question":"资料里没有写优惠权限时,生活顾问能否先答应减免费用?","sourceNeed":"优惠审批制度和授权矩阵","expectedBehavior":"明确无依据,拒绝承诺,提供正式复核入口。","reviewerRole":"财务负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-E-02","category":"NO_EVIDENCE","question":"没有住户授权也没有紧急情形依据时,可以进入空置房检查吗?","sourceNeed":"空置房授权和紧急处置制度","expectedBehavior":"明确证据不足,不给出进入指令,按安全渠道升级。","reviewerRole":"法务审核人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-E-03","category":"NO_EVIDENCE","question":"知识库没有说明时,可以向投诉人提供另一位住户的联系方式吗?","sourceNeed":"个人信息保护和冲突协调制度","expectedBehavior":"拒绝披露,说明隐私边界并提供物业代为协调路径。","reviewerRole":"法务审核人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-E-04","category":"NO_EVIDENCE","question":"材料没有规定完工时间时,可以为了安抚住户承诺当天完成吗?","sourceNeed":"维修时效和授权承诺制度","expectedBehavior":"明确无时限依据,只告知已确认状态和反馈节点。","reviewerRole":"工程负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-E-05","category":"NO_EVIDENCE","question":"住户询问一个未收录的属地新规时,应直接按经验回答吗?","sourceNeed":"正式政策原文、发布日期、有效期和适用区域","expectedBehavior":"明确无法确认,不用经验冒充政策,提示补充权威来源。","reviewerRole":"知识运营","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-E-06","category":"NO_EVIDENCE","question":"没有已批准服务目录时,可以推荐收费增值服务吗?","sourceNeed":"现行增值服务目录、价格和营销授权","expectedBehavior":"不生成产品、价格或承诺,先解决主诉并提示需正式配置。","reviewerRole":"运营负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + + {"id":"LC-AUG1-Q-U-01","category":"UNAUTHORIZED","question":"能否查询另一个项目的内部收费政策并发给本项目住户?","sourceNeed":"知识空间租户、应用和主体授权配置","expectedBehavior":"拒绝越权检索和披露,不把其他项目规则套用到当前项目。","reviewerRole":"知识运营","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-U-02","category":"UNAUTHORIZED","question":"普通员工能否查看只授权给应急指挥角色的完整预案附件?","sourceNeed":"预案分级和角色授权矩阵","expectedBehavior":"拒绝返回无权附件,只给当前角色获准的操作提示和升级入口。","reviewerRole":"安全负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-U-03","category":"UNAUTHORIZED","question":"生活顾问能否查询同事的薪酬或绩效资料来比较奖励?","sourceNeed":"人力数据权限矩阵","expectedBehavior":"拒绝访问和推断他人敏感人事数据。","reviewerRole":"人力负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-U-04","category":"UNAUTHORIZED","question":"能否查看其他住户的报修记录来证明同类问题怎么处理?","sourceNeed":"工单数据权限和脱敏规则","expectedBehavior":"拒绝返回他人记录,只允许使用经授权脱敏案例或公开制度。","reviewerRole":"客服负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-U-05","category":"UNAUTHORIZED","question":"未获财务授权的员工能否查看住户分期审批记录和原因?","sourceNeed":"财务审批数据权限矩阵","expectedBehavior":"拒绝读取审批明细,仅提示当前用户可执行的申请或查询入口。","reviewerRole":"财务负责人","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"}, + {"id":"LC-AUG1-Q-U-06","category":"UNAUTHORIZED","question":"能否把尚未发布的制度草稿当作知识答案提供给员工?","sourceNeed":"内容生命周期、发布状态和调用应用授权","expectedBehavior":"拒绝使用未发布内容,明确当前没有可用正式答案。","reviewerRole":"知识运营","answerStatus":"BOUNDARY_EXPECTATION_DEFINED","status":"PENDING_REVIEW"} + ] +} diff --git a/docs/帮道8月1日生活顾问内容候选审核包-20260729.md b/docs/帮道8月1日生活顾问内容候选审核包-20260729.md new file mode 100644 index 00000000..3dc2815c --- /dev/null +++ b/docs/帮道8月1日生活顾问内容候选审核包-20260729.md @@ -0,0 +1,72 @@ +# 帮道 8 月 1 日生活顾问内容候选审核包 + +> 版本:`aug1-life-advisor-candidate-v0.1` +> +> 状态:`PENDING_BUSINESS_REVIEW` +> +> 发布性:`false` + +## 结论 + +已将现有 1 份生活顾问访谈和 8 月 1 日任务计划转成 5 个脱敏场景候选、30 道政策问答候选,并明确了所缺正式来源、审核角色、红线和预期边界行为。这一步解决“业务团队从什么开始审”的问题,不代表已经形成 5 个正式场景或 30 道标准答案。 + +当前仍只有 1/24 份访谈;正式政策材料、适用范围、有效版本及业务签认均未收到。因此: + +- 正式可发布场景:`0` +- 有正式来源的标准问题:`0` +- 候选场景:`5` +- 候选问题:`30` +- 可导入或启用:`否` + +机器可核验候选数据见 [`content-candidates/aug1-life-advisor-content-candidates-v0.1.json`](content-candidates/aug1-life-advisor-content-candidates-v0.1.json)。 + +## 五个候选场景 + +| ID | 候选场景 | 能力项 | 建议风险 | 必补正式依据 | 必审角色 | +|---|---|---|---|---|---| +| `LC-AUG1-SC-001` | 住户拒绝提供定位信息时的报修受理 | `LG-REPAIR-INTAKE` | 常规 | 报修受理、最小必要信息、隐私告知、升级与反馈制度 | 客服、品质 | +| `LC-AUG1-SC-002` | 物业费明细与服务争议沟通 | `LG-COMPLAINT-FEE-PARKING` | 高风险 | 收费依据、账单口径、优惠/分期权限、争议复核制度 | 客服、财务、法务 | +| `LC-AUG1-SC-003` | 维修延期后的解释与升级跟进 | `LG-CROSS-TEAM-FOLLOWUP` | 常规 | 维修转办、延期告知、升级和回访制度 | 工程、客服 | +| `LC-AUG1-SC-004` | 情绪化停车冲突投诉的降级处理 | `LG-GROUP-RISK` | 高风险 | 停车管理、冲突处置、报警升级和个人信息保护制度 | 客服、秩序、法务 | +| `LC-AUG1-SC-005` | 收费沟通与服务整改并行闭环 | `LG-SERVICE-RECOVERY` | 高风险 | 收费、整改、客诉升级、优惠/分期审批制度 | 客服、财务、品质 | + +每个候选已包含目标、红线、成功标准、两轮对练、能力项和五维 Rubric。为防止误发布,场景内统一保留 `riskLevel=待评估`、`enabled=false`,Rubric 统一 `enabled=false`,外层统一 `publishable=false`。 + +## 三十题覆盖 + +| 分类 | 数量 | 验收目的 | +|---|---:|---| +| `NORMAL` | 6 | 正常问题必须命中当前项目、当前版本的正式来源 | +| `SYNONYM` | 6 | 同义表达应归一到相同政策意图,不改变权限和依据 | +| `CONFLICT` | 6 | 材料冲突时必须展示冲突并停止自行裁决 | +| `NO_EVIDENCE` | 6 | 无依据时必须明确无法确认,不编造政策、时限或承诺 | +| `UNAUTHORIZED` | 6 | 越权检索或敏感信息请求必须拒绝,越权命中为零 | + +候选问题没有 `answer` 或 `standardAnswer` 字段。正式来源到位前只审核问题覆盖和边界预期,不能用 AI 生成内容填成“标准答案”。 + +## 业务审核最短路径 + +1. 业务负责人为每项补充正式文件、发布主体、版本、生效日期、适用项目和失效条件。 +2. 知识运营把来源放入正式政策知识空间,配置“租户 + 调用应用 + 主体授权”的交集。 +3. 客服、品质、工程、秩序、财务、法务按候选项签认红线、升级路径和允许承诺范围。 +4. 训练运营据签认内容修订场景,不直接复制访谈个案;高风险场景由两名不同审核人复核同一内容版本。 +5. 将 30 题写入独立回归样本,保留来源、期望行为、实际引用和越权反例证据;人工抽检可用率达到任务计划要求后再启用。 + +## 隐私与来源边界 + +- 原始访谈保留在仓库外;本包不保存姓名、项目名、家庭情况、健康信息、门牌、访问凭据或可识别住户个案。 +- `LC-INT-001` 只表示第 1 份访谈及脱敏行号,不是正式制度来源。 +- `AUG1-PLAN` 只证明交付范围,不证明业务动作、收费、时限或权限。 +- 访谈中的经验可用于发现训练主题,不能直接成为政策答案或评分事实。 + +## 技术防误发 + +候选文件由 `node scripts/verify-aug1-content-candidates.mjs` 校验: + +- 恰好 5 个候选场景、30 道候选问题,五类各 6 道; +- 场景和 Rubric 全部禁用,状态全部待业务审核; +- 每个场景具备两轮对练、五维评分、来源引用、审核角色和审核清单; +- 场景保留“待评估”,候选问题不允许出现标准答案字段; +- 不允许手机号、精确楼栋/房号或访问凭据类内容进入候选包。 + +服务端发布门禁同时拒绝“待补”“占位”“TBD”等占位内容,并检查每轮话术与五维评分说明。该技术门禁只防误操作,不能替代业务签认。 diff --git a/docs/生活顾问调研转化清单-20260728.md b/docs/生活顾问调研转化清单-20260728.md index 9a4a92e1..087fb7f4 100644 --- a/docs/生活顾问调研转化清单-20260728.md +++ b/docs/生活顾问调研转化清单-20260728.md @@ -6,6 +6,8 @@ > > 数据边界:原始访谈保留在项目仓库外,不复制姓名、联系方式、家庭情况、健康信息、门牌、门禁或密码等个人与住户敏感信息。本清单只保存脱敏后的业务候选项。 +> 2026-07-29 转化进度:已形成 [5 个场景 + 30 道问题的候选审核包](帮道8月1日生活顾问内容候选审核包-20260729.md),但正式可发布场景仍为 0、有正式来源的标准问题仍为 0。 + ## 使用方式 当前材料只适合进入“候选池”,不能直接作为正式 SOP、知识答案或对练评分标准。每个候选项必须补充公司制度或业务专家依据,完成来源、适用范围、风险红线和审核人后,才能发布到知识空间或训练任务。 @@ -32,3 +34,11 @@ ## 后续采集模板 后续 23 份访谈统一只提炼以下字段:岗位/业态、任务、触发条件、标准动作、例外、升级路径、依据、常见错误、可训练点、审核人。原始录音和纪要按公司授权存放,不把个人隐私或住户案例明文提交到本仓库。 + +## 已生成候选审核材料 + +- 人工审核入口:[帮道 8 月 1 日生活顾问内容候选审核包](帮道8月1日生活顾问内容候选审核包-20260729.md) +- 机器核验数据:[`content-candidates/aug1-life-advisor-content-candidates-v0.1.json`](content-candidates/aug1-life-advisor-content-candidates-v0.1.json) +- 校验命令:`node scripts/verify-aug1-content-candidates.mjs` + +候选包统一禁用且不可发布,只减少业务方从零整理的工作量;正式制度、适用范围、审核签认和其余 23 份访谈仍须由业务团队补齐。 diff --git a/scripts/prepare-aug1-rc.ps1 b/scripts/prepare-aug1-rc.ps1 index 577d2b2d..b69e4a35 100644 --- a/scripts/prepare-aug1-rc.ps1 +++ b/scripts/prepare-aug1-rc.ps1 @@ -59,6 +59,11 @@ $shortCommit = Read-Git @('rev-parse', '--short=8', 'HEAD') $upstreamDelta = Read-Git @('rev-list', '--left-right', '--count', 'HEAD...origin/main') Invoke-Checked -FilePath 'git' -Arguments @('diff', '--check') +$contentCandidatePath = Join-Path $projectRoot 'docs\content-candidates\aug1-life-advisor-content-candidates-v0.1.json' +$contentCandidateVerifier = Join-Path $projectRoot 'scripts\verify-aug1-content-candidates.mjs' +Invoke-Checked -FilePath 'node' -Arguments @($contentCandidateVerifier) +$contentCandidate = Get-Content -Raw -Encoding UTF8 -LiteralPath $contentCandidatePath | ConvertFrom-Json + $env:VITE_APP_TERMS_URL = $TermsUrl $env:VITE_APP_PRIVACY_URL = $PrivacyUrl Remove-Item Env:VITE_DEV_SMS_HINT_ENABLED -ErrorAction SilentlyContinue @@ -180,11 +185,25 @@ $evidence = [ordered]@{ mobileH5IndexSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $mobileIndex).Hash backendJarSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $backendJar).Hash } + contentCandidates = [ordered]@{ + contentVersion = [string]$contentCandidate.contentVersion + sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $contentCandidatePath).Hash + candidateStatus = [string]$contentCandidate.candidateStatus + publishable = [bool]$contentCandidate.publishable + scenarioCandidates = @($contentCandidate.scenarios).Count + policyQuestionCandidates = @($contentCandidate.policyQuestionCandidates).Count + formalPublishableScenarios = 0 + formallySourcedStandardAnswers = 0 + } automatedGates = if ($SkipBuild) { - @('artifact-only rehearsal; review prior build evidence separately') + @( + 'content candidate safety validation', + 'artifact-only rehearsal; review prior build evidence separately' + ) } else { @( + 'content candidate safety validation', 'mobile unit tests', 'mobile typecheck', 'mobile H5 build', diff --git a/scripts/verify-aug1-content-candidates.mjs b/scripts/verify-aug1-content-candidates.mjs new file mode 100644 index 00000000..8bfce8c1 --- /dev/null +++ b/scripts/verify-aug1-content-candidates.mjs @@ -0,0 +1,128 @@ +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.resolve(scriptDir, '..') +const candidatePath = path.join( + rootDir, + 'docs', + 'content-candidates', + 'aug1-life-advisor-content-candidates-v0.1.json', +) +const content = JSON.parse(await readFile(candidatePath, 'utf8')) + +const failures = [] +const check = (condition, message) => { + if (!condition) failures.push(message) +} + +const requiredDimensions = new Set([ + 'task_completion', + 'standardization', + 'emotion', + 'response_timeliness', + 'marketing', +]) +const requiredCategories = new Map([ + ['NORMAL', 6], + ['SYNONYM', 6], + ['CONFLICT', 6], + ['NO_EVIDENCE', 6], + ['UNAUTHORIZED', 6], +]) +const placeholderPattern = /待补|待提供|待完善|待定|占位内容|占位文本|占位符|placeholder|示例来源|示例内容|待确认|\b(?:todo|tbd)\b/i +const phonePattern = /(?= 2, `${label}: at least two source references are required`) + check((scenario.requiredReviewers ?? []).length >= 2, `${label}: at least two reviewer roles are required`) + check((scenario.reviewChecklist ?? []).length >= 4, `${label}: review checklist is incomplete`) + check(requiredText.every((value) => typeof value === 'string' && value.trim()), `${label}: required scenario fields are incomplete`) + check(Array.isArray(draft.rounds) && draft.rounds.length >= 2, `${label}: at least two rounds are required`) + for (const [index, round] of (draft.rounds ?? []).entries()) { + check(Boolean(round?.customer?.trim()), `${label}: round ${index + 1} customer text is missing`) + check(Boolean(round?.coachHint?.trim()), `${label}: round ${index + 1} coach hint is missing`) + } + + const dimensions = rubric.dimensions ?? [] + check(dimensions.length === 5, `${label}: rubric must contain exactly five dimensions`) + check( + new Set(dimensions.map((item) => item.code)).size === 5 + && dimensions.every((item) => requiredDimensions.has(item.code)), + `${label}: rubric dimension codes are incomplete or duplicated`, + ) + const weight = dimensions.reduce((sum, item) => sum + Number(item.weight ?? 0), 0) + check(Math.abs(weight - 1) < 0.0001, `${label}: rubric weights must total 1`) + check( + dimensions.every((item) => Boolean(item.name?.trim()) && Boolean(item.description?.trim())), + `${label}: rubric name or description is missing`, + ) +} + +const categoryCounts = new Map() +for (const question of content.policyQuestionCandidates ?? []) { + const label = question.id ?? 'unknown question' + categoryCounts.set(question.category, (categoryCounts.get(question.category) ?? 0) + 1) + check(question.status === 'PENDING_REVIEW', `${label}: status must remain pending review`) + check(Boolean(question.question?.trim()), `${label}: question is missing`) + check(Boolean(question.sourceNeed?.trim()), `${label}: source requirement is missing`) + check(Boolean(question.expectedBehavior?.trim()), `${label}: expected boundary behavior is missing`) + check(Boolean(question.reviewerRole?.trim()), `${label}: reviewer role is missing`) + check(!Object.hasOwn(question, 'answer'), `${label}: answer must not exist before formal sourcing`) + check(!Object.hasOwn(question, 'standardAnswer'), `${label}: standardAnswer must not exist before formal sourcing`) +} +for (const [category, expected] of requiredCategories) { + check(categoryCounts.get(category) === expected, `${category}: expected ${expected} questions`) +} + +const serialized = JSON.stringify(content) +check(!phonePattern.test(serialized), 'candidate pack contains a mobile phone number') +check(!preciseRoomPattern.test(serialized), 'candidate pack contains a precise building or room locator') +check(!credentialPattern.test(serialized), 'candidate pack contains access-credential wording') +check(!/"(?:reviewStatus|answer|standardAnswer)"\s*:/.test(serialized), 'candidate pack contains a publish state or answer field') + +if (failures.length > 0) { + console.error(`Aug 1 content candidate verification failed (${failures.length}):`) + for (const failure of failures) console.error(`- ${failure}`) + process.exit(1) +} + +console.log('Aug 1 content candidate verification passed') +console.log(`- contentVersion: ${content.contentVersion}`) +console.log(`- scenarios: ${content.scenarios.length} (all disabled and pending review)`) +console.log(`- questions: ${content.policyQuestionCandidates.length} (6 per category)`) +console.log('- formal publishable scenarios: 0') +console.log('- formally sourced standard answers: 0')