create-yss-spec 3.3.6 → 3.3.7

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.
Files changed (103) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/template/instance-runtime.js +8 -2
  4. package/src/validation/snapshot.js +7 -0
  5. package/template/.agents/skills/.strategic-design-skills-manifest.json +5 -3
  6. package/template/.agents/skills/archify/SKILL.md +1 -1
  7. package/template/.agents/skills/yss-stage-decision/SKILL.md +13 -4
  8. package/template/.agents/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
  9. package/template/.agents/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
  10. package/template/.agents/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
  11. package/template/.agents/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
  12. package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
  13. package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
  14. package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
  15. package/template/.agents/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
  16. package/template/.agents/skills/yss-stage-decision/references/validation-rules.md +2 -2
  17. package/template/.agents/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
  18. package/template/.agents/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
  19. package/template/.agents/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
  20. package/template/.agents/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
  21. package/template/.agents/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
  22. package/template/.agents/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
  23. package/template/.codex/skills/archify/SKILL.md +1 -1
  24. package/template/.codex/skills/yss-stage-decision/SKILL.md +13 -4
  25. package/template/.codex/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
  26. package/template/.codex/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
  27. package/template/.codex/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
  28. package/template/.codex/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
  29. package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
  30. package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
  31. package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
  32. package/template/.codex/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
  33. package/template/.codex/skills/yss-stage-decision/references/validation-rules.md +2 -2
  34. package/template/.codex/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
  35. package/template/.codex/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
  36. package/template/.codex/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
  37. package/template/.codex/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
  38. package/template/.codex/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
  39. package/template/.codex/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
  40. package/template/.cursor/skills/archify/SKILL.md +1 -1
  41. package/template/.cursor/skills/yss-stage-decision/SKILL.md +13 -4
  42. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
  43. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
  44. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
  45. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
  46. package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
  47. package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
  48. package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
  49. package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
  50. package/template/.cursor/skills/yss-stage-decision/references/validation-rules.md +2 -2
  51. package/template/.cursor/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
  52. package/template/.cursor/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
  53. package/template/.cursor/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
  54. package/template/.cursor/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
  55. package/template/.cursor/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
  56. package/template/.cursor/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
  57. package/template/.pi/skills/archify/SKILL.md +1 -1
  58. package/template/.pi/skills/yss-stage-decision/SKILL.md +13 -4
  59. package/template/.pi/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
  60. package/template/.pi/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
  61. package/template/.pi/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
  62. package/template/.pi/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
  63. package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
  64. package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
  65. package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
  66. package/template/.pi/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
  67. package/template/.pi/skills/yss-stage-decision/references/validation-rules.md +2 -2
  68. package/template/.pi/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
  69. package/template/.pi/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
  70. package/template/.pi/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
  71. package/template/.pi/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
  72. package/template/.pi/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
  73. package/template/.pi/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
  74. package/template/docs/process/frontend-backend-delivery.md +7 -4
  75. package/template/docs/process/schemas/backend-delivery.schema.json +4 -0
  76. package/template/docs/process/schemas/frontend-delivery-acceptance-v1.schema.json +200 -0
  77. package/template/docs/process/schemas/frontend-delivery-acceptance.schema.json +37 -3
  78. package/template/docs/process/schemas/frontend-strategic-preflight.schema.json +32 -0
  79. package/template/docs/process/schemas/lifecycle-checkpoint.schema.json +2 -1
  80. package/template/docs/process/schemas/strategic-consumer-feedback.schema.json +40 -0
  81. package/template/docs/process/schemas/strategic-design-handoff-v3.schema.json +743 -0
  82. package/template/docs/process/schemas/strategic-design-handoff-v4.schema.json +712 -0
  83. package/template/docs/process/schemas/strategic-feedback-adjudication.schema.json +42 -0
  84. package/template/docs/process/schemas/strategic-handoff-domain-strategy-v3.schema.json +216 -0
  85. package/template/docs/process/schemas/strategic-handoff-import-receipt.schema.json +39 -0
  86. package/template/docs/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json +148 -0
  87. package/template/docs/process/strategic-handoff-package.md +37 -4
  88. package/template/docs/process/templates/lifecycle-checkpoint-template.yaml +1 -0
  89. package/template/docs/process/templates/stage-checkpoint-template.md +1 -0
  90. package/template/scripts/lib/approval-record.mjs +2 -1
  91. package/template/scripts/lib/backend-delivery.mjs +15 -3
  92. package/template/scripts/lib/frontend-delivery-boundary.mjs +20 -4
  93. package/template/scripts/lib/frontend-delivery.mjs +66 -2
  94. package/template/scripts/lib/strategic-feedback.mjs +33 -0
  95. package/template/scripts/lib/strategic-handoff-consumption.mjs +5 -0
  96. package/template/scripts/lib/strategic-handoff-io.mjs +1 -0
  97. package/template/scripts/lib/strategic-handoff.mjs +101 -10
  98. package/template/scripts/lib/user-decision-reuse.mjs +38 -0
  99. package/template/scripts/strategic-feedback +10 -0
  100. package/template/scripts/verify-frontend-strategic-preflight +12 -0
  101. package/template/skills-lock.json +10 -10
  102. package/template.manifest.json +1 -0
  103. package/template.snapshot.json +6 -5
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://yss.local/schemas/domain-strategy-v2.json",
3
+ "$id": "https://yss.local/schemas/domain-strategy-current.json",
4
4
  "title": "YSS 业务边界与规则设计",
5
5
  "type": "object",
6
6
  "required": ["schema_version", "domain_strategy_id", "domain_version", "status", "contexts", "subdomains", "relationships", "scenarios", "concept_candidates", "invariants", "context_snapshot", "downstream_mapping", "evidence_refs", "approval"],
