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
@@ -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 ?? {};
@@ -1,16 +1,41 @@
1
- schema_version: 2
1
+ schema_version: 3
2
2
  stage_decision_id: stage-decision.supplier-admission
3
- package_version: v2
3
+ package_version: v3
4
4
  status: approved
5
5
  problem_statement: 合规准入决定分散,采购无法稳定判断当前有效资格。
6
6
  target_users: [采购专员、合规专员]
7
7
  mvp: [提交供应商申请、合规准入决定、采购侧有效性校验]
8
8
  non_goals: [供应商结算、采购付款、通用工作流平台]
9
- success_criteria: [新采购订单 100% 校验当前有效准入决定]
10
- test_seams: [supplier-application-submit, admission-validity-check]
11
- confirmed_decisions: [ComplianceReview 拥有准入规则语义,ProcurementExecution 只消费决定]
12
- assumptions: [供应商已有唯一 subject_ref]
13
- constraints: [保留现有采购订单流程]
9
+ success_criteria:
10
+ - id: success-criterion.current-admission
11
+ statement: 新采购订单 100% 校验当前有效准入决定。
12
+ source_refs: [rule.current-admission-required]
13
+ evidence_refs: [evidence.discovery.compliance]
14
+ status: confirmed
15
+ test_seams:
16
+ - id: test-seam.admission-validity-check
17
+ statement: admission-validity-check
18
+ source_refs: [scenario.admission-expires, invariant.current-admission-required]
19
+ evidence_refs: [evidence.discovery.compliance]
20
+ status: confirmed
21
+ confirmed_decisions:
22
+ - id: decision.compliance-owns-admission-rules
23
+ statement: ComplianceReview 拥有准入规则语义,ProcurementExecution 只消费决定。
24
+ source_refs: [rule.current-admission-required]
25
+ evidence_refs: [evidence.domain-strategy-review.supplier]
26
+ status: confirmed
27
+ assumptions:
28
+ - id: assumption.unique-subject-ref
29
+ statement: 供应商已有唯一 subject_ref。
30
+ source_refs: [scenario.supplier-submits]
31
+ evidence_refs: [evidence.discovery.supplier]
32
+ status: confirmed
33
+ constraints:
34
+ - id: constraint.preserve-procurement-flow
35
+ statement: 保留现有采购订单流程。
36
+ source_refs: [scenario.admission-expires]
37
+ evidence_refs: [evidence.discovery.compliance]
38
+ status: confirmed
14
39
  unresolved_items:
15
40
  - item: 供应商历史资料迁移范围
16
41
  type: deferred
@@ -25,7 +50,7 @@ context_snapshot:
25
50
  term_refs: [Global/Supplier, ComplianceReview/AdmissionDecision]
26
51
  domain_strategy_ref:
27
52
  domain_strategy_id: domain-strategy.supplier-admission
28
- domain_version: v2
53
+ domain_version: v3
29
54
  digest: <domain-strategy-digest>
30
55
  status: approved
31
56
  persisted_ref: domain-strategy.yaml
@@ -38,17 +63,21 @@ impact_assessment:
38
63
  cross_repo: false
39
64
  high_risk: false
40
65
  downstream_mapping:
41
- - domain_change: admission-decision-validity
42
- consumer: spec-synthesis
66
+ - mapping_id: mapping.backend-admission-validity
67
+ source_refs: [rule.current-admission-required, invariant.current-admission-required]
68
+ consumer_capability: backend-technical-design
43
69
  propagation: direct
44
70
  reapproval_condition: 领域策略版本或有效性规则变化
45
- - domain_change: admission-decision-validity
46
- consumer: ticket-decomposition
71
+ evidence_refs: [evidence.domain-strategy-review.supplier]
72
+ - mapping_id: mapping.frontend-admission-validity
73
+ source_refs: [scenario.admission-expires]
74
+ consumer_capability: frontend-engineering-design
47
75
  propagation: transitive
48
76
  reapproval_condition: Spec 基线变更
77
+ evidence_refs: [evidence.domain-strategy-review.supplier]
49
78
  evidence_refs: [evidence.discovery.supplier, evidence.domain-strategy-review.supplier]
