create-yss-spec 3.3.7 → 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 (131) hide show
  1. package/README.md +12 -13
  2. package/package.json +1 -1
  3. package/template/.agents/skills/.strategic-design-skills-manifest.json +2 -2
  4. package/template/.agents/skills/implementation-repo-onboarding/SKILL.md +2 -2
  5. package/template/.agents/skills/yss-application/SKILL.md +1 -1
  6. package/template/.agents/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
  7. package/template/.agents/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
  8. package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
  9. package/template/.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
  10. package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
  11. package/template/.agents/skills/yss-prototype-stage/SKILL.md +10 -1
  12. package/template/.agents/skills/yss-technical-design/SKILL.md +2 -0
  13. package/template/.agents/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
  14. package/template/.agents/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
  15. package/template/.agents/skills/yss-technical-design/references/technical-design.schema.json +288 -10
  16. package/template/.agents/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
  17. package/template/.agents/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
  18. package/template/.agents/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
  19. package/template/.agents/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
  20. package/template/.agents/skills/yss-web-controller/SKILL.md +1 -1
  21. package/template/.agents/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
  22. package/template/.agents/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
  23. package/template/.codex/skills/implementation-repo-onboarding/SKILL.md +2 -2
  24. package/template/.codex/skills/yss-application/SKILL.md +1 -1
  25. package/template/.codex/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
  26. package/template/.codex/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
  27. package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
  28. package/template/.codex/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
  29. package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
  30. package/template/.codex/skills/yss-prototype-stage/SKILL.md +10 -1
  31. package/template/.codex/skills/yss-technical-design/SKILL.md +2 -0
  32. package/template/.codex/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
  33. package/template/.codex/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
  34. package/template/.codex/skills/yss-technical-design/references/technical-design.schema.json +288 -10
  35. package/template/.codex/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
  36. package/template/.codex/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
  37. package/template/.codex/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
  38. package/template/.codex/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
  39. package/template/.codex/skills/yss-web-controller/SKILL.md +1 -1
  40. package/template/.codex/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
  41. package/template/.codex/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
  42. package/template/.cursor/skills/implementation-repo-onboarding/SKILL.md +2 -2
  43. package/template/.cursor/skills/yss-application/SKILL.md +1 -1
  44. package/template/.cursor/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
  45. package/template/.cursor/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
  46. package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
  47. package/template/.cursor/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
  48. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
  49. package/template/.cursor/skills/yss-prototype-stage/SKILL.md +10 -1
  50. package/template/.cursor/skills/yss-technical-design/SKILL.md +2 -0
  51. package/template/.cursor/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
  52. package/template/.cursor/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
  53. package/template/.cursor/skills/yss-technical-design/references/technical-design.schema.json +288 -10
  54. package/template/.cursor/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
  55. package/template/.cursor/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
  56. package/template/.cursor/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
  57. package/template/.cursor/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
  58. package/template/.cursor/skills/yss-web-controller/SKILL.md +1 -1
  59. package/template/.cursor/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
  60. package/template/.cursor/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
  61. package/template/.pi/skills/implementation-repo-onboarding/SKILL.md +2 -2
  62. package/template/.pi/skills/yss-application/SKILL.md +1 -1
  63. package/template/.pi/skills/yss-application/references/profiles/existing-domain-driven-maven.md +5 -0
  64. package/template/.pi/skills/yss-application/references/profiles/existing-layered-mvc-maven.md +5 -0
  65. package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -2
  66. package/template/.pi/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +4 -0
  67. package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +6 -1
  68. package/template/.pi/skills/yss-prototype-stage/SKILL.md +10 -1
  69. package/template/.pi/skills/yss-technical-design/SKILL.md +2 -0
  70. package/template/.pi/skills/yss-technical-design/references/engineering-design.schema.json +224 -0
  71. package/template/.pi/skills/yss-technical-design/references/technical-design-common.schema.json +6 -0
  72. package/template/.pi/skills/yss-technical-design/references/technical-design.schema.json +288 -10
  73. package/template/.pi/skills/yss-technical-design/scripts/engineering-design.mjs +30 -0
  74. package/template/.pi/skills/yss-technical-design/scripts/generate-schema.mjs +2 -1
  75. package/template/.pi/skills/yss-technical-design/scripts/validate-technical-design.mjs +20 -8
  76. package/template/.pi/skills/yss-technical-design/tests/engineering-scenarios.mjs +12 -0
  77. package/template/.pi/skills/yss-web-controller/SKILL.md +1 -1
  78. package/template/.pi/skills/yss-web-controller/references/profiles/existing-domain-driven-maven.md +5 -0
  79. package/template/.pi/skills/yss-web-controller/references/profiles/existing-layered-mvc-maven.md +5 -0
  80. package/template/README.md +4 -6
  81. package/template/docs/agents/backend-architecture-profiles.md +4 -0
  82. package/template/docs/agents/yss-skill-registry.yaml +13 -0
  83. package/template/docs/design/templates/prototype-confirmation-template.md +2 -2
  84. package/template/docs/design/templates/prototype-review-checklist.md +2 -2
  85. package/template/docs/plan/templates/market-analysis-template.md +3 -3
  86. package/template/docs/plan/templates/user-pain-points-template.md +3 -3
  87. package/template/docs/process/delivery-preflight.md +72 -0
  88. package/template/docs/process/document-writing.md +5 -1
  89. package/template/docs/process/existing-backend-architecture.md +31 -0
  90. package/template/docs/process/existing-ui-baseline.md +40 -0
  91. package/template/docs/process/frontend-backend-delivery.md +19 -6
  92. package/template/docs/process/implementation-repo-integration.md +6 -0
  93. package/template/docs/process/schemas/backend-architecture-identity.schema.json +111 -14
  94. package/template/docs/process/schemas/delivery-preflight-input.schema.json +158 -0
  95. package/template/docs/process/schemas/delivery-preflight-result.schema.json +159 -0
  96. package/template/docs/process/schemas/digital-human-task-package.schema.json +1 -1
  97. package/template/docs/process/schemas/existing-ui-baseline.schema.json +365 -0
  98. package/template/docs/process/schemas/frontend-delivery-acceptance-v3.schema.json +308 -0
  99. package/template/docs/process/schemas/frontend-strategic-preflight-v2.schema.json +114 -0
  100. package/template/docs/process/schemas/strategic-design-handoff-v5.schema.json +932 -0
  101. package/template/docs/process/schemas/strategic-handoff-export-v2.schema.json +378 -0
  102. package/template/docs/process/strategic-handoff-package.md +21 -13
  103. 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
  104. package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +83 -67
  105. 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
  106. 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
  107. package/template/scripts/backend-delivery +20 -4
  108. package/template/scripts/lib/approved-execution-context.mjs +106 -0
  109. package/template/scripts/lib/backend-architecture.mjs +12 -0
  110. package/template/scripts/lib/backend-delivery.mjs +10 -6
  111. package/template/scripts/lib/delivery-preflight.mjs +238 -0
  112. package/template/scripts/lib/existing-backend-architecture.mjs +139 -0
  113. package/template/scripts/lib/existing-ui-baseline.mjs +84 -0
  114. package/template/scripts/lib/frontend-delivery.mjs +23 -15
  115. package/template/scripts/lib/implementation-contract-compiler.mjs +19 -5
  116. package/template/scripts/lib/json-schema.mjs +19 -4
  117. package/template/scripts/lib/profile-skill-sync.mjs +26 -0
  118. package/template/scripts/lib/strategic-handoff-consumption.mjs +6 -5
  119. package/template/scripts/lib/strategic-handoff-design-targets.mjs +2 -3
  120. package/template/scripts/lib/strategic-handoff-io.mjs +10 -0
  121. package/template/scripts/lib/strategic-handoff.mjs +47 -29
  122. package/template/scripts/lib/task-package.mjs +11 -3
  123. package/template/scripts/lib/technical-design-boundary.mjs +9 -1
  124. package/template/scripts/lib/ui-baseline.mjs +26 -0
  125. package/template/scripts/lib/user-decision.mjs +4 -1
  126. package/template/scripts/preflight-delivery +14 -0
  127. package/template/scripts/strategic-handoff +19 -4
  128. package/template/scripts/sync-profile-skills +4 -5
  129. package/template/skills-lock.json +15 -15
  130. package/template.snapshot.json +3 -3
  131. 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 { createApprovedExecutionContext, createApprovedRecompilationContext, assertApprovedExecutionContext } from './approved-execution-context.mjs';