7
7
  "properties": {
8
8
  "traceability_version": {"const": 1},
9
- "rule_catalog": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["rule_id", "statement", "responsible_context", "status"], "properties": {"rule_id": {"type": "string", "pattern": "^rule\\.[a-z0-9][a-z0-9-]*$"}, "statement": {"type": "string", "minLength": 1}, "responsible_context": {"type": "string", "minLength": 1}, "status": {"const": "confirmed"}}}, "minItems": 1},
10
- "schema_version": {"const": 2},
9
+ "rule_catalog": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["rule_id", "statement", "responsible_context", "status"], "properties": {"rule_id": {"type": "string", "pattern": "^rule\\.[a-z0-9][a-z0-9-]*$"}, "statement": {"type": "string", "minLength": 1}, "responsible_context": {"type": "string", "minLength": 1}, "status": {"const": "confirmed"}, "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}, "minItems": 1},
10
+ "schema_version": {"enum": [2, 3]},
11
11
  "domain_strategy_id": {"type": "string", "pattern": "^domain-strategy\\.[a-z0-9][a-z0-9-]*$"},
12
12
  "domain_version": {"type": "string", "pattern": "^v[0-9]+$"},
13
13
  "status": {"enum": ["draft", "ready-for-human", "approved", "stale", "blocked"]},
@@ -18,7 +18,7 @@
18
18
  "concept_candidates": {"type": "array", "items": {"$ref": "#/$defs/concept"}},
19
19
  "invariants": {"type": "array", "items": {"$ref": "#/$defs/invariant"}},
20
20
  "context_snapshot": {"$ref": "#/$defs/contextSnapshot"},
21
- "downstream_mapping": {"type": "array", "items": {"$ref": "#/$defs/mapping"}},
21
+ "downstream_mapping": {"type": "array"},
22
22
  "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
23
23
  "approval": {"$ref": "#/$defs/approval"}
24
24
  },
@@ -132,6 +132,20 @@
132
132
  "reapproval_condition": {"type": "string", "minLength": 1}
133
133
  }
134
134
  },
135
+ "mappingV3": {
136
+ "type": "object",
137
+ "additionalProperties": false,
138
+ "required": ["mapping_id", "source_refs", "consumer_capability", "impacts", "propagation", "reapproval_condition", "evidence_refs"],
139
+ "properties": {
140
+ "mapping_id": {"type": "string", "pattern": "^mapping\\.[a-z0-9][a-z0-9-]*$"},
141
+ "source_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^(rule|scenario|invariant)\\.[a-z0-9][a-z0-9-]*$"}},
142
+ "consumer_capability": {"enum": ["backend-technical-design", "frontend-engineering-design", "delivery-coordination"]},
143
+ "impacts": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
144
+ "propagation": {"enum": ["direct", "transitive", "not-applicable", "stale"]},
145
+ "reapproval_condition": {"type": "string", "minLength": 1},
146
+ "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}
147
+ }
148
+ },
135
149
  "approval": {
136
150
  "type": "object",
137
151
  "required": ["approval_ref", "approver", "persisted_ref", "current_version"],
@@ -142,5 +156,61 @@
142
156
  "current_version": {"type": "string", "pattern": "^v[0-9]+$"}
143
157
  }
144
158
  }
145
- }
159
+ },
160
+ "allOf": [
161
+ {
162
+ "if": {"properties": {"schema_version": {"const": 3}}, "required": ["schema_version"]},
163
+ "then": {
164
+ "required": ["traceability_version", "rule_catalog"],
165
+ "properties": {
166
+ "traceability_version": {"const": 1},
167
+ "rule_catalog": {
168
+ "items": {
169
+ "required": ["evidence_refs"],
170
+ "properties": {
171
+ "evidence_refs": {
172
+ "type": "array",
173
+ "minItems": 1,
174
+ "uniqueItems": true,
175
+ "items": {"type": "string", "minLength": 1}
176
+ }
177
+ }
178
+ }
179
+ },
180
+ "scenarios": {
181
+ "items": {
182
+ "required": ["rule_refs", "critical", "success_results", "evidence_refs"],
183
+ "properties": {
184
+ "evidence_refs": {
185
+ "type": "array",
186
+ "minItems": 1,
187
+ "uniqueItems": true,
188
+ "items": {"type": "string", "minLength": 1}
189
+ }
190
+ }
191
+ }
192
+ },
193
+ "invariants": {
194
+ "items": {
195
+ "required": ["rule_ref", "evidence_refs"],
196
+ "properties": {
197
+ "evidence_refs": {
198
+ "type": "array",
199
+ "minItems": 1,
200
+ "uniqueItems": true,
201
+ "items": {"type": "string", "minLength": 1}
202
+ }
203
+ }
204
+ }
205
+ },
206
+ "downstream_mapping": {"items": {"$ref": "#/$defs/mappingV3"}}
207
+ }
208
+ },
209
+ "else": {
210
+ "properties": {
211
+ "downstream_mapping": {"items": {"$ref": "#/$defs/mapping"}}
212
+ }
213
+ }
214
+ }
215
+ ]
146
216
  }
@@ -4,12 +4,12 @@
4
4
 
5
5
  ## 必须包含
6
6
 
7
- - 问题陈述、目标用户、MVP、非目标、成功标准和可执行的测试 seam;
8
- - 已确认决策、假设、约束和未决项;
7
+ - 问题陈述、目标用户、MVP、非目标,以及带稳定 ID、来源 ID 和证据的成功标准与可执行测试 seam;
8
+ - 带稳定 ID、来源 ID 和证据的已确认决策、假设、约束,以及有责任人的未决项;
9
9
  - `context_snapshot`:只引用根 `CONTEXT.md`,并绑定结构化术语身份、全文摘要和引用集摘要;
10
10
  - `domain_strategy_ref`:领域战略合同的稳定 ID、版本、digest 和批准状态;
11
11
  - UI/API/Data/Backend/Frontend/跨仓/高风险影响面布尔值;
