1. 28 Jun, 2026 1 commit
    • perf(persona): recompute CLI 加 --concurrency(默认1)—— I/O-bound,并发 ~4.5x · b5de1a0c
      实测推翻旧注记"persona 重算 CPU-bound、异步并发不提速":每患者 recompute 含 ~15 次
      串行 DB 往返(2 次全量 fact 读 + 多次 findFirst),实为 **I/O-bound**。单进程分块
      Promise.all 并发可重叠这些 DB 等待 —— 本地 500 患者 17s(串行)→ 4s(并发16),~4.5x,
      concurrency 8-16 后 plateau(DB 成瓶颈)。正确性验证:500/500 各 1 active persona、
      0 重复版本、0 空特征(不同患者独立写 + 同患者不并发,无竞争)。
      
      - --concurrency=N:分块并发(默认 1 向后兼容)
      - --shard=i/k:多进程分片(按患者序号取模,吃多核;DB 先饱和时用)
      - 大批(>500)只打进度行不逐条刷屏;orderBy id 稳定序供分片
      
      全量重算 131689 患者:7h(串行)→ ~1.6h(并发12)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  2. 27 Jun, 2026 23 commits
    • fix: agent 审查的一致性/UX 收口(by-id 守卫、死代码、陷阱注释、集团级诊所筛选) · c1b44721
      后端:
      - plan.recomputeByPatient:patient 守卫补 source_unit(与其它 by-id 守卫对齐;防跨品牌触发重算 + 存在性探测)
      - persona.getByVersion:删(无 scope 的 findUnique by-id,死代码,属之前修过的泄漏模式)
      - cold-import:改"sourceUnit:''(过渡)"陷阱注释为真实语义(从 manifest 解析,勿退回 '')
      - auth.allClinics:去"恰好两品牌"硬编码,改 Object.values(MOCK_PRESETS) 遍历;refresh/mcp 陈旧注释(clinicIds→orgScope)
      
      前端:
      - auth-store:setTokens 同 sub 沿用上次 clinicIds 占位,消除 silent refresh 时诊所筛选闪空(M2 竞态)
      - visibleClinics(user) helper:clinicIds 空(集团级=不限)→ 回退字典全部诊所,修集团登录后诊所筛选空白(M1);
        4 处(plans-list/patient-picker 筛选 + plans/plan-detail 计数)统一用它
      
      两端 tsc 0 错。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(auth): expandOrgScope fail-closed — 树外 ref 不再退化成"不限品牌"(跨品牌泄漏) · 42b9e8da
      agent 审查(3 个独立都挖到)的真·安全洞:expandOrgScope 对树里找不到的 ref 兜底成
      "当裸 clinicId、sourceUnits 留空"。而瑞尔/瑞泰共用 tenantId,sourceUnits=[] 被下游当
      "不限品牌" → 跨品牌看全部患者(含真号 reveal)。触发:退化树(无数据)、新诊所未进树、
      10min 缓存陈旧。
      
      - org-scope:探测 branded(树有非空 source_unit);品牌制租户 + 非空 scope 却没解析出
        任何品牌 → 塞 DENY_SOURCE_UNIT 哨兵(患者查询返回空),不退化成不限。命中集团根仍合法不限。
        单命名空间/退化树(无品牌)→ 维持 [] 不限(正确)。回传 unresolved 供刷新重试。
      - org-tree:展开有 unresolved → 强制重派生树一次再展开(避免误拒刚摄入的新诊所合法用户);
        forceRefresh 30s 防抖,挡 bogus ref 刷库。
      
      单测:品牌ref/诊所ref正常、集团根→不限、未知ref→DENY、混合(含已解析品牌)→限该品牌不DENY、
      单命名空间未知ref→不限。活体三级 + REST 无回归。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(auth): org 树从摄入数据派生(零硬编码 id,跨环境自适应) · 2a858612
      之前 org-tree 复用 jvs-dw-preset 的硬编码 clinic UUID 建树 —— 把通用隔离路径绑死到
      mock 演示数据 + 写死 id(换环境/换 DW 即废)。改为从摄入数据派生:
      
      - OrgTreeService(注入 Prisma):$queryRaw 从 patient_transactions.clinic_id JOIN
        patients.source_unit 派生「品牌→诊所」,按 (hostId,tenantId) 缓存(10min TTL);
        无数据→退化树。对任何 host/环境自适应,零硬编码 id。
      - AuthModule 导出 OrgTreeService;tenant-scope 拦截器(from+mergeMap 异步展开,ALS 不变)
        与 mcp-auth 注入它,每请求 expandScope。
      - jvs-dw-preset 退回**仅 mockLogin 用**(dev,env 门控):集团/品牌 slug 是稳定逻辑名;
        clinic UUID/中文名仅演示展示,无数据源,换环境降级成 id —— 隔离仍由 DB 派生树保证正确。
      
      验证(DB 派生树):诊所级石琳 sourceUnits=['瑞尔']/2诊所、品牌瑞尔/3、瑞泰/2、集团不限、
      REST /plans 仅命中本诊所 —— 与原硬编码结果一致。tsc 0 错。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(auth): JWT 改装通用 orgScope,过滤维每请求展开(设计 B) · 7fd4c0d8
      JWT 是 host 也能解码的凭证,原先装展开后的 sourceUnits/clinicIds(PAC 内部 schema 词汇),
      与"host 只发 org_scope"的契约不一致、泄漏内部表示。改为 JWT 只装 orgScope(与契约同词汇),
      过滤维留到每请求展开:
      - AccessTokenPayload / RefreshPayload:sourceUnits/clinicIds → orgScope;issueTokens 不再展开,直接签
      - org-tree.ts(纯函数 expandScope:按 tenantId 选 host org 树展开)+ jvs-dw-preset.ts(树数据,与 mock 共用)
      - tenant-scope.interceptor(REST)/ mcp-auth(MCP)每请求 expandScope → scope.{sourceUnits,clinicIds}
      - 前端展不开 orgScope → 新增 GET /auth/session 返回展开后的 clinicIds/sourceUnits + role/permissions/dictionary;
        auth-store 加 loadSession(登录/F5 后异步补进 user),组件读 user.clinicIds 不变
      - weixin-aibot 自签 token 同步改 orgScope;契约文档 §2/§3 同步
      
      验证:JWT 解码只含 orgScope;/auth/session 展开正确;REST /plans 诊所过滤生效(石琳仅本诊所);
      集团级 orgScope=[grp]→clinicIds=[]/sourceUnits=[] 不限。两端 tsc 0 错。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(web): 模拟登录对话框分清两套权限 —— 角色=功能,选谁/哪级=数据范围 · fd57d73d
      原文案把数据权限说得像跟着角色("看自己的/看本团队所有任务"其实是 view_own/view_all
      功能权限,却像在描述数据范围)。点透两轴(正交):
      - 顶部加一句:① 角色=能做什么(功能);② 选谁/哪级=能看哪些诊所品牌(数据权限,跟角色无关)
      - ① 标题 "登录权限(角色)" → "角色 · 能做什么(功能权限)";角色 desc 改成纯功能口径
      - 集团按钮 "集团·X 身份" → "数据范围 · 集团级 …… 角色沿用上方「X」"(数据范围与角色解耦)
      - ② "选客服" → "数据范围 · 选客服 = 该客服的诊所(诊所级)+ 上方角色"
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(auth): 契约数据权限收敛为 orgScope 唯一入口(删 clinicIds/sourceUnits 入参) · aefe33ed
      第一性:换了 org-scope 数据权限后,clinicIds/sourceUnits 是 expandOrgScope 的**展开产物**
      (PAC 内部过滤维),不该出现在对外契约入参里 —— host 只该传"用户可达的 org 节点"。
      
      - TokenExchangeUser:删 clinicIds + sourceUnits,orgScope 转必填(任意层级:集团/区域/品牌/诊所)
        诊所级即传诊所 id;没品牌概念的 host 同理。clinicIds/sourceUnits 仅留在内部 JWT AccessTokenPayload。
      - issueTokens:永远走 orgScope 展开;非 jvs-dw host 用退化树(根=tenantId,无子)→
        orgScope 里诊所 id 当裸诊所透传、sourceUnits 留空、根 id=不限。删显式 fallback 分支。
      - auth-login-contract.md:org_scope 从"P2 可选增强"翻正为唯一契约入口;派生表/示例/实现现状同步
      - 活体三级复验:集团 []/0(不限);品牌 ["瑞尔"]/3;诊所 ["瑞尔"]/2
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • refactor(auth): mockLogin 走对外契约同一路径(issueTokens 单一真理源)+ 修 ['default'] bug · e7c3b3eb
      第一性:模拟登录 = 模拟 host 调换票,本应产出契约同形 user 上下文(核心 orgScope)
      并走与 exchangeToken **同一条**展开+签发。之前 mockLogin 把 expand+sign 抄了一遍,已飘:
      - 抽出 private issueTokens(host, user):orgScope→expandOrgScope→sourceUnits+clinicIds→签 access/refresh
      - exchangeToken 与 mockLogin 都委托它;mockLogin 只负责"preset → 契约 user(算 orgScope)"
      - 删 exchangeToken 的 clinicIds 空→['default'] 兜底:空=不限(下游 plan 过滤 length>0 才加),
        兜底会把集团级误锁成不存在的诊所 → 啥都看不到(mockLogin 本就没这兜底,统一后契约路径一并修)
      
      活体验证三级:集团 sourceUnits=[]/clinics=0(不限);品牌 ["瑞尔"]/3;诊所 ["瑞尔"]/2;瑞泰 ["瑞泰"]/2
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(auth): org 树改递归结构 —— 层级无限(支持多级区域) · 07a32a03
      之前 OrgTree 是写死的 3 层形状 { groupId, brands:{clinics[]} },塞不进区域层。
      改成递归 OrgNode(children 无限嵌套):集团→多级区域→品牌→诊所→…均可。
      expand 逻辑不变 —— 过滤永远只落 sourceUnits/clinicIds 两维,中间层只是分组,
      展开任意节点 = 走子树收集这两类叶子属性。
      
      - OrgNode 递归类型 + buildTree(通用) ;buildOrgTree(3 层糖)签名/tree.groupId 不变
      - sourceUnit 命名空间**向下继承**:scope 到单诊所时带上祖先品牌,
        否则 sourceUnits 退化成空(不限品牌)= 越权看全品牌患者(测试抓出,已修)
      - 扁平索引按 id **聚合**(数组),同一品牌跨多个区域时全收,不被 Map 覆盖丢诊所
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(auth): org 树从配置派生(去 hardcode)+ 模拟登录支持集团/品牌/诊所三级 · 44539c27
      - org-scope:删 JVS_DW_ORG_TREE hardcode,改 buildOrgTree(从配置/数据派生);
        实现对所有 host 通用,树数据 3 层本就在数据里(yaml 只为区域等数据外的层)
      - auth:jvsDwOrgTree() 从 MOCK_PRESETS 派生;exchangeToken + mockLogin 都走 expandOrgScope
      - mockLogin:数据范围统一 org-scope —— 诊所级(选客服)/品牌级(仅品牌)/集团级(orgLevel=group,
        sourceUnits=[] 看全部品牌全部诊所);MockLoginRequest 加 orgLevel
      - 前端:加"以集团身份登录"按钮(跨品牌,验证数据权限最上一层)
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(auth): 标准数据权限 org-scope(内存树展开,不建表) · db2c0874
      数据权限 = 用户的 org 范围(任意层级:集团/品牌/诊所)。expandOrgScope() 把
      org 节点展开成 sourceUnits + clinicIds(已有过滤层不变):
        诊所 ref → 该诊所 + 所属品牌;品牌 ref → 该品牌 + 其下全部诊所;
        集团 ref / 空 → 不限。多 ref 取并集。
      - org-scope.ts:jvs-dw 内存树(ruier-grp→瑞尔/瑞泰→诊所)+ expandOrgScope
      - 契约 exchangeToken 接 orgScope(优先于显式 sourceUnits/clinicIds)→ 展开
      - TokenExchangeUserSchema 加 orgScope
      验证:诊所/品牌/集团/跨品牌四级展开均正确。通用态树放 manifest yaml。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(tenant): agent 审查发现的 4 个 HIGH · a6d7eb22
      ① source_event_id 品牌盲 → 数据丢失(HIGH):synthesizer 的幂等键
         ${ctx}:${resource}:${subjectId}:${updatedAt} 不含品牌,两品牌同 subjectId+
         updatedAt 撞键被去重跳过 → 丢 transaction/fact。并进 source_unit(re-import 仍幂等)。
      ② persona by-id 跨品牌泄漏(HIGH):/patients/:id/persona 的 getCurrent 无 scope 过滤
         (Persona 非 source_unit 模型,纵深防御只注 tenantId 不注品牌)。加经 patient 关系的
         sourceUnit 过滤;controller + 2 处 MCP 调用传 scope。
      ③ MCP by-id 泄漏(HIGH):MCP @Public 无 ALS 上下文,纵深防御不兜;
         assertPatientInScope / activeRecallPlan 只按 host+tenant,补 sourceUnit 过滤。
      ④ exchangeToken 真 host SSO 丢 sourceUnits(latent):TokenExchangeUserSchema 加
         source_units + exchangeToken 透传(否则真 host 登录 = 不限品牌)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(auth): 登录对外契约(集团模型) · b16f58b0
      host→PAC 登录输入规范:host_id + group_id + user{id,name,role} +
      source_units(品牌数据范围,空=不限)+ clinic_ids;PAC 派生 permissions/
      dictionary,signAccessToken 签发新契约 token。mock 与真 host 共用 signAccessToken
      + 同一 token 形状,差别仅输入来源。含隔离保证映射。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(tenant): 纵深防御 — Prisma 扩展按 ALS 上下文自动注入 tenantId/source_unit · 927021a0
      - tenant-context.ts:请求级 AsyncLocalStorage(tenantId/sourceUnits)
      - tenant-guard.extension.ts:$extends 对 14 张 scoped 表的列表/聚合读查询自动注入
        tenantId(+ source_unit 模型注品牌);findUnique 不注(破坏唯一键,by-id 走显式守卫);
        无 ALS 上下文(系统任务)不注;$queryRaw 不经扩展(召回引擎自带隔离闸)。env
        PAC_TENANT_GUARD=warn 可切观察模式
      - prisma.service:构造器 return this.$extends(...) 透明接入(services 不改)
      - tenant-scope.interceptor:handler 在 tenantAls.run 内执行(subscribe 包裹)
      
      验证(已删一次性 CLI):注错 tenant→0、限品牌→该品牌数、系统任务不注、findUnique 不破。
      关键:Prisma 惰性查询,await 须在 als.run 内 — interceptor 的 handler 正满足。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • chore(web): 模拟登录对齐集团模型(品牌 vs 租户正名) · fa71191b
      ruier/ruitai 注明为**品牌(source_unit)**,同属集团租户 ruier-grp;
      类型 TenantSlug→BrandSlug。功能不变(slug 经后端 MOCK_PRESETS 映射为
      tenantId=ruier-grp + sourceUnit)。正式 SSO 对外契约(host_id+group_id+
      org_scope)待 host 接入时实现。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • fix(tenant): dispatcher source_unit 串通 + reconcile 守卫升级(零碎待办) · df2271eb
      - pipeline-dispatcher:加 identityNamespaceField,patient upsert/index/patientIdResolver
        全按 (source_unit,external_id) 复合键(pull/push/reconcile 多命名空间 host 也正确;
        单命名空间默认 '' 不变)
      - reconcile 守卫:除 distinct tenant 外也查 distinct source_unit —— 合集团后 jvs-dw
        只剩 1 tenant 但 2 source_unit,旧守卫拦不住会撞号;现多命名空间一律转 cold-import
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(tenant): P0-5 by-id 守卫补品牌隔离 · 7554f95b
      患者详情(reveal/timeline)、plan 详情/分配/回收、plan-aggregate 详情:
      by-id 取数也按 patient.sourceUnit 圈品牌(findUnique→findFirst + patient 过滤,
      命中不到→现有 NotFound 守卫接住)。sourceUnits 空=不限(集团级角色)。
      至此 §4.4 数据范围在患者粒度的品牌隔离全覆盖(搜索/列表/by-id)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(tenant): P0-4 查询层品牌隔离(scope.sourceUnits)+ 去写死 UUID · 62168f4f
      - token/scope 加 sourceUnits(用户所属品牌;空=不限,集团级角色)
      - 数据范围在患者粒度按品牌圈:患者搜索 + plan 列表(patient.sourceUnit,覆盖召回池)
      - auth mock 预设 tenantId→ruier-grp + sourceUnit;weixin fallback→ruier-grp
      - mcp/timeline/weixin 手动 scope 补 sourceUnits
      
      至此 P0 代码完整:3 表 source_unit + 摄入 + 集团 slug + 品牌隔离查询。
      本地重摄已验证:external_id 261067 → 王辰(瑞尔)/王文彦(瑞泰)两条独立行,
      facts source_unit 全填,tenant=ruier-grp。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(tenant): P0-3 patient_facts/return_visits 加 source_unit + fact-writer 版本链隔离 · 7325beed
      - schema+migration:patient_facts (host,tenant,source_unit,subject_id,version)、
        patient_return_visits (host,tenant,source_unit,external_id)
      - fact-writer:writeDraft/bulkWrite 的版本链 findFirst + 史表查 + liveLatest map
        全按 (source_unit, subject_id) 复合键隔离;create 带 source_unit
        (集团内 subject_id 品牌级撞 631,不隔离会取错品牌版本=改坏 fact 历史)
      - parser-pipeline:按 patientId 查患者 source_unit(批内一次取),注入 PrismaService
      - cold-import:return_visit upsert 带 source_unit
      - 删旧原地迁移脚本 — 改用"清空重摄"(本地/服务器均可),无原地改 fact 版本链风险
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(refactor): 记录前置审发现 — source_unit 需加 3 张表(facts/return_visits 也撞键) · 5c50cfc3
      实施期 pre-check:patient_facts.subject_id 跨品牌撞 631、patient_return_visits
      .external_id 撞 611,均品牌级,合 tenant 会撞键。故 source_unit 必须加在
      patients + patient_facts + patient_return_visits 三张表;fact-writer 版本链
      findFirst 也须按 source_unit 过滤。transactions(撞0)、relations(UUID)免。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(tenant): P0-2 摄入侧 source_unit 真填值 + manifest 切集团 slug · a5b0184b
      - manifest.schema: 加 identity_namespace_field
      - tenant-resolver: 加 buildSourceUnitResolver(从 identity_namespace_field 解析)
      - cold-import: source_unit 从 rawSource 解析,串入 patient upsert / stub /
        patientIndex(按 (source_unit,external_id) 复合键防撞号)/ patientIdResolver
      - manifest.yaml: 删写死 tenant_map UUID,改 tenant_id: ruier-grp(集团 slug)+
        identity_namespace_field: brand
      - 加数据迁移脚本(未执行)
      
      ️ 前置审发现 patient_facts.subject_id(撞631)、patient_return_visits.external_id
      (撞611)也是品牌级,合 tenant 会撞键 — 这俩也需 source_unit。迁移待补这两表后再跑。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(tenant): P0-1 患者 source_unit 列 + 唯一键(结构地基,行为不变) · cb2d0a5f
      - patients.source_unit(通用命名空间,NOT NULL 默认 ''):各宿主"在哪层发患者号"
        不同,jvs-dw=品牌;'' 非 null 防唯一索引 NULL-distinct 失效
      - 唯一键 (host,tenant,external_id) → (host,tenant,source_unit,external_id);
        存量 source_unit='' 常量,新键与旧键唯一性等价,迁移安全(95k 患者完整)
      - 3 处患者 upsert + 2 CLI 改新键(过渡用 sourceUnit:'';stub 与真主档同 '' 不双行)
      - patientReturnVisit 的 (host,tenant,external_id) 键未动(只改 patients)
      
      下一步:source_unit 真填值(assembler sourceUnit:brand)+ tenant→集团 slug remap。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(refactor): 纠正 §4.4 框架 — 非独立隔离策略,是换槽位在查询层的对应 · 12ffbfd5
      澄清:无独立隔离策略,一切由数据范围控制;现状隔离=代码 where(tenant_id),
      非 RLS/非 schema。brand 从 tenant_id 槽搬到 source_unit 槽后,品牌隔离的
      where 判别列随之 tenant_id→source_unit。数据范围两粒度:plan 按诊所、
      患者按品牌(source_unit),同源于 org 位置。默认本品牌(复刻现状),P1 可配置。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(refactor): 收敛为最终最小方案 — 画像/召回零改动 + slug 去写死UUID · c21e0221
      核查摄入/画像/召回全链后重写计划:
      - 关键发现:画像&召回计算零改动(重算驱动是 distinct tenantId 循环 +
        per-patient,tenant=集团自动适配;召回 SQL 仅 scope 过滤)
      - tenant=集团用稳定 slug(ruier-grp),删 4 处写死 DW UUID
      - brand→source_unit(发证命名空间,进键);换槽位不合并、472撞号仍分开
      - org_tree 最简一行 yaml,不建表;子表不冗余
      - 改动集中:schema+摄入(4处upsert)+去写死UUID(4处)+少数隔离查询面
        (患者搜索/召回池NULL-clinic/by-id guard 加 source_unit 默认过滤)
      - 迁移前置:审 subject_id 跨品牌不撞;一刀完成 品牌→集团+UUID→slug
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  3. 26 Jun, 2026 7 commits
    • docs(refactor): 权限模型三概念分离 + org_units 推到 P2 + 去子表冗余 · 8d74885e
      - 新增 §7.5 权限模型:功能权限(角色,代码级,保持)/ 数据范围(应绑 org 子树,
        非角色逃生门)/ 数据共享策略(集团级结构配置)三者正交,现状把前两者搅在一起
      - org_units 整体推到 P2:P0/P1 用 source_unit 字符串列 + 集团级品牌共享策略即可,
        不需要建树;org_units 只在多级组织/层级数据范围/rollup 时才引入
      - 子表不冗余 source_unit:召回 SQL 与 plan 列表本就 join patient,过滤近乎免费
      - scope 分期标注:brandSharingPolicy=P1,orgUnitSubtree=P2
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(refactor): 据评审收敛 — 通用 source_unit / unit_type 非层级 / 多集团 / 定纵深防御 · d7d0f342
      - 患者命名空间限定符 brand → 通用 source_unit(manifest 声明源字段,brand 仅瑞尔实例)
      - org_units.unit_type 字符串枚举且 ≠ 层级:层级靠 parent_id 树,支持多级区域/无品牌等任意形状
      - host:集团=1:N 确认,现有 schema 已支持,无需改
      - 集团 ID 三来源:宿主 group_id 透传 / PAC 配 tenant_map→集团 / 必填定义;集团 UUID 新发
      - 纵深防御定稿:Prisma $extends + AsyncLocalStorage,RLS 作 P2+ 硬化
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(refactor): 租户模型品牌级→集团级通用组织模型改造方案 · 5cb3f2c7
      设计稿(未动代码)。核心:tenant_id 语义 品牌→集团,brand 降为患者键
      命名空间限定符 + org_units 节点,品牌共享改为可配置策略;引入 org_units
      组织树。含迁移核心洞察(换槽位不加信息、472 撞号不合并)、四层影响面、
      schema/摄入/认证/查询隔离/clinic 各改动点、一次性迁移步骤、P0-P3 分期、
      待决策项、关键文件改动清单。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • docs(persona): 注明重算串行 — CPU-bound,异步并发不提速 · 498e038e
      persona 重算 16 特征是 Node 单线程 CPU 计算,实测 conc=8 不提速反略慢;
      加注释解释为何保持串行(真要提速需 profile 或多进程/worker)。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(plan): 患者搜索支持病历号(medical_record_number) · d0797f48
      关键词搜索 OR 增 medicalRecordNumber 模糊匹配;左栏 placeholder
      「患者号」→「病历号」。注:externalId 非病历号,medical_record_number 才是。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(ai): AI 摘要/话术触发感知 — 仅医生建议时不假设牙已缺失 · 9a895395
      reason.leadTrigger 透传到话术/召回简报:lead=建议(无诊断)→ 病种名用
      建议名(如「建议种植」),不套「缺失牙」;诊断/影像仍以诊断为准。
      召回简报 prompt 增来源标注【医生建议·非确诊缺失】+ 硬规则 + 建议示例,
      修复张宇晨这类"仅建议"被 LLM 说成"缺牙空了X个月"。
      reason-line 前端 subLabel 同步触发感知。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(plan): 三源召回信号统一标定 — 置信度/优先级/打分一致 · 7eb8dbed
      诊断 1.0 / 建议 0.8 / 影像AI 0.5 三源置信度直用为打分因子(下限0.5),
      取代旧 0.75/0.9/1.0 分档;triggers 按 诊断>建议>影像 排序定 lead,
      reason 文案由 lead 来源驱动(建议为首不再套子场景诊断词)。
      priority-hover 提示同步为「诊断1.0/建议0.8/影像0.5」。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  4. 25 Jun, 2026 6 commits
    • fix(web): 全屏按钮挪到 JSON 原文块(RawJson)而非整个证据链面板 · c0d27880
      上一版加错位置。改到 RawJson 控制行右侧:全屏时 JSON 块 fixed inset-0 覆盖全屏、<pre> 去 max-h-80 改 flex-1 overflow-auto 撑满可滚,Esc 退出。便于看全大段原始报文 JSON。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • feat(web): 召回归因调试-证据链面板加全屏按钮 · 225756d5
      为什么召·证据链(T0)面板右上角加全屏切换(Maximize2/Minimize2);CSS 固定覆盖式全屏(fixed inset-0 z-50,内容区 overflow-auto 可滚),Esc 退出。便于看长 JSON 原始报文。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • perf(plan): selectHits 子场景可选并行 — 83K 实测 1.6-2.6×,逐 hit 等价 · 0c04e0b5
      10 个子场景(K00-K09)查询彼此独立,合并/去重与顺序无关。新增 PAC_RECALL_SUBSCENARIO_CONCURRENCY env(>1 开分块 Promise.all 并行,默认 1 串行、行为不变)。83K 患者本地实测:串行 50s→并行(conc=4)31s(warm 1.6×)/ cold 123s→47s(2.6×),固定 now 下两路 59032 hits 逐 hit 完全一致(等价性证实)。规则零改动,仅改执行方式。prod 建议设 =4。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • perf(plan): patient_facts(patient_id,type,status) 索引 — 召回 selectHits ~3.6× · ea857067
      EXPLAIN 定位:selectHits 的 sig 信号扫描 + gap resolvedTeeth 各子查询都按 (patient_id,type,status) 过滤,原仅 (patient_id,status) 索引 → type 走内存 Filter,facts/患者多时 heap fetch 超线性膨胀(本地单条 K08 查询读 1.3GB 页面产 81 行)。加复合索引后所有子查询命中 Bitmap Index Scan。本地 selectHits warm 7.3s→2.0s(~3.6×),prod 13万患者收益更大。
      
      迁移用 CREATE INDEX IF NOT EXISTS:prod 大表可先手动 CREATE INDEX CONCURRENTLY 预建免锁表,再 deploy 时本句 no-op。规则零改动、纯加速。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • test(plan): runAllForHost 批量化等价性测试 + 修复 stale-close spec 过时 mock · e1ffafbc
      新增 plan-engine-batch.spec.ts:忠实内存 mock 端到端验证批量路径 5 种结局(create/unchanged/superseded/suppressed/stale-close)+ 混合并发 + createMany 日志,与逐患者语义等价。修 plan-engine-stale-close.spec.ts 过时 mock(status:{in}匹配、加 persona.findMany/createMany)与返回形态断言(toMatchObject);assigned 0命中关闭对齐 2026-06 决策。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
    • perf(plan): runAllForHost 批量化 — bulk 预取 + 分块并发 + 日志 createMany · 5b5379af
      夜间全 host plan 重算的 upsert 段原为逐患者串行(每人 2 读 latest/snoozed + persona + 2 日志写),13万患者 ~49min。改为:循环前一次性 bulk 预取 latest(含 reasons)/snooze 抑制集/active persona → 逐患复用同一 upsertPlan 判定(结果等价)→ 写操作分块并发(PAC_PLAN_BATCH_CONCURRENCY 默认 8)→ PlanGenerationLog 收集后 createMany(每患仍一行,口径不变)。selectHits 全表扫不在本次范围(时间规则可改,每轮须全量重选)。单刷 recomputeForPatient 不传 prefetched,行为不变。
      
      Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
      luoqi committed
  5. 24 Jun, 2026 3 commits