create-yss-spec 3.3.6 → 3.3.8

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 (222) hide show
  1. package/README.md +12 -13
  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 +6 -4
  6. package/template/.agents/skills/archify/SKILL.md +1 -1
  7. package/template/.agents/skills/implementation-repo-onboarding/SKILL.md +2 -2
  8. package/template/.agents/skills/yss-application/SKILL.md +1 -1
  9. package/template/.agents/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
  10. package/template/.agents/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
  11. package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
  12. package/template/.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
  13. package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
  14. package/template/.agents/skills/yss-prototype-stage/SKILL.md +10 -1
  15. package/template/.agents/skills/yss-stage-decision/SKILL.md +13 -4
  16. package/template/.agents/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
  17. package/template/.agents/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
  18. package/template/.agents/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
  19. package/template/.agents/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
  20. package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
  21. package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
  22. package/template/.agents/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
  23. package/template/.agents/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
  24. package/template/.agents/skills/yss-stage-decision/references/validation-rules.md +2 -2
  25. package/template/.agents/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
  26. package/template/.agents/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
  27. package/template/.agents/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
  28. package/template/.agents/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
  29. package/template/.agents/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
  30. package/template/.agents/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
  31. package/template/.agents/skills/yss-technical-design/SKILL.md +2 -0
  32. package/template/.agents/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
  33. package/template/.agents/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
  34. package/template/.agents/skills/yss-technical-design/references/technical-design.schema.json +288 -10
  35. package/template/.agents/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
  36. package/template/.agents/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
  37. package/template/.agents/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
  38. package/template/.agents/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
  39. package/template/.agents/skills/yss-web-controller/SKILL.md +1 -1
  40. package/template/.agents/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
  41. package/template/.agents/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
  42. package/template/.codex/skills/archify/SKILL.md +1 -1
  43. package/template/.codex/skills/implementation-repo-onboarding/SKILL.md +2 -2
  44. package/template/.codex/skills/yss-application/SKILL.md +1 -1
  45. package/template/.codex/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
  46. package/template/.codex/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
  47. package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
  48. package/template/.codex/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
  49. package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
  50. package/template/.codex/skills/yss-prototype-stage/SKILL.md +10 -1
  51. package/template/.codex/skills/yss-stage-decision/SKILL.md +13 -4
  52. package/template/.codex/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
  53. package/template/.codex/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
  54. package/template/.codex/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
  55. package/template/.codex/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
  56. package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
  57. package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
  58. package/template/.codex/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
  59. package/template/.codex/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
  60. package/template/.codex/skills/yss-stage-decision/references/validation-rules.md +2 -2
  61. package/template/.codex/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
  62. package/template/.codex/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
  63. package/template/.codex/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
  64. package/template/.codex/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
  65. package/template/.codex/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
  66. package/template/.codex/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
  67. package/template/.codex/skills/yss-technical-design/SKILL.md +2 -0
  68. package/template/.codex/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
  69. package/template/.codex/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
  70. package/template/.codex/skills/yss-technical-design/references/technical-design.schema.json +288 -10
  71. package/template/.codex/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
  72. package/template/.codex/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
  73. package/template/.codex/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
  74. package/template/.codex/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
  75. package/template/.codex/skills/yss-web-controller/SKILL.md +1 -1
  76. package/template/.codex/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
  77. package/template/.codex/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
  78. package/template/.cursor/skills/archify/SKILL.md +1 -1
  79. package/template/.cursor/skills/implementation-repo-onboarding/SKILL.md +2 -2
  80. package/template/.cursor/skills/yss-application/SKILL.md +1 -1
  81. package/template/.cursor/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
  82. package/template/.cursor/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
  83. package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
  84. package/template/.cursor/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
  85. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
  86. package/template/.cursor/skills/yss-prototype-stage/SKILL.md +10 -1
  87. package/template/.cursor/skills/yss-stage-decision/SKILL.md +13 -4
  88. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
  89. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
  90. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
  91. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
  92. package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
  93. package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
  94. package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
  95. package/template/.cursor/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
  96. package/template/.cursor/skills/yss-stage-decision/references/validation-rules.md +2 -2
  97. package/template/.cursor/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
  98. package/template/.cursor/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
  99. package/template/.cursor/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
  100. package/template/.cursor/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
  101. package/template/.cursor/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
  102. package/template/.cursor/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
  103. package/template/.cursor/skills/yss-technical-design/SKILL.md +2 -0
  104. package/template/.cursor/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
  105. package/template/.cursor/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
  106. package/template/.cursor/skills/yss-technical-design/references/technical-design.schema.json +288 -10
  107. package/template/.cursor/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
  108. package/template/.cursor/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
  109. package/template/.cursor/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
  110. package/template/.cursor/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
  111. package/template/.cursor/skills/yss-web-controller/SKILL.md +1 -1
  112. package/template/.cursor/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
  113. package/template/.cursor/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
  114. package/template/.pi/skills/archify/SKILL.md +1 -1
  115. package/template/.pi/skills/implementation-repo-onboarding/SKILL.md +2 -2
  116. package/template/.pi/skills/yss-application/SKILL.md +1 -1
  117. package/template/.pi/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
  118. package/template/.pi/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
  119. package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
  120. package/template/.pi/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
  121. package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
  122. package/template/.pi/skills/yss-prototype-stage/SKILL.md +10 -1
  123. package/template/.pi/skills/yss-stage-decision/SKILL.md +13 -4
  124. package/template/.pi/skills/yss-stage-decision/references/domain-strategy-contract.md +4 -3
  125. package/template/.pi/skills/yss-stage-decision/references/domain-strategy-v2.schema.json +8 -0
  126. package/template/.pi/skills/yss-stage-decision/references/domain-strategy-v3.schema.json +8 -0
  127. package/template/.pi/skills/yss-stage-decision/references/domain-strategy.schema.json +75 -5
  128. package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-contract.md +4 -4
  129. package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-v2.schema.json +8 -0
  130. package/template/.pi/skills/yss-stage-decision/references/stage-decision-package-v3.schema.json +8 -0
  131. package/template/.pi/skills/yss-stage-decision/references/stage-decision-package.schema.json +59 -9
  132. package/template/.pi/skills/yss-stage-decision/references/validation-rules.md +2 -2
  133. package/template/.pi/skills/yss-stage-decision/scripts/migrate-contract-v3.mjs +143 -0
  134. package/template/.pi/skills/yss-stage-decision/scripts/validate-domain-strategy.mjs +46 -4
  135. package/template/.pi/skills/yss-stage-decision/scripts/validate-stage-decision-package.mjs +48 -4
  136. package/template/.pi/skills/yss-stage-decision/tests/fixtures/valid-stage-decision-package.yaml +42 -13
  137. package/template/.pi/skills/yss-stage-decision/tests/fixtures/valid-supplier-domain.yaml +37 -5
  138. package/template/.pi/skills/yss-stage-decision/tests/run-scenarios.mjs +116 -23
  139. package/template/.pi/skills/yss-technical-design/SKILL.md +2 -0
  140. package/template/.pi/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
  141. package/template/.pi/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
  142. package/template/.pi/skills/yss-technical-design/references/technical-design.schema.json +288 -10
  143. package/template/.pi/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
  144. package/template/.pi/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
  145. package/template/.pi/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
  146. package/template/.pi/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
  147. package/template/.pi/skills/yss-web-controller/SKILL.md +1 -1
  148. package/template/.pi/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
  149. package/template/.pi/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
  150. package/template/README.md +4 -6
  151. package/template/docs/agents/backend-architecture-profiles.md +4 -0
  152. package/template/docs/agents/yss-skill-registry.yaml +13 -0
  153. package/template/docs/design/templates/prototype-confirmation-template.md +2 -2
  154. package/template/docs/design/templates/prototype-review-checklist.md +2 -2
  155. package/template/docs/plan/templates/market-analysis-template.md +3 -3
  156. package/template/docs/plan/templates/user-pain-points-template.md +3 -3
  157. package/template/docs/process/delivery-preflight.md +72 -0
  158. package/template/docs/process/document-writing.md +5 -1
  159. package/template/docs/process/existing-backend-architecture.md +31 -0
  160. package/template/docs/process/existing-ui-baseline.md +40 -0
  161. package/template/docs/process/frontend-backend-delivery.md +21 -5
  162. package/template/docs/process/implementation-repo-integration.md +6 -0
  163. package/template/docs/process/schemas/backend-architecture-identity.schema.json +111 -14
  164. package/template/docs/process/schemas/backend-delivery.schema.json +4 -0
  165. package/template/docs/process/schemas/delivery-preflight-input.schema.json +158 -0
  166. package/template/docs/process/schemas/delivery-preflight-result.schema.json +159 -0
  167. package/template/docs/process/schemas/digital-human-task-package.schema.json +1 -1
  168. package/template/docs/process/schemas/existing-ui-baseline.schema.json +365 -0
  169. package/template/docs/process/schemas/frontend-delivery-acceptance-v1.schema.json +200 -0
  170. package/template/docs/process/schemas/frontend-delivery-acceptance-v3.schema.json +308 -0
  171. package/template/docs/process/schemas/frontend-delivery-acceptance.schema.json +37 -3
  172. package/template/docs/process/schemas/frontend-strategic-preflight-v2.schema.json +114 -0
  173. package/template/docs/process/schemas/frontend-strategic-preflight.schema.json +32 -0
  174. package/template/docs/process/schemas/lifecycle-checkpoint.schema.json +2 -1
  175. package/template/docs/process/schemas/strategic-consumer-feedback.schema.json +40 -0
  176. package/template/docs/process/schemas/strategic-design-handoff-v3.schema.json +743 -0
  177. package/template/docs/process/schemas/strategic-design-handoff-v4.schema.json +712 -0
  178. package/template/docs/process/schemas/strategic-design-handoff-v5.schema.json +932 -0
  179. package/template/docs/process/schemas/strategic-feedback-adjudication.schema.json +42 -0
  180. package/template/docs/process/schemas/strategic-handoff-domain-strategy-v3.schema.json +216 -0
  181. package/template/docs/process/schemas/strategic-handoff-export-v2.schema.json +378 -0
  182. package/template/docs/process/schemas/strategic-handoff-import-receipt.schema.json +39 -0
  183. package/template/docs/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json +148 -0
  184. package/template/docs/process/strategic-handoff-package.md +51 -10
  185. package/template/docs/process/templates/lifecycle-checkpoint-template.yaml +1 -0
  186. package/template/docs/process/templates/stage-checkpoint-template.md +1 -0
  187. package/template/docs/user-guide//346/212/200/346/234/257/350/256/276/350/256/241/347/224/250/346/210/267/346/214/207/345/215/227.md +43 -0
  188. package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +83 -67
  189. package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214/347/264/242/345/274/225.md +9 -10
  190. package/template/docs/user-guide//350/256/276/345/244/207/345/200/237/347/224/250/350/264/257/347/251/277/346/241/210/344/276/213.md +12 -9
  191. package/template/scripts/backend-delivery +20 -4
  192. package/template/scripts/lib/approval-record.mjs +2 -1
  193. package/template/scripts/lib/approved-execution-context.mjs +106 -0
  194. package/template/scripts/lib/backend-architecture.mjs +12 -0
  195. package/template/scripts/lib/backend-delivery.mjs +21 -5
  196. package/template/scripts/lib/delivery-preflight.mjs +238 -0
  197. package/template/scripts/lib/existing-backend-architecture.mjs +139 -0
  198. package/template/scripts/lib/existing-ui-baseline.mjs +84 -0
  199. package/template/scripts/lib/frontend-delivery-boundary.mjs +20 -4
  200. package/template/scripts/lib/frontend-delivery.mjs +76 -4
  201. package/template/scripts/lib/implementation-contract-compiler.mjs +19 -5
  202. package/template/scripts/lib/json-schema.mjs +19 -4
  203. package/template/scripts/lib/profile-skill-sync.mjs +26 -0
  204. package/template/scripts/lib/strategic-feedback.mjs +33 -0
  205. package/template/scripts/lib/strategic-handoff-consumption.mjs +11 -5
  206. package/template/scripts/lib/strategic-handoff-design-targets.mjs +2 -3
  207. package/template/scripts/lib/strategic-handoff-io.mjs +11 -0
  208. package/template/scripts/lib/strategic-handoff.mjs +143 -34
  209. package/template/scripts/lib/task-package.mjs +11 -3
  210. package/template/scripts/lib/technical-design-boundary.mjs +9 -1
  211. package/template/scripts/lib/ui-baseline.mjs +26 -0
  212. package/template/scripts/lib/user-decision-reuse.mjs +38 -0
  213. package/template/scripts/lib/user-decision.mjs +4 -1
  214. package/template/scripts/preflight-delivery +14 -0
  215. package/template/scripts/strategic-feedback +10 -0
  216. package/template/scripts/strategic-handoff +19 -4
  217. package/template/scripts/sync-profile-skills +4 -5
  218. package/template/scripts/verify-frontend-strategic-preflight +12 -0
  219. package/template/skills-lock.json +18 -18
  220. package/template.manifest.json +1 -0
  221. package/template.snapshot.json +6 -5
  222. package/template/docs/user-guide//346/210/230/346/234/257/350/256/276/350/256/241/345/255/220/351/241/271/347/233/256/347/224/250/346/210/267/346/211/213/345/206/214.md +0 -35