50
79
  approval:
51
80
  approval_ref: .agents/skills/yss-stage-decision/tests/fixtures/stage-decision-approval.yaml
52
81
  approver: role.product-manager
53
82
  persisted_ref: docs/.scratch/supplier/stage-decision/stage-decision-package-v1.yaml
54
- current_version: v2
83
+ current_version: v3
@@ -1,7 +1,19 @@
1
- schema_version: 2
1
+ schema_version: 3
2
2
  domain_strategy_id: domain-strategy.supplier-admission
3
- domain_version: v2
3
+ domain_version: v3
4
4
  status: approved
5
+ traceability_version: 1
6
+ rule_catalog:
7
+ - rule_id: rule.application-material-complete
8
+ statement: 申请材料必须完整
9
+ responsible_context: SupplierManagement
10
+ status: confirmed
11
+ evidence_refs: [evidence.discovery.supplier]
12
+ - rule_id: rule.current-admission-required
13
+ statement: 新采购订单必须引用当前有效的准入决定。
14
+ responsible_context: ComplianceReview
15
+ status: confirmed
16
+ evidence_refs: [evidence.discovery.compliance]
5
17
  contexts:
6
18
  - context_id: SupplierManagement
7
19
  name: 供应商管理
@@ -64,6 +76,10 @@ scenarios:
64
76
  preconditions: [供应商资料可编辑]
65
77
  commands: [SubmitSupplierApplication]
66
78
  rules: [申请材料必须完整]
79
+ rule_refs: [rule.application-material-complete]
80
+ critical: true
81
+ success_results: [完整申请进入合规审查]
82
+ evidence_refs: [evidence.discovery.supplier]
67
83
  events: [SupplierApplicationSubmitted]
68
84
  failure_results: [材料不完整时拒绝提交]
69
85
  responsible_context: SupplierManagement
@@ -73,7 +89,11 @@ scenarios:
73
89
  actor: compliance-officer
74
90
  preconditions: [批准决定存在]
75
91
  commands: [EvaluateAdmissionValidity]
76
- rules: [过期决定不得支持新采购订单]
92
+ rules: [新采购订单必须引用当前有效的准入决定。]
93
+ rule_refs: [rule.current-admission-required]
94
+ critical: true
95
+ success_results: [过期准入决定不能支持新采购订单]
96
+ evidence_refs: [evidence.discovery.compliance]
77
97
  events: [SupplierAdmissionExpired]
78
98
  failure_results: [采购下单时资格校验失败]
79
99
  responsible_context: ComplianceReview
@@ -94,6 +114,8 @@ invariants:
94
114
  responsible_context: ComplianceReview
95
115
  scenario_refs: [scenario.admission-expires]
96
116
  verification_method: 领域场景测试和事件版本校验
117
+ rule_ref: rule.current-admission-required
118
+ evidence_refs: [evidence.discovery.compliance]
97
119
  context_snapshot:
98
120
  context_ref: CONTEXT.md
99
121
  context_schema_version: 1
@@ -101,13 +123,23 @@ context_snapshot:
101
123
  referenced_terms_digest: <referenced-terms-digest>
102
124
  term_refs: [Global/Supplier, ComplianceReview/AdmissionDecision]
103
125
  downstream_mapping:
104
- - domain_change: admission-decision-validity
126
+ - mapping_id: mapping.backend-admission-validity
127
+ source_refs: [rule.current-admission-required, invariant.current-admission-required]
128
+ consumer_capability: backend-technical-design
105
129
  impacts: [spec, openapi, procurement-acceptance-tests]
106
130
  propagation: stale
107
131
  reapproval_condition: ComplianceReview 决定和有效期重新批准
132
+ evidence_refs: [evidence.discovery.compliance]
133
+ - mapping_id: mapping.frontend-admission-validity
134
+ source_refs: [scenario.admission-expires]
135
+ consumer_capability: frontend-engineering-design
136
+ impacts: [ui, frontend]
137
+ propagation: direct
138
+ reapproval_condition: 场景结果或视觉基线变化
139
+ evidence_refs: [evidence.discovery.compliance]
108
140
  evidence_refs: [evidence.discovery.supplier, evidence.discovery.compliance]
