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