@@ -1,3 +1,4 @@
1
+ import { uiBaselineSourceKeys, uiBaselineKind } from './ui-baseline.mjs';
1
2
  import { existsSync, readdirSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { spawnSync } from 'node:child_process';
@@ -12,7 +13,7 @@ const strategyBasis=value=>({
12
13
  ...without(value,['domain_version','approval','rule_catalog','scenarios','invariants']),
13
14
  invariants:(value.invariants||[]).map(item=>without(item,['statement'])),
14
15
  });
15
- export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consumer='tactical'}={}) {
16
+ export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consumer='tactical',readOnly=false}={}) {
16
17
  project(root);
17
18
  ensure(['tactical','frontend'].includes(consumer),'未知战略消费者');
18
19
  if (data.schema_version === 2 && consumer === 'tactical') ensure(['domain-driven','layered-mvc'].includes(data.architecture?.family), '未知技术设计架构');
@@ -23,6 +24,11 @@ export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consum
23
24
  const expectedBase=`docs/handoffs/${receipt.bundle_id}/${receipt.version}`;
24
25
  ensure(binding.import_receipt_ref===`${expectedBase}/import-receipt.json` && receipt.package_ref===`${expectedBase}/package`,'导入收据路径与包身份不一致');
25
26
  ensure(binding.bundle_digest===receipt.bundle_digest,'战术绑定与导入收据摘要不一致');
27
+ if(receipt.schema_version===2){
28
+ const capability=consumer==='frontend'?'frontend-engineering-design':'backend-technical-design';
29
+ const route=receipt.routes.find(item=>item.capability===capability);
30
+ ensure(route&&route.activation!=='not-applicable'&&binding.route_id===route.route_id,`${capability} 未绑定当前 Handoff v4 route_id`);
31
+ }
26
32
  const result=spawnSync(process.execPath,[path.join(ROOT,'scripts/verify-context-reconciliation'),'--root',root,safe(root,binding.context_reconciliation_ref)],{encoding:'utf8'});
27
33
  ensure(result.status===0,`目标术语对账未通过: ${result.error?.message || result.stderr}`);
28
34
  const context=parseContextContract({root});
@@ -69,15 +75,15 @@ export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consum
69
75
  }else {ensure(['pending','conflict'].includes(row.disposition),`非法承接状态: ${id}`);mark(id,row.disposition,row);}
70
76
  }
71
77
  for(const[id,row]of rows)if(!known.has(id))mark(id,'removed-or-unknown-source',row);
72
- for(const key of ['spec_ref','prototype_ref','visual_baseline_ref','business_ticket_set_ref'])if(original.handoff.source[key].digest!==current.handoff.source[key].digest){all=true;issues.push({source_id:key,reason:'upstream-asset-stale'});}
78
+ for(const key of new Set(['spec_ref','business_ticket_set_ref',...uiBaselineSourceKeys(original.handoff),...uiBaselineSourceKeys(current.handoff)]))if(original.handoff.source[key]?.digest!==current.handoff.source[key]?.digest){all=true;issues.push({source_id:key,reason:'upstream-asset-stale'});}
73
79
  // Rule changes can be scoped to declared dependencies. Boundary/decision or
74
80
  // runnable-prototype changes need the tactical contract rebound as a whole.
75
81
  const stageKeys=['package_version','approval','domain_strategy_ref'];
76
- if(digest(strategyBasis(original.strategy))!==digest(strategyBasis(current.strategy)) || digest(without(original.stage,stageKeys))!==digest(without(current.stage,stageKeys)) || digest(original.config.prototype)!==digest(current.config.prototype)) {
82
+ if(digest(strategyBasis(original.strategy))!==digest(strategyBasis(current.strategy)) || digest(without(original.stage,stageKeys))!==digest(without(current.stage,stageKeys)) || uiBaselineKind(original.handoff)!==uiBaselineKind(current.handoff) || digest(original.config.prototype||null)!==digest(current.config.prototype||null)) {
77
83
  all=true;issues.push({source_id:'strategic-design-basis',reason:'upstream-design-stale'});
78
84
  }
79
85
  const blockedHere=all||(sliceRef?blocked.has(sliceRef):issues.length>0);
80
86
  return {result:blockedHere?'blocked':'verified',bundle_digest:current.manifest.bundle_digest,consumed_bundle_digest:receipt.bundle_digest,...(consumer==='frontend'?{frontend_acceptance_digest:digest(data)}:{tactical_digest:digest(data), ...(data.schema_version === 2 ? {technical_design_digest:digest(data)} : {})}),scope:sliceRef||`whole-${consumer}-design`,coverage_complete:[...known.keys()].every(id=>rows.has(id)),block_all:all,blocked_slice_refs:[...blocked].sort(),issues};
81
- });
82
- });
87
+ },{readOnly});
88
+ },{readOnly});
83
89
  }
@@ -2,14 +2,13 @@ import { ensure } from './strategic-handoff-io.mjs';
2
2
 