12
- - 每个下游消费者的传播方式、重新批准条件、证据引用和批准记录。`approval.approval_ref` 必须指向可读取且通过 `scripts/verify-approval-record` 的 `check.stage-decision-package-approved` 会签记录;`approval.approver` 必须与记录中的 `role_id` 一致。
12
+ - 每个消费者能力的稳定 `mapping_id`、领域来源 ID、传播方式、重新批准条件、证据引用和批准记录。`approval.approval_ref` 必须指向可读取且通过 `scripts/verify-approval-record` 的 `gate.stage-decision-package-approved` 会签记录;`approval.approver` 必须与记录中的 `role_id` 一致。
13
13
 
14
14
  ## 未决项规则
15
15
 
@@ -19,4 +19,4 @@
19
19
 
20
20
  只有 `approved` 且引用的领域战略为 `approved`、版本当前的阶段包,才可以进入 Spec synthesis。任何领域战略 digest、版本或影响面变化都使阶段包 `stale`,并要求重新评估。
21
21
 
22
- 阶段包使用 `schema_version: 2`。旧版字符串路径或 Markdown 锚点只能通过迁移器读取;无法唯一映射到 `<ContextId>/<EnglishIdentifier>` 时返回 `migration-required`,不得猜测。
22
+ 新写入阶段包使用 `schema_version: 3`,且只能引用 Domain Strategy v3。v2 仅只读;修改或重新交付须用 `scripts/migrate-contract-v3.mjs` 生成未批准的 `draft` 后补齐来源并重新批准。旧版字符串路径或 Markdown 锚点先通过上下文迁移器读取;无法唯一映射到 `<ContextId>/<EnglishIdentifier>` 时返回 `migration-required`,不得猜测。
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://yss.local/schemas/stage-decision-package-v2-compat.json",
4
+ "allOf": [
5
+ {"$ref": "stage-decision-package.schema.json"},
6
+ {"properties": {"schema_version": {"const": 2}}, "required": ["schema_version"]}
7
+ ]
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://yss.local/schemas/stage-decision-package-v3.json",
4
+ "allOf": [
5
+ {"$ref": "stage-decision-package.schema.json"},
6
+ {"properties": {"schema_version": {"const": 3}}, "required": ["schema_version"]}
7
+ ]
8
+ }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://yss.local/schemas/stage-decision-package-v2.json",
3
+ "$id": "https://yss.local/schemas/stage-decision-package-current.json",
4
4
  "title": "YSS Stage Decision Package",
5
5
  "type": "object",
6
6
  "required": ["schema_version", "stage_decision_id", "package_version", "status", "problem_statement", "target_users", "mvp", "non_goals", "success_criteria", "test_seams", "confirmed_decisions", "assumptions", "constraints", "unresolved_items", "context_snapshot", "domain_strategy_ref", "impact_assessment", "downstream_mapping", "evidence_refs", "approval"],
7
7
  "properties": {
8
- "schema_version": {"const": 2},
8
+ "schema_version": {"enum": [2, 3]},
9
9
  "stage_decision_id": {"type": "string", "pattern": "^stage-decision\\.[a-z0-9][a-z0-9-]*$"},
10
10
  "package_version": {"type": "string", "pattern": "^v[0-9]+$"},
11
11
  "status": {"enum": ["draft", "ready-for-human", "approved", "stale", "blocked"]},
@@ -13,16 +13,16 @@
13
13
  "target_users": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
14
14
  "mvp": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
15
15
  "non_goals": {"type": "array", "items": {"type": "string", "minLength": 1}},
16
- "success_criteria": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
17
- "test_seams": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
18
- "confirmed_decisions": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
19
- "assumptions": {"type": "array", "items": {"type": "string", "minLength": 1}},
20
- "constraints": {"type": "array", "items": {"type": "string", "minLength": 1}},
16
+ "success_criteria": {"type": "array", "minItems": 1},
17
+ "test_seams": {"type": "array", "minItems": 1},
18
+ "confirmed_decisions": {"type": "array", "minItems": 1},
19
+ "assumptions": {"type": "array"},
20
+ "constraints": {"type": "array"},
21
21
  "unresolved_items": {"type": "array", "items": {"$ref": "#/$defs/unresolved"}},
22
22
  "context_snapshot": {"$ref": "#/$defs/contextSnapshot"},
23
23
  "domain_strategy_ref": {"$ref": "#/$defs/domainRef"},
24
24
  "impact_assessment": {"$ref": "#/$defs/impact"},
25
- "downstream_mapping": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/mapping"}},
25
+ "downstream_mapping": {"type": "array", "minItems": 1},
26
26
  "evidence_refs": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
27
27
  "approval": {"$ref": "#/$defs/approval"}
28
28
  },
@@ -84,6 +84,31 @@
84
84
  "reapproval_condition": {"type": "string", "minLength": 1}
85
85
  }
86
86
  },
87
+ "recordV3": {
88
+ "type": "object",
89
+ "additionalProperties": false,
90
+ "required": ["id", "statement", "evidence_refs"],
91
+ "properties": {
92
+ "id": {"type": "string", "pattern": "^(success-criterion|test-seam|decision|assumption|constraint)\\.[a-z0-9][a-z0-9-]*$"},
93
+ "statement": {"type": "string", "minLength": 1},
94
+ "source_refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^(rule|scenario|invariant)\\.[a-z0-9][a-z0-9-]*$"}},
95
+ "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
96
+ "status": {"enum": ["confirmed", "candidate", "stale"]}
97
+ }
98
+ },
99
+ "mappingV3": {
100
+ "type": "object",
101
+ "additionalProperties": false,
102
+ "required": ["mapping_id", "source_refs", "consumer_capability", "propagation", "reapproval_condition", "evidence_refs"],
103
+ "properties": {
104
+ "mapping_id": {"type": "string", "pattern": "^mapping\\.[a-z0-9][a-z0-9-]*$"},
105
+ "source_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "pattern": "^(rule|scenario|invariant)\\.[a-z0-9][a-z0-9-]*$"}},
106
+ "consumer_capability": {"enum": ["backend-technical-design", "frontend-engineering-design", "delivery-coordination"]},
107
+ "propagation": {"enum": ["direct", "transitive", "not-applicable", "stale"]},
108
+ "reapproval_condition": {"type": "string", "minLength": 1},
109
+ "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}
110
+ }
111
+ },
87
112
  "approval": {
88
113
  "type": "object",
89
114
  "required": ["approval_ref", "approver", "persisted_ref", "current_version"],
@@ -94,5 +119,30 @@
94
119
  "current_version": {"type": "string", "pattern": "^v[0-9]+$"}
95
120
  }
