@antprofuse/saddle-workflow 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -9,7 +9,7 @@ description: 盘点或推进 Islands 项目从 Spec、adapt、设计、测试到
9
9
 
10
10
  ## Skill 消费规则
11
11
 
12
- 所有 Skill 间引用使用 npm 官方 registry 的 `@latest`,禁止写死依赖 Skill 版本。进入任务时查询并安装所需包的 latest,完整读实际包的 SKILL.md 及任务所需参考;不要只看包名或沿用上下文记忆。可在仓库外独立工具目录安装:
12
+ 默认通过 npm 官方 registry 的 `@latest` 获取发布 Skill;用户明确指定 PR/源码时,按下述显式源码规则消费。进入任务时查询并安装所需包的 latest,完整读实际包的 SKILL.md 及任务所需参考;不要只看包名或沿用上下文记忆。可在仓库外独立工具目录安装:
13
13
 
14
14
  ```bash
15
15
  npm install --prefix <工具目录> --no-save --package-lock=false @antprofuse/saddle-islands-design@latest --registry https://registry.npmjs.org/
@@ -17,6 +17,8 @@ npm install --prefix <工具目录> --no-save --package-lock=false @antprofuse/s
17
17
 
18
18
  示例占位路径须替换,包名按阶段选择。记录实际解析版本、摘要和本轮使用组合用于复现;这不是在 Skill、业务契约或用户起手模板中固定版本。一次协作批次先对齐同一组实际版本,不在运行途中自动漂移。后续任务重新解析 latest;发现更新或不兼容时重新评估受影响产物,不默认旧产物失效或自动升级产品运行时。
19
19
 
20
+ 用户明确指定 PR 或未发布源码作为本轮规则时,记录仓库、准确 commit、包目录与文件摘要,相关消费者统一使用该组源码;标明未发布,不能称 npm latest 已更新,也不能再用旧 npm 同名规则覆盖它。这是显式源码消费,不是静默回退。
21
+
20
22
  未发布包、安装失败、包间规则冲突须明确反馈;不得悄悄使用草稿或同仓库旧目录冒充 latest。Skill 自身 package.version、格式版本及历史证据版本不是依赖钉死。不得自动改用户模板。维护、提交、推送、发布分别依授权。
21
23
 
22
24
  ## 阶段路由
@@ -41,14 +43,34 @@ npm install --prefix <工具目录> --no-save --package-lock=false @antprofuse/s
41
43
 
42
44
  每项决策先明确唯一规则归属、依据、影响产物、必须同步的 Skill、检查方式、未决部分和迁移权限。按[变更流程](references/changes.md)形成一次可追溯交接。讨论记录放协作仓库;可执行规范落规则所有者,消费者只引用并落实其本阶段义务,避免多处独立定义。
43
45
 
44
- 已确认的一对一主入口脉络必须保留:后端契约 → 唯一前端请求入口 → 唯一后端注册入口。页面/场景可多对一,内部辅助模块可拆分或共享,不增加重复协议实现。
46
+ 已确认的一对一主入口脉络必须保留:后端契约 → 唯一前端请求入口 → 唯一后端注册入口。同一页面语义起点内的多种数据场景可归属同一契约,不同起点独立契约,内部辅助模块可拆分或共享,不增加重复协议实现。
45
47
 
46
48
  具体大小写、派生命名与新格式尚未批准时,执行已有正式契约与现行设计规则,不能用提案静默改名。后续统一命名规范归本 Skill,design 分配身份,各实现 Skill 消费,测试检查关联;Saddle 不负责项目命名策略。
47
49
 
50
+ ## 缺陷倒查与持续改进
51
+
52
+ 发现验收通过但实际结果错误,按 [变更闭环](references/changes.md) 从结果倒查测试预期、断言和执行证据,再检查输入是否充分。测试不能以历史全绿自证正确;输入清晰则修测试设计,输入有歧义则反馈其所有者。每轮准备和交付复核读取相关未闭环缺陷及通用模式,不把业务个例全部堆入 Skill。
53
+
48
54
  ## 完成判定
49
55
 
50
56
  交接必须区分输入自洽、设计完备、测试设施可运行、Mock 黑盒通过、真实联调通过和生产可发布。不得以数量、编译或某单层通过代替下一层。未决事项具体标出影响边界,已确认且独立的部分可并行,不要求无关真实下游先实现。
51
57
 
52
58
  用户授权范围外的仓库、源码、外部服务不修改。推送前按当前权限核对实际远端到候选提交的完整 diff 文件树,不能只看 merge 单亲差异。报告/截图/日志在仓库外本地散文件,不打包、不进源码历史。
53
59
 
54
- 本项目当前授权:只有 Spec 仓库 push 前须用户 review diff;Topics 和自有 Skill 源码可及时提交/push,不需逐次审批。此为当前项目授权,不自动扩大到其他用户项目;使用者以各自最新明确授权为准。npm 发布仍需明确发布授权。
60
+ 权限以当前用户授权为准,不把某次项目的审批策略固化为通用 Skill 规则。分支交接、最终交付/master 合入与 npm 发布分别核对;未授权的阶段不能由上一阶段成功自动放行。
61
+
62
+ ## 确定性语义身份
63
+
64
+ [身份规范](references/semantic-identity.md)及 scripts/semantic_identity.py 是算法唯一来源;设计阶段生成、追加登记并校验,研发只消费已有 ID。新后端契约按设计 Skill 的当前格式;旧项目须同步迁移全部消费者后交付,不静默改名。包始终以 npm latest 获取,算法规则由唯一实现冻结。分支链路身份另行设计,本版不扩展。
65
+
66
+ 后端契约不再包含 coverage。测试阶段直接消费 Spec 正式起点、分支及结果与契约身份,按下述各层规则建立覆盖清单与测试关联;不得从契约内旧 coverage 或既有脚本反推分母。各层 case 定义读取所属测试 Skill;契约身份算法不在各测试包内重复实现,Islands Case ID 不套用后端契约身份算法。
67
+
68
+ ## 分层测试覆盖策略
69
+
70
+ 小程序测试建设归 `saddle-minifish-test`,先于且独立于产品研发。目录、独立 case、Mock 与原生断言的方法由该 Skill 唯一维护;workflow 只路由职责,不复制其具体规则。其 `references/islands-miniapp-case-tree.md` 和 `references/native-miniapp-validation.md` 分别承载覆盖组织与观察方法。
71
+
72
+ 后端测试读取 [固定边界 case 与有效覆盖](references/case-coverage.md),其边界归一化与组合规则不用于改造 Islands 前端目录。两层均区分覆盖建设、设施验证与产品执行证据,不用产品未完成阻止可独立建设的测试。
73
+
74
+ ## 任务规划与过程复盘
75
+
76
+ Owner在协同任务启动前与交付收尾读取[任务规划与过程复盘](references/task-planning-review.md)。节点按[简短复盘模板](references/task-retrospective.md)记录耗时、输入缺口或错误引起的试错、依赖等待与改进建议;Owner核实关键路径和自身规划责任,形成下一轮具体改进。报告留在任务证据区,不进业务Git;仅经证据支持的通用经验进入Skill。规划经验归本Skill,测试与产品技术经验分别归对应Skill,避免多套入口。
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@antprofuse/saddle-workflow",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "盘点或推进 Islands 项目从 Spec、adapt、设计、测试到实现的交接,确定工程决策归属、影响范围和各阶段 Skill。用于跨阶段协调或规则变更;不替代业务设计、平台研发或生产发布决策。",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "files": [
7
7
  "SKILL.md",
8
- "references"
8
+ "references",
9
+ "scripts/*.py"
9
10
  ],
10
11
  "publishConfig": {
11
12
  "access": "public",
@@ -0,0 +1,69 @@
1
+ # 固定边界 case 与有效覆盖
2
+
3
+ 本文件以下归一化、组合选取、case 拆合与身份规则适用于后端测试。小程序测试必须改读 `saddle-minifish-test` 的 `references/islands-miniapp-case-tree.md`,不把以下规则用于重新设计 Islands 前端目录。测试阶段先冻结源责任基线与覆盖范围,再按确定的分区逐批物化完整case、编写、复用和执行测试;API 设计不承担 coverage。业务依据限当前 Spec、Prototype、公开平台合同及明确批准的工程约定,不能用产品现状补需求。
4
+
5
+ ## 被测对象决定边界
6
+
7
+ 先声明被测对象、直接输入/输出和拥有的逻辑。前端边界包括页面入参、前置页面状态、宿主环境/事件,以及前端直接调用的后端或外部接口的请求、返回、失败和相关时序。后端接口背后的SQL、gRPC、分支或外部服务内部过程均在前端边界外;这些过程变化若未改变前端边界,不增加前端 case。
8
+
9
+ 后端以自身公开入口、可信上下文和直接依赖的请求/返回、数据库状态/副作用为边界,不展开被依赖服务内部实现。两层的分母独立,从共同源责任分别投影,不按后端路径数乘出前端 case 数。
10
+
11
+ 归一化前先将来源世界投影到被测对象边界。边界外差异产生相同的本层输入、答复、事件及相关时序时,合并本层 case,并保留多个来源关联。边界内不同值也可按有据的分支等价类选代表,但不能只因预期最终画面相同就合并:不同的本层处理、请求、失败或等待行为仍需独立覆盖。扩展数量以本层程序的分支和行为差异为依据。
12
+
13
+ ## Case 的单位
14
+
15
+ 一个 case 是一次触发后连续程序行为,在所有影响被测对象本次分支的边界条件下的一种确定情况。定义中固定:正式来源起点、触发输入、前置状态、外部预期请求及返回/失败、相关宿主环境、影响结果的时序,以及结束条件。未知或遗漏的边界不得隐含为成功。
16
+
17
+ 一般以人为中断结束本次 case;下次用户操作或回调是新起点。例如页面打开并自动查询直到等待用户、点击支付直到交给收银台等待用户、收到支付结果后的自动处理分别成 case。普通自动 RPC 不因此拆开。前端以公开页面/宿主事件触发,后端以公开接口请求触发并保留页面起点归属;两层分别定义边界,不强求 case 数相同。
18
+
19
+ 局部确定序列是例外:一系列动作仅影响同一局部状态,固定边界下没有条件分叉、没有新的独立业务选择时,可以在一个 case 内连续操作并充分断言,不按点击数量强制拆分。例如同一公告的展开、收起可合验;进入收银台等待用户选择仍拆分。记录合并依据和每步预期;若后续变更新增分支或边界,则重新拆分。不能用这个例外合并跨业务阶段或不同边界组合。
20
+
21
+ 前置准备可复用先前操作,但必须与被验收的触发区分;除上述局部确定序列外,不把多个独立用户动作串成一个 case。迟到响应可以来自前序准备,当前 case 固定其交付时序并检查当前行为。参数化每一种不同边界组合分别列举;一份运行文件或循环不是一个 case 的依据。
22
+
23
+ ## 先定分母和选取理由
24
+
25
+ 1. 从当前正式输入取得起点、条件、分支、结果、元素和副作用义务;保留来源身份与原文坐标。编译工具 TODO 或不能投影首先是工具/分析缺口,不直接认定 Spec 缺失。
26
+ 2. 列出边界等价类、相关性与可达约束。值可合并为同一类,必须说明为何不改变被测规则;边界值仍单列。排除不可达组合要有依据。
27
+ 3. 每条正式分支及正常、失败、空、等待等已有语义都要有见证。复合判断验证条件的独立影响;无法独立影响时记录约束,验证优先级、互斥、遮蔽及回退。不能用分支命中替代这些组合责任。
28
+ 4. 对共享状态、数据依赖、因果顺序及竞争时序建立联合 case,包含前序失败禁止后续调用等反例。独立区域不机械做全局笛卡尔积,也不能仅声称独立:记录依赖分析与组合削减理由。pairwise、单因素或 MC/DC 都不能单独证明整个业务覆盖完整;存在多条件共同作用时补相应联合组合。
29
+ 5. 物化完整 case 清单、边界预期和归一化描述后去重,交付确定数量、待明确数量及复用/新增/退役明细。源目录、ASSERT、脚本和已通过数量不能替代 case 分母。策略由测试设计负责,不把专业组合选择推给用户。
30
+
31
+ 归一化描述保留源起点和边界含义,排除断言排列、报告路径等非语义差异。只有证明边界语义等价才合并;新增断言不新建 case,边界组合变化须重新核定身份。沿项目统一身份工具生成标识,不自行发明另一套摘要算法;尚无 case 身份实现时显式保留规范原文与唯一性检查,不能冒称自动身份机制已经完成。
32
+
33
+ ## 覆盖公式与有效性
34
+
35
+ 维护单一关联:来源义务 → case → 具体断言 → 可检测的错误 → 执行证据。至少分别报告:
36
+
37
+ - case 执行覆盖 = 实际执行 case 数 / 已确定的适用 case 数。
38
+ - case 通过率 = 通过 case 数 / 已确定的适用 case 数。
39
+ - 义务验证覆盖 = 本次已执行且通过有效断言的义务数 / 本层适用来源义务数。
40
+
41
+ 分母附具体清单与排除依据;未确定、未映射、未运行、失败与不适用分别呈现,不从报告中删除以制造全覆盖。一个 case 可以验证多项义务,一项义务也可能需要多个 case;需多个见证的义务必须全部满足才记已验证。
42
+
43
+ 在固定 case 内充分断言请求身份/参数/次数、返回消费、状态、内容、数量、显隐、适用样式/图片加载、禁止调用及副作用。只选择源中适用的义务,不机械添加无依据约束。关键共享断言和每个漏测回归须有针对性负对照,证明错误会被拒绝;前序失败时后续断言不记执行。数量或全绿都不能证明绝对无缺陷。
44
+
45
+ ## 独立分工与持续迭代
46
+
47
+ 一个完整 case 的编写和修复只归属一个执行节点;节点可批量负责多个完整 case。禁止把同一 case 按视觉、内容、请求或单个 ASSERT 分给不同成员。独立审查不改变编写归属。公共设施单一维护后供各 case 使用;Owner 在检查点处理分支与交接,不让整合节点补全拼接的半成品 case。
48
+
49
+ 并行按独立 case 集合、预计耗时与共享设施依赖规划,目标是缩短关键路径,不能用节点数量衡量效率。先明确源责任范围、分区判定与所有权,再在各区物化完整case并编写。已有正式测试输入时先满足本轮适用基线,不把额外全组合研究或全局case数量审批作为全部执行的前置。
50
+
51
+ 下一迭代按来源、边界、契约和设施差异分析影响。未变化 case 可以复用,经核对补断言而不重建;受影响部分重审并运行,新来源补分母,失效 case 记录退役原因。历史运行证据不自动证明新制品通过。漏测记录来源、漏掉的边界/断言、最小反例、修复证据与同类影响,项目具体回归留测试目录,通用规则在本规范及所属测试参考中持续归并。
52
+
53
+ ## 可判定的分工与责任满足
54
+
55
+ 分区依据是完整case的固定边界,不是自然语言标题或产品实际输出。交付可执行判定器或等价的确定性规则:对每个合法边界恰有一个分区成立,非法或未确定输入明确拒绝;用交集检查和遗漏检查核验,不能只测试几个正例。分区归属正确不证明覆盖完整。
56
+
57
+ 每条源责任保留稳定源ID、适用边界条件、必须验证的预期,以及case/具体断言/执行证据。涉及多个见证时显式列出全部要求。公共检查器应拒绝重复归属、悬空引用、未登记责任和前序失败后仍标通过;文件行存在只能证明引用有效,不能证明断言语义充分。不能机器证明的转换明确待审,并保留原责任,不由执行者自行删除或计为满足。
58
+
59
+ ## 终态预期与阻塞归因
60
+
61
+ 原型可能同时保存多个分支。以固定边界→状态变化或分支选择→预期终态→断言推导预期;静态HTML当前hidden、空节点或分支名字均不能单独证明该case终态。也不能按产品截图反推预期。判断源冲突之前,核对场景准备、转换合同、生成描述及运行时观察语义;证据不完整时记录待核,不提前要求上游修复。
62
+
63
+ 测试建设、设施验证、产品验收分阶段报告。已知产品红灯不阻塞仍可编写的独立case,也不允许降低断言。只影响部分责任的观测或环境缺口单列具体范围,其余继续;修复或新证据出现前不反复运行同一失败。无法证明的内部任务状态不能用直接RPC完成冒充,也不能未经依据要求产品新增内部观察API。
64
+
65
+ ## 经验进入下一轮的条件
66
+
67
+ 执行中在首批可读产物、阶段变化或阻塞处抽查实际边界、代码与源映射,不仅等待最终报告。发现偏差只调整必要范围,保留有效成果,遵守活动任务输入变更协议。
68
+
69
+ 问题登记至少含来源证据、事实/推论、影响责任、最终决策、实施和复验状态、下一轮预防检查。纠正旧结论时标明被替代依据;未核实推论不得变成通用规则。项目事实和回归留项目交付/报告,已确认且可复用的决策进入规则所属Skill。下一轮先核未闭环项及预防检查,再按新输入判断是否适用,不能机械继承旧业务结论。记录或解除阻塞不代表缺陷已关闭。
@@ -28,6 +28,16 @@ backend-test 负责公开配置、DDL、映射及实例校验;backend 提供
28
28
 
29
29
  先修改规则所有者,再更新实际受影响消费者;校验引用、示例、模板和检查器的一致性。不为消除冲突擅自改变业务输入。
30
30
 
31
- 所有引用保持 @latest。涉及互相依赖的新包时,先发布被依赖包并核验,再发布消费者;同一协作批次解析并共享实际版本清单。发布尚未全部完成不能通知下游“已可用”。最新包之间若冲突,暂停受影响边界并反馈,不偷偷回退版本或使用未发布本地草稿替代。
31
+ 默认发布消费保持 @latest;用户显式指定 PR/源码时,遵循 workflow 的源码消费规则,统一准确 commit 与文件摘要,不冒称已发布。涉及互相依赖的新包时,先发布被依赖包并核验,再发布消费者;同一协作批次解析并共享实际版本清单。发布尚未全部完成不能通知下游“已可用”。最新包之间若冲突,暂停受影响边界并反馈,不偷偷回退版本或使用未发布本地草稿替代。
32
32
 
33
33
  Skill 发布不自动升级现有业务交付;业务迁移仍需独立范围、diff 和验收。提交权限不等于 push 权限,push 权限不等于 npm 发布权限。
34
+
35
+ ## 测试通过却漏出缺陷
36
+
37
+ 1. 记录用户实际看到的错误、影响范围与当前输入依据。先倒查测试是否表达了正确预期、是否有足够断言、是否实际执行到目标边界,再判断测试输入本身是否缺失或冲突。不要仅因代码有错就跳过测试层复盘。
38
+ 2. 输入清晰:测试建设先修错误预期/遗漏责任,建立能检出缺陷的回归和设施反例,再让研发修复并完成自己的全量相关自测。输入歧义:返回语义所有者确认,明确受影响边界,独立工作继续。普通 TDD 红灯且预期正确时,研发内部修复即可,不为每次失败启动跨节点大循环。
39
+ 3. 独立验收同时检查断言依据与真实执行结果;核对整体组合边界,不能把各层单独通过相加当作整体正确。确认新增回归能拒绝已知错误且不拒绝合法替代。
40
+ 4. 闭环记录:缺陷及来源、漏测机制、测试/产品/输入各自变更、正反例与实际运行证据、剩余限制、规则所有者与消费者。已知错误尚不能被自动检测时,不把“已看截图”标成闭环。
41
+ 5. 提炼时先区分项目事实、测试工具修复、可复用方法、跨阶段职责。项目事实进项目回归;通用方法进所属 Skill 的单一参考;消费者只增加必要交接义务。检查同类规则,合并重复、注明替代关系;本次只影响少数 Skill 时不机械全改。
42
+
43
+ 迭代开始读取与本轮范围有关的未闭环问题,交付时核对新发现是否已进入回归或明确待办。持续积累不等于宣称测试穷尽;记录具体适用范围,不引入无来源的业务要求、发布权限或技术限制。Skill 静态校验与示例审阅不等于真实业务回归通过。
@@ -25,8 +25,8 @@
25
25
  - Spec → adapt:范围内实体、流程、Prototype 与正式测试责任可完整获取;分析缺口与业务矛盾分开记录。
26
26
  - adapt → design:承载、入口、消费数据来源及相应能力语义足以确定当前边界。TODO 按具体影响隔离;不能因为 real RPC 尚未实现就认定无法设计。
27
27
  - design → tests:请求、结果、失败、边界和来源足以形成独立预期;后端还需 DB 和共享 proto。未知输出不能由测试猜填。
28
- - tests → implementation:用例/覆盖/夹具/请求守卫及设施属于测试交付;无产品时明确接入尚待实测,后续由测试方完成,不交研发补测试 adapter。
29
- - implementation → Mock 验收:真实交付制品使用既有黑盒设施通过;各层单测和 Gate 单列。
28
+ - tests → implementation:除来源 ID,必须交接独立预期、关键禁止结果和实际观察范围;前端包括宿主/页面/外部组件的语义承载,后端包括入口/外呼/存储及副作用。用例/覆盖/夹具/请求守卫及设施属于测试交付;无产品时明确接入尚待实测,后续由测试方完成,不交研发补测试 adapter。
29
+ - implementation → Mock 验收:研发在节点内完成实现与自测闭环,真实阻塞才返回对应所有者;独立验收复核预期依据、组合边界和执行证据,不只复读通过数。真实交付制品使用既有黑盒设施通过;各层单测和 Gate 单列。
30
30
  - Mock → 真实联调:不能沿用 Mock 结论;另确认网关、组件宿主、实际 SOFA、身份与环境。
31
31
  - 真实联调 → 发布:需用户确定真实发布检查、权限、监控和回退条件。目前不能据此表自行部署。
32
32
 
@@ -0,0 +1,61 @@
1
+ # 语义身份:权威算法与追加登记
2
+
3
+ 本规范与 `scripts/semantic_identity.py` 是共同交付的唯一实现依据。其他 Skill 引用本包的 npm latest;禁止自行重写、翻译或截短 ID。算法由唯一实现、固定测试向量和 Git 管理,不在数据内部设置版本。未经单独设计不支持其他对象类型。
4
+
5
+ ## 后端契约身份
6
+
7
+ 身份对象必须且仅包含五个字符串字段:`kind` 固定 `backend_contract`,以及 `business`、`domain`、`page`、`trigger`。
8
+
9
+ - business 逐字取正式业务根目录名;domain 逐字取 Spec 领域标识,page 逐字取该领域正式页面标识。登记范围是业务根;不宣称不同业务根中的任意同名项目全球唯一。
10
+ - trigger 使用页面有效声明模型内的 RFC 6901 JSON Pointer,逐段引用 Spec 原文字段/命名映射;`~`、`/` 分别转义为 `~0`、`~1`。它定位正式触发声明,不取描述文本,不翻译命名,不引用输出代码。初始化示例 `/页面/页面初始化事务`;交互定位到其完整组件声明的 `交互`。
11
+ - 定位必须唯一解析到该页面的一个语义触发。一个交互声明含不同触发时不能把整段当作一个起点;必须使用源头正式可区分的位置,并记录依据。不能自行编号、取第一个调用、发明 purpose,不能将列表重排视为语义不变。
12
+ - 页面返回和初始化是不同宿主事件;仅继承初始化配置不等于它们是相同起点。缺省生效的事件只能按当前 Islands 明确的语言规则定位,并在契约 callSites 留下该规则来源,不伪造 Spec 显式声明。没有可解析来源时反馈具体缺口。
13
+ - 中断前的连续请求可聚合;中断后的后续请求以正式续接位置区分。不要为同一连续触发按实体拆接口,也不要跨不同触发复用契约身份。
14
+ - 组件树中的祖先分支若属于声明路径必须保留,不能误删而造成同名组件混淆;测试样例、播放状态、场景选择和运行数据不进入身份。
15
+ - 非身份说明、字段结构、测试与执行结果不参与摘要。正式领域/页面/触发位置变化创建新身份,旧登记保留;这是身份变更,不是偷偷修改登记。无页面的服务入口不伪造页面,本机制暂不覆盖。
16
+
17
+ ## 显式声明与语言缺省来源
18
+
19
+ 有效声明模型是原始 Spec 加上当前 Islands 明确规定的缺省行为,不是设计者补造业务。trigger 标识生效事件;callSites.triggerSource 独立保存其实际证据,不参与摘要。显式/缺省配置切换但事件不变时,身份不变。
20
+
21
+ - 显式位置:`triggerSource: {kind: explicit, path: <页面源文件>, pointer: <与trigger相同>}`,要求严格解析且归属该页面。
22
+ - 页面返回缺省:trigger 固定使用源语言原名 `/页面/页面返回时事务`;`triggerSource: {kind: languageDefault, path: <页面源文件>, pointer: /页面, rule: {path: docs/language/visual.md, heading: 页面初始化事务与终点响应}}`。要求返回键确实缺省、页面映射存在,冻结 Islands 该章节明确规定缺省返回复用初始化。实际初始化配置若缺省,同样按语言规定为空配置;不能伪造返回键已在原文件出现。
23
+ - 上述 languageDefault 只允许该已核实的页面返回规则;不能用于为任意不可解析位置兜底。其他缺省事件必须先补正式源规则与校验。显式空返回配置 `{}` 属于 explicit,不能继续当作继承。
24
+ - `path` 为当前项目源文件位置;rule.path 相对冻结 Islands 根,heading 为正式章节原文。版本/提交及复核证据留任务输入快照,不加入业务身份或契约仓库跨分支引用。
25
+
26
+
27
+ ## 精确算法
28
+
29
+ 1. 拒绝未知/缺失字段、非字符串、空字符串、首尾 Unicode White_Space(固定为 U+0009–000D、0020、0085、00A0、1680、2000–200A、2028、2029、202F、205F、3000)、U+0000–001F、孤立代理码位。大小写敏感,不做 trim 或翻译。
30
+ 2. trigger 必须以 `/` 开头,且 `~` 后只能为 `0` 或 `1`,按 RFC 6901 解释;完整源解析由项目检查器完成。字符串统一 Unicode NFC。字段名固定 ASCII;按字段名字典序排序。
31
+ 3. 生成 UTF-8 JSON:无 BOM,无额外空白,无末尾换行;中文不转义,双引号和反斜线采用 JSON 转义。当前算法不接受数值、数组或嵌套对象,避免依赖各语言数值/集合序列化差异。
32
+ 4. 对上述字节计算 SHA-256,取摘要前16字节,以32个小写十六进制字符表示,前置 `api_`。完整 ID 固定36字符。
33
+ 5. 截断摘要不保证数学上绝无碰撞;登记时发现同 ID 不同归一化身份立即拒绝,禁止加随机后缀、临时加长或覆盖历史记录。
34
+
35
+ 固定向量(单行 JSON 无尾换行):
36
+
37
+ ```json
38
+ {"business":"bill-deploy","domain":"生活缴费","kind":"backend_contract","page":"查账结果","trigger":"/页面/页面初始化事务"}
39
+ ```
40
+
41
+ 期望 ID:`api_91d5e0b55164bb5b60240fa030519c23`。其他正反例见 `scripts/test_semantic_identity.py`。
42
+
43
+ ## 登记表
44
+
45
+ 业务根 `code/identity-registry.json` 进入 Git,形状为 `{"<id>":<归一化身份对象>}`。只新增,不删除、不修改已有记录;保存 identity 的完整源语义定位;不额外保存源文件路径、显示文案、状态或时间戳。不包含 identityVersion 或 registryVersion。
46
+
47
+ 相同身份再次登记幂等;同一身份在当前契约集合出现两份定义则重复报错。契约退休仅移除契约文件,登记记录永存。索引只用于身份一致性与历史碰撞检测,不承担业务路由或运行时查询。
48
+
49
+ ```sh
50
+ python3 <workflow-package>/scripts/semantic_identity.py generate identity.json
51
+ python3 <workflow-package>/scripts/semantic_identity.py register identity.json --registry code/identity-registry.json
52
+ python3 <workflow-package>/scripts/semantic_identity.py check --registry code/identity-registry.json --baseline /tmp/base-registry.json --active /tmp/active-contract-identities.json
53
+ ```
54
+
55
+ active 是从当前**全部**后端契约严格解析导出的 `[{"id":...,"identity":...}]`,不是手工挑选清单。项目契约校验器负责 YAML 安全解析、全量发现以及来源可解析性;本工具不假称校验业务来源。
56
+
57
+ baseline 必须从真实目标分支的共同基线提取,不能用待审当前文件代替。首次引入时需核实 Git 基线确实无登记表才使用空表。分支合并取登记并集,不解决为“只保留一侧”;Owner 合并后重新检查完整集合及目标分支历史。登记变更串行执行。
58
+
59
+ 现有语义命名 ID 不得静默改成摘要。项目迁移必须同步文件、契约引用、前端调用、后端入口、日志和测试,按交付范围评审。仅更新 Skill 不代表项目已经迁移。
60
+
61
+ 来源检查使用同包 `validate_trigger_source(identity, triggerSource, document, page_path=实际源文件路径, language_guide=冻结Islands指南全文)`。document 必须由当前页面真实YAML严格解析,不用样例或手工构造替代。工具检查页面路径归属、指针解析、缺省前提和指南依据;真实触发语义、完整输入绑定及流程可达性仍由项目设计检查。当前源文件路径采用 Islands 的 `业务根/spec/domains/领域/visual/页面.yaml` 布局,其他布局须明确支持后再消费,不猜别名。
@@ -0,0 +1,29 @@
1
+ # 任务规划与过程复盘
2
+
3
+ 操作CoTask前读取平台cotask-owner协议;本文归saddle-workflow,承载Owner的规划经验,不另定义工具协议。任务授权和用户当前要求优先。
4
+
5
+ ## 启动前
6
+
7
+ - 范围指向可索引的具体目录/原ID或具体交付对象,不只分配“处理测试”“完善前端”等分类。说明完成条件、文件所有权、依赖输入与输出位置。
8
+ - 在放行并行任务前核清共同入口、真实请求边界、可调用公共接口、工具与规则版本。未决业务选择列出依据供用户决策,不派开放任务靠执行者猜。
9
+ - 按写入冲突决定并行:区域测试在文件和准备接口互不冲突时可并行;同一页面产品实现采用单写者。公共设施、全局索引有唯一维护者,各区域独立文件和分支,集成后以精确版本交接。
10
+ - 给执行者实际可访问的规则和工具,不把Owner已读过/Skill源码已改当成成员已获得。需要发布的依赖先发布并核安装内容;Owner规划规则不必整篇复制给执行者,但本轮交付要求必须明确注入任务。
11
+ - 任务应有范围内修复权限,普通测试红灯分清产品或设施责任,不因为一次失败就阻塞或新增节点。真正源冲突与缺少不可替代输入才升级。
12
+
13
+ ## 节点完成与Owner复盘
14
+
15
+ 任务定义要求短篇过程复盘,字段见[复盘模板](task-retrospective.md)。报告写任务共享证据区,最终由Owner持久归档,不能混入业务代码仓。无问题可以明确写无,不为模板制造问题。
16
+
17
+ Owner收尾时结合报告和真实执行记录核对:
18
+ - 总历时与关键路径,安装/有效建设/验证/等待/返工分开;并行节点时长不能直接相加当总历时。
19
+ - 输入缺失、不准确、版本不可用、职责冲突造成的试错具体是哪一条;区分Owner设计缺陷、平台限制、正常产品修复。
20
+ - 重复运行是否由代码变化/失败修正/独立验收需要支撑,不能把必要验证称浪费,也不以省时为由减少覆盖。
21
+ - 给下一轮最多几项有证据的可执行改进:改哪里、谁负责、预期省掉哪类工作、怎样验证有效。节省时间未实测时标为待验证,不能凭感觉报提升。
22
+
23
+ ## 长期沉淀与归属
24
+
25
+ 通用规划/交接/效率经验进入本workflow参考;测试覆盖与断言进入测试Skill;小程序实现进入产品Skill。业务未决值、单次失败日志、具体任务SHA不作为通用规则。
26
+
27
+ 只有来源清楚、能改变后续决策的经验才进入Skill。一次性细节留在任务复盘;不追加越来越长的强制流程。新规则先在小范围下一轮验证,再据效果保留或修正。已确认有效的技术方法必须通过实际发布/安装/任务引用链送达消费者,不能只记录在聊天或未发布源码。
28
+
29
+ 本参考由Owner直接使用。修改后执行skill-creator的quick_validate,并人工核与用户规则/其他Skill不冲突;校验通过不等于效率已证明。下轮规划显式读取最近Owner复盘和本参考,记录采用了哪项改进。
@@ -0,0 +1,13 @@
1
+ # 节点过程复盘(给Owner,不是业务验收报告)
2
+
3
+ 建议控制在一页内。复用已有日志和handoff,不额外重跑收集统计。不能精确测量的写估算及依据,无法得到的写未知。
4
+
5
+ - 节点/职责/交付:本次完成范围,引用现有handoff。
6
+ - 时间:开始/结束;安装、实际建设、验证、等待、返工的可测耗时或估算;并行重叠注明,不重复计时。
7
+ - 运行:实际运行次数,哪些因修改/失败修正/独立验收必需,哪些本可避免,附对应命令或日志索引。
8
+ - 输入问题:缺失、不准确、冲突、不可访问或版本漂移的具体输入 → 导致的尝试 → 代价 → 最终依据。没有就写无。
9
+ - 协作问题:依赖等候、公共文件冲突、职责不清、交接缺项、Owner额外介入及原因。
10
+ - 有效做法:本轮实际避免了什么重复,证据或对照是什么;无对照不声称量化提效。
11
+ - 改进建议:至多3项,包含应改的任务定义/接口/环境/Skill、适用范围、后续如何验证。区分已验证经验与待验证建议。
12
+
13
+ Owner最终汇总另写:关键路径与主要浪费;自身规划责任;采纳/暂不采纳的建议及理由;下一轮具体修改;经验进入哪个Skill、是否已真正送达消费者。报告/日志/截图不提交业务Git。
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env python3
2
+ """Semantic identity generator. Python 3 standard library only."""
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import re
8
+ import tempfile
9
+ import unicodedata
10
+ from pathlib import Path
11
+
12
+ # Explicit Unicode White_Space set, frozen for this algorithm (not Python-version-dependent strip).
13
+ EDGE_SPACE = frozenset(chr(n) for n in [*range(9,14),32,133,160,5760,*range(8192,8203),8232,8233,8239,8287,12288])
14
+
15
+ FIELDS = {'kind', 'business', 'domain', 'page', 'trigger'}
16
+
17
+
18
+ def unique_pairs(pairs):
19
+ result = {}
20
+ for key, value in pairs:
21
+ if key in result:
22
+ raise ValueError(f'duplicate JSON key: {key}')
23
+ result[key] = value
24
+ return result
25
+
26
+
27
+ def read(path):
28
+ return json.loads(Path(path).read_text(encoding='utf-8'), object_pairs_hook=unique_pairs)
29
+
30
+
31
+ def normalize(identity):
32
+ if not isinstance(identity, dict) or set(identity) != FIELDS:
33
+ raise ValueError('identity must contain exactly: ' + ', '.join(sorted(FIELDS)))
34
+ result = {}
35
+ for key, value in identity.items():
36
+ if not isinstance(value, str) or not value or (value[0] in EDGE_SPACE or value[-1] in EDGE_SPACE):
37
+ raise ValueError(f'{key}: expected nonempty string without surrounding whitespace')
38
+ if any(ord(c) < 32 or 0xD800 <= ord(c) <= 0xDFFF for c in value):
39
+ raise ValueError(f'{key}: control characters and surrogates are forbidden')
40
+ result[key] = unicodedata.normalize('NFC', value)
41
+ if result['kind'] != 'backend_contract':
42
+ raise ValueError('unsupported identity kind')
43
+ if not result['trigger'].startswith('/') or re.search(r'~(?![01])', result['trigger']):
44
+ raise ValueError('trigger: expected RFC 6901 pointer with valid escaping')
45
+ return result
46
+
47
+
48
+ RETURN_RULE = {'path': 'docs/language/visual.md', 'heading': '页面初始化事务与终点响应'}
49
+
50
+
51
+ def resolve_pointer(document, pointer):
52
+ if not isinstance(pointer, str) or not pointer.startswith('/') or re.search(r'~(?![01])', pointer):
53
+ raise ValueError('invalid source pointer')
54
+ value = document
55
+ for encoded in pointer[1:].split('/'):
56
+ key = encoded.replace('~1', '/').replace('~0', '~')
57
+ if isinstance(value, dict) and key in value:
58
+ value = value[key]
59
+ elif isinstance(value, list) and re.fullmatch(r'0|[1-9][0-9]*', key) and int(key) < len(value):
60
+ value = value[int(key)]
61
+ else:
62
+ raise ValueError('source pointer does not resolve')
63
+ return value
64
+
65
+
66
+ def validate_trigger_source(identity, source, document, *, page_path, language_guide=None):
67
+ """Validate source shape/resolution. Project compiler still verifies trigger semantics.
68
+
69
+ Caller supplies the actual parsed source and its repository path, not a sample.
70
+ language_guide is the actual frozen Islands visual.md text, not user content.
71
+ """
72
+ value = normalize(identity)
73
+ expected = f"{value['business']}/spec/domains/{value['domain']}/visual/{value['page']}.yaml"
74
+ if page_path != expected or not isinstance(source, dict) or source.get('path') != page_path:
75
+ raise ValueError('source does not belong to the identity page')
76
+ if source.get('kind') == 'explicit':
77
+ if set(source) != {'kind', 'path', 'pointer'} or source['pointer'] != value['trigger']:
78
+ raise ValueError('explicit source must match trigger')
79
+ return resolve_pointer(document, source['pointer'])
80
+ if source.get('kind') != 'languageDefault' or set(source) != {'kind', 'path', 'pointer', 'rule'}:
81
+ raise ValueError('unknown trigger source shape')
82
+ if value['trigger'] != '/页面/页面返回时事务' or source['pointer'] != '/页面' or source['rule'] != RETURN_RULE:
83
+ raise ValueError('unsupported language default')
84
+ page = resolve_pointer(document, '/页面')
85
+ if not isinstance(page, dict) or '页面返回时事务' in page:
86
+ raise ValueError('return declaration is explicit, not inherited')
87
+ heading = '## ' + RETURN_RULE['heading']
88
+ if not isinstance(language_guide, str) or heading not in language_guide:
89
+ raise ValueError('missing frozen Islands rule')
90
+ section = language_guide.split(heading, 1)[1].split('\n## ', 1)[0]
91
+ if '缺少返回事务声明时整体复用初始化事务配置' not in section:
92
+ raise ValueError('frozen Islands guide does not establish this default')
93
+ initialization = page.get('页面初始化事务', {})
94
+ if not isinstance(initialization, dict):
95
+ raise ValueError('invalid initialization configuration')
96
+ return initialization
97
+
98
+
99
+ def canonical(identity):
100
+ return json.dumps(normalize(identity), ensure_ascii=False, sort_keys=True, separators=(',', ':'))
101
+
102
+
103
+ def identifier(identity):
104
+ return 'api_' + hashlib.sha256(canonical(identity).encode('utf-8')).hexdigest()[:32]
105
+
106
+
107
+ def validate_registry(registry):
108
+ if not isinstance(registry, dict):
109
+ raise ValueError('registry must be an ID-to-identity object')
110
+ for key, identity in registry.items():
111
+ if identity != normalize(identity):
112
+ raise ValueError(f'{key}: registry identity must already be normalized')
113
+ if key != identifier(identity):
114
+ raise ValueError(f'{key}: identity digest mismatch')
115
+ return registry
116
+
117
+
118
+ def validate_append_only(base, current):
119
+ validate_registry(base)
120
+ validate_registry(current)
121
+ for key, identity in base.items():
122
+ if current.get(key) != identity:
123
+ raise ValueError(f'{key}: historical entry removed or changed')
124
+
125
+
126
+ def register(registry, identity):
127
+ validate_registry(registry)
128
+ value = normalize(identity)
129
+ key = identifier(value)
130
+ previous = registry.get(key)
131
+ if previous is not None and previous != value:
132
+ raise ValueError(f'{key}: digest collision')
133
+ registry[key] = value
134
+ return key
135
+
136
+
137
+ def check_active(registry, contracts):
138
+ validate_registry(registry)
139
+ if not isinstance(contracts, list):
140
+ raise ValueError('active contracts must be a JSON array')
141
+ seen = set()
142
+ for contract in contracts:
143
+ if not isinstance(contract, dict) or set(contract) != {'id', 'identity'}:
144
+ raise ValueError('active record must contain exactly id and identity')
145
+ key = identifier(contract['identity'])
146
+ if contract['id'] != key:
147
+ raise ValueError('active contract digest mismatch')
148
+ if key in seen:
149
+ raise ValueError(f'{key}: duplicate active definition')
150
+ seen.add(key)
151
+ if registry.get(key) != normalize(contract['identity']):
152
+ raise ValueError(f'{key}: active identity is not registered')
153
+
154
+
155
+ def main():
156
+ parser = argparse.ArgumentParser(description=__doc__)
157
+ sub = parser.add_subparsers(dest='command', required=True)
158
+ generate = sub.add_parser('generate')
159
+ generate.add_argument('identity')
160
+ append = sub.add_parser('register')
161
+ append.add_argument('identity')
162
+ append.add_argument('--registry', required=True)
163
+ verify = sub.add_parser('check')
164
+ verify.add_argument('--registry', required=True)
165
+ verify.add_argument('--baseline', required=True, help='registry extracted from trusted Git base')
166
+ verify.add_argument('--active', required=True, help='all current contracts exported as id/identity JSON array')
167
+ args = parser.parse_args()
168
+ try:
169
+ if args.command == 'generate':
170
+ value = read(args.identity)
171
+ print(json.dumps({'id': identifier(value), 'identity': normalize(value)}, ensure_ascii=False, sort_keys=True))
172
+ elif args.command == 'register':
173
+ path = Path(args.registry)
174
+ registry = read(path) if path.exists() else {}
175
+ key = register(registry, read(args.identity))
176
+ content = json.dumps(registry, ensure_ascii=False, sort_keys=True, indent=2) + '\n'
177
+ # No write occurs until validation succeeds. Commit serially; concurrent branch merges require recheck.
178
+ temporary = None
179
+ try:
180
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=path.parent, delete=False) as stream:
181
+ temporary = stream.name
182
+ stream.write(content)
183
+ stream.flush()
184
+ os.fsync(stream.fileno())
185
+ os.replace(temporary, path)
186
+ finally:
187
+ if temporary and os.path.exists(temporary):
188
+ os.unlink(temporary)
189
+ print(key)
190
+ else:
191
+ current = read(args.registry)
192
+ validate_append_only(read(args.baseline), current)
193
+ check_active(current, read(args.active))
194
+ print('identity registry and active contracts: PASS')
195
+ except (ValueError, OSError, TypeError) as error:
196
+ parser.exit(1, f'{error}\n')
197
+
198
+
199
+ if __name__ == '__main__':
200
+ main()
@@ -0,0 +1,135 @@
1
+ import copy
2
+ import json
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ from pathlib import Path
7
+ import unittest
8
+ from unittest.mock import patch
9
+ import semantic_identity as s
10
+
11
+ IDENTITY = {'kind': 'backend_contract', 'business': 'bill-deploy', 'domain': '生活缴费', 'page': '查账结果', 'trigger': '/页面/页面初始化事务'}
12
+ EXPECTED = 'api_91d5e0b55164bb5b60240fa030519c23'
13
+
14
+ class IdentityTests(unittest.TestCase):
15
+ def registry(self):
16
+ return {}
17
+
18
+ def test_fixed_vector(self):
19
+ self.assertEqual(s.identifier(IDENTITY), EXPECTED)
20
+ self.assertEqual(s.canonical(IDENTITY), '{"business":"bill-deploy","domain":"生活缴费","kind":"backend_contract","page":"查账结果","trigger":"/页面/页面初始化事务"}')
21
+
22
+ def test_order_and_unicode(self):
23
+ self.assertEqual(s.identifier(dict(reversed(list(IDENTITY.items())))), EXPECTED)
24
+ self.assertEqual(s.identifier({**IDENTITY, 'page':'café'}), s.identifier({**IDENTITY, 'page':'cafe\u0301'}))
25
+ self.assertNotEqual(s.identifier({**IDENTITY, 'page':'Payment'}), EXPECTED)
26
+
27
+ def test_invalid_identity(self):
28
+ for value in [None, '', [], {}, 1, True, ' payment', 'payment\n', '\ud800']:
29
+ with self.subTest(value=repr(value)), self.assertRaises(ValueError):
30
+ s.identifier({**IDENTITY, 'page':value})
31
+ for value in [{**IDENTITY,'name':'title'}, {k:v for k,v in IDENTITY.items() if k!='page'}, {**IDENTITY,'identityVersion':'v2'}]:
32
+ with self.assertRaises(ValueError): s.identifier(value)
33
+
34
+ def test_source_coordinates(self):
35
+ for trigger in ['page_load', '/页面/~x', '/页面/~']:
36
+ with self.assertRaises(ValueError):
37
+ s.identifier({**IDENTITY, 'trigger':trigger})
38
+ self.assertNotEqual(s.identifier({**IDENTITY, 'domain':'其他领域'}), EXPECTED)
39
+ self.assertNotEqual(s.identifier({**IDENTITY, 'business':'其他业务'}), EXPECTED)
40
+ self.assertNotEqual(s.identifier({**IDENTITY, 'trigger':'/页面/页面返回时事务'}), EXPECTED)
41
+ with self.assertRaises(ValueError):
42
+ s.identifier({**IDENTITY, 'purpose':'query'})
43
+
44
+ def test_duplicate_json_keys(self):
45
+ with self.assertRaises(ValueError): json.loads('{"id":1,"id":2}', object_pairs_hook=s.unique_pairs)
46
+
47
+ def test_idempotence_and_append(self):
48
+ registry=self.registry()
49
+ s.register(registry, IDENTITY)
50
+ base=copy.deepcopy(registry)
51
+ s.register(registry, IDENTITY)
52
+ self.assertEqual(registry, base)
53
+ s.register(registry,{**IDENTITY,'trigger':'/页面/页面返回时事务'})
54
+ s.validate_append_only(base,registry)
55
+ s.check_active(registry,[{'id':EXPECTED,'identity':IDENTITY}])
56
+ s.check_active(registry,[]) # Retired identity stays registered.
57
+
58
+ def test_historical_deletion_and_change(self):
59
+ base=self.registry();s.register(base,IDENTITY)
60
+ with self.assertRaises(ValueError): s.validate_append_only(base,self.registry())
61
+ altered=copy.deepcopy(base);altered[EXPECTED]['page']='other'
62
+ with self.assertRaises(ValueError): s.validate_append_only(base,altered)
63
+
64
+ def test_unregistered_duplicate_or_wrong_active(self):
65
+ record={'id':EXPECTED,'identity':IDENTITY}
66
+ with self.assertRaises(ValueError): s.check_active(self.registry(),[record])
67
+ registry=self.registry();s.register(registry,IDENTITY)
68
+ with self.assertRaises(ValueError): s.check_active(registry,[record,record])
69
+ with self.assertRaises(ValueError): s.check_active(registry,[{**record,'id':'api_wrong'}])
70
+
71
+ def test_simulated_digest_collision(self):
72
+ registry=self.registry();s.register(registry,IDENTITY)
73
+ with patch.object(s,'identifier',return_value=EXPECTED):
74
+ with self.assertRaisesRegex(ValueError,'collision'):
75
+ s.register(registry,{**IDENTITY,'page':'other'})
76
+
77
+ class SourceTests(unittest.TestCase):
78
+ page_path = 'bill-deploy/spec/domains/生活缴费/visual/查账结果.yaml'
79
+ guide = '## 页面初始化事务与终点响应\n缺少返回事务声明时整体复用初始化事务配置(包括成功响应)。'
80
+ def returning(self):
81
+ return {**IDENTITY, 'trigger':'/页面/页面返回时事务'}
82
+ def source(self):
83
+ return {'kind':'languageDefault', 'path':self.page_path, 'pointer':'/页面', 'rule':dict(s.RETURN_RULE)}
84
+ def validate(self, identity, source, document, **kw):
85
+ return s.validate_trigger_source(identity, source, document, page_path=self.page_path, language_guide=kw.get('guide',self.guide))
86
+ def test_inherited_return_distinct_from_initialization(self):
87
+ config={'响应':[]};doc={'页面':{'页面初始化事务':config}}
88
+ self.assertEqual(self.validate(self.returning(),self.source(),doc),config)
89
+ self.assertNotEqual(s.identifier(self.returning()),s.identifier(IDENTITY))
90
+ def test_explicit_empty_is_not_inheritance(self):
91
+ doc={'页面':{'页面返回时事务':{}}}
92
+ with self.assertRaises(ValueError):self.validate(self.returning(),self.source(),doc)
93
+ src={'kind':'explicit','path':self.page_path,'pointer':'/页面/页面返回时事务'}
94
+ self.assertEqual(self.validate(self.returning(),src,doc),{})
95
+ def test_wrong_event_rule_and_page_rejected(self):
96
+ for identity,src in [(IDENTITY,self.source()),(self.returning(),{**self.source(),'rule':{}}),(self.returning(),{**self.source(),'path':'other.yaml'})]:
97
+ with self.assertRaises(ValueError):self.validate(identity,src,{'页面':{}})
98
+ with self.assertRaises(ValueError):self.validate(self.returning(),self.source(),{'页面':{}},guide='')
99
+ def test_explicit_missing_or_wrong_pointer(self):
100
+ src={'kind':'explicit','path':self.page_path,'pointer':IDENTITY['trigger']}
101
+ with self.assertRaises(ValueError):self.validate(IDENTITY,src,{'页面':{}})
102
+ with self.assertRaises(ValueError):self.validate(IDENTITY,{**src,'pointer':'/页面'},{'页面':{}})
103
+ def test_pointer_escaping(self):
104
+ self.assertEqual(s.resolve_pointer({'a/b':{'~key':[7]}},'/a~1b/~0key/0'),7)
105
+ with self.assertRaises(ValueError):s.resolve_pointer({'a':[7]},'/a/00')
106
+
107
+ class CliTests(unittest.TestCase):
108
+ def test_lifecycle_and_failed_write(self):
109
+ script=Path(s.__file__).resolve()
110
+ with tempfile.TemporaryDirectory() as directory:
111
+ root=Path(directory)
112
+ identity=root/'identity.json'; registry=root/'registry.json'
113
+ baseline=root/'baseline.json'; active=root/'active.json'
114
+ identity.write_text(json.dumps(IDENTITY),encoding='utf-8')
115
+ def run(*args):
116
+ return subprocess.run([sys.executable,str(script),*map(str,args)],capture_output=True,text=True)
117
+ generated=run('generate',identity)
118
+ self.assertEqual(generated.returncode,0,generated.stderr)
119
+ self.assertEqual(json.loads(generated.stdout)['id'],EXPECTED)
120
+ self.assertEqual(run('register',identity,'--registry',registry).returncode,0)
121
+ original=registry.read_bytes();baseline.write_bytes(original)
122
+ self.assertEqual(run('register',identity,'--registry',registry).returncode,0)
123
+ self.assertEqual(registry.read_bytes(),original)
124
+ active.write_text(json.dumps([{'id':EXPECTED,'identity':IDENTITY}]),encoding='utf-8')
125
+ self.assertEqual(run('check','--registry',registry,'--baseline',baseline,'--active',active).returncode,0)
126
+ identity.write_text('{"page":null}',encoding='utf-8')
127
+ self.assertNotEqual(run('register',identity,'--registry',registry).returncode,0)
128
+ self.assertEqual(registry.read_bytes(),original)
129
+ registry.write_text(json.dumps({}),encoding='utf-8')
130
+ active.write_text('[]',encoding='utf-8')
131
+ rejected=run('check','--registry',registry,'--baseline',baseline,'--active',active)
132
+ self.assertNotEqual(rejected.returncode,0)
133
+ self.assertIn('removed or changed',rejected.stderr)
134
+
135
+ if __name__=='__main__': unittest.main()