1
2
  import { enforceTechnicalDesign } from './technical-design-boundary.mjs';
2
3
  import { enforceHarnessSkillScope } from "./harness-execution-scope.mjs";
3
4
  import { createHash } from "node:crypto";
@@ -6,7 +7,7 @@ import path from "node:path";
6
7
  import { parseDocument } from "../vendor/yaml.mjs";
7
8
  import { DEFAULT_REGISTRY, loadSkillRegistry } from "./skill-registry.mjs";
8
9
  import { ROOT } from "./skill-supply-chain.mjs";
9
- import { architectureDigest, assertArchitectureAgreement, validateArchitectureIdentity } from "./backend-architecture.mjs";
10
+ import { architectureDigest, assertArchitectureAgreement, validateArchitectureIdentity, verifyArchitectureEvidence } from "./backend-architecture.mjs";
10
11
  import { enforceFrontendDelivery } from "./frontend-delivery-boundary.mjs";
11
12
 
12
13
  export const DEFAULT_COMPILER_CONTRACT = path.join(
@@ -65,12 +66,14 @@ export function compileImplementationContract({
65
66
  compilerContractDigest,
66
67
  architecture_identity,
67
68
  architecture_evidence,
69
+ approved_slice,
68
70
  root = ROOT,
69
71
  slice_id,
70
72
  frontend_delivery,
71
73
  technical_design
72
74
  }) {
73
75
  assertV2(registry, compilerContract);
76
+ const recompilationExecution=approved_slice?createApprovedRecompilationContext(approved_slice,{root}):undefined;
74
77
  const deliveryInput = enforceFrontendDelivery({ slice_id, frontend_delivery }, { root });
75
78
  if (!Array.isArray(recipeIds) || !Array.isArray(requiredCapabilities) || !Array.isArray(conditions)) {
76
79
  fail("recipeIds、requiredCapabilities 与 conditions 必须是数组");
@@ -89,11 +92,11 @@ export function compileImplementationContract({
89
92
  if (architectural || architecture_identity) {
90
93
  architectureProfile = validateArchitectureIdentity(architecture_identity, registry);
91
94
  if (!architecture_evidence?.engineering_baseline || !architecture_evidence?.repository_registration || !architecture_evidence?.manifest) fail("缺少工程基线、仓库登记或 Manifest 架构证据");
92
- assertArchitectureAgreement(architecture_identity, architecture_evidence, registry);
95
+ verifyArchitectureEvidence(architecture_identity, architecture_evidence, { root, registry, execution:recompilationExecution });
93
96
  for (const recipe of orderedRecipes) if (recipe.architecture_family && recipe.architecture_family !== architecture_identity.architecture_family) fail(`Recipe ${recipe.id} 与架构族不匹配`);
94
97
  }
95
98
 
96
- enforceTechnicalDesign({ technical_design, conditions, architecture_identity, slice_id }, { root });
99
+ enforceTechnicalDesign({ technical_design, conditions, architecture_identity, slice_id }, { root, execution:recompilationExecution });
97
100
 
98
101
  const capabilitySources = new Map();
99
102
  const orderedCapabilities = [];
@@ -175,6 +178,7 @@ export function compileImplementationContract({
175
178
  ...(architecture_identity ? {
176
179
  architecture_identity: structuredClone(architecture_identity),
177
180
  architecture_identity_digest: architectureDigest(architecture_identity),
181
+ ...(architecture_identity.schema_version === 2 ? { architecture_evidence: structuredClone(architecture_evidence) } : {}),
178
182
  profile_maturity: architectureProfile.maturity,
179
183
  readiness_blockers: architectureProfile.maturity === "supported" ? [] : ["backend-profile-not-supported"],
180
184
  skill_profiles: Object.fromEntries(orderedSkills.map((skill) => [skill, architecture_identity.architecture_profile]))
@@ -202,12 +206,17 @@ export function compileDefaultImplementationContract(input = {}) {
202
206
  });
203
207
  }
204
208
 
205
- export function evaluateContractFreshness(contract, { registry, compilerContract, root = ROOT }) {
209
+ export function evaluateContractFreshness(contract, { registry, compilerContract, root = ROOT, approved_slice, readOnly = false }, execution) {
206
210
  assertV2(registry, compilerContract);
207
211
  if (contract?.schema_version !== 2) fail("Slice Implementation Contract schema v1 已停止支持;必须重新编译 v2 合同");
208
212
  const resolution = contract.resolution ?? contract;
209
213
  const reasons = [];
210
- try { enforceTechnicalDesign({ ...resolution, slice_id: contract.slice_id ?? resolution.slice_id }, { root }); }
214
+ let trustedExecution;
215
+ try {
216
+ trustedExecution=approved_slice?createApprovedExecutionContext(approved_slice,{root,contract}):execution;
217
+ if(trustedExecution)assertApprovedExecutionContext(trustedExecution,{root,contract});
218
+ } catch(error) { reasons.push(error.code||'EXECUTION_APPROVAL_INVALID');trustedExecution=undefined; }
219
+ try { enforceTechnicalDesign({ ...resolution, slice_id: contract.slice_id ?? resolution.slice_id }, { root,execution:trustedExecution,readOnly }); }
211
220
  catch { reasons.push("technical-design-stale-or-unavailable"); }
212
221
  try { enforceFrontendDelivery(contract, { root }); }
213
222
  catch { reasons.push("frontend-delivery-stale-or-unavailable"); }
@@ -216,6 +225,10 @@ export function evaluateContractFreshness(contract, { registry, compilerContract
216
225
  if (resolution.architecture_identity) {
217
226
  try { validateArchitectureIdentity(resolution.architecture_identity, registry); }
218
227
  catch { reasons.push("architecture-identity-invalid"); }
228
+ if (resolution.architecture_identity.schema_version === 2) {
229
+ try { verifyArchitectureEvidence(resolution.architecture_identity, resolution.architecture_evidence, { root, registry, execution:trustedExecution }); }
230
+ catch (error) { reasons.push(error.code ?? "architecture-evidence-stale-or-unavailable"); }
231
+ }
219
232
  if (resolution.architecture_identity_digest !== architectureDigest(resolution.architecture_identity)) reasons.push("architecture-identity-digest-changed");
220
233
  }
221
234
  return { freshness: reasons.length ? "stale" : "current", reasons };
@@ -225,6 +238,7 @@ export function validateExecutionResult(result, contract, current) {
225
238
  if (result?.schema_version !== 2) fail("YSS Skill Execution Result schema v1 已停止支持;请迁移到 v2");
226
239
  if (contract?.schema_version !== 2) fail("Slice Implementation Contract schema v1 已停止支持;必须重新编译 v2 合同");
227
240
  if (!EXECUTION_STATUSES.has(result.status)) fail(`未知 execution result status: ${result.status}`);
241
+ if(contract.resolution?.architecture_identity?.schema_version===2&&!current?.approved_slice)return {status:'stale',blockers:['EXECUTION_APPROVAL_REQUIRED']};
228
242
  const freshness = evaluateContractFreshness(contract, current);
229
243
  if (freshness.freshness === "stale") return { status: "stale", blockers: freshness.reasons };
230
244
  const consumed = result.consumed_contract ?? {};
@@ -9,7 +9,15 @@ import traceback
9
9
  from jsonschema import Draft202012Validator, FormatChecker
10
10
  validators = {}
11
11
  results = []
12
- for item in json.load(sys.stdin):
12
+ expected_bytes = int(sys.argv[1])
13
+ payload = sys.stdin.buffer.read(expected_bytes)
14
+ if len(payload) != expected_bytes:
15
+ raise ValueError(f"JSON_SCHEMA_PROTOCOL: truncated input frame: expected {expected_bytes} bytes, received {len(payload)}")
16
+ try:
17
+ jobs = json.loads(payload.decode('utf-8'))
18
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
19
+ raise ValueError(f"JSON_SCHEMA_PROTOCOL: invalid UTF-8 JSON input frame: {error}") from error
20
+ for item in jobs:
13
21
  try:
14
22
  key = (item['schemaPath'], item['formatChecker'])
15
23
  if key not in validators:
@@ -29,7 +37,8 @@ json.dump(results, sys.stdout)
29
37
  `;
30
38
 
31
39
  /** One immutable validation phase, never a cache of successful work units. */
32
- export function validateJsonSchemas(items, { cwd = process.cwd() } = {}) {
40
+ export function validateJsonSchemas(items, { cwd = process.cwd(), timeoutMs = 30000 } = {}) {
41
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new TypeError("schema timeoutMs 必须是正整数毫秒数");
33
42
  if (!Array.isArray(items)) throw new TypeError("schema batch 必须是数组");
34
43
  if (!items.length) return [];
35
44
  const jobs = items.map(({ value, schemaPath, formatChecker = true, errorStyle = "compact", label = "JSON Schema" }) => {
@@ -38,9 +47,15 @@ export function validateJsonSchemas(items, { cwd = process.cwd() } = {}) {
38
47
  if (!["compact", "verbose"].includes(errorStyle)) throw new TypeError(`未知 schema errorStyle: ${errorStyle}`);
39
48
  return { value, schemaPath: effectiveSchema, formatChecker, errorStyle };
40
49
  });
41
- const result = spawnSync("python3", ["-c", validator], { cwd, encoding: "utf8", input: JSON.stringify(jobs), maxBuffer: 16 * 1024 * 1024 });
50
+ // readall waits for EOF even after the complete JSON has arrived. A known UTF-8
51
+ // byte frame lets the validator finish when a transport keeps its write end open.
52
+ const input = Buffer.from(JSON.stringify(jobs), "utf8");
53
+ const result = spawnSync("python3", ["-c", validator, String(input.length)], { cwd, encoding: "utf8", input, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 });
54
+ if (result.error?.code === "ETIMEDOUT") throw new TypeError(`JSON_SCHEMA_TIMEOUT: schema validation exceeded ${timeoutMs}ms`);
42
55
  if (result.error || result.status !== 0) throw new TypeError(result.error?.message || `${result.stdout}${result.stderr}`.trim());
43
- const results = JSON.parse(result.stdout);
56
+ let results;
57
+ try { results = JSON.parse(result.stdout); } catch { throw new TypeError("JSON_SCHEMA_PROTOCOL: validator returned invalid JSON"); }
58
+ if (!Array.isArray(results) || results.some(item => !item || typeof item.valid !== "boolean" || typeof item.error !== "string")) throw new TypeError("JSON_SCHEMA_PROTOCOL: validator returned invalid result records");
44
59
  if (results.length !== jobs.length) throw new TypeError("schema batch 返回数量不匹配");
45
60
  return results;
46
61
  }
@@ -334,3 +334,29 @@ export function reportProfileSkillSync(plan) {
334
334
  for (const item of [...plan.changes, ...plan.issues]) counts[item.status] = (counts[item.status] ?? 0) + 1;
335
335
  return { ...plan, changes: plan.changes.map(({ content, ...item }) => item), counts };
336
336
  }
337
+
338
+ export function formatProfileSkillSync(report, mode = "--dry-run") {
339
+ const affected = [...new Set([...report.changes, ...report.issues].map(({ profile }) => profile))];
340
+ const lines = [
341
+ `profile skills: ${report.profiles.join(", ")}`,
342
+ `模式:${mode === "--apply" ? "已执行同步" : mode === "--check" ? "只读检查" : "只读预览"}`,
343
+ `受影响 profile:${affected.join(", ") || "无"}`,
344
+ ];
345
+ for (const profile of affected) {
346
+ const skills = [...new Set(report.changes.filter((item) => item.profile === profile && item.skill).map(({ skill }) => skill))];
347
+ lines.push(`${profile} 变更技能:${skills.join(", ") || "无(请查看资产差异或问题)"}`);
348
+ }
349
+ lines.push(`已有问题:${report.issues.length} 项(含冲突、缺失依赖与未登记差异)`);
350
+ for (const [status, count] of Object.entries(report.counts)) lines.push(`${status}: ${count}`);
351
+ for (const item of report.changes) lines.push(`${item.profile}: ${item.status}: ${item.path}`);
352
+ if (!affected.length) lines.push("本次未发现同步差异或问题;不代表项目验证已通过。");
353
+ else {
354
+ // Profile names come from configuration; quote them as literal shell arguments.
355
+ const profiles = affected.map((profile) => `--profile '${profile.replaceAll("'", "'\\''")}'`).join(" ");
356
+ lines.push(`后续同步一致性检查(待执行;有差异或冲突时先处理):scripts/sync-profile-skills --check ${profiles}`);
357
+ lines.push("本仓 Skill 治理验证(待执行):scripts/verify-skill-governance");
358
+ }
359
+ lines.push("文件差异、分类与 SHA-256 继续使用现有 --json 报告;本摘要不判断下游项目兼容性。");
360
+ const issues = report.issues.map((item) => [item.profile, item.status, item.skill, item.path, item.message, item.marker === undefined ? undefined : `marker=${item.marker}`].filter((value) => value !== undefined).join(": "));
361
+ return { stdout: `${lines.join("\n")}\n`, stderr: issues.length ? `${issues.join("\n")}\n` : "" };
362
+ }
@@ -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), '未知技术设计架构');
@@ -74,15 +75,15 @@ export async function verifyConsumption(data,{root=process.cwd(),sliceRef,consum
74
75
  }else {ensure(['pending','conflict'].includes(row.disposition),`非法承接状态: ${id}`);mark(id,row.disposition,row);}
75
76
  }
76
77
  for(const[id,row]of rows)if(!known.has(id))mark(id,'removed-or-unknown-source',row);
77
- 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'});}
78
79
  // Rule changes can be scoped to declared dependencies. Boundary/decision or
79
80
  // runnable-prototype changes need the tactical contract rebound as a whole.
80
81
  const stageKeys=['package_version','approval','domain_strategy_ref'];
81
- 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)) {
82
83
  all=true;issues.push({source_id:'strategic-design-basis',reason:'upstream-design-stale'});
83
84
  }
84
85
  const blockedHere=all||(sliceRef?blocked.has(sliceRef):issues.length>0);
85
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};
86
- });
87
- });
87
+ },{readOnly});
88
+ },{readOnly});
88
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('/')) {
@@ -1,21 +1,22 @@
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']]);
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
16
  const SOURCE_SCHEMAS = new Map([
17
17
  [3,['docs/process/schemas/strategic-handoff-domain-strategy.schema.json','docs/process/schemas/strategic-handoff-stage-decision-package.schema.json']],
18
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']],
19
20
  ]);
20
21
  const CAPABILITIES=['backend-technical-design','frontend-engineering-design','delivery-coordination'];
21
22
 
@@ -25,7 +26,7 @@ function versionSchema(version, schemas, label) {
25
26
  }
26
27
 
27
28
  function validateConsumerRoutes(handoff, stage) {
28
- if(handoff.schema_version!==4)return;
29
+ if(!hasConsumerRoutes(handoff))return;
29
30
  const routes=new Map();
30
31
  for(const route of handoff.consumer_routes||[]){
31
32
  ensure(!routes.has(route.capability),`消费者能力重复: ${route.capability}`);
@@ -128,7 +129,8 @@ export async function inspectSource(root, handoffRef) {
128
129
  schema(handoff,versionSchema(handoff?.schema_version,HANDOFF_SCHEMAS,'Strategic Handoff'));
129
130
  ensure(handoff.status==='approved', '交接包尚未批准');
130
131
  const config=handoff.package_export;
131
- 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 基线类型不一致');
132
134
  const source=parseContextSource(readFileSync(safe(root,'CONTEXT.md'),'utf8'));
133
135
  snapshot(handoff.source_context_snapshot,source); checkDelta(handoff,source);
134
136
  const strategy=read(safe(root,handoff.source.domain_strategy_ref.persisted_ref));
@@ -142,6 +144,7 @@ export async function inspectSource(root, handoffRef) {
142
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), '阶段包引用的战略内容不一致');
143
145
  ensure(!(stage.unresolved_items || []).some(x=>x.type==='blocker'), '阶段包含 blocker');
144
146
  validateConsumerRoutes(handoff,stage);
147
+ if(uiBaselineKind(handoff)==='existing-ui-baseline')ensure(stage.impact_assessment?.ui===false,'existing-ui-baseline 不支持 UI 影响,返回产品设计');
145
148
  const indexes=extractTraceability(strategy);
146
149
  const sourceRoles=read(safe(root,'docs/agents/digital-human-roles.yaml'));
147
150
  const roles=sourceApprovalPolicy(sourceRoles);
@@ -152,22 +155,24 @@ export async function inspectSource(root, handoffRef) {
152
155
  const currentPlan = (roles.gate_policy.dual_digital_human || []).some(rule => rule.gate === 'gate.plan-approved');
153
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';
154
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;
155
159
  const bindings={...handoff.source,handoff:{id:handoff.handoff_id,version:handoff.handoff_version,persisted_ref:handoffRef,status:'approved'}};
156
160
  for(const [key,ref] of Object.entries(bindings)) {
157
161
  const approval=config.approvals[key];
158
162
  ensure(approval,`缺少资产批准绑定: ${key}`);
159
163
  ensure(approval.gate_id===expectedGates[key],`资产使用错误批准门禁: ${key}`);
160
- 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);
161
165
  let actual;
162
- if(key==='visual_baseline_ref') {
163
- const baseline=read(safe(root,`${ref.persisted_ref}/${ref.manifest_ref}`));
164
- const result=await validateVisualBaseline(baseline,{bundleRoot:safe(root,ref.persisted_ref)});
165
- ensure(!result.errors.length,`视觉基线无效: ${result.errors.join('; ')}`);
166
- ensure(baseline.baseline_id===ref.baseline_id && baseline.version===ref.version && own([...ref.case_ids].sort(),baseline.cases.map(x=>x.case_id).sort()),'视觉基线身份不一致');
167
- actual=baseline.bundle.digest;
168
- } 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);
169
173
  if(key!=='handoff')ensure(ref.digest===actual,`源资产摘要过期: ${key}`);
170
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 为批准主体');
171
176
  await sourceApproval(record,roles,root);
172
177
  ensure(record.gate_id===approval.gate_id,`批准门禁不匹配: ${key}`);
173
178
  ensure((record.artifact_bindings || []).some(x=>x.id===(ref.id||ref.baseline_id) && x.version===ref.version && x.digest===actual),`批准记录未绑定当前资产: ${key}`);
@@ -180,19 +185,22 @@ export async function inspectSource(root, handoffRef) {
180
185
  ensure(scope.delivery_ref && own(read(safe(root,scope.delivery_ref)),delivery), '交付范围清单未覆盖当前交接内容或风险');
181
186
  ensure(scope.assets?.some(asset=>asset.ref===scope.delivery_ref && asset.digest===hash(readFileSync(safe(root,scope.delivery_ref)))), '交接内容快照缺少批准覆盖');
182
187
  for (const [key,ref] of Object.entries(handoff.source)) {
183
- const file=key==='visual_baseline_ref'?`${ref.persisted_ref}/${ref.manifest_ref}`:ref.persisted_ref;
188
+ const file=['visual_baseline_ref','existing_ui_baseline_ref'].includes(key)?`${ref.persisted_ref}/${ref.manifest_ref}`:ref.persisted_ref;
184
189
  const decisionBoundaries=key==='business_ticket_set_ref'?['gate.spec-baseline-approved',terminalGate]:[expectedGates[key]];
185
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}`);
186
191
  }
187
192
  }
188
- const receipt=previewFiles(root,config.prototype);
189
- ensure(handoff.source.visual_baseline_ref.case_ids.every(id=>receipt.case_ids.includes(id)), '离线验证未覆盖视觉基线 case');
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
+ }
190
197
  return {handoff,config,indexes,strategy,stage};
191
198
  }
192
199
 
193
200
  function collect(root, handoffRef, handoff, config) {
194
- 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];
195
- 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);
196
204
  const symbolic=config.reference_map || {};
197
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); };
198
206
  function refs(value,key='') {
@@ -208,20 +216,23 @@ function collect(root, handoffRef, handoff, config) {
208
216
  ensure(stat.isFile() && stat.size<=MAX_BYTES,`源文件类型/大小无效: ${ref}`);
209
217
  const bytes=readFileSync(full); collected.set(ref,bytes); totalBytes += bytes.length;
210
218
  ensure(collected.size<=20000 && totalBytes<=MAX_BYTES,'交接包大小超限');
211
- if(/\.(yaml|yml|json)$/.test(ref))refs(parse(bytes));
212
- 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));
213
224
  }
214
225
  return collected;
215
226
  }
216
227
  function materialize(captured, manifest, destination) {
217
228
  for(const file of manifest.files.filter(x=>x.original_ref))write(destination,file.original_ref,captured.get(file.path));
218
229
  }
219
- export async function openBundle(input, action) {
220
- 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-'));
221
232
  try {
222
233
  ensure(existsSync(input) && !lstatSync(input).isSymbolicLink(),'交接包不存在或是 symlink');
223
234
  let root=path.resolve(input);
224
- 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); }
225
236
  const manifest=read(safe(root,'manifest.json'));
226
237
  schema(manifest,'docs/process/schemas/strategic-handoff-package.schema.json');
227
238
  const {bundle_digest,...body}=manifest;ensure(bundle_digest===digest(body),'包清单摘要不一致');
@@ -233,7 +244,12 @@ export async function openBundle(input, action) {
233
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);}
234
245
  const originals=manifest.files.filter(x=>x.original_ref).map(x=>x.original_ref);
235
246
  ensure(new Set(originals.map(x=>x.toLowerCase())).size===originals.length,'源路径重复');
236
- 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()=>{
237
253
  const inspected=await inspectSource(source,manifest.handoff_ref);
238
254
  ensure(manifest.bundle_id===inspected.handoff.handoff_id && manifest.version===inspected.handoff.handoff_version,'包身份与源交接不一致');
239
255
  ensure(own(read(safe(root,'indexes/rules.json')),inspected.indexes.rules) && own(read(safe(root,'indexes/scenarios.json')),inspected.indexes.scenarios),'规则/场景索引与源资产不一致');
@@ -250,14 +266,16 @@ export async function openBundle(input, action) {
250
266
  }
251
267
  ensure(own(changes,{previous_bundle:manifest.previous_bundle,...compareIndexes(inspected.indexes,previousIndexes)}),'版本差异与实际索引不一致');
252
268
  return await action({root,manifest,...inspected,changes});
253
- } 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});}
254
272
  }