96
121
  }
97
- }
122
+ },
123
+ "allOf": [
124
+ {
125
+ "if": {"properties": {"schema_version": {"const": 3}}, "required": ["schema_version"]},
126
+ "then": {
127
+ "properties": {
128
+ "success_criteria": {"items": {"$ref": "#/$defs/recordV3"}},
129
+ "test_seams": {"items": {"$ref": "#/$defs/recordV3"}},
130
+ "confirmed_decisions": {"items": {"$ref": "#/$defs/recordV3"}},
131
+ "assumptions": {"items": {"$ref": "#/$defs/recordV3"}},
132
+ "constraints": {"items": {"$ref": "#/$defs/recordV3"}},
133
+ "downstream_mapping": {"items": {"$ref": "#/$defs/mappingV3"}}
134
+ }
135
+ },
136
+ "else": {
137
+ "properties": {
138
+ "success_criteria": {"items": {"type": "string", "minLength": 1}},
139
+ "test_seams": {"items": {"type": "string", "minLength": 1}},
140
+ "confirmed_decisions": {"items": {"type": "string", "minLength": 1}},
141
+ "assumptions": {"items": {"type": "string", "minLength": 1}},
142
+ "constraints": {"items": {"type": "string", "minLength": 1}},
143
+ "downstream_mapping": {"items": {"$ref": "#/$defs/mapping"}}
144
+ }
145
+ }
146
+ }
147
+ ]
98
148
  }
@@ -2,8 +2,8 @@
2
2
 
3
3
  验证分为结构、引用和语义三层:
4
4
 
5
- 1. **结构**:Schema 版本、稳定 ID、状态枚举、必填字段、条件字段和证据格式。
6
- 2. **引用**:上下文端点、场景、不变量、`context_snapshot.term_refs` 和下游资产引用必须可解析;`context_ref` 必须精确为根 `CONTEXT.md`,双摘要必须与当前内容一致。
5
+ 1. **结构**:Schema 版本、全局稳定 ID 唯一性、状态枚举、必填字段、条件字段和非空证据格式。
6
+ 2. **引用**:上下文端点、规则、场景、不变量、决定、测试 seam、`context_snapshot.term_refs` 和下游 mapping 来源必须可解析;`context_ref` 必须精确为根 `CONTEXT.md`,双摘要必须与当前内容一致。
7
7
  3. **语义**:禁止关系自环、未知上下文、未批准 Shared Kernel、未解释的语义/技术方向差异、职责重叠、无责任人的不变量和跨上下文直接共享实体。
8
8
 