3
3
  export function designTargets(data) {
4
4
  const design = data.schema_version === 2 ? data.design : data;
5
- const catalogs = data.schema_version === 2 && data.architecture?.family === 'layered-mvc'
5
+ const catalogs = data.schema_version === 2 && data.design_scope === 'engineering-only' ? ['component_catalog'] : data.schema_version === 2 && data.architecture?.family === 'layered-mvc'
6
6
  ? ['module_catalog', 'use_case_catalog', 'rule_catalog', 'state_transition_catalog', 'persistence_mapping', 'integration_catalog']
7
7
  : ['aggregate_catalog', 'entity_catalog', 'value_object_catalog', 'behavior_catalog', 'invariant_catalog', 'state_transition_catalog', 'domain_event_catalog', 'gateway_catalog'];
8
- const primaryKeys = { module_catalog: 'module_id', use_case_catalog: 'use_case_id', rule_catalog: 'rule_id', state_transition_catalog: 'transition_id', persistence_mapping: 'mapping_id', integration_catalog: 'integration_id', aggregate_catalog: 'aggregate_id', entity_catalog: 'entity_id', value_object_catalog: 'value_object_id', behavior_catalog: 'behavior_id', invariant_catalog: 'invariant_id', domain_event_catalog: 'event_id', gateway_catalog: 'gateway_id' };
8
+ const primaryKeys = { component_catalog: 'component_id', module_catalog: 'module_id', use_case_catalog: 'use_case_id', rule_catalog: 'rule_id', state_transition_catalog: 'transition_id', persistence_mapping: 'mapping_id', integration_catalog: 'integration_id', aggregate_catalog: 'aggregate_id', entity_catalog: 'entity_id', value_object_catalog: 'value_object_id', behavior_catalog: 'behavior_id', invariant_catalog: 'invariant_id', domain_event_catalog: 'event_id', gateway_catalog: 'gateway_id' };
9
9
  const ids = catalogs.flatMap(key => (design?.[key] || []).map(item => item[primaryKeys[key]]).filter(Boolean));
10
10
  ensure(new Set(ids).size === ids.length, '设计对象 ID 重复');
11
11
  const seams = design?.test_seams || [];
12
12
  ensure(new Set(seams.map(item => item.seam_id)).size === seams.length, '测试 seam ID 重复');
13
13
  return { ids: new Set(ids), seams: new Map(seams.map(item => [item.seam_id, item])) };
14
14
  }
15
-
@@ -1,11 +1,20 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, lstatSync, readdirSync, readFileSync, mkdirSync, writeFileSync, realpathSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
5
  import { spawnSync } from 'node:child_process';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import { validateJsonSchemas } from './json-schema.mjs';
7
8
  import { parseDocument } from '../vendor/yaml.mjs';
8
9
  export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
10
+ // A v5 package keeps the source glossary as a snapshot, never a nested CONTEXT.md.
11
+ // Only this fixed alias is supported, isolated to the current asynchronous reader.
12
+ const sourceContextSnapshots = new AsyncLocalStorage();
13
+ export function withSourceContextSnapshot(root, action) {
14
+ const roots = new Set(sourceContextSnapshots.getStore() || []);
15
+ roots.add(path.resolve(root));
16
+ return sourceContextSnapshots.run(roots, action);
17
+ }
9
18
  export const ensure = (condition, message) => { if (!condition) throw new TypeError(message); };
10
19
  export const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(k => [k, canonical(value[k])])) : value;
11
20
  export const hash = bytes => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
@@ -23,6 +32,7 @@ export function relative(ref) {
23
32
  }
