153 KiB
家谱项目全量治理设计
日期:2026-07-22
状态:阶段 0 已完成;导航任务 1—10 的静态实施和零债务门禁已经完成;A01/A04/A05 TAC 客户端与 M07 反馈客户端已经完成;MuMu 原生矩阵待执行;T01、认证、家谱工作区、M06 帮助、个人资料读写、通知读写、M10 服务端退出、M04 密码凭证与 M05 手机号换绑后端合同均处于硬门禁红灯 适用范围:当前 UniApp 家谱项目、根目录 OpenAPI 文档、全部活动页面、共享组件、测试、正式资产与后续真实接口接入
一、背景
项目第一轮国风视觉与响应式适配已经完成,并形成了可继续工作的页面基线。但此前验收包含用户抽查和代理逐页审核,不能据此认定所有页面、所有真实数据和所有跨页流程都已经最终正确。
当前根目录已经取得由 Apifox 导出的两份接口文档:
APP.openapi.jsonAPP.openapi.yaml
两份文件来自同一个 Apifox 项目,只是表示格式不同。接口由后端根据此前思维导图设计,并非根据当前页面逐项编写,因此可能混入 PC 接口,也可能存在 App 接口缺失、字段不足、状态不完整或页面与接口目标不一致等问题。
后续工作不能简单地让页面迁就现有接口,也不能默认现有页面天然正确。必须从真实业务目标出发,对页面、接口、功能、交互、导航、视觉和数据状态进行联合治理。
二、治理目标
本轮治理需要同时达到以下目标:
- 清理失效、重复、冲突或确实无用的文档、测试、资产和生成物,使工作区只保留当前有效内容。
- 区分 App 接口、PC 接口、可共用接口和缺失接口,严格分析 OpenAPI 合同。
- 建立页面、用户操作、业务状态、接口和数据结果之间的精确映射。
- 重新评估全部活动页面的视觉完整性与美观性,不把“没有溢出”当成视觉完成。
- 检查功能、权限、交互、导航、失败处理、重复操作和跨页数据一致性。
- 按业务域向后端持续提交可直接修改 Apifox 的完整接口问题单。
- 采用测试先行和小批次实施,每批均在 MuMu Android 中完成真实流程和视觉验收。
- 建立中文文档、中文进度和详细中文代码注释规范,使后续接手者能够明确知道当前做到哪一步、每一步在做什么。
三、明确不做的事情
- 不重新推翻已经建立的国风设计方向。
- 不因为旧页面曾经通过就忽略新发现的可复现问题。
- 不因为现有接口已经写好就强迫页面接受错误业务。
- 不因为现有页面已经完成就无条件要求后端照搬。
- 不一次混合实施多个业务域。
- 不用浏览器截图代替 MuMu 视觉验收。
- 不制造临时假接口、双读取兼容路径或伪持久化成功。
- 不通过删除失败测试来制造全量通过。
- 不执行 Git add、commit、push、restore、checkout 或 reset 等 Git 变更操作。
四、权威顺序与唯一所有者
发生冲突时,采用以下权威顺序:
- 用户确认的真实业务目标。
- 三人共同推演并验证的完整用户流程。
- 后端在 Apifox 中确认的接口合同。
- 页面、共享组件和运行时代码。
- 自动化测试与 MuMu 当前轮验收证据。
- 当前 OpenAPI 导出、现有 mock 和历史资料。
关键合同的唯一所有者如下:
- 接口源头:后端维护的 Apifox 项目。
- 自动分析格式:
APP.openapi.json。 - 人工阅读与跨工具格式:
APP.openapi.yaml。 - 九宫格切片与可变高度装饰面:
styles/adaptive-frame-profiles.scss。 - 密码策略:
utils/validation.js。 - 响应式覆盖范围:
tests/responsive-layout-coverage.json。 - 固定尺寸例外:
tests/responsive-layout-allowlist.json。 - 项目当前状态入口:
docs/项目当前总览.md。 - 页面与接口关系:
docs/接口与页面映射总表.md。
APP.openapi.json 与 APP.openapi.yaml 不是两个独立接口所有者,而是同一次 Apifox 导出的两个产物。两份文件不得分别手工维护,必须通过自动合同验证语义一致。
五、三人联合治理机制
5.1 人员组成
总共三人:主代理和两位评审者。三个人都必须完整审查同一个业务域,不能把页面、接口和交互割裂分工,也不能出现“该问题不属于我”的责任边界。
三个人均需覆盖:
- 页面美观与样式完整性;
- 功能和业务严谨性;
- App、PC 接口分类与合同完整性;
- 交互、导航、页面状态和跨页数据;
- 真实数据进入后的布局、反馈与业务结果。
5.2 四轮互补流程
第一轮为独立首审。三个人在不查看另外两人初步结论的前提下,分别阅读页面源码、测试、OpenAPI 和业务说明,并审查同一组 MuMu 证据,独立形成完整判断,避免首个意见造成锚定。
第二轮为交叉补漏。三份结果公开后,每个人逐条检查另外两份结论,判断是否漏掉页面状态、接口边界、视觉问题、业务风险或连锁影响。
第三轮为反向质询。合并后的结论重新交给三个人,每个人主动尝试证明结论可能错误,包括检查接口是否真的缺失、页面问题是否由测试数据造成、视觉方案在边界状态下是否仍成立,以及导航和数据刷新是否在重复进入后仍然正确。
第四轮为共同收敛。每个问题必须带有页面、状态、接口、代码位置、MuMu 证据、影响等级和处理建议。不能只记录“感觉不好看”“接口可能有问题”等不可验证意见。
三人意见不能简单少数服从多数。任意一人提出有证据的疑点时,必须继续调查;只有证据充分后才能形成共同结论。
5.3 共享工作区规则
- 审查阶段三个人只读,不边看边改。
- MuMu 由主代理统一导航、切换状态和采集截图,避免多人同时操作造成状态干扰。
- 两位评审者审查同一份已经打开确认的 MuMu 截图。
- 评审者可以要求补拍状态,主代理补充证据后三人重新判断。
- 实施阶段由主代理统一修改工作区,两位评审者负责复核方案、变更、测试与 MuMu 证据。
六、联合评审模型
每个页面使用统一中文评审结构,至少记录:
- 页面编号、页面路由和业务目标;
- 用户身份、角色、权限和前置条件;
- 进入来源、返回目标和流程终点;
- 页面全部适用状态;
- 页面全部用户操作;
- 每个操作成功、失败、取消后的页面和数据结果;
- 对应 App 接口和关联 PC 接口;
- 缺失、冲突或不完整的接口;
- 样式、美观、功能、交互和导航问题;
- 真实数据和边界数据风险;
- MuMu、源码和 OpenAPI 证据;
- 三人初审、补漏、质询和最终结论。
每个用户功能都按以下顺序推演:
进入
→ 输入或选择
→ 本地校验
→ 提交锁定
→ 发起请求
→ 成功或失败
→ 页面反馈
→ 数据刷新
→ 返回或流程结束
→ 再次进入
同时检查取消、Android 返回键、重复点击、网络失败、权限变化、目标数据过期和跨页状态一致性。
七、视觉与美观验收标准
第一轮样式审核是继续工作的基线,不是永远不能重开的封印。包括 G01—G10 在内,只要三人通过页面、接口、流程或 MuMu 证据发现明确且可复现的问题,就允许重新打开处理。
视觉验收不能只检查溢出、遮挡和响应式,还必须检查:
- 页面整体是否有协调的视觉中心和信息层级;
- 标题、正文、辅助信息、错误提示和操作区是否层级清楚;
- 字号、字重、颜色、间距、对齐、留白和视觉重心是否协调;
- 卡片、输入框、按钮、弹窗、列表项、图标、背景、边框和装饰是否完整;
- 默认、按下、选中、禁用、加载、空、错误和成功状态是否都有正式设计;
- 素材是否缺失、模糊、变形、错误裁切或风格不一致;
- 页面是否过空、过密、装饰不足或装饰堆叠;
- 同一模块与跨模块是否保持统一国风语言;
- 真实长短数据、键盘、弹窗和滚动状态下是否仍然美观。
缺少皮肤、页面明显失衡、视觉层级混乱、素材错误或半成品表现不能统一降级为低优先级体验问题。
现有国风设计体系是新增或修复样式的基础:
- 优先复用现有共享组件和正式资产。
- 能通过共享规则解决时,不在单页复制样式或素材。
- 需要扩展时,优先扩展现有视觉体系。
- 确实没有合适素材时,按真实槽位尺寸、色彩、纹理和透明要求制作正式资产。
- 如果现有页面结构经真实流程与 MuMu 证据证明明显不合理,可以重新设计该页面,但必须保持产品信息与交互合同、复用现有设计语言,并先更新设计和测试,不能写单设备补丁。
- 不用 emoji、文字符号、临时 SVG、CSS 拼图或占位框冒充正式资产。
- 新资产必须在 MuMu 中验证清晰度、比例、裁切和状态完整性。
八、接口治理与后端协作
8.1 接口分类
每个接口标记为以下一种类型:
- App 可直接使用;
- PC 专用,不进入 App;
- PC 与 App 可能共用,需要后端确认;
- App 接口存在但合同不完整;
- 页面需要但 App 接口缺失;
- 接口存在但当前页面没有合理用途;
- 接口与页面都需要重新定义。
接口审查至少覆盖鉴权、请求参数、响应字段、字段可空性、枚举、分页、错误码、权限、幂等、上传、删除、审核和状态转换。
8.2 接口问题单
每条接口问题必须包含:
- 问题编号、业务域、优先级和状态;
- 页面、路由、触发操作、用户身份和前置条件;
- 业务目标和完整使用场景;
- 现有接口路径、方法、请求、响应与当前问题;
- 建议新增或修改的接口能力;
- 鉴权、参数、字段类型、必填性、约束和成功响应;
- 分页、错误码、权限、幂等和数据副作用;
- 页面成功、失败、取消、返回和数据刷新表现;
- 验收步骤和三人共同结论。
不能只向后端提交“缺接口”或“字段不够”等模糊描述。
8.3 后端更新闭环
三人形成接口问题单
→ 用户确认少数业务决策
→ 后端修改 Apifox
→ 用户重新导出 JSON 和 YAML
→ 验证双格式语义一致
→ 检查本次接口差异
→ 运行接口合同测试
→ 页面接入真实接口
→ MuMu 验证完整状态
→ 三人共同复核
→ 问题关闭
后端提出替代方案时,三个人重新评估它是否满足业务和页面,不因接口已经完成就自动接受。
九、问题等级与暂停条件
9.1 问题等级
- 阻断级:权限越权、隐私泄露、数据丢失、重复写入、关键流程无法完成或接口无法支撑业务。立即暂停当前批次。
- 高优先级:业务流程错误、返回目标错误、状态结果不一致、关键错误无反馈、主要页面缺少样式或呈现半成品。当前业务域不得带问题完成。
- 中优先级:局部状态样式、交互反馈、文案层级或边界数据存在问题。必须在该业务域闭环前解决。
- 体验优化:不影响理解和任务完成的细节打磨,可在本批后段处理。
9.2 必须暂停的情况
- 真实业务目的无法确定;
- 页面和接口表达不同状态模型;
- 缺少关键 App 接口;
- 后端字段、权限或错误码不足以安全实现;
- 三人对业务正确性仍有实质分歧;
- 共享合同的唯一所有者尚未确定;
- 没有有效 MuMu 证据;
- 测试无法先复现问题;
- 同一问题连续三个实现假设失败。
等待后端时,将当前批次标记为“待后端处理”,转向下一个没有依赖关系的业务域,不制作临时兼容路径。
十、阶段 0:项目基线清理
10.1 清理原则
旧内容不归档。确认已失效、已替代或确实无用后,直接从工作目录删除。Git 历史承担追溯作用,但本轮不执行 Git 变更命令。
清理候选无需用户逐项确认,但必须由三个人共同复核:
- 三人一致确认无用:由主代理统一删除。
- 任意一人提出有效疑点:继续调查,暂不删除。
- 审查人员不同时修改或删除文件。
10.2 文档清理
文档只有在内容被当前权威文档完整覆盖、没有独有有效决策、且不会继续作为执行入口时,才能删除。删除旧文档时必须同时清理所有指向旧入口的引用。
10.3 测试清理
每个测试需要记录保护的页面或功能、具体合同、当前有效性、重复关系和失败价值,并归类为保留、合并、重写或删除。
- 测试失败不是删除理由。
- 对应功能删除或合同被新唯一测试完整覆盖时,才能删除旧测试。
- 只断言旧文档文字、旧素材路径或无业务价值内部实现的测试应重写或删除。
- 截图采集工具不计入合同测试总数。
- 浏览器测试可以保留独立逻辑价值,但不能作为视觉验收。
- 生成物依赖先判断生成流程是否仍为正式能力。
10.4 资产清理
资产引用扫描必须覆盖 Vue、JavaScript、SCSS、JSON、配置、设计清单、构建脚本、CSS 变量和动态路径。
资产分为正式运行时资产、共享视觉资产、设计母版、构建输入、可再生成产物、临时 MuMu 证据、重复资产、被替代资产和已删除功能专用资产。
重复素材需要比较文件哈希、像素尺寸、透明区域、压缩质量和交互状态用途。确认合并后只保留一个公共所有者,更新全部引用后删除副本。
10.5 清理顺序与验证
确认当前权威资料
→ 删除失效文档
→ 清理重复或失效测试
→ 建立完整资产引用关系
→ 清理无用资产和生成物
→ 删除残留引用
→ 运行聚焦与全量验证
→ MuMu 检查代表流程
阶段 0 只清理已经能证明无效的内容,不顺便重构业务代码,也不开始接口接入。
十一、文档体系与生命周期
11.1 核心当前文档
docs/项目当前总览.md:项目唯一入口和当前进度。docs/家谱项目全量治理设计.md:长期有效的治理规则。docs/家谱项目全量治理实施计划.md:当前执行批次与验证要求;整体完成后将长期内容合并到总览并删除。docs/接口与页面映射总表.md:页面、功能、状态与接口关系的唯一总表。
11.2 临时文档
docs/项目基线清理清单.md:阶段 0 使用,清理完成并合并有效信息后删除。docs/评审/<业务域>联合评审.md:业务域审查期间使用,完成并合并长期结论后删除。docs/待后端处理接口问题.md:只保留尚未解决的问题;后端合同通过验证后删除对应条目。
11.3 生命周期
草稿
→ 三人评审
→ 当前有效
→ 内容进入唯一所有者
→ 过程文档删除
不保留“旧版”“最终版2”“备份”或历史归档。任何规则只能有一个当前所有者。
十二、测试策略
测试体系按以下层级组织:
- 项目基础合同:页面清单、路由、共享组件、唯一所有者、响应式、编译和 OpenAPI 双格式一致性。
- 接口合同测试:路径、方法、参数、鉴权、响应字段、错误码、分页和状态枚举。
- 业务状态测试:校验、权限、状态转换、防重复提交、失败重试、过期和数据刷新。
- 导航流程测试:进入、返回、取消、完成和重复进入后的栈与数据状态。
- 纯函数运行测试:密码、验证码、字段归一化和状态机等纯逻辑。
- MuMu 验收:视觉、美观、键盘、安全区、手势和 Android 返回键。
每个确认问题必须测试先行:
第一步:写出能够复现问题的失败测试
第二步:运行并确认测试因目标问题失败
第三步:实施最小修改
第四步:运行聚焦测试
第五步:运行全局响应式合同和编译审计
第六步:运行全量合同
第七步:在 MuMu 验证完整流程和视觉状态
全量测试数量可以因清理和新增真实合同发生变化,不以保持原有数量为目标。
十三、中文文档与代码注释规范
- 新建规格、计划、报告和问题单使用中文文件名和中文内容。
- 接口路径、字段名、函数名等技术标识保留原文,并提供中文解释。
- Vue、JavaScript、SCSS 和测试脚本继续使用英文技术文件名,避免工具兼容问题。
- 测试代码同样使用中文注释说明准备条件、执行步骤、预期结果和合同目的。
- 复杂业务函数必须使用详细中文注释说明每一步的业务目的、执行顺序、状态变化、失败处理和设计原因。
- 简单赋值不堆叠无意义注释,但不能让复杂流程只能依赖阅读实现细节才能理解。
复杂流程注释采用明确步骤形式,例如:
// 第一步:校验页面必填字段,阻止无效请求进入后端。
// 第二步:锁定提交状态,防止连续点击产生重复记录。
// 第三步:调用业务接口,并根据明确的后端状态更新页面。
// 第四步:成功后刷新来源数据,再结束当前流程。
// 第五步:失败时保留用户输入,并提供可理解的重试入口。
十四、精准代码与复杂度控制
所有新增或修改代码都必须以最小、精准、可验证为标准。代码行数少不是唯一目标,但每一行都必须能够追溯到当前确认的问题、业务合同或验证要求。
- 一个模块、函数或状态对象只承担一个清晰职责。
- 业务规则必须有唯一所有者,页面、组件、工具和测试不得复制同一判断。
- 改变合同后同步删除旧字段、旧入口、旧分支和旧注释,不保留“以防万一”的兼容读取。
- 不为单次使用创建抽象层,也不为了未来可能需求增加配置、参数或扩展点。
- 不把多个互不相关的业务流程塞进同一个大函数;复杂流程按明确业务步骤拆分,并保持输入、输出和副作用可见。
- 优先使用清晰命名、早返回和直接数据流,避免深层嵌套、隐式全局状态和难以追踪的副作用。
- 只有两个以上真实消费者需要同一规则时才提取共享能力,不能为了“看起来高级”制造工具层。
- 不新增没有必要的依赖,不用大范围重构解决局部问题。
- 中文注释解释“为什么、当前步骤和业务边界”,不能逐字翻译语法,也不能用大量注释掩盖混乱实现。
- 测试验证对外业务合同和关键状态,不把内部实现细节固化成脆弱断言。
- 每次实施只处理一个可独立验证的批次;相邻但无关的问题只记录,不顺手修改。
每个代码批次完成前,三个人必须共同检查:
- 是否存在重复规则、重复请求或重复状态。
- 是否引入不必要的抽象、依赖、参数或兼容分支。
- 函数和文件是否具有清晰单一职责。
- 旧路径、孤立变量、失效注释和无用导入是否由本批产生。
- 是否可以用更直接、更少副作用的方式表达同一业务。
- 每个修改是否都有对应失败测试、业务证据和 MuMu 验证。
任意评审者能够用证据指出代码复杂、重复或所有权不清时,该批次不能进入完成状态。
十五、项目阶段顺序
阶段 0:项目基线清理
清理失效文档、测试、资产和生成物,建立可信验证基线。
阶段 1:全项目共享合同
建立 OpenAPI 一致性、App/PC 分类、登录态、权限、统一错误、分页、上传、防重复提交和导航栈语义。
导航栈先形成统一设计和测试合同,具体修改随业务域逐页验证,不机械批量替换。
阶段 2:认证与账户
覆盖 A 系列,并同时审查 M03—M05 依赖的账户安全合同,包括注册、登录、短信验证码、找回密码、登录态失效、修改密码、换绑手机号和注销。
阶段 3:家谱主体与申请
覆盖 G 系列,包括列表、创建、概览、搜索、加入申请、审核、角色、权限、设置、字辈谱和家谱上下文。
阶段 4:世系与成员
覆盖 T 系列,包括树图、成员选择、详情、添加、编辑、关系、目录、隐私、权限和保存后的跨页一致性。
阶段 5:家族圈与内容媒体
覆盖 F 系列,包括动态、文章、评论、相册、照片、视频、上传、分页、草稿、发布、编辑和删除。
阶段 6:人物与族务记录
覆盖 R 系列,包括人物档案、礼物、祭祀、成长、人生事件、备忘、功德、时间线和汇总。
阶段 7:消息通知
覆盖 N 系列,包括未读、已读、全部已读、消息目标、过期、权限失效和业务结果一致性。
阶段 8:个人中心与设置
覆盖剩余 M 系列,包括资料、帮助、反馈、推广、会员订单、协议、退出登录和账户状态。
阶段 9:跨模块最终闭环
使用真实接口完整验证认证、创建或加入家谱、管理成员、发布内容、新增记录、接收消息、返回个人中心、退出和重新进入。
十六、每个业务域的固定执行流程
三人独立首审
→ 交叉补漏
→ 反向质询
→ 共同结论
→ 后端接口问题单
→ Apifox 修改与双格式导出
→ 五方案比较与批次设计
→ 失败测试
→ 最小实施
→ 聚焦、全局和编译验证
→ MuMu 完整流程与视觉验收
→ 删除旧合同和无用内容
→ 业务域完成
每个确认问题提出五个可行方案,从业务正确性、全局一致性、回归风险、维护成本和 MuMu 可验证性选择最优方案。
十七、进度与用户沟通
每个业务域只使用以下中文状态:
未开始
基线清理中
独立首审中
交叉复核中
待业务确认
待后端修改
待实施
实施中
MuMu 验证中
回归验证中
已完成
开始批次时报告当前阶段、业务域、目标、页面、接口、状态、后端依赖和明确不处理范围。
执行过程中只报告有实际价值的进展,包括已完成内容、三人发现、当前检查、阻断问题和下一步,不向用户倾倒无关工具日志。
只有三个人无法根据证据安全决定的真实业务问题才打断用户,例如角色权限、永久删除、重新申请、费用权益、隐私范围或会改变产品含义的流程分歧。
提问时一次只问一个业务问题,并说明发生位置、当前冲突、已排除方案、推荐方案、影响和阻塞范围。
十八、业务域完成定义
一个业务域只有同时满足以下条件才能标记完成:
- 三人完成独立首审、交叉补漏和反向质询;
- 每个页面的全部适用状态都有本轮 MuMu 证据;
- 页面视觉达到完整、统一、美观的正式成品标准;
- 所有页面操作都有明确业务结果;
- 进入、返回、取消、完成、失败和重复进入均通过验证;
- 每个页面数据需求都映射到明确 App 接口;
- 若当前批次存在接口缺口,后端已在 Apifox 完成相应修复;
- JSON 与 YAML 来自同次导出且语义一致;
- 聚焦测试、全局响应式合同、编译审计和全量合同通过;
- 真实接口数据下再次完成 MuMu 验收;
- 没有遗留旧路径、旧字段、废弃测试或无用资产;
- 阻断级和高优先级问题为零;
- 其他未解决事项有明确负责人和状态。
- 本批代码通过三人复杂度审查,不存在新增重复规则、无意义抽象、旧兼容路径或职责混乱的大函数。
十九、当前实施前置条件
阶段 0 已经完成。后续实施必须满足:
- 当前业务域已经由三个人完成独立首审、交叉补漏、反向质询和最终收敛。
- 设计结论已经进入本文件,实施步骤已经进入唯一实施计划。
- 需要后端变更时,先形成可直接修改 Apifox 的问题单;新导出通过双格式和接口差异合同后才能接入。
- 每个批次先写失败测试,再实施最小代码;不得同时混合导航、短信、持久化和全局无障碍等多个阶段。
- 业务代码变更后必须运行聚焦合同、全局响应式合同、编译审计和全量合同,并在当前 MuMu 复核完整流程。
用户已经授权三位审查者自行裁决有充分项目证据的专业方案。只有角色权限、隐私范围、永久删除、收费权益、法律责任或确实无法从现有业务判断的产品含义才再次询问用户。
二十、导航栈语义统一设计
20.1 当前证据
任务 3 实施前的只读扫描得到以下统一口径:
- 52 个活动页面中共有
navigateTo 59、navigateBack 11、redirectTo 14、reLaunch 7,合计 91 次直接调用。 - 活动共享组件另有
navigateBack 2、reLaunch 2,活动页面与组件共 95 次。 - 封存的 A06 另有 2 次,完整页面与组件源码共 97 次;项目没有
switchTab调用。 - MuMu 已复现 F01→F03→“返回家族圈”后留下两个 F01,A01→A04→“登录”后留下两个 A01,T03 可连续叠出三个同路由页面。
- 栈深为 1 时直接进入 F03,当前
PageHeader会回到固定 G01,而不是业务父页 F01。
任务 3 已把共享页头和自定义底栏的直接调用清零,并删除无活动消费者的通用页面旧入口;任务 4—8 依次迁移认证、G、T、F、R,任务 9 完成 N/M、安全通知目标、账号表单返回守卫与退出会话清理,任务 10 删除最后一个零生产消费者的 TreeMemberForm.vue 及其旧专属合同。当前 SFC 分段词法扫描的迁移债务已经清零:pages/components 中 navigateTo、navigateBack、redirectTo、reLaunch、getCurrentPages 与业务页面路径字面量均为零,声明式 navigator、动态 Uni 属性、Uni 对象逃逸与 switchTab 也为零;验证结果为 MIGRATION-DEBT=0。路由与页面栈合同分别只由 utils/navigation-routes.js 和 utils/navigation.js 持有,后续业务批次不得恢复页面私有路径或栈判断。
因此问题不是某几个按钮写错,而是项目没有统一表达“打开页面、替换步骤、返回来源、完成流程、切换根页和直接进入回退”的语义合同。
20.2 八类问题的五方案终选
以下评分依次为“业务正确性/全局一致性/回归安全/维护成本/MuMu 可验证性”,5 分最好。每类只保留最终选中方案,其他方案不得作为兼容路径残留。
| 问题类型 | 方案一 | 方案二 | 方案三 | 方案四 | 方案五与终选 |
|---|---|---|---|---|---|
| 导航所有权 | 各页继续直接调用 2/1/5/1/2 |
只封装页头 2/2/4/3/3 |
无注册表的字符串工具 3/3/4/3/4 |
只有注册表、页面仍直调 4/4/3/3/4 |
路由注册表+唯一导航网关 5/5/3/4/5 |
| 三个根页 | 改原生 TabBar 4/2/2/2/4 |
navigateTo 1/1/4/3/3 |
redirectTo 2/2/3/3/4 |
页面各自混用 2/1/4/2/2 |
保留自定义 Tabbar,统一 goRoot 5/5/4/5/5 |
| 返回与直接进入 | 固定 navigateBack 1/1/4/4/2 |
页头保存 fallback URL 2/2/4/2/3 |
一律回根页 2/3/3/4/4 |
查询参数传原始 return URL 3/2/2/2/3 |
栈感知返回+规范父页+类型化来源 5/5/3/4/5 |
| 列表/详情/编辑完成 | redirectTo 列表 2/2/3/3/4 |
reLaunch 列表 1/2/2/4/4 |
固定 navigateBack(delta) 3/2/2/2/3 |
页面全局存整对象 3/2/2/1/2 |
returnTo/finishPage+一次性结果 5/5/3/4/5 |
| T03 连续看亲属 | 重复 navigateTo 3/2/4/3/3 |
每次 redirectTo 2/2/3/3/4 |
每次 reLaunch 1/2/2/4/4 |
原生栈设上限 3/2/3/2/3 |
单个 T03+页内成员轨迹 5/5/3/4/5 |
| 认证流程终点 | 保持混用 2/1/4/2/3 |
全部 navigateBack 2/2/3/4/3 |
全部 redirectTo 3/2/3/3/4 |
全部 reLaunch 2/3/2/4/4 |
按进入、取消、完成和会话终止分别处理 5/5/4/4/5 |
| 通知与外部目标 | 后端下发原始 URL 1/1/2/3/2 |
N02 自己匹配字符串 2/2/3/2/3 |
任意 URL 白名单正则 3/3/2/2/3 |
每个消息类型写独立跳转 4/3/3/2/4 |
targetType+业务 ID 本地注册表映射 5/5/4/4/5 |
| Android 返回与未保存 | 只处理页头 2/2/4/3/2 |
只处理 Android 键 2/2/4/3/2 |
每页自由实现 3/2/3/2/3 |
所有页面总是确认 2/3/2/3/4 |
统一优先级状态机 5/5/3/4/5 |
20.3 唯一所有者
utils/navigation-routes.js是 52 个活动路由的唯一语义注册表,拥有路由键、路径、页面类型、规范父页、父页参数映射、根页、必填参数、可选参数、允许来源和目标页允许消费的结果操作枚举。utils/navigation.js是项目唯一允许调用uni.navigateTo、uni.redirectTo、uni.reLaunch和uni.navigateBack的业务模块;项目没有原生 tabBar,uni.switchTab在该模块内外都禁止。- 页面和组件只调用语义方法,不拼接页面路径,不保存 fallback URL,不接收后端原始跳转 URL。
- 注册表中的路径集合必须与
pages.json的 52 个活动路由精确相等;缺失、重复和陈旧条目均使合同失败。
路由参数只能使用注册表声明的标量值。ID 统一按字符串处理并逐项编码,不允许把对象、完整来源 URL、页面快照或领域数据塞进查询参数。
20.4 语义方法
openPage(routeKey, params, sourceKey)
打开普通子页;sourceKey 必须等于当前真实页面。当前已经是同一路由且关键参数相同则不重复入栈。
goBack()
栈内有上一页时 navigateBack;没有时按当前路由的规范父页逐级回退。
returnTo(routeKey, targetParams = {})
按路由键寻找最近实例并精确返回;显式 targetParams 必须与最近实例一致,目标不在栈内时才用于构造合法回退 URL;普通返回不产生流程结果。
finishPage(routeKey, targetParams, result)
是完成并回传结果的唯一公开入口;调用方必须显式提供目标全部必填参数,当前来源页与目标页共同声明且实际存在的上下文字段必须相等;校验参数和类型化结果后精确返回,结果只能包含 operation、entityId 和 refresh。
goRoot(routeKey, params)
只接受 A01、G01、F01、M01 四个根语义;使用 reLaunch 清理旧流程。
handleBackPress(event, requestBack)
同步适配 UniApp 的 onBackPress:网关自己的 navigateBack 回调来源返回 false 放行,其余来源同步返回 true,并异步执行页面唯一 requestBack,避免递归拦截。
五个公开导航语义方法都返回 Promise,并共享一个在途转场锁:相同参数和相同结果语义的重复调用复用同一个 Promise,其他并发转场明确返回忙碌结果。一次性结果写入必须与“取得新锁”原子发生;复用或忙碌调用不得改写结果,导航失败必须回滚。每次转场用独立 flight 身份释放锁,success/fail 必须在 Promise settle 前释放自己的 flight;迟到的 complete 只能清理原 flight,不能清掉已经开始的新转场。所有调用方导航参数、流程结果和返回守卫上下文只接受普通对象的 own enumerable data properties;Symbol、访问器、不可枚举字段和原型继承字段一律拒绝,校验后只使用同一次读取形成的冻结快照,禁止重复 getter 读取或校验后别名篡改。redirectTo 只允许由网关内部的规范父链回退和目标缺栈返回使用;当前没有可公开授权的“替换步骤”边,因此不暴露无消费者的替换方法。
一次性结果只存在于当前 JavaScript 进程。网关用模块内 WeakMap 为页面实例分配原始值身份令牌,结果 envelope 只保存令牌,不强引用或保存页面/Vue 实例。栈内返回时结果绑定反向搜索选中的最近目标令牌和该实例的规范业务参数;缺栈重建时绑定发起页令牌与完整目标参数。只有当前真实栈顶正是目标实例(或缺栈重建出的非发起页)且业务参数完全一致时才能消费,错页、同路由的更远实例和其他家谱上下文只能得到 null,也不得删除正确结果。结果必须在原生目标页 onShow 发生前随取得转场锁原子写入,消费一次后立即删除;新完成流程取得锁时淘汰已经错过目标生命周期的未消费旧结果,失败时不复活陈旧结果。字段键精确为 operation、entityId(可选)、refresh;operation 必须属于目标路由注册的 resultOperations,entityId 若存在必须为非空字符串,refresh 必须是布尔值。它不是领域数据持久化,不允许加入 treeVersion、genealogyId、focusId、payload,也不允许保存完整对象、表单内容、列表快照或接口响应。
sourceKey 只证明当前 JavaScript 进程内的一次真实导航:openPage 必须核对它与 getCurrentPages() 的当前真实路由一致。栈深为 1 或外部直接进入时,查询串里的 sourceKey 一律视为不可信并忽略,只按注册表的 parent/parentParamMap 建立回退目标;外部链接不能伪造来源合同。
20.5 根页与页头
- G01、F01、M01 继续使用自定义
AppTabbar;切换根页统一goRoot,点击当前项不做任何跳转。 - A01 是认证根页;登录、注册完成取得有效会话后统一
goRoot(G01)。 - G01、F01、M01 不显示普通返回箭头。左侧品牌只有绑定明确动作时才可点击,否则为非交互品牌标识。
- 根页右侧分别保留 G01 消息、F01 发布、M01 资料操作。
- 普通
PageHeader删除fallbackUrl和直接路由判断;普通返回统一进入导航网关,未保存页面使用同一个返回守卫。 - 根页没有弹层时,Android 返回交给系统;有弹层时先关闭弹层。
20.6 返回、取消与完成
返回优先级固定为:
关闭最上层预览、弹窗、搜索或抽屉
→ 消费 T03 等页面内部轨迹
→ 页面正在提交时阻止离开并给出状态
→ 有未保存输入时显示放弃确认
→ 执行普通 goBack
页头返回、页面取消按钮和 Android 返回键必须进入同一状态机,不能出现三个不同结果。
关键流程终点:
- A04 取消或“已有账号”返回 A01;注册并建立会话后
goRoot(G01)。 - A05 取消返回 A01;只有未来真实重设接口成功才写入
password-reset结果并回 A01。本地state=success、计时器或视觉占位成功态只能普通返回,不得生成业务成功结果。 - A01 登录成功
goRoot(G01);会话失效和退出成功也只允许goRoot(A01)。 - 当前 F02、F06、F07、F09 只形成明确标注“尚未提交服务器”的独立本地预览;F03 评论草稿不插入评论列表、不清空、不增加计数,F05 收藏明确禁用,F10 明确未开放。F01—F10 均不产生写成功结果;F02/F03 返回精确 F01,F06 新建回 F04、编辑回精确 F05,F09 无结果回同一
genealogyId + albumId的 F08。真实写接口返回服务器 ID 后才能同轮启用完成结果。 data/mock.js是 F 系列 feed/article/album 只读夹具的唯一 owner,只公开按genealogyId列表和按复合身份详情的深拷贝查询;跨谱、未知实体或缺失身份失败关闭。utils/api.js::createFeed在 mock 模式以WRITE_UNAVAILABLE拒绝,不得修改列表冒充发布成功。- R02 回 R01;R04 回 R03;R06、R07 回 R05。
- 当前 T04/T05/T06 只生成“尚未提交服务器”的本地预览,不产生写成功结果;用户确认放弃预览后,T04/T06 无结果回 T01,T05 使用
goBack()精确回原 T03 实例,T08 普通返回 T03,T07 可进入 T03。新增亲属、编辑成员和编辑关系的完成定位只能在真实版本化写接口同轮启用。 - 当前阶段
data/mock.js::treeMembers是唯一可变成员夹具 owner,utils/api.js是唯一写入口;T01/T03—T08 只能通过listTreeMemberFixtures(genealogyId)、findTreeMemberFixture(genealogyId, personId)取得含亲属数组深拷贝的快照。成员身份必须由家谱与成员复合定位,缺失、未知或跨家谱身份失败关闭;只有 T04 的明确首位成员模式允许没有personId。Task 18 接入真实新图合同后必须原子删除该临时 owner、选择器和旧id/parentId模型,不保留双读适配层。 - N02 默认回 N01;通知业务目标失效、无权限或字段不足时留在 N02 的明确状态,不猜测页面。
2026-07-22 新线上 OpenAPI 已确认密码登录、短信登录和注册统一返回 RAppLoginVo,其 data 引用 AppLoginVo,唯一会话字段为 access_token。因此 A04 的正式终点不是“注册后再登录”,而是保存有效会话后直接 goRoot(G01);受保护旧快照的响应形状不再作为兼容读取路径,在真实注册与行为验证尚未接入前也不得把当前占位反馈冒充注册完成。
20.7 T03 单实例成员轨迹
- T03 原生页面实例只保留一个。初始路由
personId是不可变的宿主页路由身份,初始成员成功读取后先把轨迹初始化为恰好[initialPersonId];初始读取失败不建立轨迹。点击亲属只更新页面内活动成员personId和成员轨迹,不改 URL,也不再navigateTo同一路由。 - 新成员资料成功读取后才写入轨迹;读取失败保留原成员和原轨迹。
- 返回先逐项弹出成员轨迹,轨迹结束后才返回 T01、T07、R02 或实际来源。
- 当前 T05 本地预览以
goBack()返回并保留原 T03 宿主页身份、活动成员和轨迹,T08 普通返回也不新增 T03。未来真实成员更新结果只允许刷新与entityId相同的当前活动成员,不得切换轨迹或改写宿主页身份。 - 如果 T03 已位于原生栈下方,再次打开同一
genealogyId的 T03 时必须回到该实例,并用目标页允许的类型化一次性操作请求加载目标成员;不得创建第二个 T03。若栈中 T03 属于不同genealogyId,网关返回T03_CONTEXT_CONFLICT并拒绝压栈;若迁移前遗留栈已有多个 T03,则返回T03_STACK_CONFLICT,不得只激活最近实例后宣称栈已唯一。调用方必须先返回或通过根语义清理冲突上下文。 - 从直接链接进入 T03 且没有来源页时,规范父页为带当前成员定位参数的 T01;缺少
genealogyId时继续回退 G01。
20.8 通知和外部进入
- 后端只返回
targetType与该业务所需 ID,不返回客户端路由字符串。 targetType必须由本地固定映射转换为路由键,并先验证登录态、权限、目标存在性和必填参数。- 外部进入先建立正确根页,再打开目标;目标失效时落入可理解的消息状态,不打开空白页面。
- 未知
targetType、多余参数和原始 URL 必须明确拒绝。
20.9 导航完成定义
- 页面和组件中直接调用五种 Uni 导航 API 的扫描结果为零。
- 路由注册表与
pages.json精确闭合。 - 所有根页、普通页、直接进入、完成回流和异常回退满足上述不变量。
- MuMu 对每个受影响流程验证进入、返回、取消、完成和重复进入;T03 验证 A→B→C→B→A→来源且原生栈不增长。
- 每个迁移批次独立运行聚焦合同、全局响应式合同、编译审计和全量合同,不进行机械一次性替换。
二十一、T01 大规模世系树专项设计
21.1 已证实的 P0 与当前上限
当前 T01 不是只在“几百代”时才有风险,默认 6 人数据已经确定性断线:
- 布局得到 103 的横坐标为 550,唯一子女 106 的横坐标为 675。
- 父干线只画在 550,水平支线却只覆盖子女的
minX..maxX,唯一子女时只位于约 675—680,两段完全不相交。 NODE_HALF_HEIGHT=47,普通节点实际半高为 43,只有选中节点半高为 47;普通节点端点天然留下约 4rpx 间隙。- 现有 10 代×12 人压力数据中,约 45/54 个父分组会出现相同父干线断开;测试只断言线段数量大于 30,因此稳定漏报。
- 当前布局每一代都重新过滤全体成员,最坏为
O(G×N),一人一代时接近O(N²);300 代单链约生成 13233 行 CSS Grid track。 - 页面、公共 mock 与 OpenAPI 分别使用
parentId、无关系字段、递归children/spouses+fatherId/motherId,直接接接口可能只显示根节点或完全没有连线。
21.2 五方案终选
评分依次为“业务正确性/全局一致性/回归安全/维护成本/MuMu 可验证性”。
| 方案 | 评分 | 结论 |
|---|---|---|
| 修补现有 CSS Grid 与全量 DOM | 2/4/4/4/5 |
可修眼前断线,无法满足百代与宽支系,拒绝。 |
| 只改为可折叠线性目录 | 4/5/4/4/5 |
作为无障碍和降级模式保留,不能替代图谱。 |
| 全量 SVG 整洁树 | 3/3/3/3/4 |
几何易测,但数千图元和超大世界尺寸风险高,拒绝。 |
| Canvas 画线、DOM 叠节点 | 4/4/2/2/3 |
App-vue 跟手手势中存在跨层不同帧错位风险,拒绝。 |
| 渐进图窗口+视口单 Canvas 同画节点和边 | 5/5/3/3/5 |
最终方案。 |
DCloud 官方文档明确说明 App-vue 的流畅 Canvas 动画应把绘制放在视图层并避免频繁跨层通信;renderjs 只支持 App-vue/H5,Vue 3 项目不能直接在当前 script setup 页面内使用该写法。因此使用普通 Options API 的专用 Canvas 组件承载 renderjs,T01 页面继续负责低频业务编排:
- https://uniapp.dcloud.net.cn/tutorial/renderjs
- https://uniapp.dcloud.net.cn/component/canvas.html
- https://uniapp.dcloud.net.cn/tutorial/performance.html
21.3 产品与页面结构
T01 定位为“以焦点成员为中心的可探索世系窗口”,不是一次性展开整本族谱的手机长图。
- 焦点优先级固定为:路由
selectedId→ 当前账号绑定成员 → 后端主始祖。合法state=EMPTY显示空谱并允许录入首位成员;只有state=POPULATED缺少有效焦点或窗口入口时才显示合同异常,客户端不猜测成员。 - 初始窗口固定为焦点上两代、下两代;这只是首次窗口,不是最大世代。
- 主世系是骨架;配偶并排,每段家庭关系具有独立联合点,子女从对应联合点向下。
- 同一家庭 Scene 在任何状态下最多直接显示 5 名真实子女。投影先沿 primary 入边从焦点反向推导受保护主路径;每个祖先家庭都必须保留通向焦点的那名子女,再用相邻 order 补足最多 5 人,不能把焦点祖先链聚合掉。其余成员形成“前 N 人”“后 N 人”聚合节点;存在隐藏可见成员时点击后以最近的稳定人物 ID 重建窗口,只有隐私成员时聚合节点不可聚焦。原焦点进入会话历史,聚合不在原图无限展开,也不截断后端数据。
- 顶部继续使用
PageHeader,下方为薄上下文工具条;一级操作是搜索和全谱,图谱/列表切换、关系图例、关系维护和阅读帮助进入更多。 - 左侧大面积代际栏改为 Canvas 内轻量标尺;缺代显示“中间缺 N 代”,不能把第 14 世和第 50 世误画成相邻世代。
- 右下控制组提供放大、缩小、适合当前支系和回到焦点,点击区域至少 44dp。
- 资料抽屉只有收起与半屏两档;完整资料进入既有 T03,禁止在 T01 再复制一套全屏人物详情。
- 抽屉吸附完成后才更新视口底部 inset,并把选中节点移入剩余安全区域;不得覆盖节点。
21.4 缩放、定位和概览
缩放范围为 0.30—2.00,默认按当前窗口适配且不自动放大超过 1.0:
| LOD | 缩放 | 内容 |
|---|---|---|
| L0 | 0.30—0.44 | 当前已加载窗口的家庭、支系聚合与数量,不显示姓名。 |
| L1 | 0.45—0.74 | 姓名和隐私、缺失等状态。 |
| L2 | 0.75—1.34 | 标准人物节点、世代、关系和支系。 |
| L3 | 1.35—2.00 | 在标准节点上增加生卒摘要。 |
- 阈值使用 0.05 迟滞,避免临界缩放闪烁;Canvas 文字低于 12sp 时隐藏次要信息,不继续压小字号。
- 双指缩放以手势中心为基点;按钮每次按固定比例缩放;搜索定位后至少进入 L2。
- 单击只选中并打开抽屉;双击、点击居中或搜索结果才改变焦点并重建上二下二窗口。
REDACTED节点只允许显示通用隐私说明,不允许双击、居中、进入详情或成为焦点。 - 当前会话内保留最多 20 条定位记录;只保存
focusPersonId、treeVersion、matrix,不做最近浏览和离线持久化。 - 全谱是独立的“世代×支系”聚合视图,不是把全部人物缩小。它只展示权限过滤后的密度、根、支系覆盖和待归支统计,点击后再请求局部窗口。
- 图谱和线性列表消费同一规范图、焦点、树版本、已加载边界和搜索结果;不存在独立的页内聚合展开状态。Canvas 不可用或无障碍需要时,列表仍能搜索、选择可见成员并进入 T03/T04/T06,隐私成员只显示不可跳转的通用说明。
21.5 四类端点与异常数据
| 状态 | 后端或客户端事实 | 页面行为 |
|---|---|---|
| 未录入 | 后端 boundary MISSING |
连到完整占位端点;有权限才显示补录。 |
| 隐私隐藏 | 后端 boundary REDACTED |
连到匿名端点,不返回可推断身份的信息。 |
| 尚未加载 | 后端 boundary UNLOADED |
显示数量和展开动作,必须携带版本绑定 cursor。 |
| 加载失败 | 客户端 boundary runtime FAILED |
保留原图与 cursor,只局部重试。 |
重复 ID、缺引用、自环、主图循环、多条主入边、反向代际和版本错配不能进入正常布局。部分合法数据仍可展示时显示诊断横幅;不允许通过不画线来隐藏错误。
21.6 传输合同
Apifox 是接口唯一源头,本地 JSON/YAML 只是同版本只读导出。旧递归 LineagePersonTreeView 必须被一个规范窗口原子替换,客户端不保留兼容读取。
LineageGraphWindow
├─ version { schemaVersion, treeVersion, generatedAt }
├─ state EMPTY | POPULATED
├─ genealogyId
├─ nodes[]
├─ familyUnits[]
├─ edges[]
└─ window { focusPersonId, entryPersonIds, scope, generationRange, returnedNodeCount, boundaries[] }
关键字段合同:
- 所有人员、家庭、关系和引用 ID 都是非空字符串;非空
avatarOssId也必须是字符串,未设置头像时只允许null。后端int64不得作为 JSON Number 传给 JavaScript。稳定人物 ID 不得以redacted:开头;匿名人物 ID 精确使用redacted:{treeVersion}:{token},客户端可据此阻止它进入 FOCUS、locator、搜索、写接口和 overview 跳转目标。 LineageGraphWindow以state为 discriminator 使用oneOf。EMPTY 只允许nodes/familyUnits/edges=[],且focusPersonId=null、entryPersonIds=[]、generationRange=null、returnedNodeCount=0、boundaries=[];这是合法空谱,不是合同错误。POPULATED 必须有非空 nodes、引用其中 VISIBLE 节点的非空focusPersonId、至少一个引用当前 nodes 的entryPersonIds、非空 generationRange,且returnedNodeCount === nodes.length。entryPersonIds精确等于“当前返回窗口中没有 primary 入边的节点集合”,不是全谱始祖列表;secondary 入边不会取消窗口入口身份。每个入口节点的entryReason为GENEALOGY_ROOT/WINDOW_CUT/DISCONNECTED_COMPONENT,非入口节点为null;每个入口在当前窗口恰有零条 primary 入边,每个非入口恰有一条 primary 入边,因此从任意焦点沿 primary 必然回溯到某个入口。GENEALOGY_ROOT表示全谱主森林根,WINDOW_CUT表示其 canonical primary 父边在窗口外,DISCONNECTED_COMPONENT表示没有可达全谱根的 canonical primary 链;因此第 50 代焦点的上二代窗口不会被伪装成全谱根。全谱根只由 overview 的可见根/隐私根计数和 locator 的根可见性分支表达。- 人物节点以
visibility为 discriminator 使用oneOf。VisibleLineagePerson固定包含id、generation、displayName、sex、avatarOssId、branchId、branchPath、order、visibility=VISIBLE、entryReason;其id是可用于 FOCUS、locator、搜索和写接口的稳定人物 ID,sex只允许MALE/FEMALE/UNKNOWN。RedactedLineagePerson只允许id、generation、displayName、order、visibility=REDACTED、entryReason,其中 displayName 固定为通用“隐私成员”,id 是仅在当前treeVersion内可作当前图内部引用的不可解析 opaque ID,不得用于 FOCUS、locator、搜索或写接口,也不得进入任何focusPersonId/targetPersonId;不得返回 sex、avatar、branchId、branchPath 或其他身份字段。两类节点的generation都是大于等于 1 的整数,order是非负整数。 FamilyUnit包含稳定id、anchorPersonId、partnerRelationship、partners、order。partners只允许一个单亲成员,或同一伴侣关系中的两人;每项包含personId、partnerRole、order,partnerRole只允许ANCHOR/PARTNER,且恰有一个 ANCHOR 与anchorPersonId相等。双人家庭的partnerRelationship必含relationshipId、relationshipKind=PARTNER、relationType、status,其中relationType为MARRIAGE/PARTNERSHIP/UNKNOWN,status为ACTIVE/ENDED/UNKNOWN;单亲家庭该字段为null。同一人物的不同伴侣关系必须拆成不同家庭单元。ParentChildEdge从家庭单元指向一个子女,包含id、familyUnitId、childId、lineageParentId、parentRelations、primary、order。每个parentRelations项都包含稳定relationshipId、relationshipKind=PARENT_CHILD、personId、parentRole、relationType;parentRole使用FATHER/MOTHER/PARENT/GUARDIAN/UNKNOWN,relationType使用BIOLOGICAL/ADOPTIVE/STEP/GUARDIAN/UNKNOWN。lineageParentId必须同时出现在该边的父母关系和家庭成员中,子女不得同时是该家庭成员;primary 入边与entryPersonIds必须满足上一条的零条/恰一条森林不变量,布局不得根据数组顺序猜测主世系父母。- 所有父子关系都参加缺引用、自环和有向循环校验,不能只检查
primary主森林;primary只决定布局主干,不降低次要收养、继亲或监护关系的数据正确性要求。 - boundary 锚点可以是 PERSON、FAMILY_UNIT 或 WINDOW;PERSON/FAMILY_UNIT 的
anchorId必须引用对应实体,WINDOW 的anchorId固定为null。方向为 ANCESTORS、DESCENDANTS、LATERAL,原因只允许 MISSING、REDACTED、UNLOADED;hiddenCount只能是非负整数或null,cursor 只在 UNLOADED 时为非空字符串,其他原因固定为null。网络 FAILED 是客户端状态。 - 当前窗口内返回但匿名化的人物使用
Node.visibility=REDACTED;窗口之外因权限隐藏的拓扑使用boundary.reason=REDACTED。同一个隐藏对象在同一响应中不得既作为匿名节点又作为隐藏边界,避免重复计数和关系推断。 - 每个窗口对其返回的边保持引用闭包;服务端
limit绝对上限 500。 - cursor 与
genealogyId、treeVersion、boundaryId绑定;版本变化返回409 TREE_VERSION_CHANGED。 - 关系写操作携带
If-Match,成功返回新版本和受影响的人员、家庭及关系 ID。
/lineage/tree 使用两个互斥查询模式,不能由客户端拼出第三种组合:
mode=FOCUS:focusPersonId可选且只能是调用者可见的稳定人物 ID,REDACTED opaque ID 不得作为焦点;缺省时服务端按“当前账号绑定成员→主始祖”确定并在响应中返回实际焦点。不可见、过期或不存在的焦点统一返回404 LINEAGE_FOCUS_NOT_AVAILABLE,不泄漏成员是否存在;ancestorDepth/descendantDepth是 0—20 的整数、可选且默认 2;boundaryId/cursor/treeVersion必须缺席。mode=BOUNDARY:boundaryId/cursor/treeVersion三项必填;focusPersonId/ancestorDepth/descendantDepth必须缺席。- 两种模式的
limit都是整数,最小 1、默认 200、最大 500;任何混合、缺项或多余组合返回 HTTP422与LINEAGE_QUERY_INVALID。接口同时明确400/401/403/404/422/429/5xx响应。
全谱概览不复用窗口的 returnedNodeCount 冒充全谱总数。/lineage/tree/overview 只接受必填查询参数 treeVersion;服务端只返回该快照,版本已变化时返回 409 TREE_VERSION_CHANGED,客户端不得把不同版本的概览与窗口并用。
LineageOverview 包含同一 { schemaVersion, treeVersion, generatedAt } 以及 state、genealogyId、genealogyPersonCount、genealogyRootPersonIds、redactedGenealogyRootCount、generationRange、buckets,并以 state=EMPTY/POPULATED 使用 oneOf。EMPTY 精确要求 genealogyPersonCount=0、genealogyRootPersonIds=[]、redactedGenealogyRootCount=0、generationRange=null、buckets=[]。POPULATED 要求 genealogyPersonCount>0、非空 generationRange 和 buckets,且 genealogyRootPersonIds.length + redactedGenealogyRootCount >= 1;各 bucket 的 visibleCount/redactedCount/unloadedCount 均为非负整数,三类计数在全部 buckets 的总和等于 genealogyPersonCount。
genealogyRootPersonIds 只包含调用者可见、可用于 FOCUS 的稳定人物 ID;隐私根只计入 redactedGenealogyRootCount,绝不返回 opaque ID。bucket 的 focusPersonId 只能是该 bucket 内可见稳定人物 ID;没有可见目标时固定为 null。
定位接口返回 { treeVersion, genealogyId, personId, rootVisibility, rootPersonId, pathCompleteness, ancestorPathSegments, generation, branchId }。根可见性与路径完整性是两个独立 discriminator:rootVisibility=VISIBLE 时 rootPersonId 是可见稳定 ID,rootVisibility=REDACTED 时 rootPersonId=null;pathCompleteness=COMPLETE 表示路径没有隐私缺口,pathCompleteness=REDACTED_GAPS 表示含一个或多个隐私段,允许“可见根→隐私中间祖先→可见目标”。
ancestorPathSegments 以 kind 使用 oneOf:VISIBLE 段精确为 { kind: "VISIBLE", personIds: [...] },数组非空且只含可见稳定 ID;REDACTED 段精确为 { kind: "REDACTED", hiddenCount },hiddenCount 为正整数或因权限不披露时为 null,不得包含任何人物 ID。相邻段 kind 必须交替,整条路径最后一个 VISIBLE 段必须以目标 personId 结束;可见根时第一个 VISIBLE 段必须从 rootPersonId 开始,隐私根时第一段必须是 REDACTED。COMPLETE 恰好没有 REDACTED 段,REDACTED_GAPS 至少有一段。任何分支都不得返回客户端路由或隐私 opaque ID。
21.7 客户端所有者与处理链
OpenAPI 响应
→ normalize
→ validate
→ project
→ 确定性 family-unit tidy-tree layout
→ Scene 与空间索引
→ 平台 Canvas 渲染器
utils/lineage/是规范化、校验、投影、布局和空间索引的唯一客户端所有者。normalize负责按order、id作一次确定性排序,最坏复杂度为O(N log N + E);validate通过索引和一次图遍历保持O(N+E);project只消费已规范化顺序并保持O(N+E),不得在投影阶段重复全量排序。project只从已校验的完整窗口图生成当前可见投影:先由唯一 primary 入边推导焦点到窗口入口的受保护人员与家庭,再为每个家庭选出包含受保护子女的最多 5 名真实子女;无受保护子女的家庭稳定选择 order 最小的 5 人。其余连续区间生成带direction、hiddenCount、focusable、targetPersonId的“前 N 人/后 N 人”虚拟聚合节点;存在隐藏可见成员时focusable=true且targetPersonId指向最靠近当前可见窗口的稳定人物 ID,仅含隐私成员时focusable=false、targetPersonId=null。点击可聚焦聚合节点后以该成员重新聚焦,不存在expandedAggregateIds或不断增长的页内展开状态。完整规范图继续供搜索和线性列表使用;布局只消费投影,Scene 不得再次决定聚合规则。- 布局按
order、id稳定排序,自底向上计算子树轮廓;家庭联合点必须位于可见子女跨度中心,单子女时严格同轴。 - Scene 根对象精确为
{ sceneVersion, treeVersion, focusPersonId, bounds, items }。utils/lineage/scene.js是sceneVersion唯一所有者,按treeVersion + projectionSignature + LINEAGE_LAYOUT_VERSION + fontMetricsVersion生成确定性摘要;Scene 使用世界坐标并只由可 JSON 序列化的数组、对象、字符串、数字和布尔值组成,不得跨 renderjs 边界传递Map、函数、Vue、DOM、rpx、Canvas context、空间索引或平台对象。瞬时selectedId不属于 Scene payload,也不参与sceneVersion。 components/LineageViewport.vue使用普通 Options API;App-vue/H5 的 renderjs 持有 Scene、相机、手势、LOD、裁剪、命中和逐帧绘制。- Canvas 物理尺寸始终等于视口,不等于几百代的世界尺寸;只绘制视口加 overscan 内对象。
- 节点、文字、联合点、关系线、聚合节点、端点、代际标尺和选中光晕全部在同一 Canvas、同一矩阵、同一帧绘制;选中光晕由 renderjs 使用独立
selectedId和 Scene 内稳定人物 Rect 动态绘制,不修改 Scene、布局或版本。 - DOM 只保留 PageHeader、工具条、两档抽屉、搜索、全谱概览和线性列表。
- mp-weixin 复用同一 Scene 的 Canvas 适配器;窗口超过能力或持续不达性能门槛时进入线性列表,不另造关系规则。
- renderjs 读取
scene.sceneVersion后先验证版本与 payload、在视图层重建空间索引,再原子交换 Scene、索引和版本;缺少版本或同一版本对应不同 payload 必须拒绝并保留旧 Scene。平移、缩放与惯性期间零跨层通信;只在手势结束、焦点变化或业务确需边界请求时回传相机状态。 - 视口、底部 inset 或 DPR 变化时先重设 transform,再把 backing store 设为视口乘 DPR、CSS 尺寸设为逻辑视口;字体就绪或回退字体变化后重新测量文字、生成新的
fontMetricsVersion并生成新 Scene,不能沿用旧测量值。
21.8 “连线永不断”不变量
- 每个家庭成员都恰有一个可追踪的
family:{familyUnitId}:partner:{personId}连接器指向联合点;单亲联合点若与人物锚点重合,可以省略孤立的零长度笔画,但关系对象仍可反查。 - 折线相邻段端点在世界坐标中完全相等,DPR 变换后误差不超过 0.5 个物理像素。
- 单子女使用连续路径,不创建孤立零长度横梁。
- 锚点从实际 Scene Rect 计算,不允许全局半高常量。
- 选中态只画外部光晕,不改变节点宽高、布局或锚点。
- 每条
ParentChildEdge恰好对应一个edge:{edgeId}连接器,其起点必须是familyPoint(familyUnitId),终点必须是实际子女人物锚点;Scene connector 也能反查唯一传输 edge。 - 主关系线不得穿过无关人物节点,纯主树的主关系交叉数必须为零。
- 视口裁剪依据路径包围盒;即使两个端点都在视口外,只要路径穿过视口就必须绘制。
- 未录入、隐私、未加载和聚合关系必须连接到明确端点,不允许悬空或静默消失。
- 新窗口通过校验后原子替换旧 Scene;不得用新节点配旧边或新边配旧节点。
21.9 后端最小改动
为避免静默破坏既有 v1 消费者,后端新增并固定以下四条 v2 路径:
GET /genealogy/app/v2/genealogies/{genealogyId}/lineage/tree
GET /genealogy/app/v2/genealogies/{genealogyId}/lineage/tree/overview
GET /genealogy/app/v2/genealogies/{genealogyId}/lineage/persons/{personId}/locator
PATCH /genealogy/app/v2/genealogies/{genealogyId}/lineage/relationships/{relationshipId}
/lineage/tree查询参数固定为mode、focusPersonId、ancestorDepth、descendantDepth、boundaryId、cursor、limit、treeVersion,按 FOCUS/BOUNDARY 互斥规则校验;不再接受branchId、generation。/lineage/tree/overview必须携带当前treeVersion,只返回同版本的代际与支系聚合桶,并只为存在可见稳定人物 ID 的桶提供focusPersonId;版本变化返回409 TREE_VERSION_CHANGED。/locator返回目标人物、根可见性分支、ancestorPathSegments隐私安全分段祖先路径、代数、支系和树版本;REDACTED 段不得返回人物 ID,任何分支都不得返回 opaque ID。- 现有人物搜索只需稳定返回字符串
personId;新增人物、编辑人物和编辑关系接口必须加入版本并发合同。当前 OpenAPI 没有 T06 所需的关系修改入口,因此新增上述关系 PATCH。不可变relationshipKind只允许PARTNER/PARENT_CHILD,必须与服务端既有关系一致;请求使用以该字段为 discriminator 的oneOf:PARTNER 分支只更新relationType/status,PARENT_CHILD 分支只更新relationType/parentRole。每个分支除relationshipKind外至少包含一个可修改字段,省略字段保持原值;只有 discriminator 的空更新返回422 RELATIONSHIP_PATCH_EMPTY。参与人和relationshipKind不可在 PATCH 中偷换,也不以客户端删除再新增模拟修改。 - 旧 v1
/genealogy/app/genealogies/{genealogyId}/lineage/tree保持原合同供既有消费者使用;当前 App 只接入上述四条明确的/genealogy/app/v2/...路径,不双读、不做运行时版本探测。
四条操作统一声明 200/400/401/403/404/422/429/5XX;tree、overview 和 relationship PATCH 还必须声明 409。稳定业务码字段唯一固定为错误响应根层必填字符串 businessCode,对应 HTTP 响应使用 oneOf 分支中的单值 enum;OpenAPI 3.1 也可使用 const。description、example 或无关 metadata 中出现同名文本都不构成合同。generationRange 固定为关闭额外字段的 { minGeneration, maxGeneration },两项均为大于等于 1 的整数;minGeneration <= maxGeneration 由运行时校验。
接口门禁分为两层,不能互相冒充:tests/lineage-openapi-contract.ps1 的静态层只验证 OpenAPI 能结构化表达的路径、参数全集与单字段边界、响应引用闭包、discriminator/oneOf、精确字段集、枚举、字符串 ID、错误响应、If-Match、JSON/YAML 引用一致性以及 v1 隔离。平铺 query Parameter Objects 即使在 OpenAPI 3.1 中也不能证明 FOCUS/BOUNDARY 的跨参数互斥;入口集合、引用、环、bucket 总和、隐私根、locator 分段首尾、cursor/version 绑定、409/422 真实行为及 PATCH 省略字段保持原值同样必须由运行时 validator 与部署集成测试验证,禁止用 description、example 或关键词命中制造假绿。支持 schema 的文件名不由客户端另造,固定根模型之外沿真实 $ref 闭包验证结构;YAML 静态层只证明限定块内的路径、operationId 和完整 component 引用闭包与 JSON 一致,字段语义仍必须在后端同版本导出后逐项复核。
21.10 明确删除的旧路径
同一迁移中删除:页面内置运行时 members、递归 children/spouses、单一 parentId、fatherId/motherId 与 parentId 双读、person.id || person.personId、API 或 fixture 的 x/y、数字 int64 ID、默认“族人/主支”语义兜底、顶层 .map(toTreeNode)、全量 CSS Grid track、多 <view> 拼线、Canvas 线叠 DOM 节点、选中态改尺寸和全局 NODE_HALF_HEIGHT。
21.11 测试与 MuMu 门槛
测试先锁定当前 103→106、普通节点 4rpx 和 10×12 大量父干线断开,再依次覆盖:
- 合法单根、单亲、多配偶、多根、收养、隐私、字符串大 ID 和窗口边界。
- 旧递归结构、
parentId、数字 ID、重复、缺引用、自环、循环、多主入边和版本错配必须明确失败。 - 6 人、10×12、20×20、5×100、100×3、300×1、单家庭 200 子女、缺第 2—49 代、乱序输入和极不平衡树;500 代单链只作纯逻辑校验,不作为 MuMu 手工搜索目标。
- 每条线的锚点、连续段、反查、穿节点、交叉、DPR、视口裁剪和选中前后几何。
- 搜索定位、聚合跳转与返回焦点、版本冲突、焦点保持、Canvas context 恢复和图谱/列表一致性。
当前 MuMu 不启动、不关闭,不调整模拟器窗口缩放比例、设备分辨率、方向或系统字体;App 内双指缩放属于 T01 功能验证。只在现有 emulator-5554 / 720×1280 / 320dpi 配置验证:
- 默认首屏和 103→106;单指平移、双指缩放、按钮缩放及四级 LOD。
- 10、50、100、300 代搜索或概览定位。
- 宽支系聚合、前后人数、聚合跳转、回到上一焦点,且受保护祖先链始终连续。
- 多配偶联合点及子女归组;未录入、隐私、未加载和局部失败端点。
- 两档抽屉、T03/T04/T06/T07 返回定位、长姓名、同名搜索、空谱、无权限和异常关系。
- 60 秒连续操作、20 次聚合跳转与搜索,以及自动几何探针和同场景逐边视觉探针;两者都为零违规才通过。
性能门槛:单窗口不超过 500 人;dataReady 定义为规范图完成校验的时间点,interactive 定义为首帧绘制结束且命中索引可用,二者间隔不超过 800ms;输入到反馈 p95 不超过 100ms;持续手势帧耗时 p95 不超过 32ms;不连续出现两帧超过 100ms;20 次聚合跳转与搜索后驻留内存相对稳定态增长不超过 15%。每项 MuMu 时延指标至少采样 30 次并按 nearest-rank 计算 p95;手势帧由 renderjs 的 requestAnimationFrame 记录,输入延迟从视图层触摸时间戳量到下一完成帧;内存先预热 3 轮,再在相同空闲点比较 20 轮。不把全 App 冷启动混进 T01 门槛。未达到门槛时缩小窗口或进入列表,不能提高上限掩盖问题。
二十二、认证、TAC 与可访问安全设计
22.1 唯一所有者
utils/auth-verification.js唯一拥有APP_SMS_LOGIN/APP_REGISTER/APP_FORGOT_PASSWORD三个认证场景、4 位短信码和 require/verify 响应边界。components/TacVerification.vue唯一拥有 TAC 浮层、renderjs 加载、可见生命周期、焦点和返回行为;页面不得直接操作供应商全局对象。static/tac/js/jiapu-tac-adapter.js唯一拥有 TianAi challenge、proof 与 verify payload 映射。后端提供的tac.css/tac.min.js/icon.png/dun.jpeg保持原字节,由专项哈希合同保护。utils/api.js唯一拥有已审查请求的 HTTP 200 严格 envelope、15 秒超时、RequestTask 中止和错误分类,并拥有认证密码摘要、validToken发码、AppLoginVo.access_token会话写入及反馈 wire payload。页面不复制 header、地址、响应兼容、token 读取或请求控制器。
22.2 页面与状态机
A01 默认密码登录并已接入线上密码 wire、access_token 持久化和 G01 跳转;默认 mode 保持 mock。remote 联调先展示 static TAC,但登录端点尚未消费 validToken,所以这不是安全闭环,API-AUTH-TAC-001 仍阻止发布;微信登录也不得以视觉入口冒充已接通。A01/A04/A05 的短信发码顺序固定为:本地字段和协议校验 → /captcha/require → 严格 TAC challenge/verify → 取得服务端 validToken → /genealogy/app/auth/sms/code。验证码精确 4 位,注册或重设提交只消费短信码,不再重复 TAC。手机号或密码变化、刷新 challenge、切换验证方法、返回或卸载都使旧上下文失效;超时、空响应、非 JSON、重复回调和迟到回调失败关闭并保留表单。
短信状态覆盖可发送、验证中、发送中、60 秒倒计时、失败重试、到期、手机号变更和重复点击。客户端倒计时不是服务端限流证据;最终仍须用真实服务验证手机号/IP/设备/租户限流、前后台恢复、过期和并发重放。注册成功只读取 RAppLoginVo → AppLoginVo.access_token,保存会话后直接进入 G01;找回成功返回 A01,不自动登录。
22.3 后端验证中心合同
VerificationCenter 是唯一授权 owner,供应商只提供 evidence,不直接签发或消费短信票据。/captcha/require 建立绑定 tenant、client、scene、规范化 subject 和风险策略版本的 verificationSessionId;required=false 也直接签发供指定 audience 使用的一次性 grant,不能让短信端点出现无票据分支。同一 session 只允许一个活动 challenge;刷新或切换方法使旧 challenge 失效但不清零累计失败。
/captcha/verify 的请求以 evidence/provider 为 discriminator 使用 oneOf,根和每个 payload 都 additionalProperties:false。provider、type、scene 和 subject 以服务端 challenge 记录为真,客户端字段不能改变绑定。只有供应商 evidence 与本地风险策略共同通过才签发 opaque validToken;票据绑定 session、tenant、client、scene、subject、challenge、method、assurance、audience 和过期时间。/sms/code 在同一事务中完成 ISSUED → CONSUMED 与唯一短信 outbox 创建;同一幂等键返回原结果,不同键或并发重放不能产生第二条短信任务。
当前后端门禁固定为:API-AUTH-TAC-001 补齐密码登录票据;API-AUTH-TAC-002 修复真实 challenge 空 500 与错误 envelope;API-AUTH-TAC-003 关闭验证 schema 并建立 discriminator;API-AUTH-TAC-004 落实 session、多方法、required=false 票据和原子消费。AUTH-TAC-OPENAPI-CONTRACT BLOCKED 解除前,runtimeConfig.mode 必须保持 mock。
22.4 不降风控的可访问路径
TianAi 指针滑块不能因外层 dialog 可聚焦就被宣称为 TalkBack 或键盘可完成;不得生成固定键盘轨迹,也不得检测辅助技术后免验证。不存在 accessibility=true、skipCaptcha、供应商故障放行或客服直接发短信等旁路。
三人交叉质询后的统一 P0 是 provider-neutral 验证中心与可恢复的文字/中继 MANUAL_REVIEW。文字渠道只是可访问通信媒介,各 scene 仍有独立身份或号码控制证据。案件继承且不可改写原 session 的 subject/scene,具备去重、RBAC、主体/IP/设备/审核员限额、完整审计、服务时段、容量和 SLA;高风险找回或换号双人复核。坐席只提交决定,验证中心才可签票;异步案件和批准授权在合理期限内可恢复,用户重新进入原流程时才激活短时 token,避免在通知前过期,也不得要求残障证明。
中国大陆非交互风控供应商只进入限时 POC;必须在真实 UniApp Android WebView 中证明 TalkBack、外接键盘、Switch Access、弱网、超时、异常/重放票据、误杀和攻击拦截指标,达标后才可成为默认自动路径,不能预先宣称符合无障碍标准。音频验证码只作为另一个 POC 候选,必须验证可懂度、听障覆盖与 ASR 对抗,不能单独上线或成为唯一替代。设备断言只有在同一 subject 的已认证会话绑定硬件保护私钥、服务端 nonce、RP/App 绑定、userVerification=required、短时单次、防重放并检查撤销时才可独立放行;普通设备指纹、完整性或仅 user-presence 只能作为风险信号,注册和未绑定设备不得使用。
客户端现有壳层只完成 dialog 命名、说明关联、初始聚焦、Tab 圈定、Escape/Android 返回、焦点恢复、原生刷新/关闭、48px 目标和小视口滚动。最终必须在 MuMu 用 TalkBack、外接键盘与非拖动路径完成 A01/A04/A05;证据缺失时 ANDROID-AUTH-ACCESSIBILITY-RELEASE BLOCKED 必须保持红灯。
二十三、领域上下文与反馈提交设计
utils/genealogy-context.js 是当前家谱词法 ID 与失效 tombstone 的唯一持久 owner,utils/session.js 是账号令牌边界。只在首次且没有历史上下文或失效标记时自动选择首个可用家谱;调用方给定的新列表不再包含历史 ID,或显式目标不可用时,必须清理 ID、写入 tombstone 并让用户明确重选,后续重载不能用列表第一项继续渲染。令牌损坏、退出和账号切换都同步清理 ID 与标记;导航一次性结果不得承担领域持久化。实际后台撤权仍需 workspace 在 onShow 或失效事件中重新取得服务端列表,当前基础不能被表述为实时权限刷新已完成。
M07 的唯一接口 owner 是 appApi.submitFeedback。请求只允许 feedbackContent/feedbackType/contactInfo 三个普通字符串数据字段;内容必填,类型和联系方式可选且无客户端枚举映射;请求选项不能覆盖认证要求。remote 只接受 POST /genealogy/app/feedback 的 HTTP 200 与整数成功 code,反馈响应未声明 data 必填且页面不消费实体;mock 必须抛 WRITE_UNAVAILABLE。页面提交中锁定表单并捕获规范快照;成功和 uncertain 回流都再次核对当前快照,保留回执或未知结果并阻止原样重复写,迟到输入保持为尚未提交的新内容。超时、断网、意外 2xx/3xx、HTTP 408/5xx 或响应失真进入 uncertain,只有确定拒绝允许重试。本地不持久保存反馈原文或哈希 tombstone;跨重启 at-most-once 必须由未来服务端幂等键或状态查询合同解决,不能以隐私数据、无期限锁或伪保证替代。
23.1 家谱工作区只读合同
家谱工作区第一批唯一选择 GET /genealogy/app/genealogies/mine(appListMyGenealogies)持有当前账号可访问集合,选择 GET /genealogy/app/genealogies/{genealogyId}/overview(appGetGenealogyOverview)持有 G05 展示;两个 operationId 全局唯一,路径只开放 GET 且无 request body,语义重复的 GET /{genealogyId} 不同时接入。页面不把 fixture 与远端事实混用,也不在两个详情响应之间择优补字段。
线上 AppGenealogyVo.genealogyId 当前是 JSON integer/int64。最大合法 int64 经 JavaScript JSON 解析后会不可逆失真,事后 String() 无法恢复,因此响应身份与 overview path 都必须引用非空、1—128 位 URL-safe 词法 GenealogyId;仅拒绝 unsafe number 可以失败关闭,却会使 OpenAPI 合法用户永久不可用,只能用于关闭功能开关的预研,不能作为正式兼容方案。
首批最小实体只消费 genealogyId/genealogyName/canView/canManage/canEditContent/roleType。RListAppGenealogyVo/RAppGenealogyVo 都是关闭额外字段且精确只有 required code/data 的成功 envelope,code 固定 200,data 分别为 AppGenealogyVo[] 与单个 AppGenealogyVo;实体上述字段进入 required,ID 与名称分别引用 GenealogyId/GenealogyName,三项 capability 为非 null 布尔值,roleType 提供至少两个稳定非空枚举供 G01 分组和标签。AppGenealogyVo 其他字段保持可选:地点、堂号、人数、简介等有值才展示,未知额外实体字段允许忽略。若产品坚持保留当前 fixture 的来源、管理者、认证、始祖、上级谱、支系、更新时间和激活人数,则后端必须另补合同;首批不为复刻 mock 强制 22 个字段全部必填。
canView 是进入 availableIds 的授权投影,不能从 roleType/status/memberStatus 猜值;/mine 以 x-current-account-viewable-only=true 和 x-revocation-policy=OMIT_ONLY_AFTER_CONFIRMED_ACCESS_LOSS 机器声明只返回当前账号仍可查看的家谱。G01 仅在新列表成功校验后 reconcile;网络、超时、5xx 和畸形响应保留旧现场并显示错误,成功列表确认消失才写 tombstone。G05 每次加载先清空旧数据、取消迟到请求,只消费 /overview;对象无权、撤权与不存在统一 NON_DISCLOSING_GENEALOGY_NOT_AVAILABLE,能力缺失按最低权限处理,不回退本地角色。
当前线上 OpenAPI 没有有效 security 声明且媒体写作 */*,但无令牌实测三条读取均被拒绝、实际 Content-Type 为 JSON,因此两项是必须修复的发布文档质量问题,不单独声称已经公开泄漏。生产合同不再允许 HTTP 200 包业务错误:两条读取都 required SaToken 与 1—128 位非空 clientid,只允许 typed application/json;/mine 精确 200/401/429/500,/overview 精确 200/400/401/404/429/500,分别固定 GENEALOGY_ID_INVALID/AUTH_REQUIRED/GENEALOGY_NOT_AVAILABLE/RATE_LIMITED/GENEALOGY_WORKSPACE_UNAVAILABLE。每个响应必须包含固定单值 enum Cache-Control: private, no-store,429 另有 1—300 秒 Retry-After;只允许 traceparent/tracestate/x-request-id/x-correlation-id 四种非语义 tracing header,禁止 ETag 与其他语义 header。component parameter/response/header/schema 解析只接受 exact local #/components/<section>/... 引用,拒绝外部或错分区 $ref 冒充 owner;四种 tracing header 的 component 引用也必须 exact local,内联时只接受 allowed-key 关闭且带非 null 的 string schema 的 Header Object。clientid、成功/错误 code/data/businessCode、实体字段与 role 枚举都用 allowed-key 集拒绝 nullable、readOnly/writeOnly、冲突组合或其他未拥有的验证关键字。真正发布门禁还包括有效令牌下的无凭证、跨账号、撤权、删除、typed HTTP、畸形 JSON 与取消反例。
tests/genealogy-workspace-openapi-contract.ps1 以结构化 JSON 为判断对象并先要求受保护 JSON/YAML 语义深比较通过,唯一拥有上述 path/method/operationId、SaToken/clientid、词法参数、响应身份、typed envelope、required cache headers、required 与 capability/role 边界;它全局递归扫描 APP GET 的 200 schema 闭包,除 /overview 外任何返回 RAppGenealogyVo/AppGenealogyVo 或结构等价单谱实体的旁路详情都失败,/mine 的数组投影不误算成单谱读取。当前旧双导出与线上文档都不能通过。G11 门禁只消费该门禁的 PASS 结论,不复制 overview 合同。门禁关闭前不建立 G01/G05 专属字段 adapter,不让客户端成为第二份猜测合同。问题固定为 API-GENEALOGY-WORKSPACE-001 无损身份与最小 schema 闭包、API-GENEALOGY-WORKSPACE-002 可访问集合与能力投影、API-GENEALOGY-WORKSPACE-003 typed HTTP、缓存与对象级授权行为。
23.2 M06 帮助内容只读合同
M06 第一批唯一选择 GET /genealogy/app/help-articles 持有完整 FAQ 列表。线上 HelpArticleVo 已同时包含 helpCategory/helpTitle/helpContent,所以单页手风琴不再调用 /{helpId};详情端点只保留为未来文章深链或其他 surface 的候选,不进入当前页面。这样当前页面不读取、比较、缓存、序列化或回传 helpId,线上 JSON integer/int64 虽仍是未来详情债务,却不会参与 M06 身份或行为。
成功响应必须由 RListHelpArticleVo 唯一拥有,并把 code/data 设为 required;data 是允许为空的 HelpArticleVo[]。每行只要求 required、非空的 helpCategory/helpTitle/helpContent:分类是可直接展示的标签,正文首版明确为纯文本,数组只含当前可发布文章且顺序就是展示顺序。分类 enum、详情 ID、封面、浏览量、状态和排序字段都不由客户端消费;“全部”是唯一客户端分类值。富文本、Markdown、图片和文章详情只有在另立格式与内容安全合同时才可进入。
adapter 必须使用 allowlist 投影为 {category,title,content},不得 spread 原对象。每次合法响应先按原始顺序分配仅限当前 generation 的 ordinal 展示键,再进行搜索和分类;筛选后的 index 绝不能成为 key。搜索、分类、刷新或原子替换列表前清空展开项;唯一列表 controller 与 generation 共同拒绝取消后的迟到响应。重复标题或正文不会产生 key 冲突,也不能据此生成持久身份。
页面状态固定为加载、正常、服务端空列表、搜索/筛选无结果、失败可重试和认证失效;失败不得回退成本地五条内容并冒充线上成功。搜索只匹配标题与纯文本正文,分类从返回标签按首次出现顺序动态去重。分类与标题使用原生按钮语义并满足至少 44dp 触控目标;分类补 aria-pressed,问题补 aria-expanded/aria-controls,加载、计数、失败和展开状态提供适当播报。最终系统字号、TalkBack、焦点和视觉只能在 MuMu 验收。
当前受保护 JSON/YAML 仍把列表写成通用 ListResult/RList;线上虽已出现 RListHelpArticleVo/HelpArticleVo,两者仍无 required,正文格式、仅发布与排序保证也未定义。匿名实测列表、带分类列表和详情均为 HTTP 200+{code:401,data:null},而线上文档声明 HTTP 401 string 且 operation 没有有效 security。后端可以选择规范 HTTP 401 或稳定业务 401,但文档、部署和 validator 必须一致。tests/help-center-openapi-contract.ps1 当前输出 HELP-CENTER-OPENAPI-CONTRACT BLOCKED;问题固定为 API-M06-001 专用响应与最小 required 闭包、API-M06-002 纯文本/发布范围/顺序语义、API-M06-003 认证与错误承载一致性。门禁通过前不写 live-only adapter。
23.3 个人资料只读合同
GET /genealogy/app/auth/profile 是 M01 身份卡、M02 表单初值和 M03 绑定手机号展示的唯一 wire owner。首批不消费 userId/avatar/status/tenantId/userNo/sex/birthday/registerSource/loginIp/loginDate/clientKey/deviceType;数字 ID 与状态字典因此不构成本批门禁。M01 当前显示的“创建者”是家谱级角色,不属于账号 profile,必须删除而不是向后端索要错误字段。
成功响应固定为 RAppProfileVo → AppProfileVo,envelope 的 code/data required。wire 实体仅把 phone 设为 required,并要求 canonical ^1[3-9]\d{9}$;nickName/realName/email 可选,省略是唯一“未设置”形态,出现则拒绝 null、空串和边界空白,姓名为 1—30 字符,邮箱为有效格式且 1—100 字符。把四项全部 required 会让没有实名或邮箱的合法旧账号拖垮 M01/M03;省略可选字段后由 adapter 统一为空串,不是旧字段兼容或双模型。GET 只定义读取;未来 PUT 必须另证省略字段是否保持原值并只发送 dirty fields,不能在本批预判。
唯一 profile normalizer 在 mock 与 remote 中输出固定 {maskedPhone,phoneAccessibleLabel,nickName,realName,email}。原始手机号只在函数局部完成格式校验和掩码,页面、缓存、错误详情、日志与路由不得出现明文;可选字段只有真正缺席才规范成空串,出现但非法时整份响应失败。所有其他响应字段使用 allowlist 丢弃。appApi.getProfile 必须经 resolveRuntimeMode() 区分 mock/remote;非法配置失败关闭,remote 走严格 envelope、15 秒超时和 request controller,不以 hasRemoteConfig() ? remote : fixture 把错误配置伪装成本地数据。三个页面可以各自读取同一 adapter,但不建立跨会话缓存。
M01 在 onShow 刷新并以 controller+generation 拒绝迟到结果;资料 loading/error 只替换身份卡,服务菜单和底栏不因普通读取失败而消失。旧 query.state=error 与“重新查看即 ready”是假状态,接线时删除;通知未读数在通知域接通前不得与真实 profile 混装为线上事实。M02 首次加载成功后再填充三个字段并重设 baseline,加载不能制造脏表单;普通 GET 失败可重试,编辑期间不后台刷新覆盖输入,保存仍明确是本地校验,不能冒充 PUT。M03 只让手机号行局部加载/失败,密码入口不依赖 profile;M05 的当前手机号展示在真正 remote 前也必须消费同一掩码 owner 或隐藏,不能保留 fixture。
账号失效交给会话 owner,普通 network/timeout/5xx/畸形响应只产生可重试读取失败,没有写请求的 uncertain 状态。实施测试必须证明 optional 缺席、出现非法值、明文不泄漏、超大 userId/avatar 丢弃、错误配置、取消、迟到回调、M01/M03 局部失败和 M02 baseline。交互同时改为原生按钮,补加载播报、M02 字段错误关联与聚焦、44dp 目标、长昵称换行和装饰图隐藏;最终系统字号、TalkBack、纹理对比和返回流程仍只由 MuMu 验收。
当前受保护双导出仍引用通用 ObjectResult/RObject;线上 RAppProfileVo/AppProfileVo 没有 required、phone pattern 或姓名/邮箱边界,GET operation 也缺有效 security/clientid。匿名真实响应是 HTTP 200+业务 code=401,文档却列 HTTP 401 string。tests/profile-openapi-contract.ps1 当前输出 PROFILE-OPENAPI-CONTRACT BLOCKED;问题固定为 API-PROFILE-READ-001 专用响应与最小字段闭包、API-PROFILE-READ-002 可选字段与隐私投影、API-PROFILE-READ-003 认证/媒体/错误一致性。门禁通过前不写 live-only adapter。
23.4 通知读取与已读状态合同
通知必须分成读取批次和写入批次,不能为了让页面看起来完整而在同一个本地 clone 中伪造读写闭环。读取只由不带 readStatus 筛选的 GET /genealogy/app/notifications 与 GET /genealogy/app/notifications/unread-count 持有;前者返回当前账号完整活动通知集合,集合最多 200 条、按最新优先,后者精确统计同一集合中 readStatus=UNREAD 的条目。两个独立请求之间若有新消息或多端读状态变化,短暂不相等是合法并发结果,页面分别展示成功响应并在下一次刷新收敛。
读取成功的最小 wire 形状为 RListNotificationVo → NotificationVo[] 与 RNotificationUnreadCount → int32。两层 code/data required;计数范围为 0—200。通知首批只 required noticeTitle/noticeContent/publishTime/readStatus:标题 1—50,正文 1—1000、完整且为 plain text,时间为带时区的 RFC3339,状态精确枚举 READ/UNREAD。客户端不解释 HTML、Markdown、服务端 URL 或 bizType,也不从标题、类型或摘要猜业务目标。N01 只将摘要显示限制在最多 160 个 Unicode 字素,N02 必须持有同一响应中的未截断正文。
读取 adapter 首版公开模型固定为 {snapshotKey,title,content,publishedAt,unread}。snapshotKey 使用成功响应 generation 与映射前 ordinal 组成的词法 key;裸 ordinal、数组筛选后 index、服务端 int64 ID 和内容哈希都不能成为页面身份。唯一内存快照不可变且不落盘;成功刷新原子替换 generation,退出或账号切换立即清空。N02 在进入时解析并持有该 generation 的完整条目,应用重启、旧 generation、直接构造或未知 key 统一显示“请返回消息中心重新打开”,不能虚构“服务端已删除/已撤回”。
读取批次显式丢弃 notificationId/genealogyId/senderUserId/senderPhone/bizId/bizType/noticeType 等服务端字段;N01 的通用审核 CTA、空态 G10 按钮和 N02 目标按钮一并删除。只有后端以后提供闭合的 bizType → route key+必填词法参数+权限/失效语义 字典,并为每种目标给出越权、删除和跨谱反例,才能另立目标分流批次;任何原始 URL 都不得执行。M01 与 G01 共同消费未读数 owner,文案只称“未读消息”,可见数大于 99 时显示 99+,可访问名称仍包含真实数量;加载或错误只影响各自消息入口,不锁死根页其他功能。
写入批次由 POST /genealogy/app/notifications/{notificationId}/read 与 POST /genealogy/app/notifications/read-all 唯一持有。此时唯一 notification controller 可以从同一个严格列表响应私有保留 notificationId,但页面模型、路由、日志和持久缓存仍只见 snapshotKey;ID 必须在列表实体与 path 中同为 1—128 位 URL-safe opaque string,禁止 int64 经 JavaScript 解析后再转字符串。该迁移必须原子删除首版“完全丢弃 ID”的内部实现和 N01/N02/M01/G01 所有 fixture 计数与本地 clone 写入口,不保留双 owner。
单条已读和全部已读对当前账号必须幂等,重复调用成功且无重复副作用。认证失效返回稳定 401;不存在与跨账号单条 ID 统一为 404 NOTIFICATION_NOT_AVAILABLE,避免泄露实体存在性。read-all 的截止点是服务端接收该请求时当前账号已经存在的活动通知,截止点之后并发到达的消息保持未读;成功后客户端重取列表和未读数,不用本地递减猜结果。超时、断网、HTTP 408/5xx 或畸形响应属于结果未知,依靠幂等重试或重新读取收敛;明确 4xx 才是确定拒绝。
当前受保护双导出把列表返回写成通用 ListResult/RList,完全缺少 unread-count 路径与专用通知模型;线上虽出现专用 VO 和四条操作,但 wrapper/VO 无 required,readStatus 无枚举、内容无长度/纯文本/完整性、列表无容量和顺序,ID 仍是 int64,operation 也缺有效 security/clientid。匿名 list/count 实测均为 HTTP 200+业务 code=401,与文档 HTTP 401 string 冲突。tests/notification-read-openapi-contract.ps1 和 tests/notification-read-state-openapi-contract.ps1 当前分别输出 NOTIFICATION-READ-OPENAPI-CONTRACT BLOCKED 与 NOTIFICATION-READ-STATE-OPENAPI-CONTRACT BLOCKED;问题固定为 API-NOTIFICATION-READ-001—003 和 API-NOTIFICATION-STATE-001—003。双门禁通过前不写 live-only adapter,也不把本地已读行为称为成功。
23.5 M02 个人资料写入合同
M02 继续只使用 PUT /genealogy/app/auth/profile,不同时发布 PATCH 或第二写入口。由于页面只拥有 nickName/realName/email,PUT 必须在 operation 与专用 AppProfileMergeUpdateBody 中明确是原子 dirty-only merge,而不是整个 profile replacement:只允许出现 1—3 个真正改变的可编辑字段,出现字段更新、省略字段保持,额外字段关闭;同一 payload 重复设置相同值不能产生重复通知或其他业务副作用。后端若无法证明 presence-aware merge,就必须先替换现有操作,客户端不能从“字段 optional”推断安全。
nickName 出现时必须是去边界空白的 1—30 字符,空串和 null 均非法;这允许没有昵称的旧账号只修改其他字段,但不允许把已有昵称删除。realName/email 的精确空串只在 request command 中表示清空,省略仍表示保持;非空值分别满足 1—30 和 email 格式/1—100。null、纯空白、边界空白都非法,服务端不以隐式 trim 把空白偷偷解释成 clear。GET 和成功响应中,清空后的可选字段继续以属性省略表达,不能把请求命令形状泄漏回读取模型。
并发只使用一个 opaque profileVersion owner。AppProfileVo.profileVersion required,值为 1—128 位 URL-safe string;PUT required If-Match 携带同一 token,请求 body 不重复版本。后端以当前 principal+tenant+version 原子 compare-and-set,成功 200 返回完整 canonical RAppProfileVo 与新版本;旧版本返回 409,唯一稳定码固定为 PROFILE_VERSION_CHANGED。本项目 T01 已使用 body version+If-Match+409 模式,资料写入沿用同一并发语义;H5 CORS 必须允许 If-Match。
客户端先依赖 23.3 的 GET 取得 canonical 初值和版本,完成异步回填后建立 baseline。normalizeProfileUpdate 只从普通自有数据属性提取脏字段,禁止 getter、symbol、prototype、avatar/sex/birthday 和额外属性;clean 不发请求。提交开始时冻结三个输入并捕获规范快照、版本与 session generation;成功只接受严格 HTTP 200 typed envelope,以返回模型原子回填表单和 baseline。确定的 400/401/409/422 保留草稿并进入对应状态,409 不静默覆盖。
超时、断网、408/5xx、取消或畸形成功响应都是 outcome unknown。客户端不得自动重复 PUT,而先重新 GET:若本次所有脏字段均等于提交值,视为已提交;仍等于旧 baseline 才可由用户重试;出现第三值或版本无法归因则进入 conflict,并保留当前草稿供用户明确选择重新载入或基于最新值继续。页面卸载、退出和账号切换中止等待并清空未持久草稿;RequestTask 取消不证明服务端没有落库,旧 session generation 的迟到响应永远不能污染新账号。
M02 当前把 currentUser.name 同时填入昵称和真实姓名,是必须删除的 PII 伪造;500ms 定时器只形成本地预览,不是接口状态。真正实施时状态至少包括 loading、ready/dirty、saving、success、error、uncertain、conflict、auth-expired;保存期间锁定输入和返回,成功重置 dirty,失败保留草稿。头像假按钮、相册权限和 int64 avatar 不混入本批;“邮箱用于接收通知”在验证/送达合同缺失时删除。原生输入和按钮补齐 aria-invalid/aria-describedby、错误关联、首错聚焦、busy/status 播报、email 键盘类型与 44dp 目标,最终只由 MuMu 验收。
受保护双导出仍使用旧 ProfileUpdateBody,缺 realName/email,示例还出现 schema 外 regionCode/addressDetail,成功为 generic RObject。线上改为 AppProfileUpdateBody → RAppProfileVo,但 body 无 required/minProperties/关闭额外字段/merge/version,响应无 required,operation 无 security/clientid,媒体还是 */* 且只列 200/401。tests/profile-update-openapi-contract.ps1 当前输出 PROFILE-UPDATE-OPENAPI-CONTRACT BLOCKED;问题固定为 API-PROFILE-UPDATE-001 唯一 merge owner 与字段命令、002 版本并发和 409、003 typed 响应/认证/错误/CORS、004 超时对账与账号隔离。读取和写入门禁都通过前不接 M02 真实保存。
23.6 M10 当前设备退出合同
DELETE /genealogy/app/auth/logout 唯一语义是撤销 Authorization bearer 所属的当前设备 credential family,包括同一登录会话的 refresh 能力(如果未来存在);同账号其他设备保持有效。“退出全部设备”必须是另一条接口、另一层确认和另一份权限合同。200 只在撤销已经传播到所有鉴权节点、旧 access token 不能再访问任何受保护接口且旧 refresh 不能换取新 token 时返回;已经在鉴权前通过的并发业务请求无法回滚,产品文案不得承诺取消所有进行中操作。
DELETE 没有 body,required SaToken 和与 token client 绑定的非空 clientid。为形成唯一幂等成功出口,凡能验证为该 client 历史签发的 active、revoked 或 expired credential,重复调用都返回相同的 HTTP 200 RVoid,不产生额外副作用;伪造、格式非法和 client 不匹配才返回 401 RLogoutRejected,稳定码只允许 TOKEN_INVALID/TOKEN_CLIENT_MISMATCH,它们不是撤销成功。200/401 均为 application/json 且 Cache-Control: private, no-store,RVoid 的 integer code required,同时声明 400/429/500。
客户端唯一 owner 是服务级 logoutCoordinator,而不是 M10、A01 和 session 各存一份。一次同步临界段按顺序:捕获 A 的 token/clientid 和当前 session epoch;通过 session owner 立即 bump epoch 并且只清一次 token、家谱上下文与所有已登记账号缓存;使用显式 A 快照创建不绑定页面 controller 的 DELETE RequestTask;发布非敏感 attempt 并立即 goRoot(A01)。该同步段没有 await,创建请求同步失败也不恢复 token。M10 页面永远拿不到 token,请求不会因 M10 卸载或 reLaunch 主动 abort。
异步 callback、catch 和 finally 只能按 attemptId 更新 coordinator,绝不能再次 session.clear();否则 A 的迟到响应会抹掉随后登录的 B。coordinator 的公开内存态固定为 {attemptId,logoutEpoch,status},status 为 pending/confirmed/unconfirmed/not-revoked,不含 token、header、响应 payload 或服务端消息。最多允许用同一 A 快照做一次同进程短时有界重试;不写 storage、日志或持久队列,也不从 session 重新取 token。
A01 仅在 session 仍为空、epoch 与 logoutEpoch 相等时订阅并原子消费一次状态;B 登录或 epoch 改变后丢弃 A 的迟到提示。进程被杀允许丢失提示,但本地退出已经持久完成。唯一主句始终是“已从本机退出”:pending 补“正在结束服务器会话”,严格 200 为 confirmed;network/timeout/408/429/5xx、畸形响应或 generic 401 为 unconfirmed;typed 401/400/403 为 not-revoked。所有分支都留在 A01、不恢复 token、不返回 M10、不阻塞重新登录;状态必须可播报且不能只靠短 toast 或颜色。
M10 确认前的取消和 Android 返回只关闭确认层,零请求、零清理;确认后立刻退出可关闭弹层态并防双击,根导航失败时显示“本机已退出”遮罩且禁止返回受保护页面。当前文案“本机保存的密码不会被保留”没有任何密码存储 owner 证据,实施时改为“仅退出此设备,其他设备不受影响”。Mock 或错误 runtime config 也必须完成本机退出,并准确标记远端未确认,不能为了发请求把用户困在本地会话。
当前 M10 只做 session.clear → close → goRoot(A01),本机边界正确但没有远端撤销、重复保护、epoch 或跨根提示;旧静态测试也锁定这条本地序列。受保护双导出有 DELETE、SaToken、clientid 和 200 RVoid,但缺范围、幂等、required 和错误;线上只有 200/401 */*,又缺 operation security/clientid。tests/logout-openapi-contract.ps1 当前输出 LOGOUT-OPENAPI-CONTRACT BLOCKED;问题固定为 API-LOGOUT-001 当前凭证族与撤销传播、API-LOGOUT-002 幂等成功/拒绝模型、API-LOGOUT-003 安全/媒体/no-store/部署反例。门禁通过前不新增 logout API 代码,也不把本机退出写成服务端注销成功。
23.7 M04 登录态改密与统一密码凭证合同
当前本地双导出要求 oldPassword/newPassword 为 32 个十六进制字符的 MD5,线上 AppPasswordChangeBody 也只把它放宽为大小写十六进制;登录、注册和找回同样接受静态摘要。该摘要不是一次性 proof,而是后端登录入口直接接受的密码等价物,一旦从日志、代理、调试记录或终端泄漏即可重放;同时服务端看不到原始长度与 blocklist 命中,无法成为生产策略 owner。新合同不得长期双读 raw/MD5,也不得只改 M04 留下其他入口。A01 登录、A04 注册、A05 找回和 M04 改密必须同一批删除全部 MD5 wire fallback;只在认证 HTTPS 通道提交 raw writeOnly 密码,服务端使用独立盐与 Argon2id,无法使用时才采用合规 scrypt/PBKDF2。
密码 schema 只有两个 owner:CurrentPasswordSecret 原样、不 trim,允许 1—64 Unicode code point 以兼容已有账号;NewPasswordSecret 在明确 NFC 规则后为 15—64 Unicode code point,允许空格、Unicode、粘贴和密码管理器,不设置字母/数字/符号组成规则。注册、找回与改密新值共用后者;登录与改密当前值共用前者。服务端在写入前执行常见/泄露密码 blocklist、账号级限速、当前密码重新认证和新旧不同;客户端只镜像即时提示,不能成为可绕过的权威。TAC 是反机器人证据,不代替当前密码;用户要求的登录/注册/找回 TAC 继续由认证合同持有,正常 M04 不另建一套 TAC。
M04 的唯一会话方案是 ALL,而不是返回并安装新 token,也不是保留旧 bearer。严格 HTTP 200 RVoid 只能在新 verifier 已持久化、账号 credentialEpoch 已原子递增,并且包括请求者在内的所有设备、所有 client 的既有 access/refresh/renewal 会话已跨鉴权节点失效后返回。方案 B 会要求 typed 新 token 和响应丢失恢复协议,当前 RVoid 无法承载;方案 C 会让可能被盗的旧 token 在改密后继续有效,均被否决。两个使用同一旧密码的并发请求必须通过服务端 CAS 至多一笔成功,另一笔返回 typed 409 CREDENTIAL_VERSION_CONFLICT。
PUT required SaToken、非空 clientid、application/json 和关闭额外字段的 PasswordChangeBody;body 只含 oldPassword/newPassword,confirm 永不出端。200/400/401/409/422/429/500 都必须是 JSON 且 Cache-Control: private, no-store,429 带 Retry-After。409/422 的 RPasswordChangeRejected.businessCode 只允许 CREDENTIAL_VERSION_CONFLICT/CURRENT_PASSWORD_INCORRECT/NEW_PASSWORD_SAME_AS_CURRENT/PASSWORD_POLICY_VIOLATION;服务端和网关日志、APM、分析、崩溃记录及错误消息不得含密码、摘要或请求体。
客户端在 dispatch 前由 session owner 持久化唯一非敏感 marker {sessionEpoch,startedAt},不保存 token、密码、摘要、body、重试键或 operation id。明确未写的 400/422/429 原子清 marker 并留页;严格 200、401、409 均清同 epoch 全部账号态并回 A01。network、timeout、取消、畸形 2xx 或 5xx 在发出后属于结果未知,同样清秘密与本机会话、回 A01 并只提示“请尝试使用新密码重新登录”,绝不自动重试或宣称失败。冷启动发现同 epoch marker 时,在任何受保护缓存渲染前先清会话;新登录 bump epoch,旧 marker 和 A 的迟到响应不得清掉 B。无需新增 operation-status,重新登录就是最小对账路径。
页面实施时状态至少是 ready/submitting/known-error/unknown/success:提交中冻结三个输入、显隐控制、按钮和返回;确定字段错误聚焦并关联 aria-invalid/aria-describedby,显隐改为可键盘操作的原生按钮与 aria-pressed,目标至少 44dp,持续状态可播报。成功与 unknown 的跨根提示不能只靠短 toast。M03“建议定期更新”改为风险触发建议;M04 允许自动填充、粘贴和密码管理器。tests/password-change-openapi-contract.ps1 当前输出 PASSWORD-CHANGE-OPENAPI-CONTRACT BLOCKED;API-PASSWORD-001—005、密码登录 TAC、双设备/并发/故障注入和 MuMu 原生矩阵通过前,不修改 M04 的诚实本地预览。
23.8 M05 手机号换绑与统一短信码合同
M05 当前只展示脱敏 fixture,以 500ms 定时器验证新手机号和 4 位验证码并明确“不提交服务器”;没有当前密码、TAC、远端请求、会话迁移或结果未知状态。受保护双导出的 PUT body 是 clientId/phone/smsCode,线上则只有 phone/smsCode 并返回完整 profile;两者都缺 existing-factor 再认证、号码唯一性、会话撤销和 typed 错误。共享发码接口在线上还明确忽略权限。活动 bearer 加攻击者控制的新号验证码因此足以构成账号接管,不能接线。
三人比较了“共享发码 operation 按 scene 条件鉴权”和“专用受保护 operation”。标准 OpenAPI 3.x 无法把 operation-level security 与 body 中的 sceneCode 条件绑定;security: [{}, {SaToken: []}]、文字说明或 vendor extension 都不能让通用 validator、网关和 SDK 自动拒绝匿名 APP_PHONE_CHANGE,会产生 validator 允许而 runtime 拒绝的双合同。唯一方案是 POST /genealogy/app/auth/phone/sms/code:required SaToken 与非空 clientid,闭合 body 只有 phone/validToken,scene 在服务端固定,公共 /auth/sms/code 的 enum 删除 APP_PHONE_CHANGE。两个 operation 只分协议边界,底层仍调用同一 OTP 生成、存储和限流 owner,不复制策略。
最终 PUT 的身份闭环是活动 session+raw currentPassword+新号 OTP。TAC 只防自动化,不能替代既有因子;新号 OTP 只证明新号码控制权。不强制旧号 OTP,因为旧号丢失是正常换绑原因且不会增加独立因子;成功事务改为持久化旧号安全通知 outbox。所有账号若不保证有密码,稳定返回 STEP_UP_UNAVAILABLE 并进入独立找回,M05 不得降级为 bearer+新号 OTP。最终 body 精确为 currentPassword/phone/smsCode,不含 currentPhone、clientId、validToken、challengeId 或供应商字段;它依赖 M04 先完成 raw-password/慢哈希迁移,当前 MD5 wire 不得复用。
短信码唯一 schema owner 是 SmsCodeSecret:CSPRNG 生成恰好 6 位 ASCII 十进制字符串,保留前导零,TTL 5 分钟,60 秒重发冷却,最多 5 次失败,单次消费;重发原子废止旧码且不重置累计失败计数。同一 (account,session/credentialEpoch,tenant,client,scene,newPhone) 只有一个 active generation,服务端内部 generation 足以消歧,因此最终 PUT 不新增 challengeId/attemptId。A01/A04/A05/M05、注销等活动消费者、短信模板、生成器、OpenAPI、validator、页面和测试必须同版本从 4 位原子迁移为 6 位,禁止兼容双长度。
服务端在同一事务内验证当前密码与 active OTP、执行 (tenant,canonicalPhone) 唯一约束、消费 OTP、更新手机号、递增 credentialEpoch、撤销包括当前在内的全部 access/refresh session,并持久化旧号通知 outbox;严格 200 只返回 RVoid。两个并发换绑至多一笔成功,另一笔按 credential CAS 无写入。发码 POST 的结果未知不清登录态,但按服务端冷却避免立即轰炸;最终 PUT dispatch 前复用无秘密 {sessionEpoch,startedAt} credential marker,200、401、409、network/timeout/5xx/畸形响应或进程终止均清同 epoch 本机会话回 A01且不自动重试。
M05 页面实施时状态至少为 ready/tac/sending/code-sent/submitting/known-error/unknown:新号变化作废 TAC 与 OTP,发码后锁定号码,提交中冻结全部字段和返回。当前密码支持粘贴、密码管理器和带 aria-pressed 的显隐按钮;手机号与 OTP 使用能保留前导零的文本/电话输入和 numeric inputmode,错误具备 aria-invalid/aria-describedby 与首错聚焦,发送按钮为至少 44dp 的原生按钮,倒计时和持续状态可播报但不每秒打断 TalkBack。tests/phone-change-openapi-contract.ps1 当前输出 PHONE-CHANGE-OPENAPI-CONTRACT BLOCKED;API-PHONE-001—005、M04/认证前置门禁、两设备/OTP/故障注入和 MuMu 矩阵完成前不修改诚实预览。
23.9 G03 家谱与始祖原子创建合同
G03 当前在一个页面实例中先收集家谱资料,再录入始祖,两个 320ms 定时器只生成 local-created-* 预览;没有真实 API、可信 regionCode、上下文安装或 G01/G05 远端回流。受保护双导出的 GenealogyCreateBody 和线上 AppGenealogyCreateBody 都只创建家谱;通用 /lineage/persons 又允许客户端提交代数、父母和账号字段。若按旧接口先建谱再建根,会制造非产品需求的 ROOT_REQUIRED 半成品及两次结果未知。三人先讨论了两写恢复,再反向检查是否存在跨库或“保存空谱”的硬约束;当前没有任何证据,因此一致选择原子 bootstrap。只有后端以后证明事务边界不可跨越时才重新立 saga,而不是把补偿复杂度预埋客户端。
唯一写 owner 仍是 POST /genealogy/app/genealogies,但旧 GenealogyCreateBody/AppGenealogyCreateBody 必须被 AppGenealogyBootstrapBody 原子替换,不保留双收。根对象关闭额外字段,只允许 genealogyName/surname/ancestralHall/regionCode/accessPreset/rootPerson,前五项除堂号外均必填;rootPerson 关闭额外字段,只允许 name/sex/birthDate/biography 且 name/sex 必填。服务端固定根人物 generation=1 和唯一首根,不接收 generationName/personNo/appUserId/fatherId/motherId/status;sex 只允许 MALE/FEMALE/UNKNOWN,页面默认 UNKNOWN;生日是本地日 format: date,不保留页面武断的 1800 年下限。文本按 Unicode code point、NFC 和无边界空白统一验证,长度沿用页面现有 4/24/12/20/200 上限。
严格 200 前必须在一个事务内完成配额竞争检查、家谱、OWNER 成员关系、唯一一世始祖、READY 状态和幂等成功回执;任一步失败全部回滚,通用人物 POST 只服务 READY 家谱中的普通人物,不能承担 bootstrap。数据库 bootstrap-root 标记是根身份唯一权威,不能从 generation=1 猜测:普通人物 POST 即使提交一世也不能新建/替换根;人物 PUT 对根的可编辑白名单精确且只有 name/sex/birthDate/biography,operation 以 x-bootstrap-root-editable-fields 登记这四项并以 x-bootstrap-root-noneditable-policy=REJECT_422_BOOTSTRAP_ROOT_IMMUTABLE 登记拒绝策略;任何 status/personStatus、账号绑定、世代、父母、根标记或其他字段即使出现在通用 body 中也必须以 typed 422 拒绝,不能用“值未变化”掩盖越权字段。人物 DELETE 不能删除根,parents mutation 不能给根新增或重挂父母。未 READY 固定 typed 409 GENEALOGY_NOT_READY,触碰白名单外根字段或根身份固定 typed 422 BOOTSTRAP_ROOT_IMMUTABLE。成功 GenealogyBootstrapResult 必填词法字符串 genealogyId/rootPersonId、setupState=READY、roleType=OWNER 和 canView=true;两个 ID 从 JSON 到 storage、context 和 URL 都不得经过 JavaScript Number。同姓、同名和同地区均合法,页面重复提醒只能是建议,后端不得把名称当唯一键。
访问规则的唯一 wire owner 是共享 GenealogyAccessPreset,枚举只有 MEMBER_ONLY/PUBLIC_APPLY。这不是 G03 私有别名:同一后端版本必须让 APP 家谱读取 AppGenealogyVo、bootstrap 创建和 G11 设置更新都引用它,并删除 visibility/joinMode、旧 create/update DTO 和 utils/genealogy-contracts.js 的远端数字映射;客户端不得长期同时接受新 enum 与旧 pair。设置请求同步收紧为闭合、至少一个脏字段的 AppGenealogySettingsUpdateBody,只含 genealogyName/intro/accessPreset。本任务只统一 G11 的字段合同,不代表设置写入已经可上线;G11 的 If-Match、版本/CAS、权限刷新和结果未知仍须在独立批次建立门禁。在后端合同尚未落地的当前本地预览阶段,现有 fixture 映射暂不改写,也不能冒充生产 wire。
所在地只能从 required SaToken、非空 clientid 的 GET /genealogy/app/region/search typed RegionSelectVo 选择;同版删除旧公共 /genealogy/region/search,不能让 APP 在两条 owner 间漂移。regionCode/label/selectable 必填,code 使用共享词法 GenealogyRegionCode;leaf 只表示树导航,不能被客户端猜成“可提交”。页面展示 label、只提交 code,最终 POST 在业务事务中再次验证仍可选;不强制县、乡或 leaf,避免无依据排除省市级、历史地域或要求过细隐私位置。搜索加载、无结果、失败、迟到响应和失效选项分别表达,自由文本不得反推 code。
POST required SaToken、非空 clientid 和 Idempotency-Key。GenealogyBootstrapOperationKey 精确为 gcb.{13位毫秒时间}.{22—43位 base64url 随机量},随机量必须来自至少 128 位 CSPRNG;issuedAt 从 key 提取,acceptUntil=issuedAt+10 分钟,一律以服务端时间判定,issuedAt 晚于 serverNow 5 分钟以上固定 400 OPERATION_KEY_INVALID。schema 同时用 x-issued-at-source=KEY_EPOCH_MILLISECONDS、x-accept-window-seconds=600、x-max-future-skew-seconds=300 机器锁定公式。仍在窗口内的首次请求先用短控制事务按 (account,tenant,client,path,key) 唯一 CAS 认领 PENDING、canonical digest、fencing lease 和 resolveBy;resolveBy<=claimedAt+2 分钟,并以 x-resolve-from=CLAIMED_AT/x-resolve-sla-seconds=120 锁定。相同作用域、key 与 canonical body 的已存在操作即使超过 acceptUntil 也可重放同一结果,不同 digest 固定 409 IDEMPOTENCY_KEY_REUSED;截止后仍不存在的 key 固定 409 OPERATION_KEY_EXPIRED,不得启动工作。
控制事务认领后,业务事务才原子执行配额竞争检查、家谱、OWNER、唯一一世始祖、READY 和 SUCCEEDED 回执;任一写点失败全部回滚,再以 fencing token CAS 成不可变 FAILED_NO_COMMIT。超出 lease/resolveBy 的 watchdog 也只能用同一 CAS 终结,旧 worker 失去 fencing 后不得提交,保证 PENDING 最迟 2 分钟内收敛。严格 200 只表示业务事务与成功回执均已提交;SUCCEEDED 防重记录至少覆盖实体生命周期,FAILED_NO_COMMIT 记录至少保留 30 天。
为避免把始祖姓名、生日和生平写入 UniApp 普通持久存储,崩溃恢复选择受鉴权 GET /genealogy/app/genealogy-bootstrap-operations/{operationKey},不持久化完整 body。状态响应必须用带显式 mapping 的 discriminator oneOf 精确区分 PENDING{resolveBy,retryAfterSeconds}、SUCCEEDED{result}、FAILED_NO_COMMIT,三个 status 均是单值 string;PENDING 的 retryAfterSeconds 为 1—30,避免给所有 200 错加 Retry-After。x-state-transitions 只登记 ABSENT→PENDING 与 PENDING→SUCCEEDED/FAILED_NO_COMMIT,两个终态无出边,x-terminal-immutable=true;FAILED schema 同时固定 x-domain-effects=NONE 与 x-quota-consumed=false,机器保证家谱、OWNER、始祖和配额均未提交。SUCCEEDED 返回与 POST 相同严格回执。GET 只允许 operationKey path 与 clientid header、不得有 request body,是纯读且绝不创建墓碑或推进状态:acceptUntil 前无记录返回 typed 404 BOOTSTRAP_OPERATION_NOT_AVAILABLE,同时返回服务端 acceptUntil 与 Retry-After;截止后无记录则按 key 时间可计算地返回 FAILED_NO_COMMIT 而不写库,迟到 POST 仍永久拒绝。跨 account、tenant 或 client 查询统一 404且不泄漏存在性,禁止额外 403 分叉。操作记录不进 /mine、不占业务配额,也不返回请求 body 或人物 PII;GET 零写入和 FAILED 零领域提交最终仍须数据库观测测试证明,OpenAPI 描述与扩展不能冒充实现证据。
客户端唯一 coordinator 在最终按钮校验全部字段后才冻结 canonical snapshot、生成 key,并在 dispatch 前持久化 {sessionEpoch,operationKey,startedAt};第一步 CTA 只写“下一步:录入首代”,绝不发网络请求。客户端本地校验失败不写 marker;请求对象创建失败且能证明零发出时清 marker。服务端在 claim 前返回的 400/401/403 均保证无 operation,清 marker,其中 401 还必须清会话并回登录,403 失败关闭。409 IDEMPOTENCY_KEY_REUSED 视为合同/篡改冲突,marker 转入 fatal/quarantined,不查询或安装该 key 的 status、不自动换 key;只有用户看到明确警告并显式放弃后才清 marker、重新开始。创建上限、过期 key 和 422 是确定未提交,可清 marker后修正。429 保持 marker、同 key/body,先查 status 再按 Retry-After 重试;500、网络、超时、408、发出后的取消、任意意外 2xx/3xx、畸形 200 或进程终止均是 unknown,保持 marker并查 status,绝不换 key。status 的截止前 404 保持 key,400 表示本地 marker 已损坏并安全清除,401 执行会话失效,429/500、网络、取消、意外状态和畸形 200 均保持并退避;只有 FAILED_NO_COMMIT 或可证明未认领的确定失败才清 marker。
当前进程 unknown 可用相同 key 与内存 snapshot 重放;进程重启只查状态,不从 /mine 按名字猜测,也不自动生成新 key。SUCCEEDED 的唯一次序固定为:持久化 committed receipt → 失效或定点更新 /mine 缓存 → 安装 genealogy context → 进入 G05;context 或导航失败只重试本地安装/导航,绝不重发创建。退出、换账号和 epoch 变化必须隔离旧操作;任何日志、路由、marker、遥测和崩溃记录都不得包含表单正文。
页面实施必须同步删除伪提交 owner 与 mock fixture mutation,覆盖地区加载、确定拒绝、fatal/quarantined、PENDING、结果未知、已提交待进入、上下文失败和导航失败。文本标签与 input 建立关系;访问预设使用真正 radio/aria-checked;按钮至少 44dp,提交中冻结字段和返回;错误具备 aria-invalid/aria-describedby、首错聚焦和持续状态播报;重复提醒与成功/放弃层复用具备焦点圈定和恢复的 AppDialog。当前源码仍有 clickable view、54rpx 目标、23rpx 选项、无关联错误和 UTC 日期上限,这些只登记为实施项;没有 MuMu 证据前不得宣称视觉或 TalkBack 上线。后端门禁先执行无第三方依赖的 tests/openapi-yaml-json-parity-runtime-smoke.js,以无损任意精度数字、严格 YAML mapping 分隔符和完整对象结构深比较 JSON/YAML,确保不安全整数差一也不能假绿,再由 tests/g03-bootstrap-openapi-contract.ps1 检查唯一 JSON 语义、组合 schema 内旧字段、同 scope 参数重复、根 PUT 精确白名单和结构化不可变终态;客户端门禁 tests/g03-bootstrap-client-release-gate.ps1 实际执行依赖注入的 marker/status 状态机测试,不再用 helper 名称顺序冒充行为证据。两项只是 G03 自身门禁;真实开放还必须同时通过家谱工作区读取门禁、聚焦测试全量回归与 MuMu 原生交互/无障碍验收,任一红灯都不得开放真实创建。
23.10 G06/G08/G09/G10 普通加入申请合同
普通申请与邀请码直入是两个独立业务合同。任务 36 只覆盖七个 APP operation:appSearchPublicGenealogies、appCreateGenealogyJoinApplication、appGetGenealogyJoinApplicationRequest、appListMyGenealogyJoinApplications、appWithdrawGenealogyJoinApplication、appListPendingGenealogyJoinApplications、appReviewGenealogyJoinApplication。邀请码成功后直接建立成员关系且不生成审核记录,后续必须使用独立票据和直接加入接口;G08 的 source=invite 不得调用本批普通申请 POST。
API-JOIN-001 固定三个专用 cursor page 和词法身份。搜索、mine、pending 都使用 limit=1..50、opaque cursor、无 total;cursor 绑定 tenant/account/client 与过滤条件。搜索按 updatedAt DESC, genealogyId DESC,mine/pending 按 submittedAt DESC, applyId DESC,不得跨投影复用不存在的排序字段。GenealogyId/JoinApplicationId 均为 1—128 位 URL-safe 词法字符串,禁止 JSON int64。搜索项只含识别家谱所需的谱名、姓氏、地区、可选堂号/上级谱/支系/认证标签/成员数/更新时间与单一 viewerState;不同时返回可冲突的 canApply。mine 用 discriminator 精确区分 PENDING/APPROVED/REJECTED/WITHDRAWN,只有 REJECTED 必填申请人可见 rejectionReason;pending 只含 applyId/applicantName/relationDesc/applyReason?/submittedAt,递归禁止 phone、用户、邀请人和审核人身份。
API-JOIN-002 固定闭合申请体 applicantName/relationDesc/applyReason?,前两项必填,长度依次为 1—50、1—100、1—500。JOIN_APPLICATION_TEXT_V1 是本域唯一文本规范:Unicode NFC、去首尾边界空白、CRLF/CR 归一为 LF、不折叠内部空白并按 Unicode code point 计长。旧 GenealogyJoinApplyBody/GenealogyJoinAuditBody、phone/inviterUserId 与数字 status/auditRemark 不再是 APP owner;通用 G-series 与 core-flow 测试必须把所有权移交给专项门禁。
API-JOIN-003 固定无 PII 幂等与崩溃恢复。GenealogyJoinApplicationRequestKey 为 gja.{13 位 issuedAt 毫秒}.{22—43 位 base64url CSPRNG},随机量至少 128 位;服务端按时间固定 10 分钟接受窗、5 分钟未来偏差与认领后最多 2 分钟收敛。canonical identity 覆盖 method、规范 path、genealogyId、tenant、account、client 与规范 body。同 scope/key/request 重放同一 GenealogyJoinApplicationReceipt,不同 digest 返回 IDEMPOTENCY_KEY_REUSED;同 tenant/account/genealogy 最多一个活动 PENDING,必须由数据库唯一约束保证。幂等 scope、活动唯一 scope、事务 effects、重验字段、状态迁移与 cursor 机器扩展都必须是真实 JSON 数组,逗号字符串不能冒充集合。POST 在领域事务内重验 genealogyState=READY、accessPreset=PUBLIC_APPLY 与 applicationEligibility,并与 G11 设置 PUT 共同声明 x-public-apply-coordination=ATOMIC_SINGLE_WINNER;关闭公开申请与新准入竞态只能一方提交。申请领域行和 SUCCEEDED 回执在同一业务事务提交,resolver 判定 FAILED 前不得把已提交申请误报为零写。
状态唯一 owner 是只读 GET /genealogy/app/genealogies/join-apply-requests/{requestKey}。响应以显式 mapping 的 oneOf 精确为 PENDING{resolveBy,retryAfterSeconds}、SUCCEEDED{result}、FAILED_NO_COMMIT;只允许 ABSENT→PENDING→SUCCEEDED/FAILED_NO_COMMIT,终态不可变。GET 不创建墓碑、不推进状态;接受窗内未知 key 返回带 acceptUntil/Retry-After 的非泄漏 404,窗后无记录按 key 时间零写计算 FAILED,迟到 POST 永久拒绝。跨 account/tenant/client 统一 404。客户端只持久 {sessionEpoch,requestKey,startedAt};当前进程可用冻结 snapshot 重放,重启只查 status,不把姓名、关系、理由或完整 body 落盘。
API-JOIN-004 固定撤回 CAS:DELETE 只允许当前申请人撤回 PENDING;WHERE status=PENDING 决定唯一赢家,相同撤回只返回 WITHDRAWN typed receipt,审核已赢或其他终态返回 typed 409 与无 PII current state。API-JOIN-005 固定审核 CAS:body 只允许 {decision:APPROVE} 或 {decision:REJECT,rejectionReason} 两个闭合分支;相同决定只重放 APPROVED/REJECTED 原 200,规范化后不同拒绝理由或相反决定返回 409,审核成功绝不返回 WITHDRAWN。批准在一个事务内重验权限、READY 与 PUBLIC_APPLY,完成 PENDING CAS、唯一成员关系和 APPROVED;入口 capability 不是服务端授权证明。POST 的 409 只引用 RJoinApplicationKeyConflict,DELETE/PUT 的 409 只引用带 current 的 RJoinApplicationStateConflict;禁止用联合两类业务码的宽响应 owner 产生跨操作假绿。key 复用、过期或活动 PENDING 冲突不强制携带不存在的申请 current,只有申请状态/决定冲突分支必带 current。不引入 applicationVersion、If-Match 或审核详情端点。
API-JOIN-006 固定部署与客户端发布门槛。七个 operation required SaToken/非空 clientid,所有声明响应只允许 application/json 与 Cache-Control: private, no-store,429 带 Retry-After;禁止 */*、default、int64 身份、通用 RList/RObject/RVoid、HTTP 200 包装认证错误。OpenAPI 静态扩展只能登记幂等、cursor、CAS 和事务意图,不能证明数据库实现;后端还须用数据库观测、并发与 fault injection 验证唯一约束、零写、事务、fencing、真实 HTTP/CORS。客户端门禁通过前 G06/G08/G09/G10 继续本地预览;接线时原子删除 G10 手机号、三处消息中心承诺、LOCAL_WITHDRAWN 和遗留 adapter,补齐原生按钮、44dp、提交冻结、状态播报、错误关联、首错聚焦、长文换行及 MuMu 的系统字号/TalkBack/键盘/返回/慢网/杀进程矩阵。
tests/join-application-openapi-contract.ps1 是上述七个 operation、schema 与响应闭包的唯一静态 owner,当前精确输出 JOIN-APPLICATION-OPENAPI-CONTRACT BLOCKED。只有后端同版本重导 JSON/YAML、专项门禁、后端集成反例、客户端状态机、全量回归和 MuMu 发布矩阵全部通过,普通加入申请才可称为上线闭环;静态门禁单独转绿不开放页面。
23.11 G06/M08 邀请码签发与直接加入合同
任务 37 与普通申请完全隔离。唯一产品流程为:M08 管理自己有权签发的活动邀请;G06 在 JSON body 中安全解析邀请码(不消费票据、不建立成员,仅允许轮换短时 grant 摘要),展示可信的最小家谱目标和“确认后立即成为成员、不进入审核”,用户明确确认后直接兑换;成功刷新 workspace、安装家谱 context,再进入 G01/G05。邀请码流程不进入 G08,不提交 applicantName/relationDesc/applyReason,不创建 join-application、PENDING 或审核通知。真实接线时必须原子删除 G06→G08 的 invite 导航、G08 source=invite 分支、JP2026、首个 fixture 目标和普通 canApply 资格复用;成员资料或人物关联另立合同。
API-INVITE-001 固定六个 operation:appListMyGenealogyInviteTickets、appIssueGenealogyInviteTicket、appRevokeGenealogyInviteTicket、appResolveGenealogyInviteTicket、appRedeemGenealogyInviteTicket、appGetGenealogyInviteRedemptionRequest。所有 operation required SaToken、非空 clientid、typed JSON 和 private/no-store;429 带 Retry-After。邀请码只能置于 resolve 的 JSON body,绝不进入 URL、query、导航、持久化、日志、APM、分析、自动测试、崩溃报告或发布证据截图;用户主动显示/复制/系统分享属于受提示的显式动作。列表只返回当前活动票据元数据,不返回原码、领取人或剩余目标身份。
API-INVITE-002 固定首版为单次、24 小时、服务端时间判定的 opaque bearer ticket。原码使用版本前缀和 26 位 Crockford Base32 随机量,至少 128 位 CSPRNG 熵;展示可分组,canonical 校验不接受易混淆字符。数据库以 HMAC/摘要查找,仅为同 key 幂等重放使用隔离 KMS envelope encryption 保存原码 600 秒;恢复窗内重放同一 ticket 和原码,窗后只返回同一 ticket 的无秘密 metadata 与 ISSUED_SECRET_UNAVAILABLE,不得新建票据。列表永不恢复原码。状态唯一为 ACTIVE→CONSUMED/REVOKED/EXPIRED,终态不可逆。resolve 不消费票据或建立成员,但允许写入/轮换每个 tenant/account/client/ticketVersion 唯一的短时 grant 摘要;旧 grant 立即失效、TTL 最多五分钟并清理。redemption token 绑定 tenant/account/client/inviteTicketId/ticketVersion/genealogyId/authorizationEpoch;这里的 authorizationEpoch 是服务端邀请授权版本,签发者移除/降权、邀请功能关闭或票据版本变化时提升并使旧 grant 失效。编辑输入、切账号或 session epoch 变化立即销毁客户端内存 token。
API-INVITE-003 固定签发与撤销权限。M08 从 workspace /mine 取得当前明确家谱,不新增可信 URL genealogyId;workspace 投影字段 canInviteMembers 唯一映射服务端 capability INVITE_MEMBER,服务端仍重验 READY、邀请功能和当前成员关系。POST 无可配置 body,并用数据库约束控制每个 tenant/genealogy/issuer 的活动票据上限。签发 Idempotency-Key 为 gii.{13 位毫秒}.{22—43 位 base64url CSPRNG};600 秒只限制不存在 key 的首次认领,已存在 key 永远定位同一 ticket,不同 digest 优先 409,恢复窗内重放原码、窗后只返 metadata,绝不签第二枚。客户端 marker 固定 {sessionEpoch,genealogyId,requestKey,startedAt},不得从重启后的当前 workspace 猜 genealogyId。KMS 密文、ticket、HMAC lookup 和 request receipt 作为单一提交单元,任一步失败零票据。DELETE 只允许签发者本人管理并做 ACTIVE→REVOKED CAS,相同撤销重放原 200;CONSUMED/EXPIRED 返回 typed 409 current state,跨签发者统一 404。权限降级或邀请功能关闭必须让活动票据和旧 redemption token 立即失效。
API-INVITE-004 固定兑换事务。redeem 无 request body,只通过 required、敏感的 Genealogy-Redemption-Token header 和 gir Idempotency-Key 消费当前账号 grant。服务端在同一事务内重验 token 绑定/有效期、票据 ACTIVE/版本/未过期、签发者权限、家谱 READY/邀请功能、账号可加入与容量,执行 ACTIVE→CONSUMED CAS、唯一 (tenant,genealogyId,account) MEMBER 关系和 SUCCEEDED receipt;失败全部回滚。不得建立人物、亲属关系或姓名副本。同码两账号、同账号两码、撤销/兑换、权限撤销/兑换竞态都只能有一个事务赢家;失败事务不得消费票据。已有普通 PENDING 固定 ACTIVE_PENDING_APPLICATION,引导先在 G09 撤回,邀请流程不得暗改任务 36 状态。
API-INVITE-005 固定结果未知恢复。兑换 request key 采用 gir.{issuedAt}.{CSPRNG},600 秒接受窗、300 秒未来偏差、认领后 120 秒内收敛;状态纯读 GET 只允许 ABSENT→PENDING→SUCCEEDED/FAILED_NO_COMMIT,终态不可变,已存在终态优先于 absent 计算,FAILED 保证票据未消费且成员未创建。成员、票据消耗和成功回执同事务提交,resolver/watchdog 以 fencing CAS 收敛,旧 worker 不得迟交。终态回执至少保留 30 天,客户端 marker TTL 为 30 天并先于回执清理;任何清理后也不得凭控制记录缺失否定已提交领域写。客户端 dispatch 前只持久 {sessionEpoch,requestKey,startedAt},不持久邀请码、redemption token、目标或 body;SUCCEEDED 后刷新 /mine 并只重试本地 context/导航收口,FAILED 回 G06 重新输入,unknown 不换 key。
API-INVITE-006 固定安全、交互与发布反例。invalid/expired/revoked/consumed/跨 tenant 统一非枚举 INVITE_TICKET_NOT_AVAILABLE,resolve 需要账号、设备/IP、前缀和全局多维限流与时间侧信道控制。M08 覆盖加载、无权限、无票据、签发/unknown、原码显示/复制/分享、即将过期、撤销/竞态;G06 覆盖输入、格式错、解析、可信目标、明确确认、redeem PENDING/unknown、已加入、活动普通申请、限流、票据失效、账号切换和迟到响应。原生按钮、tablist/tab/aria-selected、label、aria-invalid/describedby、首错聚焦、44dp、状态播报、长码/长谱名换行和提交冻结均为发布门槛;MuMu 覆盖 320/360/412、1.3 倍字号、TalkBack、键盘、粘贴、双击、慢网/断网、杀进程、Android 返回与系统分享取消。后端还须用真实账号、数据库观测、并发、fault injection、日志审计和正式 HTTP/CORS 证明合同,OpenAPI 扩展不能替代实现证据。
23.12 G11 家谱设置版本化写入合同
任务 38 只治理 G11 的设置读取与写入,不混入 G12 字辈、成员管理、邀请码签发或普通申请审核。全局唯一写 owner 保留 PUT /genealogy/app/genealogies/{genealogyId},operationId 固定为 appUpdateGenealogySettings 且全局只出现一次;不新增 PATCH、/settings 旁路或任何递归引用设置 body/包含任意一个 genealogyName/intro/accessPreset 单字段的第二写入口,外部或错分区同名 $ref 也按违规 owner 失败,只有精确 G03 建谱 POST 豁免。为消除裸 intro 歧义,/genealogy/app/genealogies... 写 body 根层的 intro 由设置合同保留,子域必须使用自身有界名称。单谱唯一读取 owner 继续是 GET /genealogy/app/genealogies/{genealogyId}/overview,同版删除重复的通用 GET,并对全部 /genealogy/app/ GET 以响应 schema 闭包和 allOf 属性合成拒绝任何旁路详情;G11 只消费 workspace 唯一门禁的 PASS 结论,每次进入必须 fresh 读取 overview 后才建立可编辑 baseline,不从路由、/mine 卡片、G05 旧内存对象、roleType 或 fixture 推导当前设置与权限。
API-SETTINGS-001 固定共享字段与唯一模型。家谱 ID 继续引用词法 GenealogyId;新增共享 GenealogyName,统一 G03 创建、G11 更新和 AppGenealogyVo 为非 null、NFC、无边界空白、无换行/控制字符的 1—24 Unicode code point,删除 G11 旧 20 字双上限。共享 GenealogyIntro 精确允许非 null 空串,或 NFC、无边界空白、1—80 code point 的内容;正文只允许内部 LF,拒绝 CR、tab 和其他控制字符,空串就是清空,省略才是保持,null 与纯空白非法。GenealogyAccessPreset 仍由任务 35 作为 APP 读/建/改唯一枚举 owner,只有 MEMBER_ONLY/PUBLIC_APPLY。overview 与 PUT 200 都复用关闭额外字段、精确 {code,data} 的 RAppGenealogyVo → AppGenealogyVo;后者 required genealogyId/genealogyName/intro/accessPreset/settingsVersion/canManage,不新建会重复字段规则的 settings snapshot。
API-SETTINGS-002 固定原子 dirty-only merge。请求 body 唯一为关闭额外字段、minProperties=1/maxProperties=3 的 AppGenealogySettingsUpdateBody,属性精确为 genealogyName/intro/accessPreset 且都非 required;出现字段更新、省略字段保持,body 不携带 version。服务端在同一事务锁定家谱并重新校验权限后一次应用全部出现字段,不能按整资源 PUT 把其他字段覆盖。canonical 后实际无变化可返回 200,但必须保留原版本且除审计外不产生领域副作用;客户端 clean draft 永不发请求。
API-SETTINGS-003 固定唯一并发模型。新增非 null、1—128 位 URL-safe opaque GenealogySettingsVersion,只允许比较,不得解析、递增或经过 JavaScript Number;它以 x-version-scope-fields=[genealogyName,intro,accessPreset] 和 x-version-change-policy=CANONICAL_SETTINGS_CHANGE_ONLY 锁定只在这三个 canonical 设置实际变化时更换,其他领域写与 canonical no-op 都不得误增版本。AppGenealogyVo.settingsVersion required,PUT required If-Match 引用同一 owner,请求 body 不复制 version。项目统一采用 typed HTTP 409,而不是为 G11 单独引入强 ETag/412。409 RGenealogySettingsConflict 根层只允许 oneOf/discriminator 两个关键词,每个分支只能是 exact local schema ref;GENEALOGY_SETTINGS_VERSION_CHANGED 分支 required current: AppGenealogyVo,GENEALOGY_NOT_READY 与 ACTIVE_PENDING_APPLICATIONS 分支不得带 current 或审核数据。只有仍具管理权限的调用者可得到版本/current;服务端事务内重验权限。冲突优先级固定为 version → READY → active PENDING,旧 writer 不得用过期 baseline 触发业务判断。
API-SETTINGS-004 固定访问预设迁移与普通申请并发。活动 PENDING 只阻断实际的 PUBLIC_APPLY→MEMBER_ONLY:只改名称/简介、已经是 MEMBER_ONLY 的 no-op 或 MEMBER_ONLY→PUBLIC_APPLY 不得被误阻断;同一请求还改名称时,只要访问预设迁移被阻断,整笔请求零写。活动 PENDING 返回 ACTIVE_PENDING_APPLICATIONS,G11 引导去 G10 处理,不自动拒绝、撤回或迁移申请;邀请码合同不受影响。设置 PUT 和任务 36 新申请 POST 都必须在事务内重验 genealogyState/accessPreset(申请另重验 applicationEligibility),并共同声明 x-public-apply-coordination=ATOMIC_SINGLE_WINNER;PENDING 检查、preset 更新和新申请准入竞态只能一方提交,不把实现限制为特定数据库隔离级别。
API-SETTINGS-005 固定响应、结果未知与缓存收口。PUT 精确声明 200/400/401/403/404/409/422/429/500,全部只允许 typed application/json;每个 response 必须包含固定单值 enum Cache-Control: private, no-store,429 另有 1—300 秒 Retry-After,只允许 traceparent/tracestate/x-request-id/x-correlation-id 非语义 tracing header,禁止 ETag 与其他语义 header。parameter/response/header/schema 解析只接受 exact local component ref;四种 tracing header 若内联,必须是 allowed-key 关闭且带非 null 的 string schema 的 Header Object,不能以 external/wrong-section ref 或任意 inline schema 绕过。所有标量、引用字段、成功/错误 envelope 和 fieldErrors 用 allowed-key 集拒绝 nullable、readOnly/writeOnly、冲突组合与其他未拥有的验证关键字。禁止 default、3xx、*/*、通用 RObject/Void、可加 debug 字段的宽 envelope 和 HTTP 200 包业务错误。200 返回完整 canonical RAppGenealogyVo;400、401、403、404、422、429 为确定失败,500 GENEALOGY_SETTINGS_OUTCOME_UNKNOWN、网络、超时、408、取消和畸形 2xx 都是结果未知。客户端提交时冻结 {baseline,dirtyPatch,settingsVersion,sessionEpoch},未知后不自动第二次 PUT,而 fresh 读取 overview:全部脏字段等于目标只可陈述“当前设置已与所选内容一致”,不能证明本次请求成功;仍为 baseline 才允许用户明确重试;部分匹配或第三值进入字段级三方比较。成功原子回填 canonical baseline,失效 /mine、overview、公开搜索和公开预览缓存并重新校验 context;权限/成员关系已变时清 context 回 G01,否则回 G05。
API-SETTINGS-006 固定客户端与发布激活条件。本批只建立 OpenAPI 门禁与权威文档,不创建因生产 coordinator 不存在而恒红、只扫描源码 token 或复制 OpenAPI 断言的占位 client gate。后端同版双导出、workspace 读取和共享 owner 门禁转绿后,客户端批次第一项写操作必须先新增实际执行生产 normalizer/coordinator 的纯 Node 失败测试,再实现 adapter/coordinator;覆盖 24/80 code point、dirty/clear、三个 409、三方比较、unknown 对账、权限撤销、双击/取消/迟到响应、sessionEpoch、canonical 回填、缓存/context 收口。随后才可原子删除 fixture 数字映射、本地成功 timer 和“禁止 appApi”旧断言。页面至少有 loading、ready/dirty、saving、saved、validation、uncertain/reconciling、conflict、no-permission、auth-expired、unavailable;用原生 button/radio、真实 label、aria-invalid/describedby、首错聚焦、busy/live 状态、44dp 与返回冻结。G11 OpenAPI、workspace/shared owner、可执行客户端行为门禁、聚焦/全量回归及 MuMu 320/360/412、1.3 倍字号、TalkBack、软键盘、双击、慢网/断网/杀进程、Android 返回与权限/版本竞态必须全部通过,OpenAPI 绿只表示可以开始客户端 TDD,不表示允许直接接线或上线。
tests/g11-settings-openapi-contract.ps1 是全局唯一设置写入口、body、settingsVersion/If-Match、409 oneOf、响应矩阵与设置字段 owner 的唯一静态门禁;任务 35 的 G03 门禁只继续拥有 bootstrap、共享 accessPreset/旧 DTO 删除及其读取传播,不再检查设置 operation、body 或响应,workspace 与 join 门禁分别唯一拥有读取基础和申请准入反向协调。当前专项门禁稳定输出 G11-SETTINGS-OPENAPI-CONTRACT BLOCKED 和 48 项缺口;受保护 OpenAPI 未修改。
23.13 G12 字辈集合版本化保存合同
任务 39 只治理 G12 字辈集合读取与原子保存,不混入逐条编辑、人物世代、T01 高亮、邀请码或 G11 设置。API-POEM-001 固定全局唯一 owner 为 GET/PUT /genealogy/app/genealogies/{genealogyId}/generation-poems,operationId 分别且全局唯一为 appGetGenerationPoemSet、appUpdateGenerationPoemSet。同版删除旧 collection POST、逐行 PUT、/batch/preview、/batch/save、/management 及其旧 schema/response;不引入 preview token、服务端 preview/status 或第二写入口。GET/PUT 200 共用关闭额外字段的 RGenerationPoemSetSnapshot → GenerationPoemSetSnapshot,快照精确为 {genealogyId,poemSetVersion,items},只投影 ACTIVE、严格按 generationNo 升序;普通读只要求 canView,维护写在事务内重验 tenant、canEditContent 与 READY。
API-POEM-002 固定唯一请求与完整候选语义。AppGenerationPoemSetUpdateBody 精确 required {items,disableMissing},items 为 0—500 个严格 generationNo 升序的 AppGenerationPoemSetItem。已有 ACTIVE 行必须提交当前词法 poemId,允许把稳定 ID 显式移动到新世代;新行省略 poemId,引用已停用/未知 ID 失败。generationNo 是唯一排序 owner,范围 1—2147483647;generationText 允许重复。disableMissing=false 保留全部未声明 baseline ACTIVE,true 软停用遗漏项;空 false 是 no-op,空 true 停用全部,历史绝不物理删除。服务端必须依次 RESOLVE_BASELINE_ACTIVE_IDS、VALIDATE_DECLARED_STRICT_ORDER、构造声明目标、应用遗漏策略、按 generationNo 排序 merged candidate,再验证 ID/世代唯一、GENERATION_SLOT_CONFLICT、连续性、VALIDATE_FINAL_ACTIVE_CAPACITY,随后 ALLOCATE_UNIQUE_NEW_IDS 并验证新 ID 非空、唯一、非 baseline,最后 WRITE_ATOMICALLY。swap 必须显式声明所有受影响行,全部成功或零领域写且外部不可见中间态。
API-POEM-003 固定唯一并发与文本模型。GenerationPoemSetVersion 是非空、词法、不可解析且永不复用的 ACTIVE 语义版本;PUT required If-Match 引用同一 owner,body 不重复版本,不发布 ETag/412。canonical no-op 返回当前 200 并保持版本,ACTIVE 语义变化必须生成新版本。字辈 ID 与版本均只能词法比较,不经 JavaScript Number。GenerationPoemText 必须为 well-formed UTF-16、NFC、1—50 Unicode code point、无边界空白,并拒绝 C0/C1、CR/LF/tab、bidi override/isolate、方向标记、zero-width、BOM、word joiner、行/段分隔符与孤立 surrogate;不以 UTF-16 code unit 截断。
API-POEM-004 固定 typed HTTP、授权和部署 owner。GET 精确声明 200/400/401/404/429/500,PUT 另含 403/409/422;成功、固定错误、409 union 和 422 fieldErrors 都是关闭额外字段的 typed application/json,禁止 default、3xx、*/*、200 包业务错误或通用 RObject。所有响应 required Cache-Control: private, no-store,429 required Retry-After;SaToken 必须是精确 header apiKey Authorization 且 security requirement 是 JSON 数组,clientid 非空。浏览器预检唯一声明 owner 为顶层 x-app-gateway-policies.APP_GATEWAY_PREFLIGHT,精确允许 Authorization/Content-Type/If-Match/clientid、GET/PUT/OPTIONS、显式部署 origin allowlist 和 600 秒;operation 只引用该 owner。正式发布仍须真实 gateway OPTIONS/preflight smoke,导出扩展不能替代部署证据。
API-POEM-005 固定冲突与结果未知恢复。409 只允许 POEM_SET_VERSION_CONFLICT + current 和 GENEALOGY_NOT_READY;422 精确包含请求顺序、重复 ID/世代、slot collision、非连续、容量、文本与行不可用,校验失败全程零写。网络、超时、408、取消、5xx 和畸形 2xx 都按 unknown。客户端冻结 baselinePoemSetVersion、baselineOrderedItems、declaredItems、disableMissing 与 sessionEpoch,按相同规则构造 effective target 后 fresh GET,执行 FRESH_GET_THREE_WAY_NO_AUTO_PUT:existing 按 poemId+generationNo+NFC 文本匹配,NEW 按 generationNo+文本匹配且返回 ID 必须非空、唯一、非 baseline;比较要求同基数、无额外 ACTIVE 行,忽略版本判断语义等价,但变化 target 必须有新且未复用版本。先判 current==target 并执行 CURRENT_EQUALS_TARGET_FIRST_NO_ATTRIBUTION,只确认当前事实;再判 current==old 允许用户明确重试,否则进入 divergent,任何分支都不自动 PUT。
API-POEM-006 固定客户端、无障碍与激活条件。本批只建立受保护 OpenAPI 门禁、对抗合同、Unicode 运行时证据和中文权威文档;G12 在全部后端门禁转绿前保持明确本地预览。后端同版双导出通过后,客户端第一项写操作必须新增直接执行生产 normalizer/coordinator 的纯 Node 失败测试,再实现唯一 adapter/coordinator,并原子删除 fixture、timer、本地成功、旧 preview/save 假合同及“禁止 appApi”旧断言。页面覆盖 loading/empty/editing/validating/saving/saved/unknown/conflict/no-permission/auth-expired/unavailable、清空二次确认、草稿冻结、live region、首错聚焦、原生按钮、44dp 与返回保护;MuMu 覆盖 320/360/412、1.3 倍字号、TalkBack、软键盘、0/1/500×50、swap、双击、慢网/断网/杀进程、权限撤销、READY 变化和账号切换。未来 T01 高亮只消费正式 ACTIVE 快照,不能成为字辈写 owner。
唯一静态 owner 为 tests/g12-generation-poem-openapi-contract.ps1,当前输出 G12-GENERATION-POEM-OPENAPI-CONTRACT BLOCKED;合法 zero issues 种子、大小写严格 JSON Pointer、ref 图、HTTP 方法、callback、候选/unknown/CORS 和 annotation 非 owner 由 tests/g12-generation-poem-openapi-adversarial-contract.ps1 覆盖,字辈 Unicode 由 tests/g12-generation-poem-unicode-contract-runtime-smoke.js 覆盖。G-series 只保留 owner 转移说明,不再重复 G12 合同;受保护 OpenAPI 与页面保持未修改。
23.14 F01/F03 家族动态与一级评论读取合同
任务 40 只治理 F01 动态列表、F03 动态详情和正常一级评论读取,不混入 F02 发布、点赞、评论/回复写、回复读取或媒体文件读取。受保护双导出中的 APP 路径已有谱上下文,页面与 fixture 也以 (genealogyId,feedId) 为身份;因此三方否决全局 feed 路由,保留谱内解析和授权。2026-07-23 线上 OpenAPI 3.1.0 与受保护旧导出都不能直接接页:两者分别存在 /feeds 与 /feeds/page、/comments 与 /comments/page 双读取 owner,无稳定 operationId,身份仍为 int64,feed 使用通用或无 required 的响应,线上 FamilyFeedCommentVo 还暴露手机号、业务用户 ID、内部审核人/动作/原因、状态和持久化字段。双导出内部 parity 只能证明两份旧文件相同,不能证明它们与线上同版本或适合发布。
API-FEED-READ-001 固定三个唯一 owner:GET /genealogy/app/genealogies/{genealogyId}/feeds 的 appListFamilyFeeds、GET /genealogy/app/genealogies/{genealogyId}/feeds/{feedId} 的 appGetFamilyFeed、GET /genealogy/app/genealogies/{genealogyId}/feeds/{feedId}/comments 的 appListFamilyFeedRootComments。同版本删除两个 /page GET;非分页 collection GET 成为唯一 cursor owner。写方法、likes、replies 和媒体读取不由本批声明或删除,不能借本批旁路接入。三个 operationId 在全局各出现一次,GET 无 request body。
API-FEED-READ-002 固定词法身份与无 PII 最小投影。GenealogyId/FamilyFeedId/FamilyFeedCommentId 均为 1—128 位 URL-safe opaque string,客户端只比较、不解析,不得进入 JavaScript Number。AppFamilyFeedReadItem 是 closed required {feedId,feedContent,authorDisplayName,publishedAt,hasMedia}:正文为 FAMILY_FEED_TEXT_V1、1—300 Unicode code point;作者名是经过授权的展示投影,原账号缺失时由服务端提供非空退化文案;时间是服务端生成且不可修改的 RFC3339。hasMedia 只证明存在附件,不泄露 mediaOssIds;在独立媒体读取合同通过前客户端必须展示诚实占位,不能把媒体动态静默伪装为完整纯文字内容。标题、标签、点赞、状态、排序值和总评论数不是当前页面的可靠服务端字段,不从 fixture 或正文猜值。
AppFamilyFeedRootCommentReadItem 是 closed required {commentId,commentContent,authorDisplayName,publishedAt};正文为 FAMILY_FEED_COMMENT_TEXT_V1、1—1000 Unicode code point。comments collection 只投影当前正常可见的一级评论,排除回复、删除占位、parent/level、手机号、账号/业务用户 ID、tenant、状态、remark、审核字段和 createBy/updateBy。详情不嵌 comments,评论独立加载和独立失败;没有 total 时客户端只能显示“已加载 N 条”,不得把已加载数组长度冒充服务端总数。
API-FEED-READ-003 固定对象级可见性。三个 GET 每次请求都重新验证 tenant、genealogy 与 membership;详情和评论另验证 feed 属于 path 家谱且当前可见。未知 ID、错谱、动态删除/隐藏、无权、撤权或 cursor 跨主体/跨谱/跨 feed 重放统一 typed 404 FAMILY_FEED_NOT_AVAILABLE,不以 403 或不同消息泄露资源存在性。未认证固定 401。后端只能从已通过可见性过滤的根评论集合生成响应和 cursor;前一页授权不延续为后一页授权。
API-FEED-READ-004 固定 opaque keyset cursor,不采用 pageNum/pageSize。首请求不带 cursor;limit 可省略且默认 20,范围 1—50;refresh 必须丢弃旧 cursor;nextCursor 缺席表示结束,响应不含 total、pageNum 或 hasMore 冗余真值。feed 按 (publishedAt DESC, feedId DESC_ORDINAL),一级评论按 (publishedAt ASC, commentId ASC_ORDINAL)。cursor 绑定 tenant/account/authSession/client、genealogyId、评论的 feedId、projection/order/limit、windowUpperBound 与 lastTuple,并具有完整性和过期校验;篡改或过期返回 400 FAMILY_FEED_CURSOR_INVALID,不静默从首页重启。
读取窗口精确命名为 UPPER_BOUND_KEYSET_LATEST_VISIBLE,不能宣传成严格 MVCC snapshot:首次请求确定上界,新插入内容等 refresh 后进入;后续读取前删除、审核隐藏或撤权的内容省略;仍可见但已编辑的正文按该页读取时的最新可见版本返回。无 total,因此上述变化不会伪造恒定总量;keyset 与不可变 publishedAt/词法 ID tie-breaker 只保证在合同窗口内不因新增头部内容产生 offset 重复或漏项。
API-FEED-READ-005 固定 typed HTTP、安全与缓存。三个 GET 精确声明 200/400/401/404/429/500,只允许 closed typed application/json;禁止 default、3xx、*/*、通用 RList/RObject/PageResult、HTTP 200 包业务错和开放 DTO。所有 response 必须有单值 Cache-Control: private, no-store,429 另有 1—300 秒 Retry-After;operation security 必须是精确 SaToken 数组且空 scopes,required clientid 为非空 1—128 字符串。读取复用唯一 APP_GATEWAY_PREFLIGHT,OpenAPI 扩展只登记 owner;正式 H5 origin 的真实 OPTIONS/preflight、凭证与拒绝 origin/header/method 反例仍由部署门禁证明。
API-FEED-READ-006 固定激活顺序。tests/family-feed-read-openapi-contract.ps1 是三条 GET、词法 ID、closed projection、cursor、可见性、typed response 与 no-PII 闭包的唯一静态 owner,当前输出 FAMILY-FEED-READ-OPENAPI-CONTRACT BLOCKED、Issues: 85。tests/family-feed-read-openapi-adversarial-contract.ps1 用完整 zero issues 合法种子和 58 个独立变异覆盖旧 owner、外部/多跳 ref、大小写、security owner/数组、HEAD/OPTIONS/callback、PII/审核字段、int64、offset/total、错误 403 分流、wildcard 与缓存头,输出 FAMILY-FEED-READ-OPENAPI-ADVERSARIAL-CONTRACT PASS MUTANTS=58;它只证明静态门禁会正确转绿和拒绝反例,不解除后端红灯。后端还须以同版本 JSON/YAML/live /v3/api-docs、真实账号、权限撤销、跨谱 cursor、并发插入/删除/隐藏、数据库观测与正式 HTTP/CORS 关闭 P0。
后端绿前 F01/F03 保持按复合身份深拷贝且失败关闭的 fixture,runtimeConfig.mode 保持 mock,不把 dormant appApi.getFeeds 宽分支接页,也不在远端失败后静默回退 fixture。后端三份证据通过后,客户端第一项写操作必须是直接执行生产 read normalizer/coordinator 的纯 Node 失败测试;随后原子迁移 F01 列表和 F03 详情/评论,删除 fixture 锁、嵌入评论、伪重试和旧“必须 fixture”断言。状态必须覆盖首屏 loading/empty/error、refresh、load-more/end/局部失败、详情 404、评论独立失败、hasMedia 占位、离页取消、sessionEpoch/切谱/切账号迟到响应、权限撤销、滚动与焦点恢复;原生 button、44dp、live region、长文换行和 MuMu 320/360/412、1.3 倍字号、TalkBack、键盘、慢网/断网/返回矩阵是发布条件。
二十四、当前精确执行顺序
后续不再按页面样式迁移重做,而按以下独立阶段执行:
- 导航栈语义统一:静态任务 1—10 已完成,MuMu 流程矩阵待执行。
- T01 大规模世系树:设计与 OpenAPI 红灯已完成;等待后端新图合同后按规范化、布局、Scene、Canvas 和页面交互分批实施。
- TAC 认证:客户端与静态/纯运行时门禁已完成;等待后端关闭
API-AUTH-TAC-001—004,随后执行真实环境与 Android 发布门禁。 - 领域上下文基础与 M07 真实反馈客户端已完成;家谱工作区三人审查与 OpenAPI 红灯已完成,等待后端关闭
API-GENEALOGY-WORKSPACE-001—003后再接 G01/G05。 - G03 原子 bootstrap 三人审查与 OpenAPI 红灯已完成;等待后端关闭
API-G03-001—005后,再实现严格 coordinator、无 PII 状态恢复、地区选择、context/G05/G01 闭环和独立 MuMu 矩阵。 - M06 帮助三人审查与 list-only OpenAPI 红灯已完成;等待后端关闭
API-M06-001—003后再接严格 adapter。等待期间继续下一个无依赖只读域,不与验证码、T01 或支付混合。 - M01/M02/M03 个人资料读取三人审查与 OpenAPI 红灯已完成;等待后端关闭
API-PROFILE-READ-001—003后再接掩码 adapter,不与资料写入混合。 - 通知读取和已读写入已分别完成三人审查与 OpenAPI 红灯;后端先关闭读取合同后实现无 ID 内存快照,再在独立写批次私有迁移字符串 ID、删除伪本地读状态并验证并发收敛。
- M02 资料写入三人审查与 OpenAPI 红灯已完成;等待读取与
API-PROFILE-UPDATE-001—004同时关闭后,再分 normalizer、API 和页面状态实现。 - M10 当前设备退出三人审查与 OpenAPI 红灯已完成;等待后端关闭
API-LOGOUT-001—003后,再实现 session epoch、logoutCoordinator 和 A01 一次性状态。 - M04 登录态改密与四条密码 wire 三人审查、OpenAPI 红灯已完成;等待后端关闭
API-PASSWORD-001—005后,原子迁移共享策略、MD5 消费者、session marker、页面状态机与全设备撤销。 - M05 手机号换绑与全活动 OTP 三人审查、OpenAPI 红灯已完成;等待后端关闭
API-PHONE-001—005且 M04/认证前置门禁通过后,再原子迁移六位码、专用受保护发码、最终 PUT 与 credential marker。等待期间转向 G/F/R,不混入本批。 - 任务 36 普通加入、任务 37 邀请码直入、任务 38 G11 设置、任务 39 G12 字辈集合和任务 40 F01/F03 家族动态读取的三人审查、专项 OpenAPI 红灯与 owner 移交已完成;等待后端分别关闭 API-JOIN/API-INVITE/API-SETTINGS/API-POEM/API-FEED-READ 后按各自客户端 TDD 顺序实施,任何单项 OpenAPI 转绿不开放页面。当前继续下一个独立业务域。
- 全局文字层级与无障碍第二轮。
- 生产配置、隐私权限、可观测性、构建发布、升级回滚与 MuMu 全流程终审。
T01 必需的线性目录和 44dp 控件随 T01 一起完成;认证浮层的壳层无障碍不扩张为全项目已通过。外部门禁阻塞时继续无依赖批次,但任何真实成功、接口兼容或视觉通过都必须有对应证据。