109
141
  approval:
110
142
  approval_ref: .agents/skills/yss-stage-decision/tests/fixtures/domain-strategy-approval.yaml
111
143
  approver: role.product-manager
112
144
  persisted_ref: docs/.scratch/supplier/domain/domain-strategy-v1.yaml
113
- current_version: v2
145
+ current_version: v3
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { buildDecisionFixture } from "../../../../scripts/fixtures/user-decision/build-fixture.mjs";
3
2
  import { createHash } from "node:crypto";
4
3
  import { spawnSync } from "node:child_process";
5
4
  import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
@@ -14,6 +13,7 @@ const validator = join(testsRoot, "..", "scripts", "validate-domain-strategy.mjs
14
13
  const packageValidator = join(testsRoot, "..", "scripts", "validate-stage-decision-package.mjs");
15
14
  const contextValidator = join(projectRoot, "scripts", "verify-context-contract");
16
15
  const migrationTool = join(testsRoot, "..", "scripts", "migrate-context-references.mjs");
16
+ const v3MigrationTool = join(testsRoot, "..", "scripts", "migrate-contract-v3.mjs");
17
17
  const validTemplate = join(testsRoot, "fixtures", "valid-supplier-domain.yaml");
18
18
  const validPackageTemplate = join(testsRoot, "fixtures", "valid-stage-decision-package.yaml");
19
19
 
@@ -29,6 +29,58 @@ function digestYaml(source) {
29
29
  return `sha256:${createHash("sha256").update(JSON.stringify(canonical(document.toJS({ maxAliasCount: 0 })))).digest("hex")}`;
30
30
  }
31
31
 
32
+ function parseYaml(source) {
33
+ const document = parseDocument(source, { maxAliasCount: 0, uniqueKeys: true });
34
+ if (document.errors.length) throw new Error(document.errors[0].message);
35
+ return document.toJS({ maxAliasCount: 0 });
36
+ }
37
+
38
+ function toV2Domain(source) {
39
+ const value = structuredClone(parseYaml(source));
40
+ value.schema_version = 2;
41
+ value.domain_version = "v2";
42
+ delete value.traceability_version;
43
+ delete value.rule_catalog;
44
+ for (const scenario of value.scenarios) {
45
+ delete scenario.rule_refs;
46
+ delete scenario.critical;
47
+ delete scenario.success_results;
48
+ delete scenario.evidence_refs;
49
+ }
50
+ for (const invariant of value.invariants) {
51
+ delete invariant.rule_ref;
52
+ delete invariant.evidence_refs;
53
+ }
54
+ value.downstream_mapping = [{
55
+ domain_change: "admission-decision-validity",
56
+ impacts: ["spec", "openapi", "procurement-acceptance-tests"],
57
+ propagation: "stale",
58
+ reapproval_condition: "ComplianceReview 决定和有效期重新批准"
59
+ }];
60
+ value.approval.current_version = "v2";
61
+ return value;
62
+ }
63
+
64
+ function toV2Stage(source, domainDigest) {
65
+ const value = structuredClone(parseYaml(source));
66
+ value.schema_version = 2;
67
+ value.package_version = "v2";
68
+ for (const field of ["success_criteria", "test_seams", "confirmed_decisions", "assumptions", "constraints"]) {
69
+ value[field] = value[field].map((item) => item.statement);
70
+ }
71
+ value.domain_strategy_ref.domain_version = "v2";
72
+ value.domain_strategy_ref.digest = domainDigest;
73
+ value.domain_strategy_ref.persisted_ref = "domain-strategy-v2.yaml";
74
+ value.downstream_mapping = [{
75
+ domain_change: "admission-decision-validity",
76
+ consumer: "spec-synthesis",
77
+ propagation: "direct",
78
+ reapproval_condition: "领域策略版本或有效性规则变化"
79
+ }];
80
+ value.approval.current_version = "v2";
81
+ return value;
82
+ }
83
+
32
84
  function run(command, file, contextRoot) {
33
85
  return spawnSync(process.execPath, [command, file, "--root", contextRoot], { cwd: projectRoot, encoding: "utf8" });
34
86
  }
@@ -56,34 +108,59 @@ context_schema_version: 1
56
108
  | 准入决定 | 合规审查形成的准入结论。 | AdmissionDecision | ComplianceReview | 避免:\`审批结果\` |
57
109
  `;
58
110
 
59
- const temporaryRoot = await mkdtemp(join(tmpdir(), "yss-stage-decision-v2-"));
111
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "yss-stage-decision-v3-"));
60
112
  try {
61
113
  await writeFile(join(temporaryRoot, "CONTEXT.md"), contextSource);
62
114
  const contextResult = spawnSync(process.execPath, [contextValidator, "--root", temporaryRoot, "--allowed-context", "SupplierManagement", "--allowed-context", "ComplianceReview", "--allowed-context", "ProcurementExecution", "--term-ref", "Global/Supplier", "--term-ref", "ComplianceReview/AdmissionDecision", "--json"], { cwd: projectRoot, encoding: "utf8" });
63
115
  if (contextResult.status !== 0) throw new Error(`context fixture should pass: ${contextResult.stderr}`);
64
116
  const context = JSON.parse(contextResult.stdout);
65
117
 
66
- const domainApprovalRef = join(temporaryRoot, "domain-approval.json");
67
118
  const domainSource = (await readFile(validTemplate, "utf8"))
68
- .replace(".agents/skills/yss-stage-decision/tests/fixtures/domain-strategy-approval.yaml", domainApprovalRef)
69
119
  .replace("<document-digest>", context.document_digest)
70
120
  .replace("<referenced-terms-digest>", context.referenced_terms_digest);
71
121
  const domainFile = join(temporaryRoot, "domain-strategy.yaml");
72
122
  await writeFile(domainFile, domainSource);
73
- const domainDecision = buildDecisionFixture(join(temporaryRoot, "domain-decision"), { boundary: "check.domain-strategy-approved", subjectRef: domainFile });
74
- const domainApproval = parseDocument(await readFile(join(testsRoot, "fixtures/domain-strategy-approval.yaml"), "utf8")).toJS();
75
- await writeFile(domainApprovalRef, JSON.stringify({ ...domainApproval, subject_ref: domainFile, approval_scope: domainDecision.requirement.scope, user_decision_ref: domainDecision.ref }));
76
123
  const pass = run(validator, domainFile, temporaryRoot);
77
- if (pass.status !== 0) throw new Error(`valid v2 fixture should pass: ${pass.stderr}`);
124
+ if (pass.status !== 0) throw new Error(`valid v3 fixture should pass: ${pass.stderr}`);
125
+
126
+ const v2Domain = toV2Domain(domainSource);
127
+ const v2DomainSource = JSON.stringify(v2Domain, null, 2);
128
+ const v2DomainFile = join(temporaryRoot, "domain-strategy-v2.yaml");
129
+ await writeFile(v2DomainFile, v2DomainSource);
130
+ const v2Pass = run(validator, v2DomainFile, temporaryRoot);
131
+ if (v2Pass.status !== 0) throw new Error(`read-only v2 fixture should pass: ${v2Pass.stderr}`);
132
+
133
+ const migratedDomainV3 = spawnSync(process.execPath, [v3MigrationTool, v2DomainFile, "--kind", "domain-strategy", "--root", temporaryRoot], { cwd: projectRoot, encoding: "utf8" });
134
+ if (migratedDomainV3.status !== 0) throw new Error(`v2 domain strategy should migrate: ${migratedDomainV3.stderr}`);
135
+ const migratedDomainV3Value = JSON.parse(migratedDomainV3.stdout);
136
+ if (migratedDomainV3Value.schema_version !== 3 || migratedDomainV3Value.status !== "draft" || migratedDomainV3Value.approval.approval_ref !== "migration-required") throw new Error("domain v2 -> v3 migration must reset approval and status");
137
+
138
+ const duplicateRule = join(temporaryRoot, "duplicate-rule.yaml");
139
+ const duplicateRuleValue = parseYaml(domainSource);
140
+ duplicateRuleValue.rule_catalog.push(structuredClone(duplicateRuleValue.rule_catalog[0]));
141
+ await writeFile(duplicateRule, JSON.stringify(duplicateRuleValue, null, 2));
142
+ expectBlocked(run(validator, duplicateRule, temporaryRoot), /rule_id.*重复|重复.*rule_id/, "duplicate v3 rule id");
143
+
144
+ const danglingRule = join(temporaryRoot, "dangling-rule.yaml");
145
+ const danglingRuleValue = parseYaml(domainSource);
146
+ danglingRuleValue.scenarios[0].rule_refs = ["rule.missing"];
147
+ await writeFile(danglingRule, JSON.stringify(danglingRuleValue, null, 2));
148
+ expectBlocked(run(validator, danglingRule, temporaryRoot), /rule_refs.*未声明|悬空/, "dangling v3 rule ref");
149
+
150
+ const missingEvidence = join(temporaryRoot, "missing-rule-evidence.yaml");
151
+ const missingEvidenceValue = parseYaml(domainSource);
152
+ missingEvidenceValue.rule_catalog[0].evidence_refs = [];
153
+ await writeFile(missingEvidence, JSON.stringify(missingEvidenceValue, null, 2));
154
+ expectBlocked(run(validator, missingEvidence, temporaryRoot), /evidence_refs/, "missing v3 evidence");
78
155
 
79
156
  const legacyFile = join(temporaryRoot, "legacy-domain.yaml");
80
- await writeFile(legacyFile, domainSource.replace("schema_version: 2", "schema_version: 1"));
157
+ await writeFile(legacyFile, JSON.stringify({ ...v2Domain, schema_version: 1 }, null, 2));
81
158
  expectBlocked(run(validator, legacyFile, temporaryRoot), /migration-required/, "legacy domain contract");
82
159
 
83
160
  const migratableLegacy = join(temporaryRoot, "migratable-legacy-domain.yaml");
84
- const migratableLegacySource = domainSource
85
- .replace("schema_version: 2", "schema_version: 1")
86
- .replace(/context_snapshot:\n[\s\S]*?\ndownstream_mapping:/, "terminology_refs: [CONTEXT.md#Supplier, contexts/ComplianceReview/CONTEXT.md#AdmissionDecision]\ndownstream_mapping:");
161
+ const migratableLegacyValue = { ...structuredClone(v2Domain), schema_version: 1, terminology_refs: ["CONTEXT.md#Supplier", "contexts/ComplianceReview/CONTEXT.md#AdmissionDecision"] };
162
+ delete migratableLegacyValue.context_snapshot;
163
+ const migratableLegacySource = JSON.stringify(migratableLegacyValue, null, 2);
87
164
  await writeFile(migratableLegacy, migratableLegacySource);
88
165
  const migrated = spawnSync(process.execPath, [migrationTool, migratableLegacy, "--root", temporaryRoot], { cwd: projectRoot, encoding: "utf8" });
89
166
  if (migrated.status !== 0) throw new Error(`migratable v1 contract should migrate: ${migrated.stderr}`);
@@ -91,7 +168,7 @@ try {
91
168
  if (migratedValue.schema_version !== 2 || migratedValue.terminology_refs !== undefined || migratedValue.context_snapshot.context_ref !== "CONTEXT.md" || migratedValue.context_snapshot.term_refs.join(",") !== "ComplianceReview/AdmissionDecision,Global/Supplier") throw new Error("migrated context snapshot is incomplete");
92
169
 
93
170
  const ambiguousLegacy = join(temporaryRoot, "ambiguous-legacy-domain.yaml");
94
- await writeFile(ambiguousLegacy, migratableLegacySource.replace("CONTEXT.md#Supplier, contexts/ComplianceReview/CONTEXT.md#AdmissionDecision", "contexts/ComplianceReview/CONTEXT.md"));
171
+ await writeFile(ambiguousLegacy, JSON.stringify({ ...migratableLegacyValue, terminology_refs: ["contexts/ComplianceReview/CONTEXT.md"] }, null, 2));
95
172
  const ambiguousMigration = spawnSync(process.execPath, [migrationTool, ambiguousLegacy, "--root", temporaryRoot], { cwd: projectRoot, encoding: "utf8" });
96
173
  expectBlocked(ambiguousMigration, /migration-required.*无法唯一定位|无法唯一定位.*migration-required/, "ambiguous legacy reference");
97
174
 
@@ -115,32 +192,48 @@ try {
115
192
  await writeFile(unknownContext, domainSource.replace("to_context: ComplianceReview", "to_context: UnknownContext"));
116
193
  expectBlocked(run(validator, unknownContext, temporaryRoot), /未引用已声明上下文|未在领域战略中登记/, "unknown context");
117
194
 
118
- const packageApprovalRef = join(temporaryRoot, "package-approval.json");
119
195
  const packageSource = (await readFile(validPackageTemplate, "utf8"))
120
- .replace(".agents/skills/yss-stage-decision/tests/fixtures/stage-decision-approval.yaml", packageApprovalRef)
121
196
  .replace("<document-digest>", context.document_digest)
122
197
  .replace("<referenced-terms-digest>", context.referenced_terms_digest)
123
198
  .replace("<domain-strategy-digest>", digestYaml(domainSource));
124
199
  const packageFile = join(temporaryRoot, "stage-decision.yaml");
125
200
  await writeFile(packageFile, packageSource);
126
- const packageDecision = buildDecisionFixture(join(temporaryRoot, "package-decision"), { boundary: "check.stage-decision-package-approved", subjectRef: packageFile });
127
- const packageApproval = parseDocument(await readFile(join(testsRoot, "fixtures/stage-decision-approval.yaml"), "utf8")).toJS();
128
- await writeFile(packageApprovalRef, JSON.stringify({ ...packageApproval, subject_ref: packageFile, approval_scope: packageDecision.requirement.scope, user_decision_ref: packageDecision.ref }));
129
201
  const packagePass = run(packageValidator, packageFile, temporaryRoot);
130
- if (packagePass.status !== 0) throw new Error(`valid v2 stage decision package should pass: ${packagePass.stderr}`);
202
+ if (packagePass.status !== 0) throw new Error(`valid v3 stage decision package should pass: ${packagePass.stderr}`);
203
+
204
+ const v2Stage = toV2Stage(packageSource, digestYaml(v2DomainSource));
205
+ const v2StageFile = join(temporaryRoot, "stage-decision-v2.yaml");
206
+ await writeFile(v2StageFile, JSON.stringify(v2Stage, null, 2));
207
+ const v2StagePass = run(packageValidator, v2StageFile, temporaryRoot);
208
+ if (v2StagePass.status !== 0) throw new Error(`read-only v2 stage decision package should pass: ${v2StagePass.stderr}`);
209
+
210
+ const migratedV3 = spawnSync(process.execPath, [v3MigrationTool, v2StageFile, "--kind", "stage-decision", "--root", temporaryRoot], { cwd: projectRoot, encoding: "utf8" });
211
+ if (migratedV3.status !== 0) throw new Error(`v2 stage decision should migrate: ${migratedV3.stderr}`);
212
+ const migratedV3Value = JSON.parse(migratedV3.stdout);
213
+ if (migratedV3Value.schema_version !== 3 || migratedV3Value.status !== "draft" || migratedV3Value.approval.approval_ref !== "migration-required") throw new Error("v2 -> v3 migration must reset approval and status");
214
+
215
+ const wrongSourceRef = join(temporaryRoot, "stage-decision-wrong-source-ref.yaml");
216
+ const wrongSourceRefValue = parseYaml(packageSource);
217
+ wrongSourceRefValue.success_criteria[0].source_refs = ["rule.missing"];
218
+ await writeFile(wrongSourceRef, JSON.stringify(wrongSourceRefValue, null, 2));
219
+ expectBlocked(run(packageValidator, wrongSourceRef, temporaryRoot), /source_refs.*未在领域战略中声明|悬空/, "stage decision dangling source ref");
131
220
 
132
221
  const packageLegacy = join(temporaryRoot, "legacy-stage-decision.yaml");
133
- await writeFile(packageLegacy, packageSource.replace("schema_version: 2", "schema_version: 1"));
222
+ await writeFile(packageLegacy, JSON.stringify({ ...v2Stage, schema_version: 1 }, null, 2));
134
223
  expectBlocked(run(packageValidator, packageLegacy, temporaryRoot), /migration-required/, "legacy stage decision contract");
135
224
 
136
225
  const packageBlocker = join(temporaryRoot, "stage-decision-blocker.yaml");
137
- await writeFile(packageBlocker, packageSource.replace("type: deferred", "type: blocker"));
226
+ const blockerValue = parseYaml(packageSource);
227
+ blockerValue.unresolved_items[0].type = "blocker";
228
+ await writeFile(packageBlocker, JSON.stringify(blockerValue, null, 2));
138
229
  expectBlocked(run(packageValidator, packageBlocker, temporaryRoot), /blocker/, "blocker package");
139
230
 
140
231
  const packageWrongArray = join(temporaryRoot, "stage-decision-wrong-array.yaml");
141
- await writeFile(packageWrongArray, packageSource.replace("target_users: [采购专员、合规专员]", "target_users: [42]"));
232
+ const wrongArrayValue = parseYaml(packageSource);
233
+ wrongArrayValue.target_users = [42];
234
+ await writeFile(packageWrongArray, JSON.stringify(wrongArrayValue, null, 2));
142
235
  expectBlocked(run(packageValidator, packageWrongArray, temporaryRoot), /target_users\[0\]/, "package non-string array item");
143
236
  } finally {
144
237
  await rm(temporaryRoot, { recursive: true, force: true });
145
238
  }
146
- process.stdout.write("业务边界与规则设计 v2 场景验证通过\n");
239
+ process.stdout.write("业务边界与规则设计 v2/v3 场景验证通过\n");
@@ -16,7 +16,7 @@ Create a self-contained, interactive HTML diagram from a small typed JSON specif
16
16
 
17
17
  When the current repository contains `yss-project.yaml`, these rules override the upstream update and delivery commands below:
18
18
 
19
- - Treat Archify as a conditional specialist. Use it only when the user or the current YSS contract explicitly requires an architecture, workflow, sequence, data-flow, lifecycle, or Mermaid-beautification artifact. A diagram is derived evidence; it never replaces Spec, ADR, OpenAPI, Tactical Design Contract, Slice Implementation Contract, review, or approval evidence.
19
+ - Treat Archify as a conditional specialist. Use it only when the user or the current YSS contract explicitly requires an architecture, workflow, sequence, data-flow, lifecycle, or Mermaid-beautification artifact. A diagram is derived evidence; it never replaces the active profile's architecture contract (such as backend Tactical Design or Frontend Engineering Design), Spec, ADR, OpenAPI, Slice Implementation Contract, review, or approval evidence.
20
20
  - Do not run `scripts/check-update.mjs`. YSS pins the upstream revision in `skills-lock.json`; upgrades happen only through the repository's skill-maintenance workflow.
21
21
  - Validate with the upstream `validate` command, but deliver through the repository-safe wrapper from the repository root:
22
22
 
@@ -24,13 +24,22 @@ description: 编排 Plan 到 Spec 入口的方案决策与业务边界、协作
24
24
 
25
25
  Plan 的目标与退出条件由 `docs/process/lifecycle-registry.yaml` 持有;从 `docs/plan/templates/plan-template.md` 汇总输入。旧资产按 `docs/process/plan-migration.md` 重新整理并核查,关键未决项不得延期到 Spec,非关键项记录责任人、解决时点与接收方。
26
26
 
27
- 1. 读取 `yss-project.yaml`、`CONTEXT.md`、既有 Plan/Spec、ADR 和父 Ticket/checkpoint。
27
+ 1. 读取 `yss-project.yaml`、`CONTEXT.md`、既有 Plan/Spec、ADR 和当前 profile 的主索引/checkpoint;按 `yss-product-lifecycle` 的 `request-triage` 区分事实确认、产品决定、门禁确认和普通澄清。
28
28
  2. 分离事实、决策、假设、约束和未决项;技术事实走 `yss-research` 的 `technical-evidence`,领域边界、业务规则、MVP、非目标、成功标准或阶段推进依据等决策证据走 `strategy-evidence`,市场/竞品事实走 `competitive-intelligence`。
29
29
  3. 从业务故事、已发生的事实、规则、责任人和失败路径识别业务板块与业务责任区,不从数据库表或调用链直接反推边界。
30
30
  4. 为每个业务责任区建立统一业务词汇;对跨责任区协作记录规则提供方、规则使用方、业务决策权、信息传递方向和口径转换负责人。
31
31
  5. 记录待确认的关键业务对象,不提前指定技术模型。
32
- 6. 生成 schema v2 `domain_strategy` 和 `stage_decision_package`,使用 `context_snapshot` 绑定根 `CONTEXT.md`、术语身份、完整文档 digest、引用术语 digest、证据和下游影响映射。
33
- 7. 运行 Schema、根路径、术语引用、digest、语义一致性和传播验证;发现关键冲突时返回 `blocked`,不得生成 `approved` 包。v1 字符串引用只读并返回 `migration-required`;可唯一定位的旧锚点用 `scripts/migrate-context-references.mjs` 迁移,歧义引用不得猜测。
32
+ 6. 生成 schema v3 `domain_strategy` 和 `stage_decision_package`,使用稳定规则、场景、决定、假设、约束、成功标准、测试 seam 和 downstream mapping ID,并以 `context_snapshot` 绑定根 `CONTEXT.md`、术语身份、完整文档 digest、引用术语 digest和证据。
33
+ 7. 运行 Schema、唯一性、悬空引用、根路径、术语引用、digest、语义一致性和传播验证;发现关键冲突时返回 `blocked`,不得生成 `approved` 包。v2 仅只读;修改或重发必须使用 `scripts/migrate-contract-v3.mjs` 显式迁移为 `draft` 并重新批准。v1 字符串引用先用 `scripts/migrate-context-references.mjs` 迁移到 v2;歧义引用不得猜测。
34
+
35
+ ## 控制面引用
36
+
37
+ - 用户决定遵循 `docs/process/lifecycle/references/user-decisions.md`:决定必须绑定资产、版本、范围和证据;范围、风险、摘要或失效条件变化后重新确认。
38
+ - checkpoint 是唯一机器状态源,业务 Ticket 只保存追踪引用,不复制阶段状态。
39
+ - 每次批准、重发和消费者流转前执行 `context_reconciliation`,核验 `CONTEXT.md` 的全文与引用术语双摘要。
40
+ - 技术事实与第三方行为走 `yss-research` 的 `technical-evidence`;产品策略依据走 `strategy-evidence`;市场与竞品判断走 `competitive-intelligence`。
41
+ - 修改本 Skill 时只编辑 `.agents/skills` 权威副本,再由 `maintaining-skills` 生成投影和 `skills-lock.json`。
42
+ - 本 Skill 只能标记影响、提出待解决问题和选择消费者能力;不得预选后端架构族,不得进入技术设计或实现流程,也不得授权实现。
34
43
 
35
44
  ## 语义方向规则
36
45
 
@@ -56,7 +65,7 @@ Plan 的目标与退出条件由 `docs/process/lifecycle-registry.yaml` 持有
56
65
  - 不在业务方案设计阶段生成技术模型、生产代码或 API 契约。
57
66
  - 不把模板 Fixture 当作具体项目的业务事实。
58
67
 
59
- 详细合同、字段和验证规则见 `references/domain-strategy-contract.md`、`references/stage-decision-package-contract.md` 与 `references/validation-rules.md`。机器可读合同分别为 `references/domain-strategy.schema.json` 和 `references/stage-decision-package.schema.json`;对应验证器为 `scripts/validate-domain-strategy.mjs` 与 `scripts/validate-stage-decision-package.mjs`。
68
+ 详细合同、字段和验证规则见 `references/domain-strategy-contract.md`、`references/stage-decision-package-contract.md` 与 `references/validation-rules.md`。当前机器可读合同分别为 `references/domain-strategy-v3.schema.json` 和 `references/stage-decision-package-v3.schema.json`;v2 schema 仅用于历史读取。对应验证器为 `scripts/validate-domain-strategy.mjs` 与 `scripts/validate-stage-decision-package.mjs`。
60
69
 
61
70
  ## 战略交接快照包
62
71