24
33
  export function safe(root, ref, { missing = false } = {}) {
25
34
  relative(ref);
35
+ if (ref === 'CONTEXT.md' && sourceContextSnapshots.getStore()?.has(path.resolve(root))) ref = 'source-context.snapshot.md';
26
36
  let current = path.resolve(root);
27
37
  ensure(!existsSync(current) || !lstatSync(current).isSymbolicLink(), `根目录不能是 symlink: ${root}`);
28
38
  for (const part of ref.split('/')) {
@@ -66,6 +76,7 @@ export function archive(command, source, destination) {
66
76
  export function sourceApprovalPolicy(source) {
67
77
  const roles=structuredClone(source);
68
78
  roles.user_decision_policy ||= {gates:[]};
79
+ ensure(!(roles.user_decision_policy.required_capabilities || []).some(id => id !== 'strategic-decision-reuse-v1'), '接收工具不支持源用户决定策略,请升级工具后验包');
69
80
  const policy=roles.gate_policy;
70
81
  for(const gate of policy.product_digital_human_with_biological_veto || []) {
71
82
  if(!(policy.biological_human||[]).includes(gate) && !(policy.digital_human_review||[]).some(x=>x.gate===gate) && !(policy.dual_digital_human||[]).some(x=>x.gate===gate)) {
@@ -1,24 +1,67 @@
1
1
  import { existsSync, lstatSync, readFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, cpSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { tmpdir } from 'node:os';
4
- import { ensure, canonical, digest, hash, json, parse, read, safe, relative, files, write, project, schema, schemaBatch, archive, ROOT, sourceApprovalPolicy } from './strategic-handoff-io.mjs';
4
+ import { ensure, canonical, digest, hash, json, parse, read, safe, relative, files, write, project, schema, schemaBatch, archive, ROOT, sourceApprovalPolicy, withSourceContextSnapshot } from './strategic-handoff-io.mjs';
5
5
  import { parseContextSource, parseContextContract, resolveContextTermRefs } from './context-contract.mjs';
6
6
  import { countersignRuleForGate } from './digital-human-roles.mjs';
7
7
  import { validateApprovalRecord } from './approval-record.mjs';
8
8
  import { treeDigest } from './strategic-handoff-io.mjs';
9
- import { validateVisualBaseline } from '../../.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs';
9
+ import { uiBaselineKind, uiBaselineRef, uiBaselineCaseIds, hasConsumerRoutes, validateHandoffUiBaseline } from './ui-baseline.mjs';
10
10
  import { extractTraceability, compareIndexes } from './strategic-handoff-rules.mjs';
11
11
  const MAX_BYTES = 512 * 1024 * 1024;
12
12
  const own = (a,b) => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
13
13
  const nonempty = x => typeof x === 'string' && x.trim();
14
14
  const bytesDigest = (bytes, kind) => kind === 'canonical-json' ? digest(parse(bytes)) : kind === 'sha256-bytes' ? hash(bytes) : (() => { throw new TypeError(`未知摘要算法: ${kind}`); })();
15
+ const HANDOFF_SCHEMAS = new Map([[3,'docs/process/schemas/strategic-design-handoff-v3.schema.json'],[4,'docs/process/schemas/strategic-design-handoff-v4.schema.json'],[5,'docs/process/schemas/strategic-design-handoff-v5.schema.json']]);
16
+ const SOURCE_SCHEMAS = new Map([
17
+ [3,['docs/process/schemas/strategic-handoff-domain-strategy.schema.json','docs/process/schemas/strategic-handoff-stage-decision-package.schema.json']],
18
+ [4,['docs/process/schemas/strategic-handoff-domain-strategy-v3.schema.json','docs/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json']],
19
+ [5,['docs/process/schemas/strategic-handoff-domain-strategy-v3.schema.json','docs/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json']],
20
+ ]);
21
+ const CAPABILITIES=['backend-technical-design','frontend-engineering-design','delivery-coordination'];
22
+
23
+ function versionSchema(version, schemas, label) {
24
+ ensure(schemas.has(version), `${label} 未知 schema_version: ${String(version)};支持版本: ${[...schemas.keys()].join(', ')};请使用显式迁移器升级`);
25
+ return schemas.get(version);
26
+ }
27
+
28
+ function validateConsumerRoutes(handoff, stage) {
29
+ if(!hasConsumerRoutes(handoff))return;
30
+ const routes=new Map();
31
+ for(const route of handoff.consumer_routes||[]){
32
+ ensure(!routes.has(route.capability),`消费者能力重复: ${route.capability}`);
33
+ ensure(![...routes.values()].some(item=>item.route_id===route.route_id),`消费者 route_id 重复: ${route.route_id}`);
34
+ routes.set(route.capability,route);
35
+ }
36
+ ensure(CAPABILITIES.every(capability=>routes.has(capability)),'Handoff v4 必须完整声明 backend、frontend 与 coordination 三条消费者路由');
37
+ const impact=stage.impact_assessment||{};
38
+ const mappingByCapability=new Map();
39
+ for(const mapping of stage.downstream_mapping||[]){
40
+ ensure(CAPABILITIES.includes(mapping.consumer_capability),`未知消费者能力: ${mapping.consumer_capability}`);
41
+ const values=mappingByCapability.get(mapping.consumer_capability)||[];values.push(mapping);mappingByCapability.set(mapping.consumer_capability,values);
42
+ }
43
+ const expected=(capability,required)=>required?'required':(mappingByCapability.get(capability)||[]).some(mapping=>mapping.propagation!=='not-applicable')?'optional':'not-applicable';
44
+ const backend=routes.get('backend-technical-design'),frontend=routes.get('frontend-engineering-design'),coordination=routes.get('delivery-coordination');
45
+ const backendActive=Boolean(impact.backend||impact.api||impact.data),frontendActive=Boolean(impact.frontend||impact.ui);
46
+ ensure(backend.activation===expected(backend.capability,backendActive),'backend 消费者路由与批准的 Backend/API/Data 影响矩阵不一致');
47
+ ensure(frontend.activation===expected(frontend.capability,frontendActive),'frontend 消费者路由与批准的 Frontend/UI 影响矩阵不一致');
48
+ ensure(coordination.activation===((backend.activation!=='not-applicable'||frontend.activation!=='not-applicable')?'required':'not-applicable'),'coordination 消费者路由与交付影响不一致');
49
+ const activeBackend=backend.activation!=='not-applicable';
50
+ ensure(own(frontend.dependencies,activeBackend?[backend.route_id]:[]),'frontend 路由依赖与 backend 路由不一致');
51
+ ensure(own(coordination.dependencies,[...(backend.activation!=='not-applicable'?[backend.route_id]:[]),...(frontend.activation!=='not-applicable'?[frontend.route_id]:[])]),'coordination 路由依赖与消费者路由不一致');
52
+ for(const route of routes.values())for(const dependency of route.dependencies)ensure([...routes.values()].some(item=>item.route_id===dependency),`消费者路由依赖悬空: ${dependency}`);
53
+ for(const [capability,mappings] of mappingByCapability)for(const mapping of mappings){
54
+ const route=routes.get(capability);
55
+ ensure(mapping.propagation==='not-applicable'?route.activation==='not-applicable':route.activation!=='not-applicable',`downstream mapping 与消费者路由影响不一致: ${mapping.mapping_id}`);
56
+ }
57
+ }
15
58
 
16
59
  export async function sourceApproval(record, roles, root) {
17
60
  const options={rolesDoc:roles,requireApproved:true,root,read:ref=>readFileSync(safe(root,path.relative(root,ref).split(path.sep).join('/')))};
18
61
  if (roles.user_decision_policy.gates.includes(record.gate_id)) {
19
62
  ensure(existsSync(path.join(ROOT,'scripts/lib/user-decision.mjs')), '接收工具不支持源用户决定策略,请升级工具后验包');
20
- const { assertUserDecisionRequirement } = await import('./user-decision.mjs');
21
- assertUserDecisionRequirement({boundary:record.gate_id,subject_ref:record.subject_ref,scope:record.approval_scope,user_decision_ref:record.user_decision_ref},options);
63
+ const { assertApprovalUserDecision } = await import('./user-decision-reuse.mjs');
64
+ assertApprovalUserDecision(record, roles, options);
22
65
  }
23
66
  validateApprovalRecord(record,options);
24
67
  }
@@ -83,21 +126,25 @@ function previewFiles(root, config) {
83
126
 
84
127
  export async function inspectSource(root, handoffRef) {
85
128
  const handoff=read(safe(root,handoffRef));
86
- schema(handoff,'docs/process/schemas/strategic-design-handoff.schema.json');
129
+ schema(handoff,versionSchema(handoff?.schema_version,HANDOFF_SCHEMAS,'Strategic Handoff'));
87
130
  ensure(handoff.status==='approved', '交接包尚未批准');
88
131
  const config=handoff.package_export;
89
- schema(config,'docs/process/schemas/strategic-handoff-export.schema.json');
132
+ schema(config,handoff.schema_version===5?'docs/process/schemas/strategic-handoff-export-v2.schema.json':'docs/process/schemas/strategic-handoff-export.schema.json');
133
+ if(handoff.schema_version===5)ensure(config.ui_baseline_kind===uiBaselineKind(handoff),'交接与导出 UI 基线类型不一致');
90
134
  const source=parseContextSource(readFileSync(safe(root,'CONTEXT.md'),'utf8'));
91
135
  snapshot(handoff.source_context_snapshot,source); checkDelta(handoff,source);
92
136
  const strategy=read(safe(root,handoff.source.domain_strategy_ref.persisted_ref));
93
137
  const stage=read(safe(root,handoff.source.stage_decision_package_ref.persisted_ref));
94
- schemaBatch([[strategy,'docs/process/schemas/strategic-handoff-domain-strategy.schema.json'],[stage,'docs/process/schemas/strategic-handoff-stage-decision-package.schema.json']]);
138
+ const [strategySchema,stageSchema]=versionSchema(handoff.schema_version,SOURCE_SCHEMAS,'Strategic Handoff source');
139
+ schemaBatch([[strategy,strategySchema],[stage,stageSchema]]);
95
140
  snapshot(strategy.context_snapshot,source); snapshot(stage.context_snapshot,source);
96
141
  ensure(strategy.status==='approved' && stage.status==='approved', '战略合同/方案决策包尚未批准');
97
142
  ensure(strategy.domain_strategy_id===handoff.source.domain_strategy_ref.id && strategy.domain_version===handoff.source.domain_strategy_ref.version, '战略身份/版本不一致');
98
143
  ensure(stage.stage_decision_id===handoff.source.stage_decision_package_ref.id && stage.package_version===handoff.source.stage_decision_package_ref.version, '阶段包身份/版本不一致');
99
144
  ensure(stage.domain_strategy_ref.domain_strategy_id===strategy.domain_strategy_id && stage.domain_strategy_ref.domain_version===strategy.domain_version && stage.domain_strategy_ref.status==='approved' && stage.domain_strategy_ref.persisted_ref===handoff.source.domain_strategy_ref.persisted_ref && stage.domain_strategy_ref.digest===digest(strategy), '阶段包引用的战略内容不一致');
100
145
  ensure(!(stage.unresolved_items || []).some(x=>x.type==='blocker'), '阶段包含 blocker');
146
+ validateConsumerRoutes(handoff,stage);
147
+ if(uiBaselineKind(handoff)==='existing-ui-baseline')ensure(stage.impact_assessment?.ui===false,'existing-ui-baseline 不支持 UI 影响,返回产品设计');
101
148
  const indexes=extractTraceability(strategy);
102
149
  const sourceRoles=read(safe(root,'docs/agents/digital-human-roles.yaml'));
103
150
  const roles=sourceApprovalPolicy(sourceRoles);
@@ -108,39 +155,57 @@ export async function inspectSource(root, handoffRef) {
108
155
  const currentPlan = (roles.gate_policy.dual_digital_human || []).some(rule => rule.gate === 'gate.plan-approved');
109
156
  const terminalGate=currentPlan?'gate.plan-approved':countersignRuleForGate(roles.gate_policy,'gate.strategic-design-handoff-approved')?'gate.strategic-design-handoff-approved':'gate.stage-decision-package-approved';
110
157
  const expectedGates={domain_strategy_ref:currentPlan?'gate.plan-approved':'gate.domain-strategy-approved',stage_decision_package_ref:currentPlan?'gate.plan-approved':'gate.stage-decision-package-approved',spec_ref:'gate.spec-baseline-approved',prototype_ref:currentPlan?'gate.product-design-approved':'gate.user-confirmation',visual_baseline_ref:currentPlan?'gate.product-design-approved':'gate.user-confirmation',business_ticket_set_ref:terminalGate,handoff:terminalGate};
158
+ expectedGates.existing_ui_baseline_ref=expectedGates.prototype_ref;
111
159
  const bindings={...handoff.source,handoff:{id:handoff.handoff_id,version:handoff.handoff_version,persisted_ref:handoffRef,status:'approved'}};
112
160
  for(const [key,ref] of Object.entries(bindings)) {
113
161
  const approval=config.approvals[key];
114
162
  ensure(approval,`缺少资产批准绑定: ${key}`);
115
163
  ensure(approval.gate_id===expectedGates[key],`资产使用错误批准门禁: ${key}`);
116
- const bytes=key==='visual_baseline_ref'?null:readFileSync(safe(root,ref.persisted_ref));
164
+ const baselineKey=['visual_baseline_ref','existing_ui_baseline_ref'].includes(key);
117
165
  let actual;
118
- if(key==='visual_baseline_ref') {
119
- const baseline=read(safe(root,`${ref.persisted_ref}/${ref.manifest_ref}`));
120
- const result=await validateVisualBaseline(baseline,{bundleRoot:safe(root,ref.persisted_ref)});
121
- ensure(!result.errors.length,`视觉基线无效: ${result.errors.join('; ')}`);
122
- ensure(baseline.baseline_id===ref.baseline_id && baseline.version===ref.version && own([...ref.case_ids].sort(),baseline.cases.map(x=>x.case_id).sort()),'视觉基线身份不一致');
123
- actual=baseline.bundle.digest;
124
- } else actual=bytesDigest(bytes,approval.digest_kind);
166
+ if(baselineKey) {
167
+ const validated=await validateHandoffUiBaseline(root,handoff);actual=validated.digest;
168
+ if(key==='existing_ui_baseline_ref') {
169
+ ensure(approval.digest_kind==='sha256-bytes','既有 UI 批准必须绑定 manifest 原字节');
170
+ ensure(roles.user_decision_policy.gates.includes(expectedGates.existing_ui_baseline_ref),'既有 UI 承接必须使用产品设计真实用户决定策略');
171
+ }
172
+ } else actual=bytesDigest(readFileSync(safe(root,ref.persisted_ref)),approval.digest_kind);
125
173
  if(key!=='handoff')ensure(ref.digest===actual,`源资产摘要过期: ${key}`);
126
174
  const record=read(safe(root,approval.record_ref));
175
+ if(key==='existing_ui_baseline_ref')ensure(record.subject_ref===`${ref.persisted_ref}/${ref.manifest_ref}`,'既有 UI 用户决定必须以当前 manifest 为批准主体');
127
176
  await sourceApproval(record,roles,root);
128
177
  ensure(record.gate_id===approval.gate_id,`批准门禁不匹配: ${key}`);
129
178
  ensure((record.artifact_bindings || []).some(x=>x.id===(ref.id||ref.baseline_id) && x.version===ref.version && x.digest===actual),`批准记录未绑定当前资产: ${key}`);
130
179
  }
131
- const receipt=previewFiles(root,config.prototype);
132
- ensure(handoff.source.visual_baseline_ref.case_ids.every(id=>receipt.case_ids.includes(id)), '离线验证未覆盖视觉基线 case');
180
+ if (roles.user_decision_policy.required_capabilities?.includes('strategic-decision-reuse-v1')) {
181
+ const record=read(safe(root,config.approvals.handoff.record_ref));
182
+ const scope=read(safe(root,record.subject_ref));
183
+ ensure(scope.kind==='strategic-delivery-scope' && scope.schema_version===1, '交接必须绑定独立的交付范围清单');
184
+ const {package_export,status,...delivery}=handoff;
185
+ ensure(scope.delivery_ref && own(read(safe(root,scope.delivery_ref)),delivery), '交付范围清单未覆盖当前交接内容或风险');
186
+ ensure(scope.assets?.some(asset=>asset.ref===scope.delivery_ref && asset.digest===hash(readFileSync(safe(root,scope.delivery_ref)))), '交接内容快照缺少批准覆盖');
187
+ for (const [key,ref] of Object.entries(handoff.source)) {
188
+ const file=['visual_baseline_ref','existing_ui_baseline_ref'].includes(key)?`${ref.persisted_ref}/${ref.manifest_ref}`:ref.persisted_ref;
189
+ const decisionBoundaries=key==='business_ticket_set_ref'?['gate.spec-baseline-approved',terminalGate]:[expectedGates[key]];
190
+ ensure(scope.assets?.some(asset=>asset.ref===file && asset.version===ref.version && decisionBoundaries.includes(asset.boundary) && asset.digest===hash(readFileSync(safe(root,file)))), `交付范围清单遗漏当前资产: ${key}`);
191
+ }
192
+ }
193
+ if(uiBaselineKind(handoff)==='prototype') {
194
+ const receipt=previewFiles(root,config.prototype);
195
+ ensure(uiBaselineCaseIds(handoff).every(id=>receipt.case_ids.includes(id)), '离线验证未覆盖视觉基线 case');
196
+ }
133
197
  return {handoff,config,indexes,strategy,stage};
134
198
  }
135
199
 
136
200
  function collect(root, handoffRef, handoff, config) {
137
- const collected=new Map(), queue=[handoffRef,'CONTEXT.md','docs/agents/digital-human-roles.yaml',...Object.values(handoff.source).map(x=>x.persisted_ref),...Object.values(config.approvals).map(x=>x.record_ref),...handoff.evidence_and_version_digests,...config.additional_files,config.prototype.preview_root,config.prototype.verification_ref];
138
- if(config.prototype.profile==='H2')queue.push(config.prototype.source_root,config.prototype.lock_ref);
201
+ const collected=new Map(), queue=[handoffRef,'CONTEXT.md','docs/agents/digital-human-roles.yaml',...Object.values(handoff.source).map(x=>x.persisted_ref),...Object.values(config.approvals).map(x=>x.record_ref),...handoff.evidence_and_version_digests,...config.additional_files];
202
+ if(config.prototype)queue.push(config.prototype.preview_root,config.prototype.verification_ref);
203
+ if(config.prototype?.profile==='H2')queue.push(config.prototype.source_root,config.prototype.lock_ref);
139
204
  const symbolic=config.reference_map || {};
140
205
  const enqueue=ref=> { if(symbolic[ref])queue.push(symbolic[ref]); else if(/^https?:\/\//i.test(ref))return; else if(ref.startsWith('evidence.'))throw new TypeError(`未解析 evidence ID: ${ref}`); else if(ref.includes('/') || /\.(?:md|yaml|json|html|png|txt|log)$/.test(ref))queue.push(ref); };
141
206
  function refs(value,key='') {
142
207
  if(Array.isArray(value)) { if(key==='evidence_refs') value.forEach(enqueue); else value.forEach(v=>refs(v,key)); }
143
- else if(value && typeof value==='object') for(const [k,v]of Object.entries(value)) { if(['approval_ref','persisted_ref','user_decision_ref'].includes(k)&&typeof v==='string')enqueue(v); else if(k==='ref'&&typeof v==='string'&&!/^[a-z]+:\/\//i.test(v))enqueue(v); else refs(v,k); }
208
+ else if(value && typeof value==='object') for(const [k,v]of Object.entries(value)) { if(['approval_ref','persisted_ref','user_decision_ref','decision_reuse_ref','scope_ref','subject_ref','delivery_ref'].includes(k)&&typeof v==='string')enqueue(v); else if(k==='ref'&&typeof v==='string'&&!/^[a-z]+:\/\//i.test(v))enqueue(v); else refs(v,k); }
144
209
  }
145
210
  let totalBytes = 0;
146
211
  while(queue.length) {
@@ -151,20 +216,23 @@ function collect(root, handoffRef, handoff, config) {
151
216
  ensure(stat.isFile() && stat.size<=MAX_BYTES,`源文件类型/大小无效: ${ref}`);
152
217
  const bytes=readFileSync(full); collected.set(ref,bytes); totalBytes += bytes.length;
153
218
  ensure(collected.size<=20000 && totalBytes<=MAX_BYTES,'交接包大小超限');
154
- if(/\.(yaml|yml|json)$/.test(ref))refs(parse(bytes));
155
- queue.push(...documentLinks(ref,bytes));
219
+ const localBaseline=uiBaselineKind(handoff)==='existing-ui-baseline'&&ref.startsWith(`${uiBaselineRef(handoff).persisted_ref}/`);
220
+ if(!localBaseline&&/\.(yaml|yml|json)$/.test(ref))refs(parse(bytes));
221
+ // The entire existing UI observation directory was validated as a byte-bound file set.
222
+ // Its copied source documentation is evidence, not governance dependency instructions.
223
+ if(!localBaseline)queue.push(...documentLinks(ref,bytes));
156
224
  }
157
225
  return collected;
158
226
  }
159
227
  function materialize(captured, manifest, destination) {
160
228
  for(const file of manifest.files.filter(x=>x.original_ref))write(destination,file.original_ref,captured.get(file.path));
161
229
  }
162
- export async function openBundle(input, action) {
163
- const temp=mkdtempSync(path.join(tmpdir(),'yss-handoff-'));
230
+ export async function openBundle(input, action, {readOnly=false}={}) {
231
+ const temp=readOnly?null:mkdtempSync(path.join(tmpdir(),'yss-handoff-'));
164
232
  try {
165
233
  ensure(existsSync(input) && !lstatSync(input).isSymbolicLink(),'交接包不存在或是 symlink');
166
234
  let root=path.resolve(input);
167
- if(lstatSync(root).isFile()){ root=path.join(temp,'unpacked');mkdirSync(root);archive('unpack',path.resolve(input),root); }
235
+ if(lstatSync(root).isFile()){ ensure(!readOnly,'readonly-extraction-required: 只读验证需要已展开目录');root=path.join(temp,'unpacked');mkdirSync(root);archive('unpack',path.resolve(input),root); }
168
236
  const manifest=read(safe(root,'manifest.json'));
169
237
  schema(manifest,'docs/process/schemas/strategic-handoff-package.schema.json');
170
238
  const {bundle_digest,...body}=manifest;ensure(bundle_digest===digest(body),'包清单摘要不一致');
@@ -176,7 +244,12 @@ export async function openBundle(input, action) {
176
244
  for(const file of manifest.files){const bytes=readFileSync(safe(root,file.path));ensure(bytes.length===file.size_bytes && hash(bytes)===file.sha256,`文件摘要不一致: ${file.path}`);if(file.original_ref)relative(file.original_ref);captured.set(file.path,bytes);}
177
245
  const originals=manifest.files.filter(x=>x.original_ref).map(x=>x.original_ref);
178
246
  ensure(new Set(originals.map(x=>x.toLowerCase())).size===originals.length,'源路径重复');
179
- const source=path.join(temp,'source');mkdirSync(source);materialize(captured,manifest,source);
247
+ let source;
248
+ if(readOnly){
249
+ ensure(manifest.files.filter(file=>file.original_ref).every(file=>file.path===`payload/files/${file.original_ref==='CONTEXT.md'?'source-context.snapshot.md':file.original_ref}`),'readonly-source-layout-required: 原始源布局需要只读文件映射支持');
250
+ source=safe(root,'payload/files');
251
+ }else{source=path.join(temp,'source');mkdirSync(source);materialize(captured,manifest,source);}
252
+ const inspect=async()=>{
180
253
  const inspected=await inspectSource(source,manifest.handoff_ref);
181
254
  ensure(manifest.bundle_id===inspected.handoff.handoff_id && manifest.version===inspected.handoff.handoff_version,'包身份与源交接不一致');
182
255
  ensure(own(read(safe(root,'indexes/rules.json')),inspected.indexes.rules) && own(read(safe(root,'indexes/scenarios.json')),inspected.indexes.scenarios),'规则/场景索引与源资产不一致');
@@ -193,14 +266,16 @@ export async function openBundle(input, action) {
193
266
  }
194
267
  ensure(own(changes,{previous_bundle:manifest.previous_bundle,...compareIndexes(inspected.indexes,previousIndexes)}),'版本差异与实际索引不一致');
195
268
  return await action({root,manifest,...inspected,changes});
196
- } finally {rmSync(temp,{recursive:true,force:true});}
269
+ };
270
+ return readOnly?await withSourceContextSnapshot(source,inspect):await inspect();
271
+ } finally {if(temp)rmSync(temp,{recursive:true,force:true});}
197
272
  }
198
273
  export async function exportBundle({sourceRoot,handoffRef,output,zip=false,previous}) {
199
274
  const root=project(sourceRoot); parseContextContract({root}); relative(handoffRef);
200
275
  const current=await inspectSource(root,handoffRef);
201
276
  let prior=null;
202
277
  if(previous)prior=await openBundle(previous, b=>({manifest:b.manifest,indexes:b.indexes,handoff:b.handoff,rulesBytes:readFileSync(safe(b.root,'indexes/rules.json')),scenariosBytes:readFileSync(safe(b.root,'indexes/scenarios.json'))}));
203
- if(prior){ensure(prior.manifest.bundle_id===current.handoff.handoff_id,'前版属于不同交接');ensure(Number(current.handoff.handoff_version.slice(1))>Number(prior.manifest.version.slice(1)),'更新必须提升交接版本');for(const[key,ref]of Object.entries(current.handoff.source)){const old=prior.handoff.source[key];if(old.digest!==ref.digest)ensure(old.version!==ref.version,`源资产内容变化必须提升版本: ${key}`);}}
278
+ if(prior){ensure(prior.manifest.bundle_id===current.handoff.handoff_id,'前版属于不同交接');ensure(Number(current.handoff.handoff_version.slice(1))>Number(prior.manifest.version.slice(1)),'更新必须提升交接版本');for(const[key,ref]of Object.entries(current.handoff.source)){const old=prior.handoff.source[key];if(old&&old.digest!==ref.digest)ensure(old.version!==ref.version,`源资产内容变化必须提升版本: ${key}`);}}
204
279
  const previousBundle=prior?{bundle_id:prior.manifest.bundle_id,version:prior.manifest.version,digest:prior.manifest.bundle_digest}:null;
205
280
  const snapshotFiles=collect(root,handoffRef,current.handoff,current.config);
206
281
  const target=path.resolve(output);
@@ -213,7 +288,7 @@ export async function exportBundle({sourceRoot,handoffRef,output,zip=false,previ
213
288
  const entries=[];
214
289
  const put=(ref,bytes,original)=>{write(staging,ref,bytes);entries.push({path:ref,sha256:hash(bytes),size_bytes:bytes.length,...(original?{original_ref:original}:{})});};
215
290
  for(const [ref,bytes]of [...snapshotFiles].sort(([a],[b])=>a.localeCompare(b))) {
216
- const dest=ref===handoffRef?'handoff.yaml':ref==='CONTEXT.md'?'payload/source-context.snapshot.md':`payload/files/${ref}`;
291
+ const dest=current.handoff.schema_version===5?`payload/files/${ref==='CONTEXT.md'?'source-context.snapshot.md':ref}`:ref===handoffRef?'handoff.yaml':ref==='CONTEXT.md'?'payload/source-context.snapshot.md':`payload/files/${ref}`;
217
292
  ensure(!dest.endsWith('/CONTEXT.md'),'只允许源根词汇快照,禁止嵌套 CONTEXT.md');put(dest,bytes,ref);
218
293
  }
219
294
  put('indexes/rules.json',Buffer.from(json(current.indexes.rules)));
@@ -251,12 +326,46 @@ export async function importBundle({bundle,targetRoot}) {
251
326
  mkdirSync(path.dirname(dest),{recursive:true});const stage=mkdtempSync(path.join(path.dirname(dest),'.import-staging-'));
252
327
  try {
253
328
  cpSync(b.root,path.join(stage,'package'),{recursive:true,errorOnExist:true,force:false});
254
- const receipt={schema_version:1,bundle_id:b.manifest.bundle_id,version:b.manifest.version,bundle_digest:b.manifest.bundle_digest,package_ref:`${rel}/package`,target_context_digest:context.document_digest,status:'pending-context-reconciliation'};
255
- write(stage,'import-receipt.json',json(receipt));write(stage,'context-reconciliation-draft.json',json({schema_version:1,status:'draft',import_receipt_ref:`${rel}/import-receipt.json`,target_context_digest:context.document_digest,terms}));
256
- write(stage,'tactical-traceability-draft.json',json({schema_version:1,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,rows}));
257
- write(stage,'upstream-change-impact.json',json({schema_version:1,previous_bundle:b.manifest.previous_bundle,changed_source_ids:[...b.changes.updated,...b.changes.removed],added_source_ids:b.changes.added,status:'requires-lifecycle-reconciliation',policy:'mark-dependent-tactical-and-slice-contracts-stale; unknown-dependency-blocks-all'}));
329
+ let receipt;
330
+ if(b.handoff.schema_version===3){
331
+ receipt={schema_version:1,bundle_id:b.manifest.bundle_id,version:b.manifest.version,bundle_digest:b.manifest.bundle_digest,package_ref:`${rel}/package`,target_context_digest:context.document_digest,status:'pending-context-reconciliation'};
332
+ write(stage,'import-receipt.json',json(receipt));write(stage,'context-reconciliation-draft.json',json({schema_version:1,status:'draft',import_receipt_ref:`${rel}/import-receipt.json`,target_context_digest:context.document_digest,terms}));
333
+ write(stage,'tactical-traceability-draft.json',json({schema_version:1,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,rows}));
334
+ write(stage,'upstream-change-impact.json',json({schema_version:1,previous_bundle:b.manifest.previous_bundle,changed_source_ids:[...b.changes.updated,...b.changes.removed],added_source_ids:b.changes.added,status:'requires-lifecycle-reconciliation',policy:'mark-dependent-tactical-and-slice-contracts-stale; unknown-dependency-blocks-all'}));
335
+ }else{
336
+ const profileRef=path.join(target,'docs/process/harness-profile.yaml');
337
+ const profile=existsSync(profileRef)?read(profileRef):null;
338
+ const capabilities=profile?.handoff?.consumer_capabilities||CAPABILITIES;
339
+ ensure(Array.isArray(capabilities)&&capabilities.length&&capabilities.every(capability=>CAPABILITIES.includes(capability)),'目标 profile 未声明受支持的消费者能力');
340
+ const selected=b.handoff.consumer_routes.filter(route=>capabilities.includes(route.capability));
341
+ ensure(selected.length===capabilities.length,'目标 profile 的消费者能力在 Handoff v4 中缺失');
342
+ const commonRefs=[`${rel}/context-reconciliation-draft.json`,`${rel}/upstream-change-impact.json`];
343
+ const routeRecords=selected.map(route=>{
344
+ const artifactRefs=[...commonRefs];
345
+ if(route.activation!=='not-applicable'&&route.capability==='backend-technical-design')artifactRefs.push(`${rel}/backend-technical-traceability-draft.json`);
346
+ if(route.activation!=='not-applicable'&&route.capability==='frontend-engineering-design')artifactRefs.push(`${rel}/frontend-strategic-preflight-draft.json`,`${rel}/frontend-traceability-draft.json`);
347
+ if(route.activation!=='not-applicable'&&route.capability==='delivery-coordination')artifactRefs.push(`${rel}/consumer-status-index-draft.json`);
348
+ return {route_id:route.route_id,capability:route.capability,activation:route.activation,artifact_refs:artifactRefs};
349
+ });
350
+ receipt={schema_version:2,bundle_id:b.manifest.bundle_id,version:b.manifest.version,bundle_digest:b.manifest.bundle_digest,package_ref:`${rel}/package`,target_context_digest:context.document_digest,target_profile_id:profile?.profile_id||'yss-full-lifecycle',selected_consumer_capabilities:capabilities,routes:routeRecords,status:'pending-context-reconciliation'};
351
+ schema(receipt,'docs/process/schemas/strategic-handoff-import-receipt.schema.json');
352
+ write(stage,'import-receipt.json',json(receipt));
353
+ write(stage,'context-reconciliation-draft.json',json({schema_version:2,status:'draft',import_receipt_ref:`${rel}/import-receipt.json`,target_context_digest:context.document_digest,route_ids:selected.map(route=>route.route_id),terms}));
354
+ write(stage,'upstream-change-impact.json',json({schema_version:2,previous_bundle:b.manifest.previous_bundle,routes:selected.map(route=>({route_id:route.route_id,capability:route.capability,activation:route.activation,changed_source_ids:[...b.changes.updated,...b.changes.removed],added_source_ids:b.changes.added,status:'requires-lifecycle-reconciliation'})),policy:'mark-only-known-dependent-contracts-stale; unknown-dependency-or-design-basis-change-blocks-all'}));
355
+ const backendRoute=b.handoff.consumer_routes.find(route=>route.capability==='backend-technical-design');
356
+ const frontendRoute=b.handoff.consumer_routes.find(route=>route.capability==='frontend-engineering-design');
357
+ const coordinationRoute=b.handoff.consumer_routes.find(route=>route.capability==='delivery-coordination');
358
+ if(capabilities.includes('backend-technical-design')&&backendRoute.activation!=='not-applicable')write(stage,'backend-technical-traceability-draft.json',json({schema_version:2,route_id:backendRoute.route_id,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,rows}));
359
+ if(capabilities.includes('frontend-engineering-design')&&frontendRoute.activation!=='not-applicable'){
360
+ const preflight={schema_version:b.handoff.schema_version===5?2:1,status:'draft',route_id:frontendRoute.route_id,capability:frontendRoute.capability,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,context_reconciliation_ref:'',...(b.handoff.schema_version===5?{ui_baseline_kind:uiBaselineKind(b.handoff),ui_baseline_ref:`${rel}/package/payload/files/${uiBaselineRef(b.handoff).persisted_ref}/${uiBaselineRef(b.handoff).manifest_ref}`}:{visual_baseline_ref:`${rel}/package/payload/files/${uiBaselineRef(b.handoff).persisted_ref}/${uiBaselineRef(b.handoff).manifest_ref}`}),source_rule_refs:rows.map(row=>row.source_id),backend_dependency:{mode:backendRoute.activation==='not-applicable'?'not-applicable':'required',route_id:backendRoute.route_id,...(backendRoute.activation==='not-applicable'?{reason:backendRoute.reason,impact_refs:backendRoute.impact_refs,evidence_refs:backendRoute.evidence_refs}:{})},ready_for_agent:false};
361
+ schema(preflight,b.handoff.schema_version===5?'docs/process/schemas/frontend-strategic-preflight-v2.schema.json':'docs/process/schemas/frontend-strategic-preflight.schema.json');
362
+ write(stage,'frontend-strategic-preflight-draft.json',json(preflight));write(stage,'frontend-traceability-draft.json',json({schema_version:1,route_id:frontendRoute.route_id,import_receipt_ref:`${rel}/import-receipt.json`,bundle_digest:b.manifest.bundle_digest,rows:rows.map(({tactical_refs,test_seam_refs,...row})=>({...row,frontend_case_refs:[]}))}));
363
+ }
364
+ if(capabilities.includes('delivery-coordination')&&coordinationRoute.activation!=='not-applicable')write(stage,'consumer-status-index-draft.json',json({schema_version:1,status:'draft',import_receipt_ref:`${rel}/import-receipt.json`,routes:b.handoff.consumer_routes.map(route=>({route_id:route.route_id,capability:route.capability,activation:route.activation,consumer_status:route.activation==='not-applicable'?'not-applicable':'pending',receipt_refs:[],acceptance_refs:[],feedback_refs:[]}))}));
365
+ }
258
366
  ensure(!existsSync(dest),'导入并发冲突');renameSync(stage,dest);
259
- return {result:'imported-pending-reconciliation',receipt_ref:`${rel}/import-receipt.json`,traceability_ref:`${rel}/tactical-traceability-draft.json`};
367
+ const traceabilityRef=b.handoff.schema_version===3?`${rel}/tactical-traceability-draft.json`:receipt.routes.find(route=>route.capability==='backend-technical-design')?.artifact_refs.find(ref=>ref.endsWith('backend-technical-traceability-draft.json'))||receipt.routes.find(route=>route.capability==='frontend-engineering-design')?.artifact_refs.find(ref=>ref.endsWith('frontend-traceability-draft.json'));
368
+ return {result:'imported-pending-reconciliation',receipt_ref:`${rel}/import-receipt.json`,...(traceabilityRef?{traceability_ref:traceabilityRef}:{})};
260
369
  }finally{rmSync(stage,{recursive:true,force:true});}
261
370
  });
262
371
  }
@@ -179,13 +179,21 @@ function validateContract(value, registry, lifecycle) {
179
179
  if (!existsSync(contractPath)) fail(`Slice Implementation Contract 不存在: ${contract.slice_contract_ref}`);
180
180
  const document = parseYaml(readFileSync(contractPath, "utf8"), "Slice Implementation Contract");
181
181
  const slice = document.slice_contract || document;
182
- if (slice.schema_version !== 1 || typeof slice.slice_id !== "string" || !slice.lifecycle_refs || !slice.readiness || !Array.isArray(slice.work_units)) fail("Slice Implementation Contract 缺少 schema_version/slice_id/lifecycle_refs/readiness/work_units");
182
+ if (slice.schema_version !== 2 || typeof slice.slice_id !== "string" || !slice.lifecycle_refs || !slice.readiness || !Array.isArray(slice.work_units)) fail("Slice Implementation Contract 需要 schema v2 及 slice_id/lifecycle_refs/readiness/work_units");
183
183
  if (slice.contract_id !== contract.contract_id || slice.contract_version !== contract.contract_version || slice.status !== "approved") fail("Slice Contract 必须是当前 approved 版本");
184
184
  const sliceUnit = slice.work_units.find((item) => item && item.id === value.work_unit_id);
185
185
  if (!sliceUnit) fail(`Slice Implementation Contract 缺少 work_unit_id: ${value.work_unit_id}`);
186
186
  for (const field of ["role_id", "runtime_id", "task_package_ref", "contract_id", "contract_version", "allowed_write_paths"]) if (sliceUnit[field] === undefined) fail(`Slice Contract work_unit 缺少 ${field}`);
187
- if (sliceUnit.role_id !== value.role_id || sliceUnit.runtime_id !== value.runtime_id || sliceUnit.contract_id !== contract.contract_id || sliceUnit.contract_version !== contract.contract_version || sliceUnit.task_package_ref !== value.work_unit_id) fail("任务包与 Slice Contract work_unit 不一致");
187
+ if (sliceUnit.role_id !== value.role_id || sliceUnit.runtime_id !== value.runtime_id || sliceUnit.contract_id !== contract.contract_id || sliceUnit.contract_version !== contract.contract_version) fail("任务包与 Slice Contract work_unit 不一致");
188
188
  for (const allowed of value.allowed_write_paths) if (!sliceUnit.allowed_write_paths.includes(allowed)) fail(`allowed_write_paths 未获 Slice Contract 授权: ${allowed}`);
189
+ // Older packages used the work-unit ID here; new packages bind a readable dispatch file.
190
+ if (sliceUnit.task_package_ref !== value.work_unit_id) {
191
+ const dispatchPath = assertSafeRelativePath(sliceUnit.task_package_ref, "Slice work_unit.task_package_ref");
192
+ if (!existsSync(dispatchPath)) fail("Slice task_package_ref 不可读");
193
+ const dispatched = parseDocument(readFileSync(dispatchPath, "utf8")).toJS();
194
+ for (const field of ["task_id", "work_unit_id", "actor_id", "role_id", "runtime_id"]) if (dispatched?.[field] !== value[field]) fail("Slice task_package_ref 与当前派发身份不一致");
195
+ if (JSON.stringify(dispatched.contract) !== JSON.stringify(value.contract) || JSON.stringify(dispatched.allowed_write_paths) !== JSON.stringify(value.allowed_write_paths)) fail("Slice task_package_ref 与当前合同或写范围不一致");
196
+ }
189
197
  if (value.execution_state === "Worker") assertImplementationDecision({ slice_contract_ref: contract.slice_contract_ref, vertical_slice_ticket_ref: slice.lifecycle_refs.ticket, user_decisions: value.user_decisions });
190
198
  }
191
199
 
@@ -223,7 +231,7 @@ export function validateTaskPackageSet(packages, options = {}) {
223
231
  packages.forEach((pkg) => validateTaskPackage(pkg, options));
224
232
  const slicePackages = packages.filter((pkg) => pkg.contract.kind === "slice-implementation");
225
233
  if (slicePackages.length > 0) {
226
- const contracts = new Set(slicePackages.map((pkg) => `${pkg.contract.contract_id}@${pkg.contract.contract_version}`));
234
+ const contracts = new Set(slicePackages.map((pkg) => JSON.stringify([pkg.contract.contract_id, pkg.contract.contract_version])));
227
235
  if (contracts.size !== 1) fail("同一切片的任务包必须消费同一 contract_id/contract_version");
228
236
  const workers = new Set(slicePackages.filter((pkg) => pkg.execution_state === "Worker").map((pkg) => pkg.actor_id));
229
237
  slicePackages.filter((pkg) => pkg.execution_state === "Reviewer").forEach((pkg) => { if (workers.has(pkg.actor_id)) fail("Reviewer 不得与实现者使用同一 actor_id"); });
@@ -1,10 +1,11 @@
1
+ import { assertApprovedExecutionContext, approvedExecutionBinding } from './approved-execution-context.mjs';
1
2
  import path from 'node:path';
2
3
  import { readFileSync } from 'node:fs';
3
4
  import { spawnSync } from 'node:child_process';
4
5
  import { ROOT, read, safe, ensure, hash } from './strategic-handoff-io.mjs';
5
6
 
6
7
  // The compiler's synchronous boundary reruns the design verifier against current files.
7
- export function enforceTechnicalDesign(state = {}, { root = ROOT } = {}) {
8
+ export function enforceTechnicalDesign(state = {}, { root = ROOT, execution, readOnly = false } = {}) {
8
9
  const binding = state.technical_design;
9
10
  const affected = (state.conditions || []).some(value => ['backend-technical-design-impact','tactical-domain','tactical-domain-impact','aggregate-impact','state-machine-impact','invariant-impact','consistency-impact','domain-event-impact','gateway-boundary-impact','persistence-mapping-impact'].includes(value));
10
11
  if (!binding && !affected) return;
@@ -13,11 +14,18 @@ export function enforceTechnicalDesign(state = {}, { root = ROOT } = {}) {
13
14
  const file = safe(root, binding.ref);
14
15
  ensure(hash(readFileSync(file)) === binding.digest, '技术设计绑定摘要漂移');
15
16
  const data = read(file);
17
+ if(data.design_scope==='engineering-only')ensure(!(state.conditions||[]).some(value=>['tactical-domain','tactical-domain-impact','aggregate-impact','state-machine-impact','invariant-impact','domain-event-impact','gateway-boundary-impact','persistence-mapping-impact'].includes(value)),'engineering-only 技术设计不能承接领域模型、业务状态或持久化映射影响');
16
18
  ensure(data.status === 'approved' && data.version === binding.version, '技术设计必须为批准且绑定版本');
17
19
  const family = data.schema_version === 1 && binding.legacy_ddd === true ? 'domain-driven' : data.architecture?.family;
18
20
  ensure(family && family === state.architecture_identity?.architecture_family, '技术设计与实现架构不匹配');
19
21
  const args = [path.join(ROOT, '.agents/skills/yss-technical-design/scripts/validate-technical-design.mjs'), file, '--root', root, '--slice', state.slice_id];
20
22
  if (binding.legacy_ddd === true) args.push('--legacy-ddd');
23
+ if(readOnly)args.push('--read-only');
24
+ if(execution){
25
+ assertApprovedExecutionContext(execution,{root,technicalDesign:data,architectureIdentity:state.architecture_identity,sliceId:state.slice_id});
26
+ const approved=approvedExecutionBinding(execution);
27
+ args.push('--approved-slice',approved.ref,'--approved-slice-digest',approved.digest,'--approved-slice-approval',approved.approval_ref);
28
+ }
21
29
  const verified = spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8', timeout: 60000, maxBuffer: 2 * 1024 * 1024 });
22
30
  ensure(verified.status === 0, `技术设计不可消费: ${verified.error?.message || verified.stderr}`);
23
31
  }
@@ -0,0 +1,26 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { ensure, read, safe, hash } from './strategic-handoff-io.mjs';
3
+ import { validateVisualBaseline } from '../../.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs';
4
+ import { validateExistingUiBaseline } from './existing-ui-baseline.mjs';
5
+
6
+ export function hasConsumerRoutes(handoff) { return [4,5].includes(handoff?.schema_version); }
7
+ export function uiBaselineKind(handoff) {
8
+ ensure([3,4,5].includes(handoff?.schema_version),'未知 Strategic Handoff schema_version');
9
+ if(handoff.schema_version<5)return 'prototype';
10
+ ensure(['prototype','existing-ui-baseline'].includes(handoff.ui_baseline_kind),'未知 ui_baseline_kind');
11
+ return handoff.ui_baseline_kind;
12
+ }
13
+ export function uiBaselineRef(handoff) {
14
+ const kind=uiBaselineKind(handoff),ref=handoff.source?.[kind==='prototype'?'visual_baseline_ref':'existing_ui_baseline_ref'];
15
+ ensure(ref,'缺少 UI 基线引用');return ref;
16
+ }
17
+ export function uiBaselineCaseIds(handoff) { return uiBaselineRef(handoff).case_ids; }
18
+ export function uiBaselineSourceKeys(handoff) { return uiBaselineKind(handoff)==='prototype'?['prototype_ref','visual_baseline_ref']:['existing_ui_baseline_ref']; }
19
+ export async function validateHandoffUiBaseline(root,handoff) {
20
+ const kind=uiBaselineKind(handoff),ref=uiBaselineRef(handoff),file=safe(root,`${ref.persisted_ref}/${ref.manifest_ref}`),baseline=read(file);
21
+ const result=kind==='prototype'?await validateVisualBaseline(baseline,{bundleRoot:safe(root,ref.persisted_ref)}):validateExistingUiBaseline(baseline,{bundleRoot:safe(root,ref.persisted_ref)});
22
+ ensure(!result.errors.length,`UI 基线无效: ${result.errors.join('; ')}`);
23
+ // The immutable existing manifest records readiness; the caller verifies its current approval chain.
24
+ ensure((kind==='existing-ui-baseline'||baseline.status==='approved')&&baseline.baseline_id===ref.baseline_id&&baseline.version===ref.version&&JSON.stringify([...ref.case_ids].sort())===JSON.stringify(baseline.cases.map(item=>item.case_id).sort()),'UI 基线身份不一致');
25
+ return {kind,ref,baseline,digest:kind==='prototype'?baseline.bundle.digest:hash(readFileSync(file))};
26
+ }
@@ -0,0 +1,38 @@
1
+ import { assertUserDecisionRequirement, decisionIO, decisionDigest, decisionError } from './user-decision.mjs';
2
+
3
+ export const DECISION_REUSE_CAPABILITY = 'strategic-decision-reuse-v1';
4
+ const fail = detail => decisionError('user-decision-reuse-invalid', detail);
5
+ const sameSet = (a,b) => Array.isArray(a) && Array.isArray(b) && a.length === new Set(a).size && b.length === new Set(b).size && JSON.stringify([...a].sort()) === JSON.stringify([...b].sort());
6
+
7
+ // A reuse record is a coverage proof, never a new reply or a substitute for one.
8
+ export function assertApprovalUserDecision(record, roles, options = {}) {
9
+ const policy = roles.user_decision_policy;
10
+ const io = decisionIO(options);
11
+ if (policy.required_capabilities?.some(id => id !== DECISION_REUSE_CAPABILITY)) fail('接收工具不支持源用户决定策略,请升级');
12
+ if (!record.decision_reuse_ref) return assertUserDecisionRequirement({boundary:record.gate_id,subject_ref:record.subject_ref,scope:record.approval_scope,user_decision_ref:record.user_decision_ref}, {...options,rolesDoc:roles});
13
+ if (record.gate_id !== 'gate.strategic-design-handoff-approved' || !policy.required_capabilities?.includes(DECISION_REUSE_CAPABILITY)) fail('未登记的复用边界或能力');
14
+ const reuse = io.document(record.decision_reuse_ref);
15
+ if (reuse.schema_version !== 1 || reuse.kind !== DECISION_REUSE_CAPABILITY || reuse.target_gate !== record.gate_id || reuse.scope_ref !== record.subject_ref || !sameSet(reuse.scope, record.approval_scope)) fail('目标边界、资产或范围不匹配');
16
+ const manifest = io.document(reuse.scope_ref);
17
+ if (manifest.schema_version !== 1 || manifest.kind !== 'strategic-delivery-scope' || !Array.isArray(manifest.assets) || !manifest.assets.length || !Array.isArray(manifest.risks) || !Array.isArray(manifest.conditions)) fail('交付范围清单不完整');
18
+ if (!Array.isArray(reuse.requirements) || !reuse.requirements.length) fail('缺少原决定');
19
+ const approved = [];
20
+ for (const requirement of reuse.requirements) {
21
+ const result = assertUserDecisionRequirement(requirement,{...options,rolesDoc:roles});
22
+ const source = io.document(requirement.user_decision_ref);
23
+ for (const item of source.request.items.filter(item => result.validated.some(v => v.item_id === item.id))) approved.push({item, requirement});
24
+ }
25
+ const identities = new Set();
26
+ for (const asset of manifest.assets) {
27
+ if (!asset.ref || !asset.version || !asset.boundary || !Array.isArray(asset.scope) || !asset.scope.length || identities.has(`${asset.boundary}:${asset.ref}`)) fail('资产缺少身份、专业边界、范围或重复');
28
+ identities.add(`${asset.boundary}:${asset.ref}`);
29
+ if (decisionDigest(io.bytes(asset.ref)) !== asset.digest) fail(`资产已变化: ${asset.ref}`);
30
+ const covering = approved.filter(({item,requirement}) => item.boundary === asset.boundary && asset.scope.every(s => requirement.scope.includes(s)) && [item.subject,...item.basis].some(a => a.ref === asset.ref && a.version === asset.version && a.digest === asset.digest));
31
+ if (!covering.length) fail(`没有原决定覆盖: ${asset.ref} / ${asset.boundary}`);
32
+ }
33
+ const coveredScope = new Set(manifest.assets.flatMap(asset => asset.scope));
34
+ if (!reuse.scope.length || reuse.scope.some(s => !coveredScope.has(s))) fail('交付范围超出资产批准范围');
35
+ for (const risk of manifest.risks) if (!approved.some(({item}) => item.risks.includes(risk))) fail(`新增未接受风险: ${risk}`);
36
+ for (const condition of manifest.conditions) if (!approved.some(({item}) => item.next_actions.includes(condition))) fail(`新增授权条件: ${condition}`);
37
+ return {reused:true,manifest,validated:approved.map(({item})=>({item_id:item.id,principal_ref:item.responder_ref}))};
38
+ }
@@ -113,8 +113,11 @@ export function validateUserDecision(record, options = {}) {
113
113
  previousTime = time;
114
114
  if (response.decision === "approved") {
115
115
  const explicit = source.decision === "approved" && Array.isArray(source.item_ids) && response.item_ids.every((id) => source.item_ids.includes(id));
116
+ const contextualAll = response.selection === "explicit-all" && items.size > 1
117
+ && /是否\s*(?:明确)?(?:确认|批准|同意|接受)[^\n]{0,32}(?:以上)?全部\s*(?:\d+|[一二三四五六七八九十]+)?\s*(?:项|事项)/i.test(presentation.text)
118
+ && /^(?:确认|同意|批准|接受|可以|好的|好|yes|ok)[。.!!\s]*$/i.test(source.text.trim());
116
119
  if (!explicit && (!/(同意|批准|确认|接受|可以|好的|^好[。!!\s]*$|\b(?:approve|approved|agree|agreed|accept|yes|ok)\b)/i.test(source.text) || /(不同意|不批准|拒绝|反对|不要|需[要]?修改|请修改|暂不|reject|decline|do not approve|don't approve|disagree)/i.test(source.text))) decisionError("user-decision-ambiguous", "原文不能明确支持批准结论");
117
- if (!explicit && response.selection === "explicit-all" && items.size > 1 && !/(全部|所有|\ball\b)/i.test(source.text)) decisionError("user-decision-ambiguous", "批量批准原文必须明确全部事项");
120
+ if (!explicit && !contextualAll && response.selection === "explicit-all" && items.size > 1 && !/(全部|所有|\ball\b)/i.test(source.text)) decisionError("user-decision-ambiguous", "批量批准原文必须明确全部事项,或直接回复明确询问全部事项的当前展示");
118
121
  if (!explicit && response.selection === "explicit-items" && items.size > 1 && response.item_ids.some((id) => !source.text.includes(id) && !source.text.includes(items.get(id)?.title))) decisionError("user-decision-ambiguous", "原文未指向所选事项");
119
122
  }
120
123
  if (response.selection === "single" && items.size !== 1) decisionError("user-decision-ambiguous", "多事项不能用单项回复");
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { read } from './lib/strategic-handoff-io.mjs';
5
+ import { preflightDelivery, invalidPreflightInput } from './lib/delivery-preflight.mjs';
6
+
7
+ let result;
8
+ try {
9
+ const {values}=parseArgs({options:{input:{type:'string'},stage:{type:'string'},json:{type:'boolean'}},strict:true});
10
+ if(!values.input||!values.stage) throw new TypeError('必须提供 --input <file> 和 --stage prepare|build|export|accept');
11
+ result=await preflightDelivery(read(path.resolve(values.input)),{stage:values.stage,root:process.cwd()});
12
+ } catch(error) { result=invalidPreflightInput(error.message); }
13
+ process.stdout.write(`${JSON.stringify(result,null,2)}\n`);
14
+ process.exitCode=result.exit_code;