9
9
  以下情况必须 `blocked`:关键规则所有权未知、关键术语冲突、上下文边界未定、关键失败场景缺失、事件语义无法解释、领域专家确认缺失。
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import process from "node:process";
5
+ import { parseArgs } from "node:util";
6
+ import { parseDocument } from "../../../../scripts/vendor/yaml.mjs";
7
+
8
+ function blocked(message) {
9
+ process.stderr.write(`${JSON.stringify({ result: "blocked", reason_code: "migration-required", errors: [message] }, null, 2)}\n`);
10
+ process.exitCode = 1;
11
+ }
12
+
13
+ function parse(source, label) {
14
+ const document = parseDocument(source, { maxAliasCount: 0, uniqueKeys: true });
15
+ if (document.errors.length) throw new TypeError(`${label} YAML 非法: ${document.errors[0].message}`);
16
+ const value = document.toJS({ maxAliasCount: 0 });
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} 必须是对象`);
18
+ return value;
19
+ }
20
+
21
+ function nextVersion(value) {
22
+ const matched = String(value ?? "v2").match(/^v(\d+)$/);
23
+ return `v${Math.max(3, Number(matched?.[1] ?? 2) + 1)}`;
24
+ }
25
+
26
+ function token(value, fallback) {
27
+ const normalized = String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
28
+ return normalized || fallback;
29
+ }
30
+
31
+ function resetApproval(value, version) {
32
+ value.status = "draft";
33
+ value.approval = {
34
+ approval_ref: "migration-required",
35
+ approver: "unassigned",
36
+ persisted_ref: value.approval?.persisted_ref || "migration-output-not-persisted",
37
+ current_version: version
38
+ };
39
+ }
40
+
41
+ function migrateDomain(input) {
42
+ const output = structuredClone(input);
43
+ if (output.schema_version !== 2) throw new TypeError("Domain Strategy 迁移仅支持 schema_version: 2");
44
+ const evidence = Array.isArray(output.evidence_refs) && output.evidence_refs.length ? output.evidence_refs : ["migration-required:evidence"];
45
+ const rules = [];
46
+ const statementToId = new Map();
47
+ for (const [scenarioIndex, scenario] of (output.scenarios ?? []).entries()) {
48
+ scenario.rule_refs = [];
49
+ for (const [ruleIndex, statement] of (scenario.rules ?? []).entries()) {
50
+ let ruleId = statementToId.get(statement);
51
+ if (!ruleId) {
52
+ ruleId = `rule.${token(scenario.scenario_id?.replace(/^scenario\./, ""), `scenario-${scenarioIndex + 1}`)}-${ruleIndex + 1}`;
53
+ statementToId.set(statement, ruleId);
54
+ rules.push({ rule_id: ruleId, statement, responsible_context: scenario.responsible_context, status: "confirmed", evidence_refs: [...evidence] });
55
+ }
56
+ scenario.rule_refs.push(ruleId);
57
+ }
58
+ scenario.critical = false;
59
+ scenario.success_results = Array.isArray(scenario.events) && scenario.events.length ? [...scenario.events] : ["migration-required: 补充成功结果"];
60
+ scenario.evidence_refs = [...evidence];
61
+ }
62
+ for (const [index, invariant] of (output.invariants ?? []).entries()) {
63
+ let ruleId = statementToId.get(invariant.statement);
64
+ if (!ruleId) {
65
+ ruleId = `rule.${token(invariant.invariant_id?.replace(/^invariant\./, ""), `invariant-${index + 1}`)}`;
66
+ statementToId.set(invariant.statement, ruleId);
67
+ rules.push({ rule_id: ruleId, statement: invariant.statement, responsible_context: invariant.responsible_context, status: "confirmed", evidence_refs: [...evidence] });
68
+ }
69
+ invariant.rule_ref = ruleId;
70
+ invariant.evidence_refs = [...evidence];
71
+ }
72
+ const sourceRefs = [...(output.scenarios ?? []).map((item) => item.scenario_id), ...(output.invariants ?? []).map((item) => item.invariant_id)].filter(Boolean);
73
+ output.downstream_mapping = (output.downstream_mapping ?? []).map((mapping, index) => {
74
+ const impacts = Array.isArray(mapping.impacts) ? mapping.impacts : ["migration-required"];
75
+ const isFrontend = impacts.some((item) => /frontend|ui|visual/i.test(item));
76
+ return {
77
+ mapping_id: `mapping.${token(mapping.domain_change, `migrated-${index + 1}`)}`,
78
+ source_refs: sourceRefs.length ? sourceRefs : [rules[0]?.rule_id].filter(Boolean),
79
+ consumer_capability: isFrontend ? "frontend-engineering-design" : "backend-technical-design",
80
+ impacts,
81
+ propagation: mapping.propagation,
82
+ reapproval_condition: mapping.reapproval_condition,
83
+ evidence_refs: [...evidence]
84
+ };
85
+ });
86
+ output.schema_version = 3;
87
+ output.traceability_version = 1;
88
+ output.rule_catalog = rules;
89
+ output.domain_version = nextVersion(output.domain_version);
90
+ resetApproval(output, output.domain_version);
91
+ return output;
92
+ }
93
+
94
+ async function migrateStage(input, root) {
95
+ const output = structuredClone(input);
96
+ if (output.schema_version !== 2) throw new TypeError("Stage Decision 迁移仅支持 schema_version: 2");
97
+ const evidence = Array.isArray(output.evidence_refs) && output.evidence_refs.length ? output.evidence_refs : ["migration-required:evidence"];
98
+ let sourceRefs = [];
99
+ try {
100
+ const domainSource = await readFile(resolve(root, output.domain_strategy_ref.persisted_ref), "utf8");
101
+ const domain = parse(domainSource, "Domain Strategy");
102
+ sourceRefs = [...(domain.rule_catalog ?? []).map((item) => item.rule_id), ...(domain.scenarios ?? []).map((item) => item.scenario_id), ...(domain.invariants ?? []).map((item) => item.invariant_id)].filter(Boolean);
103
+ } catch {
104
+ sourceRefs = [];
105
+ }
106
+ const prefixes = { success_criteria: "success-criterion", test_seams: "test-seam", confirmed_decisions: "decision", assumptions: "assumption", constraints: "constraint" };
107
+ for (const [field, prefix] of Object.entries(prefixes)) {
108
+ output[field] = (output[field] ?? []).map((statement, index) => ({
109
+ id: `${prefix}.${token(statement, `migrated-${index + 1}`)}`,
110
+ statement,
111
+ source_refs: sourceRefs.slice(0, 1),
112
+ evidence_refs: [...evidence],
113
+ status: "candidate"
114
+ }));
115
+ }
116
+ output.downstream_mapping = (output.downstream_mapping ?? []).map((mapping, index) => ({
117
+ mapping_id: `mapping.${token(mapping.domain_change, `migrated-${index + 1}`)}`,
118
+ source_refs: sourceRefs.slice(0, 1),
119
+ consumer_capability: /frontend|ui/i.test(mapping.consumer ?? "") ? "frontend-engineering-design" : /coord/i.test(mapping.consumer ?? "") ? "delivery-coordination" : "backend-technical-design",
120
+ propagation: mapping.propagation,
121
+ reapproval_condition: mapping.reapproval_condition,
122
+ evidence_refs: [...evidence]
123
+ }));
124
+ output.schema_version = 3;
125
+ output.package_version = nextVersion(output.package_version);
126
+ resetApproval(output, output.package_version);
127
+ return output;
128
+ }
129
+
130
+ try {
131
+ const { values, positionals } = parseArgs({
132
+ options: { kind: { type: "string" }, root: { type: "string", default: process.cwd() } },
133
+ allowPositionals: true,
134
+ strict: true
135
+ });
136
+ const file = positionals[0];
137
+ if (!file || !["domain-strategy", "stage-decision"].includes(values.kind)) throw new TypeError("用法: migrate-contract-v3.mjs <contract.yaml> --kind <domain-strategy|stage-decision> [--root <project-root>]");
138
+ const value = parse(await readFile(file, "utf8"), "合同");
139
+ const migrated = values.kind === "domain-strategy" ? migrateDomain(value) : await migrateStage(value, values.root);
140
+ process.stdout.write(`${JSON.stringify(migrated, null, 2)}\n`);
141
+ } catch (error) {
142
+ blocked(error.message);
143
+ }
@@ -18,6 +18,7 @@ const idPatterns = {
18
18
  concept_id: /^domain-concept\.[a-z0-9][a-z0-9-]*$/,
19
19
  invariant_id: /^invariant\.[a-z0-9][a-z0-9-]*$/
20
20
  };
21
+ const consumerCapabilities = new Set(["backend-technical-design", "frontend-engineering-design", "delivery-coordination"]);
21
22
  const relationshipPatterns = new Set(["Partnership", "Customer/Supplier", "Conformist", "Anti-Corruption Layer", "Open Host Service", "Published Language", "Shared Kernel", "Separate Ways"]);
22
23
 
23
24
  function fail(errors) {
@@ -53,7 +54,7 @@ function validate(data, contextRoot) {
53
54
  if (!data || typeof data !== "object" || Array.isArray(data)) return ["合同必须是对象"];
54
55
  if (data.schema_version === 1) return ["migration-required: domain strategy v1 必须迁移到 v2 context_snapshot"];
55
56
  for (const field of required) requireField(data, field, "root", errors);
56
- if (data.schema_version !== 2) errors.push("schema_version 必须为 2");
57
+ if (![2, 3].includes(data.schema_version)) errors.push("schema_version 不受支持;支持版本: 2, 3;新交付请迁移到 3");
57
58
  if (!idPatterns.domain_strategy_id.test(String(data.domain_strategy_id ?? ""))) errors.push("domain_strategy_id 格式非法");
58
59
  if (!/^v[0-9]+$/.test(String(data.domain_version ?? ""))) errors.push("domain_version 必须形如 v1");
59
60
  if (!["draft", "ready-for-human", "approved", "stale", "blocked"].includes(data.status)) errors.push("status 非法");
@@ -71,6 +72,23 @@ function validate(data, contextRoot) {
71
72
  if (context?.subdomain_type && !["Core Domain", "Supporting Subdomain", "Generic Subdomain"].includes(context.subdomain_type)) errors.push(`${path}.subdomain_type 非法`);
72
73
  }
73
74
 
75
+ const sourceIds = new Set();
76
+ if (data.schema_version === 3) {
77
+ if (data.traceability_version !== 1) errors.push("traceability_version 必须为 1");
78
+ requireArray(data, "rule_catalog", "root", errors, 1, "object");
79
+ const rules = list(data.rule_catalog) ? data.rule_catalog : [];
80
+ unique(rules.map((item) => item?.rule_id).filter(Boolean), "rule_id", errors);
81
+ for (const [index, rule] of rules.entries()) {
82
+ const path = `rule_catalog[${index}]`;
83
+ for (const field of ["rule_id", "statement", "responsible_context", "status"]) requireString(rule ?? {}, field, path, errors);
84
+ requireArray(rule ?? {}, "evidence_refs", path, errors, 1);
85
+ if (!/^rule\.[a-z0-9][a-z0-9-]*$/.test(String(rule?.rule_id ?? ""))) errors.push(`${path}.rule_id 格式非法`);
86
+ if (rule?.responsible_context && !contextIds.includes(rule.responsible_context)) errors.push(`${path}.responsible_context 未声明`);
87
+ if (rule?.status !== "confirmed") errors.push(`${path}.status 必须为 confirmed`);
88
+ if (rule?.rule_id) sourceIds.add(rule.rule_id);
89
+ }
90
+ }
91
+
74
92
  requireArray(data, "subdomains", "root", errors, 1, "object");
75
93
  const subdomains = list(data.subdomains) ? data.subdomains : [];
76
94
  unique(subdomains.map((item) => item?.subdomain_id).filter(Boolean), "subdomain_id", errors);
@@ -110,6 +128,14 @@ function validate(data, contextRoot) {
110
128
  if (scenario?.scenario_id && !idPatterns.scenario_id.test(scenario.scenario_id)) errors.push(`${path}.scenario_id 格式非法`);
111
129
  if (scenario?.responsible_context && !contextIds.includes(scenario.responsible_context)) errors.push(`${path}.responsible_context 未声明`);
112
130
  for (const consumer of scenario?.consumers ?? []) if (!contextIds.includes(consumer)) errors.push(`${path}.consumers 引用了未声明上下文: ${consumer}`);
131
+ if (scenario?.scenario_id) sourceIds.add(scenario.scenario_id);
132
+ if (data.schema_version === 3) {
133
+ requireArray(scenario ?? {}, "rule_refs", path, errors, 1);
134
+ requireArray(scenario ?? {}, "success_results", path, errors, 1);
135
+ requireArray(scenario ?? {}, "evidence_refs", path, errors, 1);
136
+ if (typeof scenario?.critical !== "boolean") errors.push(`${path}.critical 必须是 boolean`);
137
+ for (const reference of scenario?.rule_refs ?? []) if (!sourceIds.has(reference)) errors.push(`${path}.rule_refs 引用了未声明规则: ${reference}`);
138
+ }
113
139
  }
114
140
 
115
141
  const concepts = list(data.concept_candidates) ? data.concept_candidates : [];
@@ -132,6 +158,12 @@ function validate(data, contextRoot) {
132
158
  if (invariant?.invariant_id && !idPatterns.invariant_id.test(invariant.invariant_id)) errors.push(`${path}.invariant_id 格式非法`);
133
159
  if (invariant?.responsible_context && !contextIds.includes(invariant.responsible_context)) errors.push(`${path}.responsible_context 未声明`);
134
160
  for (const reference of invariant?.scenario_refs ?? []) if (!scenarioIds.includes(reference)) errors.push(`${path}.scenario_refs 引用了未声明场景: ${reference}`);
161
+ if (invariant?.invariant_id) sourceIds.add(invariant.invariant_id);
162
+ if (data.schema_version === 3) {
163
+ requireString(invariant ?? {}, "rule_ref", path, errors);
164
+ requireArray(invariant ?? {}, "evidence_refs", path, errors, 1);
165
+ if (invariant?.rule_ref && !sourceIds.has(invariant.rule_ref)) errors.push(`${path}.rule_ref 引用了未声明规则: ${invariant.rule_ref}`);
166
+ }
135
167
  }
136
168
 
137
169
  try {
@@ -141,11 +173,21 @@ function validate(data, contextRoot) {
141
173
  }
142
174
  requireArray(data, "evidence_refs", "root", errors, 1);
143
175
  const mappings = list(data.downstream_mapping) ? data.downstream_mapping : [];
176
+ if (data.schema_version === 3) unique(mappings.map((item) => item?.mapping_id).filter(Boolean), "mapping_id", errors);
144
177
  for (const [index, mapping] of mappings.entries()) {
145
178
  const path = `downstream_mapping[${index}]`;
146
- for (const field of ["domain_change", "propagation", "reapproval_condition"]) requireString(mapping ?? {}, field, path, errors);
147
- requireArray(mapping ?? {}, "impacts", path, errors, 1);
148
- if (mapping?.propagation && !["direct", "transitive", "stale"].includes(mapping.propagation)) errors.push(`${path}.propagation 非法`);
179
+ if (data.schema_version === 3) {
180
+ for (const field of ["mapping_id", "consumer_capability", "propagation", "reapproval_condition"]) requireString(mapping ?? {}, field, path, errors);
181
+ for (const field of ["source_refs", "impacts", "evidence_refs"]) requireArray(mapping ?? {}, field, path, errors, 1);
182
+ if (!/^mapping\.[a-z0-9][a-z0-9-]*$/.test(String(mapping?.mapping_id ?? ""))) errors.push(`${path}.mapping_id 格式非法`);
183
+ if (mapping?.consumer_capability && !consumerCapabilities.has(mapping.consumer_capability)) errors.push(`${path}.consumer_capability 非法`);
184
+ for (const reference of mapping?.source_refs ?? []) if (!sourceIds.has(reference)) errors.push(`${path}.source_refs 引用了未声明来源: ${reference}`);
185
+ if (mapping?.propagation && !["direct", "transitive", "not-applicable", "stale"].includes(mapping.propagation)) errors.push(`${path}.propagation 非法`);
186
+ } else {
187
+ for (const field of ["domain_change", "propagation", "reapproval_condition"]) requireString(mapping ?? {}, field, path, errors);
188
+ requireArray(mapping ?? {}, "impacts", path, errors, 1);
189
+ if (mapping?.propagation && !["direct", "transitive", "stale"].includes(mapping.propagation)) errors.push(`${path}.propagation 非法`);
190
+ }
149
191
  }
150
192
  const approval = data.approval ?? {};
151
193
  for (const field of ["approval_ref", "approver", "persisted_ref", "current_version"]) requireString(approval, field, "approval", errors);
@@ -10,6 +10,7 @@ import { verifyContextSnapshot } from "../../../../scripts/lib/context-contract.
10
10
 
11
11
  const required = ["schema_version", "stage_decision_id", "package_version", "status", "problem_statement", "target_users", "mvp", "non_goals", "success_criteria", "test_seams", "confirmed_decisions", "assumptions", "constraints", "unresolved_items", "context_snapshot", "domain_strategy_ref", "impact_assessment", "downstream_mapping", "evidence_refs", "approval"];
12
12
  const idPattern = /^stage-decision\.[a-z0-9][a-z0-9-]*$/;
13
+ const consumerCapabilities = new Set(["backend-technical-design", "frontend-engineering-design", "delivery-coordination"]);
13
14
 
14
15
  function fail(errors) {
15
16
  process.stderr.write(`${JSON.stringify({ result: "blocked", errors }, null, 2)}\n`);
@@ -18,7 +19,8 @@ function fail(errors) {
18
19
  function nonEmpty(value) { return typeof value === "string" && value.trim().length > 0; }
19
20
  function requireField(object, field, path, errors) { if (!(field in object) || object[field] === null || object[field] === undefined) errors.push(`${path}.${field} 缺失`); }
20
21
  function requireString(object, field, path, errors) { requireField(object, field, path, errors); if (field in object && !nonEmpty(object[field])) errors.push(`${path}.${field} 必须是非空字符串`); }
21
- function requireArray(object, field, path, errors, min = 0) { requireField(object, field, path, errors); if (field in object && (!Array.isArray(object[field]) || object[field].length < min)) errors.push(`${path}.${field} 必须是至少 ${min} 项的数组`); if (field in object && Array.isArray(object[field])) object[field].forEach((item, index) => { if (!nonEmpty(item)) errors.push(`${path}.${field}[${index}] 必须是非空字符串`); }); }
22
+ function requireArray(object, field, path, errors, min = 0, itemKind = "string") { requireField(object, field, path, errors); if (field in object && (!Array.isArray(object[field]) || object[field].length < min)) errors.push(`${path}.${field} 必须是至少 ${min} 项的数组`); if (field in object && Array.isArray(object[field]) && itemKind === "string") object[field].forEach((item, index) => { if (!nonEmpty(item)) errors.push(`${path}.${field}[${index}] 必须是非空字符串`); }); }
23
+ function unique(items, label, errors) { const seen = new Set(); for (const item of items) { if (seen.has(item)) errors.push(`${label} 重复: ${item}`); seen.add(item); } }
22
24
  function canonical(value) { if (Array.isArray(value)) return value.map(canonical); if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])])); return value; }
23
25
  function digest(value) { return `sha256:${createHash("sha256").update(JSON.stringify(canonical(value))).digest("hex")}`; }
24
26
 
@@ -27,12 +29,13 @@ async function validate(data, contextRoot) {
27
29
  if (!data || typeof data !== "object" || Array.isArray(data)) return ["合同必须是对象"];
28
30
  if (data.schema_version === 1) return ["migration-required: stage decision package v1 必须迁移到 v2 context_snapshot"];
29
31
  for (const field of required) requireField(data, field, "root", errors);
30
- if (data.schema_version !== 2) errors.push("schema_version 必须为 2");
32
+ if (![2, 3].includes(data.schema_version)) errors.push("schema_version 不受支持;支持版本: 2, 3;新交付请迁移到 3");
31
33
  if (!idPattern.test(String(data.stage_decision_id ?? ""))) errors.push("stage_decision_id 格式非法");
32
34
  if (!/^v[0-9]+$/.test(String(data.package_version ?? ""))) errors.push("package_version 必须形如 v1");
33
35
  if (!["draft", "ready-for-human", "approved", "stale", "blocked"].includes(data.status)) errors.push("status 非法");
34
36
  for (const field of ["problem_statement"]) requireString(data, field, "root", errors);
35
- for (const field of ["target_users", "mvp", "non_goals", "success_criteria", "test_seams", "confirmed_decisions", "assumptions", "constraints", "evidence_refs"]) requireArray(data, field, "root", errors, ["target_users", "mvp", "success_criteria", "test_seams", "confirmed_decisions", "evidence_refs"].includes(field) ? 1 : 0);
37
+ for (const field of ["target_users", "mvp", "non_goals", "evidence_refs"]) requireArray(data, field, "root", errors, ["target_users", "mvp", "evidence_refs"].includes(field) ? 1 : 0);
38
+ for (const field of ["success_criteria", "test_seams", "confirmed_decisions", "assumptions", "constraints"]) requireArray(data, field, "root", errors, ["success_criteria", "test_seams", "confirmed_decisions"].includes(field) ? 1 : 0, data.schema_version === 3 ? "object" : "string");
36
39
 
37
40
  const unresolved = Array.isArray(data.unresolved_items) ? data.unresolved_items : [];
38
41
  for (const [index, item] of unresolved.entries()) {
@@ -56,9 +59,38 @@ async function validate(data, contextRoot) {
56
59
  if (strategy.domain_version !== domainRef.domain_version) errors.push("domain_strategy_ref.domain_version 与实际文件不一致");
57
60
  if (strategy.status !== domainRef.status) errors.push("domain_strategy_ref.status 与实际文件不一致");
58
61
  if (domainRef.digest !== digest(strategy)) errors.push("domain_strategy_ref.digest 与实际领域战略内容不一致");
62
+ if (data.schema_version === 3 && strategy.schema_version !== 3) errors.push("Stage Decision v3 必须引用 Domain Strategy v3");
59
63
  }
60
64
  } catch (error) { errors.push(`domain_strategy_ref.persisted_ref 无法读取: ${error.message}`); }
61
65
  }
66
+
67
+ if (data.schema_version === 3) {
68
+ const sourceIds = new Set([
69
+ ...(strategy?.rule_catalog ?? []).map((item) => item?.rule_id),
70
+ ...(strategy?.scenarios ?? []).map((item) => item?.scenario_id),
71
+ ...(strategy?.invariants ?? []).map((item) => item?.invariant_id)
72
+ ].filter(Boolean));
73
+ const recordFields = {
74
+ success_criteria: "success-criterion",
75
+ test_seams: "test-seam",
76
+ confirmed_decisions: "decision",
77
+ assumptions: "assumption",
78
+ constraints: "constraint"
79
+ };
80
+ const recordIds = [];
81
+ for (const [field, prefix] of Object.entries(recordFields)) {
82
+ for (const [index, record] of (Array.isArray(data[field]) ? data[field] : []).entries()) {
83
+ const path = `${field}[${index}]`;
84
+ for (const name of ["id", "statement"]) requireString(record ?? {}, name, path, errors);
85
+ requireArray(record ?? {}, "evidence_refs", path, errors, 1);
86
+ if (!new RegExp(`^${prefix}\\.[a-z0-9][a-z0-9-]*$`).test(String(record?.id ?? ""))) errors.push(`${path}.id 格式非法`);
87
+ for (const reference of record?.source_refs ?? []) if (!sourceIds.has(reference)) errors.push(`${path}.source_refs 未在领域战略中声明: ${reference}`);
88
+ if (record?.status && !["confirmed", "candidate", "stale"].includes(record.status)) errors.push(`${path}.status 非法`);
89
+ if (record?.id) recordIds.push(record.id);
90
+ }
91
+ }
92
+ unique(recordIds, "stage record id", errors);
93
+ }
62
94
  try {
63
95
  const contextIds = Array.isArray(strategy?.contexts) ? strategy.contexts.map((item) => item?.context_id).filter(Boolean) : [];
64
96
  verifyContextSnapshot(data.context_snapshot, { root: contextRoot, allowedContextIds: contextIds });
@@ -73,9 +105,21 @@ async function validate(data, contextRoot) {
73
105
  }
74
106
  const mappings = Array.isArray(data.downstream_mapping) ? data.downstream_mapping : [];
75
107
  if (mappings.length < 1) errors.push("downstream_mapping 至少需要一项");
108
+ if (data.schema_version === 3) unique(mappings.map((item) => item?.mapping_id).filter(Boolean), "mapping_id", errors);
109
+ const strategySourceIds = new Set([
110
+ ...(strategy?.rule_catalog ?? []).map((item) => item?.rule_id),
111
+ ...(strategy?.scenarios ?? []).map((item) => item?.scenario_id),
112
+ ...(strategy?.invariants ?? []).map((item) => item?.invariant_id)
113
+ ].filter(Boolean));
76
114
  for (const [index, mapping] of mappings.entries()) {
77
115
  const path = `downstream_mapping[${index}]`;
78
- for (const field of ["domain_change", "consumer", "propagation", "reapproval_condition"]) requireString(mapping ?? {}, field, path, errors);
116
+ if (data.schema_version === 3) {
117
+ for (const field of ["mapping_id", "consumer_capability", "propagation", "reapproval_condition"]) requireString(mapping ?? {}, field, path, errors);
118
+ requireArray(mapping ?? {}, "source_refs", path, errors, 1);
119
+ requireArray(mapping ?? {}, "evidence_refs", path, errors, 1);
120
+ if (mapping?.consumer_capability && !consumerCapabilities.has(mapping.consumer_capability)) errors.push(`${path}.consumer_capability 非法`);
121
+ for (const reference of mapping?.source_refs ?? []) if (!strategySourceIds.has(reference)) errors.push(`${path}.source_refs 未在领域战略中声明: ${reference}`);
122
+ } else for (const field of ["domain_change", "consumer", "propagation", "reapproval_condition"]) requireString(mapping ?? {}, field, path, errors);
79
123
  if (mapping?.propagation && !["direct", "transitive", "not-applicable", "stale"].includes(mapping.propagation)) errors.push(`${path}.propagation 非法`);
80
124
  }
81
125
  const approval = data.approval ?? {};