1. 24 Jul, 2026 4 commits
    • fix(recall): 回访摄入去掉 EMR 诊所白名单 + 历史联系摘要不再把未来排程当漏做 · 33dcecb6
      生产排查(张学军 BJ0D036664 等)带出的两个独立问题。
      
      ## 1. 摄入:删除 fact_returnvisit_out 的 org 白名单
      
      原条件 `organization_id IN (SELECT ... FROM fact_emr_treatment_out)` 本意"与核心
      数据同范围",但那张 EMR 表 **瑞尔 63 家 / 瑞泰仅 2 家**,而回访表瑞泰有 98 家:
      
        瑞尔  保留 3,238,588 / 丢 7,212        → 丢 0.2%
        瑞泰  保留   119,884 / 丢 10,039,288   → 丢 98.8%
      
      后果:PAC 的 6.3 万瑞泰患者只有 5.4% 有回访记录(瑞尔 78.9%),历史联系整体空白。
      回访是纯展示数据、不进召回信号,没理由绑定 EMR 诊所集合 → 删掉,范围交给 cohort。
      
      ️ 排查中的一个反转,记在 manifest 注释里防后人再踩:DW 的 patient_id **跨品牌复用**
      (374 万 id 中 196 万重号,如 708971 在瑞尔=张学军、瑞泰=余盼盼)。所以 cohort 必须用
      `(patient_id, brand)` 复合键;摄入侧 sourceUnit 取自本行 brand、按 `brand#patient_id`
      索引患者,跨品牌行天然各归各家,不会串号。按品牌正确切分后,瑞尔患者其实一条没漏。
      
      ## 2. 摘要:未来排程被写成"未执行,需优先补做"
      
      patient_return_visits.task_date **含未来排程**,而 prompt 既无今日锚点、又按 taskDate
      desc 排序 → 未到期的排程恒排第一被当成"最新一次",status=未回访 就被判成漏做。
      生产实测:934 条已生成摘要中 196 条最新回访是未来排程,其中 158 条(81%)带
      "未执行/补做/漏/尚未"措辞;对照组(最新在过去)仅 38%。
      
      修复按"能程序算的事实全算好、LLM 只润色"这条既有纪律:
        · orchestrator 算 today 与每条的 isFuture(确定性比较,不交给模型)
        · prompt 渲染成【未来排程·尚未到期】显式标记 + 条数警告 + 硬约束文案
        · 补 taskStatus(「已预约」≠「没做」,原先根本没喂给模型)
        · promptVersion → draft_recall_summary@2026-07-24-e
      
      ## 本地验证
      - 325 单测通过(新增 11 例锁住"未来排程不得说成漏做" + promptVersion 必须 bump)
      - 摄入:导入瑞泰患者 殷海霖(2332662,12 条回访旧过滤下一条留不住)→ 12 条全部落库
      - 摘要:吕学文(未来排程 2026-10-09 + 真·过期未执行 2026-03-28)连跑 3 次稳定输出
        「3月28日种植咨询回访未执行…需优先跟进;…10月9日已排复查洁牙邀约」
        —— 未来的说"已排"、真漏做的仍报出,两个方向都对
      
      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
      luoqi committed
    • feat(recall): 识别不准确也改永久 + 认领入口收口到列表页 + 潜在/预约过闸 · 25fc944e
      三处业务反馈跟进。
      
      ## 1. 「机会识别不准确」14d → 永久
      
      原 14d 的理由是"算法修好后本就该重新召回",但那是**我们**的排期,不该由
      患者兜着 —— 两周后同一条不准的召回原样弹回来,客服只会再关一次再骂一次,
      而算法多半还没改。真要在修复后放回来,应由「改完规则 → 定向重算/解除抑制」
      这条显式动作驱动,而不是赌"两周内一定修好"的定时器。
      
      至此 WEAK_SIGNAL_SUPPRESS_DAYS(14d)的两个使用者(treated / inaccurate)
      全部转永久 → **删除该常量**,免得后来者把新原因挂到一个已被推翻的假设上。
      
      ## 2. 认领入口收口到列表页
      
      详情页顶栏的「认领」按钮去掉,改为**只读**徽标(未认领 · 仅查看 / 仅查看)。
      同一动作两个入口会让状态同步和归属口径都变复杂;列表页本就有行内认领 +
      批量认领,那里才是自然入口。拦截文案同步改为指回列表。
      
      ️ 随之出现的缺口已一并补上:列表认领只 patchItem 本地行,详情页不会知道
         → 刚认领完回详情点操作仍被拦。为此给 plan-sync-store 加**反向通道**
         notifyClaim(列表 → 详情),认领/返池后详情页的闸即时解锁/上锁。
         assigneeUserId 用 undefined 表示"本次事件与认领无关"(不能用 null,
         那会被当成已返池而误上锁)。
      
      ## 3. 「潜在」「预约」也过认领闸
      
      顶栏跳宿主的三个动作(潜在/预约/回访)现在口径统一:全部过闸。
      「潜在」虽是查看,但看完顺手就在宿主侧动手了,而 PAC 这边没认领 = 没归属人,
      回头对不上账。统一口径比"哪个算查看"的细分更好维护。
      
      ## 验证
      - 314 单测通过(抑制期用例改用 unreachable/wrong_number 的 30d 档
        继续锁住"短档不被 outcome 60d 盖掉"这条红线)
      - 本地真实患者(吕学文 90 / 李石明 78,已从本地 CH 摄入)实测:
        未认领 → 顶栏「未认领 · 仅查看」徽标、点「潜在」被拦并提示去列表认领;
        列表点认领 → 详情页徽标即时消失、「关闭机会」弹窗正常打开(未重拉页面)
      
      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
      luoqi committed
    • feat(recall): 已完成治疗改永久抑制 + 未认领只能看不能操作 · 435cd818
      两项业务反馈驱动的改动。
      
      ## 1. 「已完成治疗」抑制期 14d → 永久
      
      原 14d(WEAK_SIGNAL_SUPPRESS_DAYS)押的注是「等一个 DW 同步周期 + 排除闸
      接手」,生产实测证伪:缺牙召回两例(吕学文 DW 无义齿完成记录、李石明义齿
      在上颌而召回在下颌)排除闸都接不住,到期只会原样弹回来再挨一次投诉。
      档位本身也不自洽 —— 「已在外院治疗」早就是永久,在别家做完永久闭嘴、
      在自己家做完两周后再问,说不通。
      
      误判不会把患者封死:抑制是**信号级**的(key=scenario|subKey,subKey 含
      牙位)且带时间锚,结案后新发的同类信号照常放行。
      WEAK_SIGNAL_SUPPRESS_DAYS 现仅剩「机会识别不准确」使用(算法问题,修好
      规则后本就该重新召回)。
      
      ## 2. 认领闸 —— 未认领只能看,不能操作
      
      submitExecution 原先只校验 status、**完全没看 assignee**,生产上因此
      出现两条 plan 在 assignee IS NULL 时被直接关闭。危害不止撞单:
      execution.service 特意保留 assigneeUserId 作业绩归属(清了会让
      「我的已完成」/ 转化率永远为 0),未认领就提交 = 这条执行没有归属人,
      统计里直接蒸发 —— 前端提示替代不了服务端约束。
      
      新增 plan/claim-guard.ts,两种拒绝态分码(20504 未认领 / 20505 他人认领,
      文案必须分开,否则组员以为自己没点对)。
      
      过闸:提交通话结果 / 关闭机会、重生成话术(含 SSE)、话术反馈、召回反馈
      不过闸:
        · assign(认领本身是入口,过闸会死锁)
        · recycle(PLAN_RECYCLE 是 leader 独占「回收 staff 已认领的单」,
          按"必须自己认领"拦会把组长回收功能整个废掉)
        · recompute(ops 工具)
        · 只读端点 + 「潜在」(查看类)+ 刷新(拉数据,不改业务态)
      
      前端同步:详情页按钮**不禁用**,点了才讲原因(禁用会让客服不知道为什么
      点不动);顶栏未认领显「认领」按钮(闸的唯一出口)、他人认领显「仅查看」。
      
      ## 验证
      - 314 个单测通过(新增 claim-guard 5 例 + treated 永久 2 例)
      - 本地端到端:未认领 3 个入口全返 20504;他人认领返 20505 带占用人;
        自己认领后放行;treated 关闭落库 snoozed_until = 36500 天(99.9 年)
      
      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
      luoqi committed
    • fix(recall): 缺牙建议治疗按年龄排序 —— >70 岁活动义齿在前、种植并行 · 3ff9c827
      现象(业务反馈):90 岁(吕学文)、78 岁(李石明)的缺牙召回,界面一律显示
      「目标 · 种植」,话术也推种植。业务反弹"为什么给 90 岁老人推种植"。
      
      根因:UI 取 signals.expectedCategories[0],而 K08 的 categories 首项恒为 implant。
           整条链路(目标标签 / 理由文案 / goal / 话术)都没有年龄维度 ——
           召回场景 SQL 与 priority-scorer 里一个年龄条件都没有。
      
      ## 修法:改在诊断→治疗的源头,不在流程末端打特例
      
      canonical-codes.ts 新增 recommendedCategoriesForAge(categories, age) 作建议侧单一源:
      候选同时含 implant + prosthodontic 时,age > DENTURE_FIRST_AGE(70)→ prosthodontic 提前。
      scenario / 前端 reason-line / 话术 prompt 全部调这一个 resolver,不各写年龄分支。
      
      ## 业务方口径(对齐后修正)
      
      初版做成了"高龄禁推种植",过重。业务反馈:**70-80 不是种植禁忌**,能否种植取决于
      骨量骨质 / 全身状况 / 患者意愿,由医生评估。故改为:
        - 建议**只调顺序**:活动义齿在前,**种植并行保留**(不删)
        - 话术从"严禁推荐种植"改为"先讲活动义齿,种植可并行提及;不主推手术、
          不承诺能不能种 —— 落到来院让医生按身体条件评估"
        - PAC 不替医生下禁忌结论
      
      ## 三条设计红线(各有用例锁住)
      
      1. 只排序不增删 —— 任何年龄下返回集合恒等于入参(种植不会被抹掉)
      2. 不碰排除闸 —— DiagnosisTreatmentMap[].categories 仍是完整候选集
         (否则高龄患者做过种植反而排除不掉 → 重复召回)
      3. 不碰"要不要召回" —— 缺牙影响咀嚼营养,高龄同样该管
      
      ## 改动
      
      - canonical-codes:DENTURE_FIRST_AGE=70 + recommendedCategoriesForAge + treatmentCategoryNameZhFor
      - reason-signals:signals 加 focusCategory / patientAge(均 optional,旧 plan 回落原行为)
      - scenario:SQL 取年龄 → 走 resolver;高龄 goal 文案(义齿为主、种植并行)
      - 前端:目标标签用 focusCategory;理由行调同一 resolver 显示「活动义齿 / 种植」
      - 话术:fact-block + stable prompt 加高龄沟通约束;三档 promptVersion bump
      
      实测(本地 75 岁真实患者重算):focusCategory=prosthodontic · patientAge=75 ·
      理由「…未启动活动义齿 / 种植」。308 tests passed,service + web typecheck 干净。
      
      ️ 存量:plan 变化判定只比 (scenario, subKey),signals 变化不升版本 →
         已有 plan 不会自动拿到新字段(前端回落原行为)。生产 775 个 80+ 缺牙患者
         需回填或强制重算才生效,另行处理。
      
      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
      luoqi committed
  2. 23 Jul, 2026 1 commit
    • fix(sync): 启动时回收僵尸同步锁,根治部署重启后 DW 滞后告警 · a91f8a37
      现象:测试服 2026-07-23 8:00 报「DW 数据滞后」,增量游标 33.7h 未推进。
      
      根因:sync_logs 用 partial UNIQUE(host_id) WHERE status='running' 作并发锁。
           部署 force-recreate 重启 service 容器时,正在跑的同步进程被杀,但那行
           running sync_log 来不及标终态 → 永久占住锁 → 之后每次 cron 增量都撞锁被
           skip → 游标不动。代码注释早写了"进程崩需依赖 stale 清理",但该清理从未实现。
      
      修复:SyncIncrementalSchedulerService.onModuleInit 注册 cron 前,先把 startedAt
           早于本进程启动时刻(PROCESS_STARTED_AT)的 running 行标 failed。
      
      判据安全性:sync 只在本 service 进程的 cron 回调、或一次性 CLI(跑完即退)里跑,
           两者都不可能比本进程启动得更早还活着。反过来,本进程启动后新建的 running
           (可能是并存的手动 CLI 真锁)用 lt 严格排除,绝不误清。
           幂等:标 failed 只释放锁,不动数据 —— 游标没推进,下次增量靠 48h 回看窗补齐。
      
      数据零丢失:回看窗默认 48h > 本次空档 33.7h,靠 source_event_id 幂等去重完整补回。
      
      测试:sync-lock-reap.spec 覆盖 回收/lt判据锁/空启动/失败不抛;295 全绿。
      
      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
      luoqi committed
  3. 22 Jul, 2026 3 commits
    • feat(recall): 话术自报家门去人名 + 关闭机会闭环 + 执行表字段清理 · 1f20bae4
      三件事,因为改动在 plan.service / execution.service / types 里交织,合成一次提交。
      
      ## 1. fix:话术自报家门串名(线上 bug)
      
      现象:登录人是李倩,话术却写"我是本诊所的客服主管晋芳辰"。
      根因:登录客服姓名被烤进 LLM 生成的正文,而 plan_scripts 的 UNIQUE 是 plan_id
           (一个 plan 一份话术、不分人),召回池又是共享的 —— 谁先打开触发生成,
           姓名就固化给后面所有人看。线上 44 条话术 100% 中招,是必然不是偶发。
      
      解法:姓名不进 LLM,改「生成期占位 → 渲染期回填」
        - 新增 shared/agent-identity.ts 作单一源(占位符 + resolveScriptAgent + renderAgentIdentity)
        - 稳健/标准/深度三档 prompt 与 format.md 统一输出 【回访客服】(沿用 v8 立的
          `{}`=填空 / `【】`=原样保留 的约定,归第二类)
        - 四个出口渲染回填:详情聚合、strict Zod 详情、同步重生成、SSE 流式
        - DraftPlanScriptInput 去掉 agent 字段
      
      副作用(正向):input_hash 不再含人名 → 同一 plan 不同客服共享 AI 缓存;
      工单转派后名字自动跟着换,不用重生成。
      
      三档实跑验证模型都保留了占位符;浏览器验证同一份缓存对两个登录人显示各自姓名。
      
      ️ 存量 44 条仍是旧正文,需部署后逐条重生成(dist/cli/ai-gen-script.cli.js)。
      
      ## 2. refactor:删 plan_executions 两个死字段
      
        - invalid_reason  从没被写过(pac-web 零引用),读回路径也没带它
        - abandon_other   写得进没人读,且信息恒冗余 —— 放弃原因是固定复选框无自由文本,
                          该列只可能取到字面量"其他",而 abandon_reasons 已含 'other'
      
      生产 0 行受影响(执行总数 2,两列全空)。
      
      ## 3. feat:宿主回访模式的「关闭机会」闭环
      
      瑞尔侧回访在宿主系统做,PAC 只提供关闭入口做闭环。**刻意不引入新概念**:
      不加 closed outcome、不加 close_reasons 列、不加 plan 状态 —— 关闭就是
      outcome='abandoned' + abandon_reasons,跟 PAC 自带表单同一条路、同一列。
      分叉只在展示层(ABANDON_REASON_META.shownIn,两个面板各渲染各的子集)。
      
        - AbandonReason 扩成两种模式的并集;近义项不合并(两模式按 host 配置二选一、
          运行时不共存,同一 host 只出现自己那套 key,统计不会碎)
        - 关闭弹窗改多选无上限;「其他」说明写 notes,不另立列
        - 原因级抑制窗:各原因 suppressDays 取 max(每个原因独立成立,取最严的);
          未配的沿用 outcome 默认 60d,行为跟改造前一致
        - 「识别不准确」不再双写 recall_feedback(该表留给拇指控件那条路)
        - outcome-form 的中文硬编码数组 + 中译映射表删除,key/label 改为同源
          (那张表里「已转介他人→treated_elsewhere」是语义错位)
      
      ## 文档
      
      新增「取数说明」(pac-docs)—— 给 DW 团队自助查生产 PG:召回池筛选条件、
      关闭操作落表、数据范围过滤(品牌/诊所/时间)、字段字典、常用 SQL。
      
      291 tests passed;service + web 双端 typecheck 干净。
      
      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
      luoqi committed
    • docs(ops): 重算对照表去掉「清简报缓存」列 — 它不是重算,降级为表下注记 · 2e31eeb7
      清缓存与 reparse/persona/plan 不是一类动作(不重算、秒级、按需重生成),
      放进同一张表会误导"改 prompt 也要跑重算"。改为表下引用块说明。
      
      顺带修补编辑残留:表格首行多一列、③ 代码块缺收尾 ```。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • docs(ops): 部署速查手册 — 本地/测试/生产三环境操作指南 · 60b0b705
      面向"自己动手部署"的 runbook,不重复 deployment.mdx 的架构介绍,直接给命令:
      - 三环境速查表(位置/目录/分支/DB/部署方式)+ 分支规约
      - 本地开发(docker 基础设施 + 原生跑应用)+ 4 个常见问题
      - 测试/生产部署命令(生产强调 COMPOSE_MANAGED=1 与 source .env)
      - 部署后要不要重算的对照表 + 固定顺序(reparse→persona→plan→清缓存)+ 耗时参考
      - 定向补数据(PAC_COHORT_ONLY_PATIENT @file)参数与分批阈值
      - 失败处理/回滚、7 个踩过的坑(含 pkill 自杀、ClickHouse 256KB 上限)、日常巡检
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
  4. 21 Jul, 2026 11 commits
    • merge: feat/return-visit-entry → main(宿主回访模式 + 关闭机会闭环) · 9d063987
      # Conflicts:
      #	apps/pac-service/tests/recall-suppression.spec.ts
      luoqi committed
    • fix(sync): 定向重摄不推进增量游标 + 大名单走文件(污染修复前置) · 736cc610
      背景:DW 病历表 patient_id 错位污染(高逸铭案例),需按名单重摄 3 万患者。
      沿用现有 PAC_COHORT_ONLY_PATIENT(cohort 收窄)时发现两个阻塞问题:
      
      1)  静默丢数据:只要 manifest 配了 incremental.per_query(jvs-dw 配了),
         cold-import 任何模式都写 cursor_after=run_start。定向轮只覆盖名单内患者,
         却推进全局水位 → 名单外患者在本轮期间的 DW 新写入被永久埋在游标下方。
         (与本次排查发现的"12,595 个患者符合 cohort 却从未摄入"是同类机制)
         修:定向模式下 cursor 保持上次水位不推进,daily 增量照常从原点接力。
      
      2) 大名单 E2BIG:3 万个 id 的逗号串约 275KB > 环境变量单值 128KB(MAX_ARG_STRLEN)。
         修:支持 PAC_COHORT_ONLY_PATIENT=@/path/to/file(每行一个 id);逗号列表照旧兼容。
      
      顺带:定向生效时打印命中患者数,便于核对名单规模。
      测试 7 例:空/空白不误判定向、逗号列表、@file(含 3 万行)、文件缺失须抛错不静默退化。
      
      生产验证(高逸铭组 7 人小批):删除→重摄后
        高逸铭 62 事实/6 诊所 → 21/1(上海时代),别人的数据清光
        刘晓旭 13 → 25、徐嘉悦 126 → 130,被吞的数据补回,诊所归属全部正确
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • fix(plan): 归属漂移的已认领单顺带返池 — 防诊所硬隔离下的孤儿单 · 1242f896
      跟进诊所重归属(7b4f0d93)的补丁:诊所隔离是硬边界(list/detail 都强制
      targetClinicId ∈ scope.clinicIds),已认领单漂出原认领人 scope 后会变成
      谁都看不见的孤儿单(assigned 不进池、新店客服非 assignee、原客服出 scope)。
      
      - unchanged 就地改归属:若 status=assigned 且诊所变了 → 返池(assignee/assignedAt/recycleAt 清空)
      - 升版本路径:clinicMoved 时 assigned 不继承,新版本直接回池(与上同规则)
      - 两路径都记 log(哪单从哪店改到哪店),上线后可巡检
      - 测试 +3:两路径返池 + 归属未变时继承不误伤(12/12);顺带修 mock seed 缺 targetClinicId
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • fix(plan): 跟进诊所归属改为患者最后到诊诊所(与诊断诊所解耦) · 7b4f0d93
      业务口径(2026-07):运营按客户最后一次到诊的诊所做跟进分类,谁家最后来过就归谁家客服
      ——原口径 target_clinic_id = 触发诊断 fact 所在诊所,会把主诊所患者顺路在别处拍片/被诊断
      的人错分到那家只去过一次的诊所(反馈案例 BJ0D046171:150+次天使大厦、1次印象城影像AI诊断
      → 却进了印象城列表)。适用全部召回场景,不止影像 AI。
      
      - target_clinic_id = 患者最近一条 encounter/emr(到诊)所在诊所;无到诊记录兜底回退诊断诊所(不置空)
      - unchanged 分支(reason 未变)也就地改归属,不升版本 → 普通重算即可修复存量错分
      - 批量 prefetch 修 uuid bug:patient_id IN (text 参数) 对 uuid 列在 PG 报类型错,
        批量整体静默退回诊断诊所(修不动);改 = ANY(::uuid[])。本地 2470 患者全量重算后残留错分 0
      - 单刷路径 resolveLastVisitClinic 直查
      
      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
      luoqi committed
    • feat(sync): 增量摄入提频至每 2 小时 — cron 0,8-22/2 + 空轮短路 · 07bc2845
      方案 A 落地(jvs-dw):
      - incremental_cron: '15 0,8-22/2 * * *'(08:15~22:15 每 2h + 过天 00:15;
        cron 小时域无 24,'0' 即用户说的"24点"次日凌晨轮)
      - 空轮短路:transactionsWritten=0 且 patientsUpserted=0 → 跳过 persona/org-tree/plan,
        空轮成本收敛到每表一条 cursor 探查(秒级)。条件保守:患者主档变化不产事务但进画像,不跳。
      
      背景:DW 当前每日 08:00 一批(实测 13:56 时三表 updated_date 仍停昨日 23 点),
      日内轮询换来的是 ① DW 迟到/失败当天自愈(原等 24h)② 手动补摄自动收口
      ③ 瑞尔 DW 计划提频 2h,PAC 先就绪。防重前提已具备:并发锁 + cursor 幂等 + 同行去重。
      计划重算链路不动(空轮短路后不会被空跑)。
      
      验证:cron 库实测 next 14 次触发序列正确;manifest 解析 auto_sync/cron 正常;tsc 干净。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • feat(sync/payment): 消费口径切实付 — amount=实付,应收降级 receivableAmount · 6dd7aefa
      业务定调(张雪反馈):一线关注真金白银,应收对免费套餐/团购客严重虚高
      (胡新丽:应收¥1,270 实付¥0;张弦:应收¥77,795 实付¥57,887)。
      
      - jvs-dw payment.yaml:amount settlement_money(实付);friday:net_receipts_this
      - 新增 receivableAmount(应收)辅助字段:canonical schema 显式声明 + moneyFields
        登记同口径归一 + payment_record.content.receivable_cents
      - discount-anchor 折扣率分母换 receivable_cents(旧事实 fallback amount_cents)
      - 顺带修掉两个隐藏口径 bug:
        ① refund 一直是实收冲减 → 原 LTV 实为"应收收入−实收退款"混口径,现自洽
        ② 储值客户双计:充值(实付)计一次 + 扣卡消费应收又计一次,现扣卡单实付0不再重复
      
      下游自动切换:profile.ltv(UI 累计消费)/ rfm.M 值 / lifecycle 消费档 → 实付口径。
      金额守恒已对账(张弦 mode 表 17 行:实付57,887/应收77,795,无多通道重复计)。
      
      上线步骤:部署后跑 reparse(rawPayload 完整保留源列,无需重拉 DW)→ persona 重算。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • feat(ai/brief): 召回简报三问→四问 — 复查切入建议(事实锚定,防编造) · ae0b29c0
      第④问「以什么切入」:打电话的低门槛开口台阶,依据按优先级取第一个可用档:
      ① 医生计划/建议/医嘱(最优先,统一 verbatim 原话属性):
         - 治疗计划 subtype 原话(如「待22萌出后择期早期矫治」)
         - 医生建议 recommendation_record.name 原文
         - 医嘱 emr_record.doctor_advice(2.2万条真实医嘱;≥6字过滤"常嘱"水词,取最近2条)
         规则:必须优先引用「」原话要点(过长精简保留医嘱词),别只报大类;
         通用套话(定期复查不适随诊)视同无原话降档;多条挑与召回原因最相关的一条
      ② 洁牙/检查锚点(①为空才用):最近一次 preventive/periodontic 距今,
         超6个月才可引用时间,不足视同没有
      ③ 通用复查邀约(不带时间数字);严禁编"该洁牙了/上次洁牙已X个月"
      
      防重复:同一天数整句只出现一次;锚点与召回原因"拖了X"同源时切入不再报数。
      字数放宽 ≤65 最佳/90 上限;兜底模板同优先级;prompt 版本 2026-07-21-d。
      
      注:已缓存旧简报不自动重生成,上线时按 type=recall_brief 清一遍缓存即可。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • docs(integration): 推送契约去掉存量快照数字(随时间失真,不入契约) · a9d230cb
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • merge: refactor/assembler-shared-dict → main(FRIDAY 摄入:共享字典分层 + 推送契约 +… · 86f5028f
      merge: refactor/assembler-shared-dict → main(FRIDAY 摄入:共享字典分层 + 推送契约 + 关系边/咨询/退费三轨 + export cohort 过滤)
      
      # Conflicts:
      #	apps/pac-service/tests/canonical-fact-layer.spec.ts
      luoqi committed
    • docs(integration): FRIDAY 推送数据契约上架文档站 — 自 refactor/assembler-shared-dict 摘取 · e152489a
      形态 A 的 15 个 source 字段定义,放接入板块、紧随 Push 通道(它是推送通道的宿主侧具体化)。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
  5. 20 Jul, 2026 18 commits
    • test(canonical): 闸3 豁免集补齐 upsert 资源 — 对齐 cold-import 排除集 · b0879852
      patient_relation / patient_return_visit 与 patient 同为 upsert 资源(不进
      transaction,刻意无 emits,yaml 头注释已写明),但闸 3 豁免名单只有 patient
      → 两条误报失败(main 同样存在)。豁免集对齐 cold-import.service.ts subjectCfgs
      排除集(patient/patient_relation/patient_return_visit)。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • test(recall): 修孤儿引用 — likelihood bonus 已删,改锁 v3 优先级因子 · 34e9c1f2
      recall-suppression.spec 引用 7eb8dbed 三源标定重构中删除的 computeLikelihoodBonus,
      套件加载失败(main 同样存在)。原 GAP2"considering 不加权"在 v3 由设计保证
      (scorer 不再消费 execution outcome);该块改测 v3 确定性因子:置信度夹逼 [0.5,1]、
      新鲜度过窗线性衰减至 0.4 地板、三维加权(急迫0.4/价值0.3/意愿0.3)、
      K08/K03 牙数分档、信任封顶 10、总分 ≤100。snoozedUntil 部分 API 未变,原测保留。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • test(ai-script): 重写话术确定性逻辑 spec — 对齐三档重构后 API · 943b0922
      f60f4cca/4fe0d973 重构删除旧 script-facts 8 函数,旧 spec 成孤儿(套件加载失败)。
      按现架构重写:shared/script-facts(智能日期/FDI俗称)、shared/pii(去名留称呼)、
      shared/disease-knowledge(病种 canonical 名)、tiers/stable/phrasing(稳健档文案,
      含 jaw_cyst 双字典 key 回归点)、shared/fact-block(厚输入事实块:全名不进 prompt/
      未成年拍片禁令/单一聚焦)。62 测锁行为。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • feat(friday): export.sh 支持 --clinics/--since/--months cohort 过滤 — 语义对齐 jvs-dw · 106edbac
      文件源宿主的诊所/时间过滤放导出侧(架构约定:host 端 dump 时过滤,PAC 不动):
      - cohort 源 = med_emr_info 正式病历(对齐 jvs-dw clinic_scope 挂 EMR 事实表);
        只收窄患者名单,入选患者全部主体/跨诊所/全历史照常导出,谓词按患者求交非行级 AND
      - settlement_modes 无患者列经结算头单 IN 子查询挂靠(纪律放行形态)
      - SQL 走 stdin(cohort IN 列表会撞 ARG_MAX);空 cohort 拒跑防退化全量;
        cohort 内 0 行表合成表头(mysql --batch 空结果连表头都不出)
      - cold-import 文件源传 --clinics/--since 时显式警告(此前静默忽略)
      - refresh-clinic-names 改合并语义:子集导出不再刷掉子集外诊所名
      
      实测(刘医生演示诊所 36 患者):EMR 53 份含 4 家其他诊所历史,零越界;
      dry-run 15 资源 746 txns 0 failed;空 cohort exit=1;0 行表合成表头可导入。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • revert(auth): 撤下品牌名字典 — 品牌无展示需求,模拟登录显示 id 即可 · 1af18042
      品牌中文名当前只有模拟登录对话框一处会用,显示 GUID 足够;为它引入一个数据源
      (tenant_apply.tenant_info)+ 一个 Host 字段 + 契约档 + CLI 分支 + session 合并,
      收益不抵复杂度 → 整条链路撤回,只留诊所名(工作台真实展示位)。
      
      - friday export.sh / manifest:删 tenant_info 导出与 brand_directory 声明
      - manifest.schema:删 brand_directory;Host.sourceUnitNames 与其迁移一并删除
        (分支未合未部署,本地 DROP COLUMN 即可,无需 down 迁移)
      - TokenDictionary 删 brands 档;auth.service 恢复 getClinicNames;controller 恢复原合并
      - refresh-clinic-names CLI 恢复单字典派生
      - listMockOrgs:品牌名直接用 source_unit id(诊所名仍派生自病历表 organization_name)
      - 契约文档:15 source 复原,「留意事项」加一条说明——品牌名未冗余进业务表,
        将来若需展示再约定推主档
      
      验证:tsc 0(svc+web);mock-orgs 19 品牌显示 id、诊所仍中文名;session 无 brands 键、
      clinics 11 家照旧;jvs-dw 全程未受影响。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • docs(integration): 说明品牌名需单独推、诊所名不用的原因(宿主冗余不对称) · c9429413
      对接方必然会问"诊所名随业务表白送,品牌名为何要单独推一张表"。实证:宿主把
      organization_name 冗余进了 21 张业务表,而 tenant_name 只在两张投诉表(不在契约范围),
      品牌名只存于 tenant_apply.tenant_info 主档 —— 故必须单独推,好在低频且仅 45 行级。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • docs(integration): FRIDAY 推送契约补齐 — 品牌主档 source、诊所名列、语义澄清记录 · 777fd744
      对齐 2026-07-20 的新落地内容:
      - 新增 §2 `tenant_info` 品牌主档 source(品牌 GUID→中文名;PAC 靠它避免前端显示 GUID),
        source 总数 15→16
      - `med_emr_info` / `med_check` 补 `organization_name` 列(诊所展示名来源;此前导出漏列,
        导致工作台诊所显示 GUID)
      - 补回丢失的 §7:改为「语义澄清记录」——时间口径/金额/两套关系枚举/treat vs dispose/
        结算 status 全 9 值 + 为何不收 7/8,逐条附源码或数据依据;另列三条推送方留意事项
        (organization_name 仅覆盖有病历的诊所、字典表全量重推、测试环境沙盒数据待生产校准)
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • feat(auth): 模拟登录按宿主分 tab;FRIDAY 数据范围用真实品牌/诊所 · 426bbfa4
      两宿主此前挤在一个面板(FRIDAY 只有一条集团级横条),数据概念互相干扰。改为宿主 tab:
      - 瑞尔集团(jvs-dw):原客服花名册路径,一字未动
      - FRIDAY 市场:身份是造的(该宿主本就无客服花名册),但**数据范围必须真实** ——
        品牌/诊所来自新端点 GET /auth/mock-orgs:结构取 org 树(派生自 patient_transactions),
        名字取 host.sourceUnitNames / clinicNames(派生自摄入源),零硬编码
      
      - 契约:MockLoginRequest 加 orgId(任意 org 节点 = 品牌 GUID / 诊所 id);新增 MockOrgs 响应
      - service:listMockOrgs(org 树 + 名字典,有名品牌与多诊所品牌排前);friday 分支支持
        orgScope=[orgId],不传则市场级
      - web:FridayPanel(角色 + 市场级 + 真实品牌/诊所两级选择 + 搜索)
      
      验证:FRIDAY tab 显示 19 品牌 / 35 诊所(瑞诚齿科 12 诊所、希尔德 3…);点"瑞诚齿科"
      品牌级登录 → 右上角 12 个诊所、召回池由 11,723 收窄到 5 人、患者诊所均属该品牌 ✓;
      瑞尔 tab 花名册与登录行为不变。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • feat(auth): 品牌/诊所展示名从摄入数据派生 — 多品牌宿主不再显示 GUID · 5ac83ffb
      FRIDAY 的 source_unit / clinic_id 都是 GUID(jvs-dw 是"瑞尔/瑞泰"中文名),工作台此前
      只能显示 GUID。诊所名机制(host.clinicNames + clinic_directory + CLI 派生)本就存在且
      支持文件源 —— 缺的是①导出漏了 organization_name 列 ②friday manifest 没配 directory
      ③品牌名没有对应机制。
      
      - 契约:TokenDictionary 加 brands 档;Host 加 sourceUnitNames(对称 clinicNames,
        迁移 20260720104938)
      - manifest:新增 brand_directory 声明(与 clinic_directory 同形)
      - CLI refresh-clinic-names 泛化:一次派生两类字典,单侧声明不误清另一侧;空结果不覆盖
      - session:getDisplayNames 一次取两类,dictionary.brands 与 clinics 同款合并
        (服务端派生打底、宿主换票传的覆盖优先)
      - friday export.sh:med_check/Mongo 补 organization_name,新增 tenant_info 品牌主档
        (apply 服务 tenant_apply.tenant_info,列名实测 name/name_ab 非 short_name)
      - ️ export.sh 加固:所有导出改 tmp→校验非空→mv,裸重定向在库抖动时会清空好文件
        (2026-07-20 踩,62MB med_emr_info.json 被清零)
      
      验证:派生诊所名 11 家 / 品牌名 45 个;friday session 下发 brands 45(成都安玉牙种植
      医院/欣美/森德口腔)+ clinics 11;jvs-dw 对照 brands 键不下发、clinics 5 家照旧。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • feat(auth): 模拟登录支持 FRIDAY 市场(第二宿主,集团级) · 17ae97bd
      工作台此前只能登 jvs-dw 租户,FRIDAY 摄入的 10.7 万患者 / 1.17 万召回计划无入口查看。
      - MockLoginRequestSchema.tenant 加 'friday'
      - auth.service 独立 early 分支:host=friday / tenantId=friday-market /
        orgScope=['friday-market'](集团根 → 展开后 sourceUnits/clinicIds 均不限)。
        多品牌 SaaS 无在岗花名册,mock 只做集团级;诊所名未摄入 → dictionary.clinics 空,
        UI 回退显示 id。jvs-dw 路径(MOCK_PRESETS)完全不变
      - 弹窗集团级横条下加青色 FRIDAY 入口,角色沿用①所选
      
      验证:mock-login 签发 token(hostId=friday/tenantId=friday-market)、瑞尔对照未受影响;
      浏览器登入实测召回池 11,723 人,详情页画像 9 项 / 治疗历史 1,621 项 / 累计消费 ¥3,320
      / AI 话术均正常。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • feat(sync/friday): 语义全实锤(源码+数据双证) + 患者主档 push 支持(partial upsert) · 4f176611
      一、时间/金额实证(2026-07-20,契约文档同步):
      - 金额=元(洁治单价均值 ¥310/客单 ¥1,730 双证);MySQL datetime=北京墙钟(_gmt_ 命名惯例误导)
      - ️ Mongo Date=北京墙钟伪装 UTC(clinicTime/createdGmtAt 小时分布双峰直读=营业时段;按真 UTC
        解释一半病历落深夜)→ export.sh Date 改直读墙钟输出 naive 字符串;EMR 链 35.6 万 fact
        清层重灌校正(-8h),修后小时分布恢复营业双峰。契约文档明令宿主 push 勿 toISOString 带 Z
      
      二、官方字典(friday-saas 源码考古,待确认清单清零):
      - contacts.relationship = PhoneRelationshipEnum(1本人…9其他)→ patient.yaml 解码,
        preferences.contactPhone 带 relationship 标签(mother/father…)
      - referee_relationship = RecommendRelationshipEnum 17 码;reverseValue+年龄差数据双证
        语义方向=customer 是 referee 的 X → patient_relation.yaml 按逆关系重写(码8 其他亲属
        纠正 friend→other;新增 5/6/11/12/13/15/16 映射;码3 按对方性别拆 father/mother)。
        关系表清层重灌:friend 3,511→541(纯码7),官方映射干净落地
      - settlement status 全 9 值:实体注释 7=流程结束/8=欠款补缴克隆单(savePatientSettlementQk
        证实 receivable 原样复制→入消费必双计)→ 现行不收 0/2/5/6/7/8 全部获官方背书
      - treat=本次治疗/dispose=处置(EmrInfoResDto 注释);预约 status 7=到诊患者变更
      
      三、患者主档 push(此前 form A 仅支持 fact 资源):
      - patient-upsert.util:upsert 数据构造抽纯函数,full(cold-import 历史语义,分毫不变)/
        partial(push 单表:未提供字段不进 update 集合、phone 永不合成假号)双语义 —— 解决
        push 单表时 contacts lookup 缺席导致存量真号被假号覆盖的根因
      - ingestRawTables 路由 upsert 资源(patient partial/patient_relation/patient_return_visit);
        纯主档推送不产 txn 不触发重算(注释明示)
      - 测试 8/8(partial 不覆盖/无清空语义/preferences 透传);E2E 冒烟:push 单行改名,
        存量 phone 15725591111 原样保留 ✓。契约文档待确认清单清零
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • test: canonical-fact-layer emits 检查豁免全部 upsert 资源(与 cold-import 三元组对齐) · 1d8a663b
      原测试只豁免 patient,后来新增的 upsert 资源 patient_relation/patient_return_visit
      (设计上无 emits)一直误报;friday/patient_relation.yaml 落地后同一缺陷第三次命中。
      豁免集合改为与 importDirectory 排除 subjectCfgs 的三元组一致。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • fix(sync): synthesizer occurredAt 缺失时确定性回退 updatedAt→createdAt,墙钟仅最终兜底 · 4b2a7b52
      原 occurredAt ?? new Date() 在 occurredAtField 缺失/不可解析时每次摄入产生新墙钟值 —
      时间锚纳入 fact 变更检测后会被放大成伪 supersede 版本抖动。改为确定性回退链
      (occurredAtField → updatedAt → createdAt),三者皆缺才落墙钟并 warn。含回退顺序测试。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • fix(sync): fact 变更检测纳入四时间锚 — 宿主纠正事件时间可传导为新版本 · 2cfc043c
      FactWriter 此前只比 content hash + status:occurredAt/plannedFor/validFrom/validUntil
      不参与 → 宿主纠正事件时间(如 FRIDAY 伪 UTC 整体 -8h)重摄永远 evidence_appended,
      时间锚永不更新。时间锚是时间轴/召回窗口 COALESCE(occurred_at,planned_for)/过期 cron
      的直接输入,属事实实质 → 纳入等值判定(毫秒精度,双 null 视为等),单写与 bulk 两路同步。
      不纳入 title/summary(展示层)。含 supersede 传导测试。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • feat(recall): 宿主回访模式 — 回访 URL 插槽三态 + 关闭机会闭环 · a96424b5
      宿主动作派发(host-message 重写):
      - actionUrls.OPEN_RETURN_VISIT / OPEN_POTENTIAL_TREATMENT 值形态定模式:
        未配置=按钮不渲染 / URL 模板=跳转(推荐) / 哨兵 postMessage=喊宿主父页(需 HOST_ORIGIN)
      - 哨兵比较大小写不敏感(裸词不可能是合法 URL,宽容拼写零误伤)
      - 管理页:配哨兵缺 HOST_ORIGIN 时黄条提示
      
      宿主回访模式布局(有回访按钮时):
      - 通话结果整列隐藏(xl 收 2 列,窄屏去「操作」tab);召回反馈拇指隐藏,宠物引导同停
      - 顶栏「关闭」危险按钮 → 关闭机会弹窗(7 原因,PAC UI 标准)
      
      关闭机会闭环(CLOSE_REASON_META 单一真理源,前后端同表):
      - 原因→outcome 映射:无意愿/价格=refused(90d)、竞品=external_treatment(永久)、
        无法联系=abandoned(30d 熔断口径)、已治/其他=abandoned(14d 默认档)、
        识别不准=marked_invalid(覆写永久→14d)+ 双写召回反馈 down
      - resolveSnoozedUntil 优先级链插入 closeReason 覆写档(熔断 > 覆写 > outcome 自带)
      - 复用 execution 通道(channel=other),状态机/触达账本/跨栏同步全继承
      - 关闭后自动跳下一位(我的进行中 → 召回池,与 /plans 入口解析器同规则)
      
      列表口径对齐:
      - 终态单(completed/abandoned)在 pool/mine 视图原地剔除(与服务端过滤一致)
      - 「我的」显式 status=assigned(服务端 view=mine 返全状态是旧列表页设计)
      
      其他:全局细滚动条(Windows 默认过粗);删 spec 中引用已重构掉的
      computeLikelihoodBonus 的过时测试块(main 上即已编译失败);新增 7 个
      closeReason 抑制覆写测试(19/19 过)
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • chore(design-sync): 组件库首次同步到 claude.ai/design(18 组件) · e1697a0d
      - .design-sync/:config(锚定 PAC Design System 项目)+ 18 个富预览 + 用法约定 + NOTES
      - src/ds/index.ts:设计系统导出桶(ui 全集 + plan-detail 原语 + toast)
      - ui/*.tsx、shared.tsx:补 JSDoc(@category 分组,转换器生成 prompt.md 用)
      - .gitignore:忽略同步暂存/构建产物
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
  6. 19 Jul, 2026 3 commits
    • fix(recall): 终态抑制加时间锚 — 结案后新发同类信号放行复活,'永久'不再误杀 · 05cdd46d
      问题:信号级抑制集只按 (scenario|subKey) 一刀切,外院/无效的永久 snooze(36500d)会把
      结案后新发的同类诊断也永久压死 —— 同牙位复发(外院种植失败回院再诊断)和 @whole 病种
      (外院牙周,多年后新发)整类终身沉默。且 cluster lead 锚最早诊断,老 fact 永远 active
      (外院治疗不进本院数据)→ 新旧必聚同 cluster,单看 lead 时间无法区分新发。
      
      修法:
      - scenario:cluster 注入 cluster_latest_occurred_at(成员 max),hit 透传
        latestSignalOccurredAt(lead/daysSince 仍锚最早,紧迫度口径不变)
      - engine:抑制集 Set<key> → Map<key, 结案锚点>;锚=结案 execution.createdAt(不可变,
        不用 updatedAt——会被召回反馈等后续写顶后);同 key 多次结案取最新锚
      - 过滤:key 命中且 latest > anchor → 放行复活;latest 缺省(旧 scenario)/锚不可解析
        (远未来哨兵)→ 维持旧行为全压,宁可多压不误放
      
      测试:tests/plan-engine-snooze-anchor.spec.ts(锚点构建 5 例 + 逃逸判定端到端);
      存量 plan-engine-batch 通过(mock 无 executions/updatedAt 走哨兵路径)。
      
      Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
      luoqi committed
    • refactor(sync/friday): 宿主字段零改名 — 撤销 tenant_id→brand_id 别名,差异由 PAC 适配层消化 · 8ebe45ab
      原则修正(用户指出):要求宿主把 tenant_id 别名成 brand_id 是把 PAC 的概念洁癖转嫁给
      宿主;字段名差异的消化本就是适配层职责。宿主(导出与将来 push)一列不改名。
      
      - export.sh:撤销全部 AS brand_id;Mongo 平铺同步 tenant_id
      - manifest:identity_namespace_field: tenant_id(指向宿主原生列)。无功能冲突:PAC 租户
        由 manifest 静态 tenant_id: friday-market 决定,StaticTenantResolver 不读行,行级
        tenant_id(品牌 GUID)仅作 source_unit 命名空间源
      - push 契约文档:brand_id → tenant_id(宿主原生列),通用约定写明概念区分责任在 PAC
      
      零数据翻腾证明:幂等键/患者唯一键用 source_unit 的值(品牌 GUID),列名不参与 ——
      原生列名重导+重摄实测:0 新 txn/357,931 全量幂等命中/0 新 fact/failed=0,
      source_unit 分布逐品牌不变。
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed
    • docs(integration): FRIDAY 推送数据契约 — 形态 A 的 15 个 source 字段定义 · 638d27fb
      Push 通道形态 A 在 FRIDAY 宿主的具体化:每个 source 的 JSON 字段清单(类型/必填/语义),
      形状与已验证的存量摄入(10.7 万患者/36 万事实)完全一致 → 宿主按此推送与存量无缝衔接
      (幂等去重/版本演进/重叠无害)。
      
      - 通用约定:id 一律字符串(存量实测坑)/updated_gmt_at 必须 bump(幂等键一半)/
        brand_id=源 tenant_id 改名/at-least-once
      - 患者关系 3 source(含 upsert 主体待 PAC 排期的如实标注)/预约/病历链 3(含 diag/treat
        数组元素结构与 status∈{3,4} 推送范围)/结算链 4(status 拆分规则钉死)/计划咨询 3
      - 待 FRIDAY 确认清单:两套关系码枚举/treat 语义/结算剩余 status
      
      Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
      luoqi committed