255
273
  export async function exportBundle({sourceRoot,handoffRef,output,zip=false,previous}) {
256
274
  const root=project(sourceRoot); parseContextContract({root}); relative(handoffRef);
257
275
  const current=await inspectSource(root,handoffRef);
258
276
  let prior=null;
259
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'))}));
260
- 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}`);}}
261
279
  const previousBundle=prior?{bundle_id:prior.manifest.bundle_id,version:prior.manifest.version,digest:prior.manifest.bundle_digest}:null;
262
280
  const snapshotFiles=collect(root,handoffRef,current.handoff,current.config);
263
281
  const target=path.resolve(output);
@@ -270,7 +288,7 @@ export async function exportBundle({sourceRoot,handoffRef,output,zip=false,previ
270
288
  const entries=[];
271
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}:{})});};
272
290
  for(const [ref,bytes]of [...snapshotFiles].sort(([a],[b])=>a.localeCompare(b))) {
273
- 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}`;
274
292
  ensure(!dest.endsWith('/CONTEXT.md'),'只允许源根词汇快照,禁止嵌套 CONTEXT.md');put(dest,bytes,ref);
275
293
  }
276
294
  put('indexes/rules.json',Buffer.from(json(current.indexes.rules)));
@@ -339,8 +357,8 @@ export async function importBundle({bundle,targetRoot}) {
339
357
  const coordinationRoute=b.handoff.consumer_routes.find(route=>route.capability==='delivery-coordination');
340
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}));
341
359
  if(capabilities.includes('frontend-engineering-design')&&frontendRoute.activation!=='not-applicable'){
342
- const preflight={schema_version: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:'',visual_baseline_ref:`${rel}/package/payload/files/${b.handoff.source.visual_baseline_ref.persisted_ref}/${b.handoff.source.visual_baseline_ref.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};
343
- schema(preflight,'docs/process/schemas/frontend-strategic-preflight.schema.json');
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');
344
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:[]}))}));
345
363
  }
346
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:[]}))}));
@@ -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
+ }
@@ -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;