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.
- package/README.md +12 -13
- package/package.json +1 -1
- package/template/.agents/skills/.strategic-design-skills-manifest.json +2 -2
- 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-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/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-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/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-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/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-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 +19 -6
- 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/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-v3.schema.json +308 -0
- package/template/docs/process/schemas/frontend-strategic-preflight-v2.schema.json +114 -0
- package/template/docs/process/schemas/strategic-design-handoff-v5.schema.json +932 -0
- package/template/docs/process/schemas/strategic-handoff-export-v2.schema.json +378 -0
- package/template/docs/process/strategic-handoff-package.md +21 -13
- 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/approved-execution-context.mjs +106 -0
- package/template/scripts/lib/backend-architecture.mjs +12 -0
- package/template/scripts/lib/backend-delivery.mjs +10 -6
- 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.mjs +23 -15
- 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-handoff-consumption.mjs +6 -5
- package/template/scripts/lib/strategic-handoff-design-targets.mjs +2 -3
- package/template/scripts/lib/strategic-handoff-io.mjs +10 -0
- package/template/scripts/lib/strategic-handoff.mjs +47 -29
- 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.mjs +4 -1
- package/template/scripts/preflight-delivery +14 -0
- package/template/scripts/strategic-handoff +19 -4
- package/template/scripts/sync-profile-skills +4 -5
- package/template/skills-lock.json +15 -15
- package/template.snapshot.json +3 -3
- 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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { assertExistingSliceStructure, createApprovedExecutionContext, verifySliceContractApproval } from './approved-execution-context.mjs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { ROOT, read, parse, safe, hash, digest, ensure, schema, sourceApprovalPolicy } from './strategic-handoff-io.mjs';
|
|
5
|
+
import { parseContextContract } from './context-contract.mjs';
|
|
6
|
+
import { validateApprovalRecord } from './approval-record.mjs';
|
|
7
|
+
import { loadSkillRegistry } from './skill-registry.mjs';
|
|
8
|
+
import * as architecture from './backend-architecture.mjs';
|
|
9
|
+
|
|
10
|
+
const STAGES=['prepare','build','export','accept'];
|
|
11
|
+
const LATER=stage=>STAGES.slice(STAGES.indexOf(stage));
|
|
12
|
+
const OWNERS={governance:'工程接入负责人',architecture:'工程基线与边界审查负责人',openapi:'接口契约负责人',technical_design:'技术设计负责人','slice_contract.execution':'生命周期编排与合同批准方',slice_contract:'生命周期编排与合同批准方',ui_baseline:'产品设计负责人',build_artifact:'后端构建负责人',build:'后端构建负责人',deployment:'后端部署与验证负责人',strategic_handoff:'战略交付方',backend_delivery:'后端交付方',local_authorization:'目标仓接收负责人'};
|
|
13
|
+
const RECOVERY={governance:'完成工程接入登记或正常初始化,读取有效 yss-project.yaml 和根 CONTEXT.md 后重跑预检',architecture:'按工程接入与 backend-architecture 合同补齐原始登记、工程基线、观测 manifest,重新边界审查并编译',openapi:'回到 OpenAPI Draft 审查与 Freeze,绑定当前字节及真实批准',technical_design:'按已登记架构更新技术设计,补齐输入与审查并重新批准','slice_contract.execution':'读取当前 Slice 字节及本地主控批准,恢复 implementation-scope 原始决定后重试',slice_contract:'重新编译 Slice Implementation Contract 并取得当前合同批准',ui_baseline:'由产品设计负责人确认现有 UI 基线;有 UI 改动则回到原型设计',build_artifact:'绑定实际 JAR 文件并与构建 artifact_digest 核对',build:'使用批准合同正常构建,并记录实际输出摘要与验证',deployment:'部署当前构建,采集同进程身份与真实验证证据',strategic_handoff:'修正战略来源与批准,重新导出当前交接资产',backend_delivery:'从正常后端交付入口重新生成与当前来源一致的交付记录',local_authorization:'由目标仓负责人确认当前生产方、项目、route 和 slice 范围'};
|
|
14
|
+
const ASSETS={openapi:'build',technical_design:'build',slice_contract:'build',ui_baseline:'export',build:'export',build_artifact:'export',deployment:'export',strategic_handoff:'export',backend_delivery:'accept',local_authorization:'accept'};
|
|
15
|
+
const errorStatus=error=>error.code==='EXECUTION_CONTRACT_INVALID'?'conflict':/stale|漂移|过期|摘要/.test(error.message)?'stale':/unsupported|未知|不支持|版本/.test(error.message)?'unsupported':/不可读|ENOENT|缺少/.test(error.message)?'missing':'conflict';
|
|
16
|
+
const docBinding=(root,binding)=>{
|
|
17
|
+
const bytes=readFileSync(safe(root,binding.ref));
|
|
18
|
+
ensure(hash(bytes)===binding.digest,`stale: 资产摘要漂移: ${binding.ref}`);
|
|
19
|
+
return parse(bytes);
|
|
20
|
+
};
|
|
21
|
+
const fileBinding=(root,binding)=>{ ensure(binding?.ref&&binding?.digest,'缺少证据 ref/digest');const bytes=readFileSync(safe(root,binding.ref));ensure(bytes.length>0&&hash(bytes)===binding.digest,`stale: 证据为空或摘要漂移: ${binding.ref}`);return bytes; };
|
|
22
|
+
const same=(a,b)=>digest(a)===digest(b);
|
|
23
|
+
|
|
24
|
+
export function invalidPreflightInput(message) {
|
|
25
|
+
return {schema_version:1,result:'invalid-input',exit_code:2,stage:null,checks:[{code:'input.protocol',status:'conflict',owner:'交付编排方',source:'delivery-preflight-input',evidence:[],blocking_stages:STAGES,recovery:'按 delivery-preflight-input.schema.json 修正输入协议',message}]};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Installed profile owns backend technical authoring; caller governance_root cannot grant it. */
|
|
29
|
+
export function backendPreflightCapability() {
|
|
30
|
+
const profileRef=path.join(ROOT,'docs/process/harness-profile.yaml');
|
|
31
|
+
const profile=existsSync(profileRef)?read(profileRef):null;
|
|
32
|
+
const profileId=profile?.profile_id||'template.full';
|
|
33
|
+
const ownsBackend=!profile||profileId==='harness.backend-delivery';
|
|
34
|
+
const required=['.agents/skills/yss-technical-design/scripts/validate-technical-design.mjs','.agents/skills/yss-technical-design/references/technical-design.schema.json','.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml'];
|
|
35
|
+
const missing=ownsBackend?required.filter(ref=>!existsSync(path.join(ROOT,ref))):[];
|
|
36
|
+
return {profile_id:profileId,owns_backend:ownsBackend,available:ownsBackend&&!missing.length,missing,
|
|
37
|
+
reason:ownsBackend?(missing.length?`后端验证运行时分发不完整: ${missing.join(', ')}`:'当前安装拥有后端技术设计与合同验证能力'):`${profileId} 不拥有后端技术设计与实现合同编译职责;由主模板或 backend profile 验证`};
|
|
38
|
+
}
|
|
39
|
+
function requireBackendCapability(key) {
|
|
40
|
+
const capability=backendPreflightCapability();
|
|
41
|
+
if(capability.available)return;
|
|
42
|
+
const error=new TypeError(`unsupported: ${capability.reason}`);
|
|
43
|
+
error.code=key==='technical_design'?'PREFLIGHT_TECHNICAL_DESIGN_UNSUPPORTED':'PREFLIGHT_SLICE_CONTRACT_UNSUPPORTED';
|
|
44
|
+
error.recovery=capability.owns_backend?'恢复当前后端 profile 的完整验证运行时分发,再从正常预检入口重跑':'将原始技术资产与批准绑定交给主模板或 backend 工程负责人核验;本 profile 通过已导出的受控交付包接收,不安装另一方创作技能';
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Read-only: never executes input commands, starts services, unpacks archives or writes receipts. */
|
|
49
|
+
export async function preflightDelivery(input,{stage,root=process.cwd()}={}) {
|
|
50
|
+
try { ensure(STAGES.includes(stage),'未知阶段;必须是 prepare|build|export|accept');schema(input,'docs/process/schemas/delivery-preflight-input.schema.json'); }
|
|
51
|
+
catch(error) { return invalidPreflightInput(error.message); }
|
|
52
|
+
root=path.resolve(root,input.governance_root||'.');
|
|
53
|
+
const deliveryKind=input.delivery_kind||'complete';
|
|
54
|
+
const checks=[], documents={}, valid=new Set(), assets=input.assets||{};
|
|
55
|
+
let execution;
|
|
56
|
+
const downstream=key=>deliveryKind==='strategic-handoff'&&(key.startsWith('architecture')||key.startsWith('slice_contract')||['technical_design','slice_contract','build','build_artifact','deployment','backend_delivery','local_authorization'].includes(key)||(key==='openapi'&&!input.assets?.openapi));
|
|
57
|
+
const add=(key,status,message,{code=key,from='prepare',evidence=[],source=key,recovery=RECOVERY[key]||RECOVERY.architecture}={})=>checks.push({code,status,owner:OWNERS[key]||OWNERS.architecture,source,evidence,blocking_stages:downstream(key)?[]:LATER(from),recovery,message});
|
|
58
|
+
const run=async(key,action,options={})=>{
|
|
59
|
+
try { await action();valid.add(key);add(key,'ready','当前原始资产与语义核验通过',options); }
|
|
60
|
+
catch(error) { add(key,errorStatus(error),error.message,{...options,...(error.recovery?{recovery:error.recovery}:{}),code:typeof error.code==='string'?error.code:options.code||key}); }
|
|
61
|
+
};
|
|
62
|
+
const pending=(key,deps,options)=>add(key,'pending',`尚不可判定,依赖未就绪: ${deps.join(', ')}`,options);
|
|
63
|
+
await run('governance',()=>{
|
|
64
|
+
const project=read(safe(root,'yss-project.yaml'));
|
|
65
|
+
ensure(project.schema_version===1&&project.repository_mode==='project-instance','交付预检只消费显式 project-instance;未知身份不得推测');
|
|
66
|
+
parseContextContract({root});
|
|
67
|
+
},{source:'yss-project.yaml / CONTEXT.md',evidence:['yss-project.yaml','CONTEXT.md']});
|
|
68
|
+
if(input.planned_initialization) add('governance','not-applicable','此为拟初始化目标的只读诊断;预检不初始化,缺失的当前阶段前提仍明确阻断',{code:'governance.planned-initialization'});
|
|
69
|
+
|
|
70
|
+
const bindings=input.architecture_bindings||{};
|
|
71
|
+
for(const key of ['repository_registration','engineering_baseline','manifest']) {
|
|
72
|
+
if(!bindings[key]) add('architecture','missing',`缺少原始架构来源: ${key}`,{code:`architecture.${key}`,source:key});
|
|
73
|
+
else await run(`architecture.${key}`,()=>{documents[key]=docBinding(root,bindings[key]);verifyArchitectureDocument(key,documents[key],input.scope,root);},{source:key,evidence:[bindings[key].ref]});
|
|
74
|
+
}
|
|
75
|
+
if(!input.architecture_identity) add('architecture','missing','缺少 architecture_identity;不能从目录或数据库字符串推断',{code:'architecture.identity'});
|
|
76
|
+
else await run('architecture.identity',()=>{
|
|
77
|
+
architecture.validateArchitectureIdentity(input.architecture_identity);
|
|
78
|
+
if(input.architecture_identity.source_kind==='existing-registration')ensure(input.architecture_identity.repository_id===input.scope.repository_id&&input.architecture_identity.project_id===input.scope.project_id,'架构身份与预检目标仓库/项目不一致');
|
|
79
|
+
},{source:'architecture_identity'});
|
|
80
|
+
if(valid.has('architecture.identity'))await run('architecture.profile',()=>{ensure(architecture.validateArchitectureIdentity(input.architecture_identity).maturity==='supported','不支持未达到 supported 的 architecture_profile');},{source:'architecture_profile'});
|
|
81
|
+
if(['build','export','accept'].includes(stage)&&assets.slice_contract)await run('slice_contract.execution',()=>{requireBackendCapability('slice_contract');execution=createApprovedExecutionContext(assets.slice_contract,{root});},{from:'build',source:assets.slice_contract.approval_ref||'slice_contract',evidence:[assets.slice_contract.ref,...(assets.slice_contract.approval_ref?[assets.slice_contract.approval_ref]:[])]});
|
|
82
|
+
const archDeps=['architecture.identity',...['repository_registration','engineering_baseline','manifest'].map(x=>`architecture.${x}`)];
|
|
83
|
+
if(archDeps.every(x=>valid.has(x)))await run('architecture',()=>{
|
|
84
|
+
ensure(typeof architecture.verifyArchitectureEvidence==='function','不支持既有工程原始证据验证,请同步支持该身份版本的模板运行时');
|
|
85
|
+
architecture.verifyArchitectureEvidence(input.architecture_identity,bindings,{root,registry:loadSkillRegistry(),execution});
|
|
86
|
+
},{source:'backend-architecture',evidence:Object.values(bindings).map(x=>x.ref)});
|
|
87
|
+
else pending('architecture',archDeps.filter(x=>!valid.has(x)),{source:'backend-architecture'});
|
|
88
|
+
|
|
89
|
+
// Load the delivery basis before checking deployment; dependency order must not hide drift.
|
|
90
|
+
if(assets.backend_delivery)try{documents.backend_delivery=docBinding(root,assets.backend_delivery);}catch{}
|
|
91
|
+
for(const [key,earliest] of Object.entries(ASSETS)) {
|
|
92
|
+
const from=key==='backend_delivery'&&deliveryKind==='backend-delivery'?'export':earliest;
|
|
93
|
+
if(deliveryKind==='backend-delivery'&&['ui_baseline','strategic_handoff'].includes(key)&&!assets[key])continue;
|
|
94
|
+
if(key==='local_authorization'&&documents.backend_delivery?.schema_version!==2) {
|
|
95
|
+
add(key,'not-applicable','当前未确认交付 v2;本地授权只在受控目标绑定协议 v2 生效',{from});continue;
|
|
96
|
+
}
|
|
97
|
+
if(key==='ui_baseline'&&input.scope.ui_mode==='not-applicable') {
|
|
98
|
+
if(input.scope.ui_not_applicable_reason?.trim())add(key,'not-applicable',input.scope.ui_not_applicable_reason,{from});
|
|
99
|
+
else add(key,'missing','无 UI 影响必须说明不适用原因',{from});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if(!assets[key]) {add(key,'missing',`缺少 ${key} 原始资产绑定${STAGES.indexOf(stage)<STAGES.indexOf(from)?`;到 ${from} 阶段前补齐`:''}`,{from});continue;}
|
|
103
|
+
await run(key,async()=>{
|
|
104
|
+
const binding=assets[key];
|
|
105
|
+
if(['technical_design','slice_contract'].includes(key))requireBackendCapability(key);
|
|
106
|
+
if(key==='build_artifact'){fileBinding(root,binding);ensure(documents.build?.artifact_digest===binding.digest,'stale: 实际制品与构建记录摘要不一致');return;}
|
|
107
|
+
const data=documents[key]=docBinding(root,binding);
|
|
108
|
+
await verifyAsset(key,data,binding,{root,input,documents,assets,execution});
|
|
109
|
+
},{from,source:assets[key].ref,evidence:[assets[key].ref,...(assets[key].approval_ref?[assets[key].approval_ref]:[])]});
|
|
110
|
+
}
|
|
111
|
+
if(deliveryKind==='backend-delivery')for(const key of ['ui_baseline','strategic_handoff'])if(!assets[key])add(key,valid.has('backend_delivery')?'ready':'pending',valid.has('backend_delivery')?'已通过交付绑定的战略包完成来源与 UI 基线只读复验':'尚不可判定,等待后端交付绑定的战略包核验',{code:`${key}.delivery-package`,from:'export',source:'backend_delivery.strategic_bundle_ref',evidence:assets.backend_delivery?[assets.backend_delivery.ref]:[]});
|
|
112
|
+
// A present but unreadable dependency must not turn cross-asset agreement into a success.
|
|
113
|
+
if(assets.slice_contract&&(!valid.has('technical_design')||!valid.has('architecture')))pending('slice_contract',['technical_design','architecture'].filter(x=>!valid.has(x)),{code:'slice_contract.dependencies',from:'build'});
|
|
114
|
+
if(assets.deployment&&!valid.has('backend_delivery'))pending('deployment',['backend_delivery'],{code:'deployment.delivery-basis',from:'export'});
|
|
115
|
+
const blocked=checks.some(x=>!['ready','not-applicable'].includes(x.status)&&x.blocking_stages.includes(stage));
|
|
116
|
+
return {schema_version:1,result:blocked?'blocked':'ready',exit_code:blocked?1:0,stage,checks};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function approved(root,binding,gates) {
|
|
120
|
+
ensure(binding.approval_ref&&binding.id&&binding.version,'缺少当前资产 id/version/approval_ref');
|
|
121
|
+
const record=read(safe(root,binding.approval_ref));
|
|
122
|
+
const roles=sourceApprovalPolicy(read(safe(root,'docs/agents/digital-human-roles.yaml')));
|
|
123
|
+
validateApprovalRecord(record,{rolesDoc:roles,requireApproved:true,root,read:ref=>readFileSync(safe(root,path.relative(root,ref).split(path.sep).join('/')))});
|
|
124
|
+
ensure(gates.includes(record.gate_id),'资产批准使用错误门禁');
|
|
125
|
+
ensure(record.artifact_bindings?.some(x=>x.id===binding.id&&x.version===binding.version&&x.digest===binding.digest),'stale: 批准未绑定当前资产身份、版本及字节');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function verifyAsset(key,data,binding,{root,input,documents,assets,execution}) {
|
|
129
|
+
ensure(data&&typeof data==='object'&&!Array.isArray(data),`${key} 必须是结构化资产`);
|
|
130
|
+
if(key==='openapi') {
|
|
131
|
+
ensure(/^3\.1\./.test(data.openapi)&&data.info?.title&&data.info?.version&&data.paths,'必须提供完整 OpenAPI 3.1 文档');
|
|
132
|
+
const operations=Object.values(data.paths).flatMap(item=>Object.entries(item).filter(([method])=>['get','put','post','delete','options','head','patch','trace'].includes(method)).map(([,operation])=>operation.operationId));
|
|
133
|
+
ensure(new Set(operations).size===operations.length&&operations.every(x=>typeof x==='string'&&x),'OpenAPI operationId 缺失或重复');
|
|
134
|
+
ensure(input.scope.operation_ids.every(id=>operations.includes(id)),'预检接口范围不在冻结 OpenAPI 中');
|
|
135
|
+
await approved(root,binding,['gate.engineering-contract-approved','gate.openapi-frozen','gate.openapi-freeze-confirmed']);
|
|
136
|
+
} else if(key==='technical_design') {
|
|
137
|
+
schema(data,'.agents/skills/yss-technical-design/references/technical-design.schema.json');
|
|
138
|
+
ensure(data.schema_version===2&&data.status==='approved','技术设计必须是已批准 v2 合同');
|
|
139
|
+
ensure(data.digest===digest(Object.fromEntries(Object.entries(data).filter(([k])=>k!=='digest'))),'stale: 技术设计自摘要不匹配');
|
|
140
|
+
ensure(data.architecture?.family===input.architecture_identity?.architecture_family,'技术设计与登记架构冲突');
|
|
141
|
+
for(const asset of data.inputs)fileBinding(root,asset);
|
|
142
|
+
const {validateTechnicalDesign}=await import('../../.agents/skills/yss-technical-design/scripts/validate-technical-design.mjs');
|
|
143
|
+
await validateTechnicalDesign(data,{root,sliceRef:input.scope.slice_id,readOnly:true,execution});
|
|
144
|
+
await approved(root,binding,['gate.engineering-contract-approved','gate.technical-design-approved']);
|
|
145
|
+
} else if(key==='slice_contract') {
|
|
146
|
+
const contract=data.slice_contract||data;
|
|
147
|
+
assertExistingSliceStructure(contract);
|
|
148
|
+
ensure(contract.schema_version===2&&contract.status==='approved'&&contract.slice_id===input.scope.slice_id,'Slice 合同版本、批准状态或切片不匹配');
|
|
149
|
+
ensure(binding.id===contract.contract_id&&binding.version===contract.contract_version,'Slice 合同身份与引用不一致');
|
|
150
|
+
const compiler=read(safe(ROOT,'.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml'));
|
|
151
|
+
for(const [section,fields]of Object.entries(compiler.slice_contract_required))for(const field of fields)ensure(Object.hasOwn(section==='root'?contract:contract[section]||{},field),`合同缺少 ${section}.${field}`);
|
|
152
|
+
ensure(contract.readiness.blockers.length===0&&contract.readiness.stale_inputs.length===0,'Slice 合同仍有 blocker 或 stale input');
|
|
153
|
+
const resolution=contract.resolution;
|
|
154
|
+
ensure(resolution.freshness==='current','Slice resolution 非 current');
|
|
155
|
+
ensure(resolution.registry_digest===digest(loadSkillRegistry()).slice(7)&&resolution.compiler_contract_digest===digest(compiler).slice(7),'stale: 编译事实源已变化,需重新编译');
|
|
156
|
+
ensure(same(resolution.architecture_identity,input.architecture_identity),'Slice 架构与登记不一致');
|
|
157
|
+
ensure(resolution.architecture_identity_digest===architecture.architectureDigest(input.architecture_identity),'stale: Slice 架构摘要漂移');
|
|
158
|
+
if(assets.technical_design)ensure(resolution.technical_design?.ref===assets.technical_design.ref&&resolution.technical_design?.digest===assets.technical_design.digest,'Slice 技术设计绑定不一致');
|
|
159
|
+
verifySliceContractApproval(binding,{root,contract});
|
|
160
|
+
} else if(key==='strategic_handoff') {
|
|
161
|
+
const {inspectSource}=await import('./strategic-handoff.mjs');
|
|
162
|
+
await inspectSource(root,binding.ref);
|
|
163
|
+
const {uiBaselineRef,uiBaselineKind}=await import('./ui-baseline.mjs');
|
|
164
|
+
const ui=uiBaselineRef(data);
|
|
165
|
+
ensure(input.scope.ui_mode!=='not-applicable','战略交接含 UI 基线,不能声明为无 UI 影响');
|
|
166
|
+
ensure(uiBaselineKind(data)===(input.scope.ui_mode==='prototype'?'prototype':'existing-ui-baseline'),'战略交接与预检 UI 来源类型不一致');
|
|
167
|
+
if(assets.ui_baseline)ensure(assets.ui_baseline.ref===`${ui.persisted_ref}/${ui.manifest_ref}`,'战略交接 UI 基线与预检输入不一致');
|
|
168
|
+
} else if(key==='ui_baseline') {
|
|
169
|
+
if(input.scope.ui_mode==='prototype') {
|
|
170
|
+
const {validateVisualBaseline}=await import('../../.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs');
|
|
171
|
+
const result=await validateVisualBaseline(data,{bundleRoot:path.dirname(safe(root,binding.ref))});
|
|
172
|
+
ensure(!result.errors.length,result.errors.join('; '));
|
|
173
|
+
} else {
|
|
174
|
+
const modulePath=path.join(ROOT,'scripts/lib/existing-ui-baseline.mjs');
|
|
175
|
+
ensure(existsSync(modulePath),'不支持 existing-ui-baseline 的当前运行时');
|
|
176
|
+
const module=await import('./existing-ui-baseline.mjs');
|
|
177
|
+
const verify=module.validateExistingUiBaseline||module.verifyExistingUiBaseline;
|
|
178
|
+
ensure(typeof verify==='function','不支持既有 UI 基线验证接口');
|
|
179
|
+
const result=await verify(data,{bundleRoot:path.dirname(safe(root,binding.ref))});
|
|
180
|
+
ensure(!result?.errors?.length&&result?.result!=='blocked',JSON.stringify(result));
|
|
181
|
+
}
|
|
182
|
+
ensure((input.scope.ui_mode==='prototype'?data.status==='approved':['ready-for-human','approved'].includes(data.status))&&data.baseline_id===binding.id&&data.version===binding.version,'UI 基线身份、版本或待确认状态不一致');
|
|
183
|
+
if(data.openapi&&assets.openapi)ensure(data.openapi.digest===assets.openapi.digest,'UI 基线与冻结 API 字节不一致');
|
|
184
|
+
await approved(root,binding,['gate.product-design-approved','gate.user-confirmation']);
|
|
185
|
+
} else if(key==='build') {
|
|
186
|
+
// Same build shape used by backend-delivery, captured independently before packaging.
|
|
187
|
+
const deliverySchema=read(safe(ROOT,'docs/process/schemas/backend-delivery.schema.json'));
|
|
188
|
+
const buildRules=deliverySchema.properties.build;
|
|
189
|
+
// Validate in memory to avoid writing a generated schema: check the published required fields.
|
|
190
|
+
for(const field of buildRules.required||[])ensure(Object.hasOwn(data,field),`构建缺少交付协议字段: ${field}`);
|
|
191
|
+
ensure(/^[a-f0-9]{40}$/.test(data.source_commit),'构建 source_commit 必须为固定 Git 提交');
|
|
192
|
+
if(documents.manifest?.source?.base_commit)ensure(documents.manifest.source.base_commit===data.source_commit,'构建与观测 manifest 的固定源码提交不一致');
|
|
193
|
+
ensure(/^sha256:[0-9a-f]{64}$/.test(data.artifact_digest),'构建制品摘要无效');
|
|
194
|
+
ensure(Object.keys(data).every(key=>Object.hasOwn(buildRules.properties,key)),'构建包含交付协议未定义字段');
|
|
195
|
+
} else if(key==='deployment') {
|
|
196
|
+
schema(data,'docs/process/schemas/backend-delivery-verification.schema.json');
|
|
197
|
+
ensure(data.kind==='backend-deployment','部署证据 kind 必须为 backend-deployment');
|
|
198
|
+
for(const result of data.results) {
|
|
199
|
+
ensure(result.exit_code===0&&Number.isFinite(Date.parse(result.executed_at))&&Date.parse(result.executed_at)<=Date.now()+60000,'部署执行结果或时间无效');
|
|
200
|
+
result.evidence.forEach(file=>fileBinding(root,file));
|
|
201
|
+
}
|
|
202
|
+
if(documents.backend_delivery) {
|
|
203
|
+
const {backendDeliveryBasis}=await import('./backend-delivery.mjs');
|
|
204
|
+
ensure(data.subject_digest===backendDeliveryBasis(documents.backend_delivery),'stale: 部署验证未绑定当前交付依据');
|
|
205
|
+
}
|
|
206
|
+
} else if(key==='backend_delivery') {
|
|
207
|
+
schema(data,`docs/process/schemas/backend-delivery${data.schema_version===2?'-v2':''}.schema.json`);
|
|
208
|
+
ensure(data.scope.slice_id===input.scope.slice_id&&input.scope.operation_ids.every(id=>data.scope.operation_ids.includes(id)),'后端交付切片/接口范围不匹配');
|
|
209
|
+
for(const asset of ['openapi','slice_contract'])if(assets[asset])ensure(data[asset].ref===assets[asset].ref&&data[asset].digest===assets[asset].digest,'后端交付引用与预检输入不一致');
|
|
210
|
+
for(const asset of [data.environment.test_data,...Object.values(data.verification)])fileBinding(root,asset);
|
|
211
|
+
if(assets.build)ensure(same(data.build,documents.build),'后端交付构建与预检构建不一致');
|
|
212
|
+
if(assets.deployment)ensure(data.verification.deployment.ref===assets.deployment.ref&&data.verification.deployment.digest===assets.deployment.digest,'后端交付部署验证不一致');
|
|
213
|
+
const {inspectBackendDelivery}=await import('./backend-delivery.mjs');
|
|
214
|
+
await inspectBackendDelivery(root,binding.ref,{readOnly:true});
|
|
215
|
+
} else if(key==='local_authorization') {
|
|
216
|
+
throw new TypeError('不支持尚未确认的目标授权协议;须先执行 S3a 并使用确认后的只读授权验证入口');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function verifyArchitectureDocument(key,data,scope,root) {
|
|
221
|
+
ensure(data&&typeof data==='object'&&!Array.isArray(data),'原始架构资产必须是对象');
|
|
222
|
+
architecture.validateArchitectureIdentity(data.architecture_identity);
|
|
223
|
+
if(data.architecture_identity.schema_version!==2)return;
|
|
224
|
+
ensure(data.repository_id===scope.repository_id&&data.project_id===scope.project_id,'原始来源与目标仓库/项目不一致');
|
|
225
|
+
if(key==='repository_registration') {
|
|
226
|
+
ensure(data.status==='current','stale: 工程登记不是 current');
|
|
227
|
+
for(const field of ['local_worktree','project_root','owner','repository_url'])ensure(typeof data[field]==='string'&&data[field].trim(),`工程登记缺少 ${field}`);
|
|
228
|
+
ensure(Array.isArray(data.verification_commands)&&data.verification_commands.length,'工程登记缺少可执行验证入口');
|
|
229
|
+
} else {
|
|
230
|
+
ensure(data.schema_version===1&&data.kind===(key==='manifest'?'existing-project-observation':'existing-engineering-baseline'),'既有原始资产来源类型/版本不匹配');
|
|
231
|
+
ensure(data.source&&/^[a-f0-9]{40}$/.test(data.source.base_commit)&&Array.isArray(data.source.files)&&data.source.files.length&&Array.isArray(data.source.roots)&&data.source.roots.length,'原始资产缺少固定源码提交/文件集/根范围');
|
|
232
|
+
ensure(Array.isArray(data.build_units)&&data.build_units.length,'原始资产缺少实际构建单元');
|
|
233
|
+
if(key==='engineering_baseline') {
|
|
234
|
+
ensure(data.status==='current','stale: 工程基线不是 current');
|
|
235
|
+
fileBinding(root,data.boundary_review);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {assertApprovedExecutionContext} from './approved-execution-context.mjs';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {readFileSync,realpathSync,lstatSync,readdirSync} from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {execFileSync} from 'node:child_process';
|
|
6
|
+
import {parseDocument} from '../vendor/yaml.mjs';
|
|
7
|
+
import {validateApprovalRecord} from './approval-record.mjs';
|
|
8
|
+
|
|
9
|
+
const sha=bytes=>`sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
10
|
+
const stable=x=>Array.isArray(x)?x.map(stable):x&&typeof x==='object'?Object.fromEntries(Object.keys(x).sort().map(k=>[k,stable(x[k])])):x;
|
|
11
|
+
export const existingArchitectureDigest=x=>sha(JSON.stringify(stable(x)));
|
|
12
|
+
const text=x=>typeof x==='string'&&x.trim().length>0;
|
|
13
|
+
const digest=x=>typeof x==='string'&&/^sha256:[a-f0-9]{64}$/.test(x);
|
|
14
|
+
function requireThat(ok,code,message){if(!ok){const e=new TypeError(`${code}: ${message}`);e.code=code;throw e;}}
|
|
15
|
+
function relative(ref){requireThat(text(ref)&&!path.isAbsolute(ref)&&!/[\\\x00-\x1f]/.test(ref)&&ref.split('/').every(x=>x&&x!=='.'&&x!=='..'),'ARCH_PATH','证据路径必须为安全相对路径');return ref;}
|
|
16
|
+
function file(root,ref){relative(ref);let current=realpathSync(root);for(const part of ref.split('/')){current=path.join(current,part);requireThat(!lstatSync(current).isSymbolicLink(),'ARCH_PATH',`不允许 symlink: ${ref}`);}requireThat(lstatSync(current).isFile(),'ARCH_PATH',`不是文件: ${ref}`);return current;}
|
|
17
|
+
function parse(bytes){const doc=parseDocument(String(bytes),{uniqueKeys:true,maxAliasCount:0});requireThat(!doc.errors.length,'ARCH_EVIDENCE_FORMAT',doc.errors[0]?.message);return doc.toJS({maxAliasCount:0});}
|
|
18
|
+
function bound(root,binding){requireThat(binding&&text(binding.ref)&&digest(binding.digest),'ARCH_EVIDENCE_REF','缺少原始证据 ref/digest');const bytes=readFileSync(file(root,binding.ref));requireThat(sha(bytes)===binding.digest,'ARCH_EVIDENCE_STALE',`原始证据已改变: ${binding.ref}`);return parse(bytes);}
|
|
19
|
+
function same(a,b,code,label){requireThat(existingArchitectureDigest(a)===existingArchitectureDigest(b),code,label);}
|
|
20
|
+
const FIELDS=['schema_version','source_kind','architecture_family','architecture_profile','repository_id','project_id','source_digest','build_units_digest'];
|
|
21
|
+
export function validateExistingArchitecture(identity,registry){
|
|
22
|
+
requireThat(identity?.schema_version===2&&identity.source_kind==='existing-registration','ARCH_SOURCE_UNSUPPORTED','仅支持 existing-registration v2');
|
|
23
|
+
requireThat(FIELDS.every(k=>identity[k]!==undefined)&&Object.keys(identity).every(k=>FIELDS.includes(k)),'ARCH_IDENTITY_SHAPE','既有身份字段缺失或混入生成事实');
|
|
24
|
+
const p=registry.existing_project_profiles?.[identity.architecture_profile];
|
|
25
|
+
requireThat(p&&p.source_kind==='existing-registration'&&p.build_system==='maven'&&p.architecture_family===identity.architecture_family,'ARCH_PROFILE_UNSUPPORTED','未支持的既有工程架构/Profile');
|
|
26
|
+
requireThat(['domain-driven','layered-mvc'].includes(identity.architecture_family)&&text(identity.repository_id)&&text(identity.project_id)&&digest(identity.source_digest)&&digest(identity.build_units_digest),'ARCH_IDENTITY_SHAPE','既有身份未绑定项目或源码/构建摘要');
|
|
27
|
+
return p;
|
|
28
|
+
}
|
|
29
|
+
const beneath=(ref,scope)=>scope==='.'||ref===scope||ref.startsWith(`${scope}/`);
|
|
30
|
+
const excluded=ref=>ref.split('/').some(x=>['.git','target','node_modules'].includes(x));
|
|
31
|
+
function listFiles(root,prefix=''){return readdirSync(path.join(root,prefix),{withFileTypes:true}).flatMap(entry=>{const ref=prefix?`${prefix}/${entry.name}`:entry.name;if(excluded(ref))return[];requireThat(!entry.isSymbolicLink(),'ARCH_PATH',`源码中出现 symlink: ${ref}`);return entry.isDirectory()?listFiles(root,ref):[ref];});}
|
|
32
|
+
function inspectMaven(root,units){
|
|
33
|
+
// Only parses XML; never invokes Maven or executes project-supplied commands.
|
|
34
|
+
const script=`import json,sys,xml.etree.ElementTree as ET\nout=[]\nfor p in json.load(sys.stdin):\n r=ET.parse(p).getroot(); ns={'m':'http://maven.apache.org/POM/4.0.0'}; q=lambda s:r.find(s,ns)\n aid=q('m:artifactId'); aid=aid if aid is not None else q('artifactId')\n deps=[e.text.strip() for e in r.findall('m:dependencies/m:dependency/m:artifactId',ns)+r.findall('m:profiles/m:profile/m:dependencies/m:dependency/m:artifactId',ns)+r.findall('dependencies/dependency/artifactId') if e.text]\n modules=[e.text.strip() for e in r.findall('.//m:modules/m:module',ns)+r.findall('.//modules/module') if e.text]\n out.append({'artifact_id':aid.text.strip() if aid is not None else '', 'dependencies':deps, 'modules':modules})\nprint(json.dumps(out))`;
|
|
35
|
+
try{return JSON.parse(execFileSync('python3',['-c',script],{input:JSON.stringify(units.map(u=>file(root,u.pom_ref))),encoding:'utf8',timeout:15000,maxBuffer:2*1024*1024}));}catch(error){requireThat(false,'ARCH_BUILD_INVALID',`无法读取实际 Maven 构建单元: ${error.message}`);}
|
|
36
|
+
}
|
|
37
|
+
export function verifyExistingArchitecture(identity,bindings,{root,registry,execution}={}){
|
|
38
|
+
const profile=validateExistingArchitecture(identity,registry);
|
|
39
|
+
requireThat(text(root),'ARCH_EVIDENCE_REF','需要治理 root');
|
|
40
|
+
const baseline=bound(root,bindings?.engineering_baseline),registration=bound(root,bindings?.repository_registration),manifest=bound(root,bindings?.manifest);
|
|
41
|
+
for(const [name,record] of Object.entries({baseline,registration,manifest})){
|
|
42
|
+
same(identity,record.architecture_identity,'ARCH_IDENTITY_CONFLICT',`${name} 架构身份与合同不一致`);
|
|
43
|
+
requireThat(record.repository_id===identity.repository_id&&record.project_id===identity.project_id,'ARCH_REPOSITORY_CONFLICT',`${name} 仓库/项目不一致`);
|
|
44
|
+
}
|
|
45
|
+
requireThat(manifest.kind==='existing-project-observation'&&manifest.schema_version===1,'ARCH_MANIFEST_KIND','需要既有工程观测 manifest,不能冒充脚手架');
|
|
46
|
+
requireThat(baseline.kind==='existing-engineering-baseline'&&baseline.schema_version===1,'ARCH_BASELINE_KIND','需要既有工程基线');
|
|
47
|
+
requireThat(registration.status==='current'&&baseline.status==='current','ARCH_EVIDENCE_STALE','登记/工程基线不是 current');
|
|
48
|
+
requireThat(text(registration.local_worktree)&&text(registration.project_root)&&text(registration.owner)&&text(registration.repository_url),'ARCH_REGISTRATION_MISSING','登记缺根路径、仓库来源或责任人');
|
|
49
|
+
for(const key of ['engineering_baseline','manifest'])same(registration.architecture_evidence?.[key],bindings[key],'ARCH_EVIDENCE_CONFLICT',`登记未引用当前${key}`);
|
|
50
|
+
const repositoryRoot=realpathSync(registration.local_worktree);
|
|
51
|
+
const projectRoot=registration.project_root==='.'?repositoryRoot:path.join(repositoryRoot,relative(registration.project_root));
|
|
52
|
+
requireThat(realpathSync(projectRoot)===projectRoot,'ARCH_PATH','工程根存在符号链接');
|
|
53
|
+
const git=(args)=>execFileSync('git',['-C',repositoryRoot,...args],{encoding:'utf8',timeout:15000,maxBuffer:32*1024*1024}).trim();
|
|
54
|
+
requireThat(git(['rev-parse','--show-toplevel'])===repositoryRoot,'ARCH_REPOSITORY_CONFLICT','登记根不是实际 Git 根');
|
|
55
|
+
const source=manifest.source;
|
|
56
|
+
requireThat(source&&/^[a-f0-9]{40}$/.test(source.base_commit)&&Array.isArray(source.files)&&source.files.length&&Array.isArray(source.roots)&&source.roots.length,'ARCH_SOURCE_MISSING','源码基线缺固定提交、文件清单或根范围');
|
|
57
|
+
requireThat(git(['rev-parse',`${source.base_commit}^{commit}`])===source.base_commit,'ARCH_SOURCE_STALE','固定基础提交不可读');
|
|
58
|
+
requireThat(git(['remote','get-url','origin'])===registration.repository_url,'ARCH_REPOSITORY_CONFLICT','实际 origin 与登记来源不一致');
|
|
59
|
+
requireThat(git(['rev-parse','HEAD'])===source.base_commit,'ARCH_SOURCE_STALE','工作树 HEAD 与批准固定输入不一致');
|
|
60
|
+
same(source,baseline.source,'ARCH_SOURCE_CONFLICT','工程基线与观测的源码范围不一致');
|
|
61
|
+
requireThat(existingArchitectureDigest(source)===identity.source_digest,'ARCH_SOURCE_STALE','固定源码基线摘要不一致');
|
|
62
|
+
for(const ref of source.roots)if(ref!=='.')relative(ref);
|
|
63
|
+
const sourcePaths=source.files.map(x=>x.path);requireThat(new Set(sourcePaths).size===sourcePaths.length,'ARCH_SOURCE_INVALID','源码路径重复');
|
|
64
|
+
const prefix=registration.project_root==='.'?'':`${registration.project_root}/`;
|
|
65
|
+
const tree=git(['ls-tree','-r',source.base_commit]).split('\n').filter(Boolean).map(line=>{const [meta,ref]=line.split('\t');return{ref,blob:meta.split(' ')[2],mode:meta.split(' ')[0]};}).filter(x=>x.ref.startsWith(prefix)).map(x=>({...x,path:x.ref.slice(prefix.length)})).filter(x=>!excluded(x.path)&&source.roots.some(scope=>beneath(x.path,scope)));
|
|
66
|
+
requireThat(tree.every(x=>['100644','100755'].includes(x.mode)),'ARCH_SOURCE_INVALID','源码范围包含 gitlink 或 symlink,需单独登记');
|
|
67
|
+
same(sourcePaths.slice().sort(),tree.map(x=>x.path).sort(),'ARCH_SOURCE_INCOMPLETE','源码清单未覆盖声明范围的固定提交文件');
|
|
68
|
+
const treeMap=new Map(tree.map(x=>[x.path,x.blob]));
|
|
69
|
+
const changes=[];
|
|
70
|
+
for(const item of source.files){
|
|
71
|
+
relative(item.path);requireThat(digest(item.sha256),'ARCH_SOURCE_INVALID','源码摘要格式无效');
|
|
72
|
+
let bytes;
|
|
73
|
+
try { bytes=readFileSync(file(projectRoot,item.path)); } catch(error) { if(error.code==='ENOENT'){changes.push(item.path);continue;} throw error; }
|
|
74
|
+
const actual=sha(bytes);const baseBlob=treeMap.get(item.path);
|
|
75
|
+
requireThat(item.base_blob===baseBlob,'ARCH_SOURCE_STALE',`基础 Git blob 不一致: ${item.path}`);
|
|
76
|
+
const currentBlob=createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
|
|
77
|
+
if(actual!==item.sha256){changes.push(item.path);continue;}
|
|
78
|
+
if(currentBlob!==baseBlob)requireThat(source.patches?.some(p=>p.path===item.path&&p.base_blob===baseBlob&&p.sha256===actual),'ARCH_SOURCE_UNDECLARED',`未登记基线补丁: ${item.path}`);
|
|
79
|
+
}
|
|
80
|
+
const observed=[...new Set(source.roots.flatMap(scope=>{ const entry=path.join(projectRoot,scope); try { const stat=lstatSync(entry); requireThat(!stat.isSymbolicLink(),'ARCH_PATH',`源码根出现 symlink: ${scope}`); return stat.isDirectory()?listFiles(projectRoot,scope==='.'?'':scope):[scope]; } catch(error) { if(error.code==='ENOENT')return [];throw error; } }))];
|
|
81
|
+
for(const ref of observed)if(!sourcePaths.includes(ref))changes.push(ref);
|
|
82
|
+
const units=manifest.build_units;
|
|
83
|
+
requireThat(Array.isArray(units)&&units.length&&units.every(u=>text(u.id)&&text(u.pom_ref)&&Array.isArray(u.roles)&&Array.isArray(u.depends_on)),'ARCH_BUILD_MISSING','缺少实际构建单元/角色/依赖');
|
|
84
|
+
same(units,baseline.build_units,'ARCH_BUILD_CONFLICT','三方构建单元映射不一致');
|
|
85
|
+
requireThat(existingArchitectureDigest(units)===identity.build_units_digest,'ARCH_BUILD_STALE','构建单元摘要不一致');
|
|
86
|
+
requireThat(new Set(units.map(u=>u.id)).size===units.length,'ARCH_BUILD_CONFLICT','构建单元重复');
|
|
87
|
+
const inspected=inspectMaven(projectRoot,units);const artifactMap=new Map(inspected.map((m,i)=>[m.artifact_id,units[i].id]));
|
|
88
|
+
for(const [i,u] of units.entries()){
|
|
89
|
+
requireThat(sourcePaths.includes(u.pom_ref),'ARCH_BUILD_CONFLICT',`POM未冻结: ${u.pom_ref}`);
|
|
90
|
+
requireThat(u.artifact_id===inspected[i].artifact_id,'ARCH_BUILD_CONFLICT',`artifactId不一致: ${u.id}`);
|
|
91
|
+
for(const module of inspected[i].modules){
|
|
92
|
+
requireThat(!module.includes('${'),'ARCH_BUILD_UNRESOLVED',`Maven module 路径尚未解析: ${u.id}`);
|
|
93
|
+
const child=path.posix.normalize(path.posix.join(path.posix.dirname(u.pom_ref),module,'pom.xml'));
|
|
94
|
+
relative(child); requireThat(units.some(unit=>unit.pom_ref===child),'ARCH_BUILD_INCOMPLETE',`实际 reactor 模块未登记: ${child}`);
|
|
95
|
+
}
|
|
96
|
+
const deps=[...new Set(inspected[i].dependencies.filter(a=>artifactMap.has(a)).map(a=>artifactMap.get(a)))].sort();
|
|
97
|
+
same([...u.depends_on].sort(),deps,'ARCH_BUILD_CONFLICT',`实际模块依赖不一致: ${u.id}`);
|
|
98
|
+
}
|
|
99
|
+
requireThat(Array.isArray(registration.allowed_write_paths)&®istration.allowed_write_paths.length,'ARCH_REGISTRATION_MISSING','登记缺少允许写范围');
|
|
100
|
+
for(const ref of registration.allowed_write_paths) if(ref!=='.')relative(ref);
|
|
101
|
+
requireThat(profile.required_roles.every(role=>units.some(u=>u.roles.includes(role))),'ARCH_BOUNDARY_MISSING','实际构建单元未覆盖架构责任');
|
|
102
|
+
requireThat(Array.isArray(baseline.boundary_scope)&&baseline.boundary_scope.length,'ARCH_BOUNDARY_MISSING','必须明确既有架构适用源码范围,不能推定整个混合仓同构');
|
|
103
|
+
baseline.boundary_scope.forEach(relative);
|
|
104
|
+
for(const unit of units)for(const role of unit.roles){
|
|
105
|
+
const refs=unit.role_paths?.[role];
|
|
106
|
+
requireThat(Array.isArray(refs)&&refs.length,'ARCH_BOUNDARY_MISSING',`缺少责任源码映射: ${unit.id}/${role}`);
|
|
107
|
+
for(const ref of refs){relative(ref);requireThat(sourcePaths.some(p=>beneath(p,ref))&&baseline.boundary_scope.some(scope=>beneath(ref,scope)),'ARCH_BOUNDARY_CONFLICT',`责任源码映射未在已冻结边界内: ${ref}`);}
|
|
108
|
+
}
|
|
109
|
+
requireThat(Array.isArray(baseline.verification_commands)&&baseline.verification_commands.length&&Array.isArray(registration.verification_commands),'ARCH_VERIFICATION_MISSING','缺少登记验证命令');
|
|
110
|
+
same(baseline.verification_commands,registration.verification_commands,'ARCH_VERIFICATION_CONFLICT','验证入口不一致');
|
|
111
|
+
const review=bound(root,baseline.boundary_review);
|
|
112
|
+
const rolesFile=path.join(root,'docs/agents/digital-human-roles.yaml');
|
|
113
|
+
const roles=parse(readFileSync(rolesFile));
|
|
114
|
+
validateApprovalRecord(review,{rolesDoc:roles,requireApproved:true,root});
|
|
115
|
+
requireThat(['check.architecture-reviewed','gate.technical-design-approved'].includes(review.gate_id)&&text(baseline.id)&&text(baseline.version),'ARCH_REVIEW_MISSING','需要架构边界审查');
|
|
116
|
+
const basis=existingArchitectureDigest(Object.fromEntries(Object.entries(baseline).filter(([k])=>k!=='boundary_review')));
|
|
117
|
+
requireThat(review.artifact_bindings?.some(x=>x.id===baseline.id&&x.version===baseline.version&&x.digest===basis),'ARCH_REVIEW_STALE','架构审查未绑定当前基线');
|
|
118
|
+
requireThat(text(review.principal_ref)&&review.principal_ref!==baseline.author,'ARCH_REVIEW_INVALID','起草者不能自审');
|
|
119
|
+
requireThat(text(baseline.author),'ARCH_REVIEW_INVALID','工程基线缺少起草者');
|
|
120
|
+
requireThat(Array.isArray(review.evidence_refs)&&review.evidence_refs.length,'ARCH_REVIEW_MISSING','缺少可读边界审查证据');
|
|
121
|
+
review.evidence_refs.forEach(ref=>file(root,ref));
|
|
122
|
+
requireThat(baseline.databases?.verification&&baseline.databases?.production,'ARCH_DATABASE_MISSING','需分别声明验证与生产数据库状态');
|
|
123
|
+
for(const [kind,record] of Object.entries(baseline.databases)){
|
|
124
|
+
requireThat(['declared','verified','unknown','not-applicable'].includes(record.status),'ARCH_DATABASE_INVALID',`${kind} 数据库状态无效`);
|
|
125
|
+
if(record.status==='verified'){
|
|
126
|
+
requireThat(text(record.engine)&&text(record.version)&&text(record.environment_id),'ARCH_DATABASE_INVALID','数据库验证未绑定环境和版本');
|
|
127
|
+
const evidence=bound(root,record.verification);requireThat(evidence.source_digest===identity.source_digest&&evidence.engine===record.engine&&evidence.version===record.version&&evidence.environment_id===record.environment_id,'ARCH_DATABASE_STALE','实库验证与声明/源码不一致');
|
|
128
|
+
requireThat(Array.isArray(evidence.results)&&evidence.results.length&&evidence.results.every(r=>text(r.command)&&r.exit_code===0&&Number.isFinite(Date.parse(r.executed_at))&&Array.isArray(r.evidence)&&r.evidence.length),'ARCH_DATABASE_UNVERIFIED','不能用数据库声明代替实际执行');
|
|
129
|
+
evidence.results.forEach(r=>r.evidence.forEach(b=>{requireThat(text(b?.ref)&&digest(b.digest),'ARCH_DATABASE_UNVERIFIED','缺少原始执行日志');requireThat(sha(readFileSync(file(root,b.ref)))===b.digest,'ARCH_DATABASE_STALE','实际执行日志摘要不一致');}));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if(changes.length){
|
|
133
|
+
requireThat(execution,'ARCH_SOURCE_STALE',`固定源码改变: ${changes.join(', ')}`);
|
|
134
|
+
const permission=assertApprovedExecutionContext(execution,{root,architectureIdentity:identity,architectureEvidence:bindings});
|
|
135
|
+
const allowed=permission.allowed_write_paths.map(x=>{if(path.isAbsolute(x)){const rel=path.relative(projectRoot,x).split(path.sep).join('/');return rel==='.'||rel===''?'.':relative(rel);}return x==='.'?'.':relative(x);});
|
|
136
|
+
requireThat(changes.every(ref=>allowed.some(scope=>beneath(ref,scope))&®istration.allowed_write_paths.some(scope=>beneath(ref,scope))),'ARCH_SOURCE_OUT_OF_SCOPE','输出增量超出批准写范围');
|
|
137
|
+
}
|
|
138
|
+
return {source_kind:'existing-registration',bindings:structuredClone(bindings),project_root:projectRoot,source_digest:identity.source_digest,build_units_digest:identity.build_units_digest,changed_files:[...new Set(changes)].sort(),profile_maturity:profile.maturity};
|
|
139
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { ensure, hash, digest, read, safe, files, schema, treeDigest } from './strategic-handoff-io.mjs';
|
|
3
|
+
|
|
4
|
+
// This is an observation bundle, not a prototype or an approval generator.
|
|
5
|
+
export function validateExistingUiBaseline(data, { bundleRoot } = {}) {
|
|
6
|
+
const errors=[];
|
|
7
|
+
try {
|
|
8
|
+
schema(data,'docs/process/schemas/existing-ui-baseline.schema.json');
|
|
9
|
+
ensure(bundleRoot,'existing-ui-baseline 需要原始 bundleRoot');
|
|
10
|
+
const consumed=new Set(['existing-ui-baseline.json']);
|
|
11
|
+
const allFiles=files(bundleRoot);
|
|
12
|
+
ensure(allFiles.length<=20000&&allFiles.reduce((total,ref)=>total+statSync(safe(bundleRoot,ref)).size,0)<=100*1024*1024,'existing-ui-baseline 文件数量或包大小超限');
|
|
13
|
+
const bound=binding=>{
|
|
14
|
+
consumed.add(binding.ref);
|
|
15
|
+
const bytes=readFileSync(safe(bundleRoot,binding.ref));
|
|
16
|
+
ensure(bytes.length>0&&hash(bytes)===binding.digest,`existing-ui-baseline 原始证据摘要不一致: ${binding.ref}`);
|
|
17
|
+
return bytes;
|
|
18
|
+
};
|
|
19
|
+
const document=binding=>{bound(binding);return read(safe(bundleRoot,binding.ref));};
|
|
20
|
+
const source=data.source, manifest=document(source.manifest);
|
|
21
|
+
ensure(source.digest===treeDigest(bundleRoot,source.root_ref),'existing-ui-baseline 固定源码摘要不一致');
|
|
22
|
+
bound(source.lock);
|
|
23
|
+
ensure(source.lock.ref.startsWith(`${source.root_ref}/`),'existing-ui-baseline 锁文件必须属于固定源码');
|
|
24
|
+
ensure(manifest.schema_version===1&&manifest.kind==='existing-ui-source-observation','existing-ui-baseline 源码观测 manifest 类型无效');
|
|
25
|
+
for(const key of ['repository_id','project_id','source_commit'])ensure(manifest[key]===source[key],`existing-ui-baseline 源码身份不一致: ${key}`);
|
|
26
|
+
ensure(manifest.source_digest===source.digest&&Array.isArray(manifest.files),'existing-ui-baseline 源码观测摘要不一致');
|
|
27
|
+
files(bundleRoot,source.root_ref).forEach(ref=>consumed.add(ref));
|
|
28
|
+
const actual=files(bundleRoot,source.root_ref).map(ref=>({path:ref.slice(source.root_ref.length+1),digest:hash(readFileSync(safe(bundleRoot,ref)))}));
|
|
29
|
+
ensure(digest([...manifest.files].sort((a,b)=>a.path.localeCompare(b.path)))===digest(actual.sort((a,b)=>a.path.localeCompare(b.path))),'existing-ui-baseline 观测文件集与实际源码不一致');
|
|
30
|
+
let routePrefix='';
|
|
31
|
+
if(data.api_route_mapping) {
|
|
32
|
+
const mapping=data.api_route_mapping;
|
|
33
|
+
for(const binding of [mapping.config,mapping.environment])ensure(binding.ref.startsWith(`${source.root_ref}/`)&&actual.some(file=>`${source.root_ref}/${file.path}`===binding.ref),'existing-ui-baseline 代理映射必须绑定固定源码');
|
|
34
|
+
const config=bound(mapping.config).toString('utf8'),environment=bound(mapping.environment).toString('utf8');
|
|
35
|
+
// A deliberately narrow existing Vite convention. Unrecognized rewrites need review;
|
|
36
|
+
// never execute a package's JavaScript to obtain a routing assertion.
|
|
37
|
+
ensure(/^\s*const apiBase = env\.VITE_API_BASE_URL \|\| ['"]\/api['"]\s*;?\s*$/m.test(config)
|
|
38
|
+
&& /proxy:\s*\{\s*\[apiBase\]:\s*\{\s*target:\s*proxyTarget,\s*changeOrigin:\s*true,\s*rewrite:\s*\(path\)\s*=>\s*path\.replace\(new RegExp\(`\^\$\{apiBase\}`\),\s*''\)\s*\}/.test(config),'existing-ui-baseline 不支持或缺少固定 Vite prefix rewrite');
|
|
39
|
+
const values=environment.split(/\r?\n/).filter(line=>/^\s*VITE_API_BASE_URL\s*=/.test(line));
|
|
40
|
+
ensure(values.length===1&&values[0].split('=').slice(1).join('=').trim()===mapping.prefix,'existing-ui-baseline 代理 prefix 与原始环境配置不一致');
|
|
41
|
+
routePrefix=mapping.prefix;
|
|
42
|
+
}
|
|
43
|
+
const build=document(data.build),capture=document(data.capture);
|
|
44
|
+
const openapi=document(data.openapi);bound(data.replay);
|
|
45
|
+
ensure(/^3\.1\.[0-9]+$/.test(openapi.openapi)&&openapi.paths,'existing-ui-baseline 需要 OpenAPI 3.1 原始接口');
|
|
46
|
+
for(const [label,record] of [['build',build],['capture',capture]]) {
|
|
47
|
+
ensure(record.schema_version===1&&record.source_commit===source.source_commit&&record.source_digest===source.digest,`existing-ui-baseline ${label} 未绑定当前源码`);
|
|
48
|
+
ensure(record.exit_code===0&&typeof record.command==='string'&&record.command.trim()&&Number.isFinite(Date.parse(record.executed_at)),`existing-ui-baseline ${label} 缺少成功执行记录`);
|
|
49
|
+
ensure(Array.isArray(record.evidence)&&record.evidence.length,`existing-ui-baseline ${label} 缺少原始日志`);record.evidence.forEach(bound);
|
|
50
|
+
}
|
|
51
|
+
ensure(build.lock_digest===source.lock.digest,'existing-ui-baseline 构建锁文件不一致');
|
|
52
|
+
ensure(build.output?.ref&&build.output.digest===treeDigest(bundleRoot,build.output.ref),'existing-ui-baseline 构建输出不一致');
|
|
53
|
+
files(bundleRoot,build.output.ref).forEach(ref=>consumed.add(ref));
|
|
54
|
+
ensure(capture.build_digest===data.build.digest&&capture.openapi_digest===data.openapi.digest,'existing-ui-baseline 截图运行与构建/API 不一致');
|
|
55
|
+
ensure(capture.ui_change==='none'&&Array.isArray(capture.cases),'existing-ui-baseline 不支持 UI 改动');
|
|
56
|
+
const ids=new Set();
|
|
57
|
+
for(const item of data.cases) {
|
|
58
|
+
ensure(!ids.has(item.case_id),`existing-ui-baseline case_id 重复: ${item.case_id}`);ids.add(item.case_id);
|
|
59
|
+
for(const ref of item.source_refs)ensure(ref.startsWith(`${source.root_ref}/`)&&actual.some(file=>`${source.root_ref}/${file.path}`===ref),`existing-ui-baseline 用例源码悬空: ${ref}`);
|
|
60
|
+
bound(item.actions);const api=document(item.api),image=bound(item.image);
|
|
61
|
+
ensure(image.length<=5*1024*1024,'existing-ui-baseline 单张截图超过 5 MiB');
|
|
62
|
+
ensure(image.length>=24&&image.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10]))&&image.toString('ascii',12,16)==='IHDR','existing-ui-baseline 截图不是 PNG');
|
|
63
|
+
ensure(image.readUInt32BE(16)===item.viewport.width&&image.readUInt32BE(20)===item.viewport.height,'existing-ui-baseline 截图与 viewport 不一致');
|
|
64
|
+
ensure(api.schema_version===1&&api.case_id===item.case_id&&api.openapi_digest===data.openapi.digest&&api.source_digest===source.digest&&api.build_digest===data.build.digest,'existing-ui-baseline API 证据来源不一致');
|
|
65
|
+
ensure(Array.isArray(api.exchanges)&&api.exchanges.length,'existing-ui-baseline 缺少真实 API 请求/响应');
|
|
66
|
+
for(const exchange of api.exchanges) {
|
|
67
|
+
ensure(typeof exchange.operation_id==='string'&&exchange.operation_id&&typeof exchange.method==='string'&&/^https?:\/\//.test(exchange.url)&&Number.isFinite(Date.parse(exchange.executed_at))&&Number.isInteger(exchange.status)&&exchange.status>=100&&exchange.status<=599,'existing-ui-baseline API 交换记录无效');
|
|
68
|
+
const operation=Object.entries(openapi.paths).flatMap(([route,item])=>Object.entries(item).filter(([method,value])=>value?.operationId===exchange.operation_id&&method.toUpperCase()===exchange.method.toUpperCase()).map(()=>route));
|
|
69
|
+
ensure(operation.length===1,'existing-ui-baseline API operation 与冻结接口不一致');
|
|
70
|
+
const routePattern=new RegExp(`^${operation[0].split('/').map(part=>/^\{[^}]+\}$/.test(part)?'[^/]+':part.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('/')}$`);
|
|
71
|
+
const observedPath=new URL(exchange.url).pathname;
|
|
72
|
+
ensure(!routePrefix||observedPath.startsWith(`${routePrefix}/`),'existing-ui-baseline API URL 与代理 prefix 不一致');
|
|
73
|
+
ensure(routePattern.test(routePrefix?observedPath.slice(routePrefix.length):observedPath),'existing-ui-baseline API URL 与冻结接口路径不一致');
|
|
74
|
+
bound(exchange.request);bound(exchange.response);
|
|
75
|
+
}
|
|
76
|
+
const observed=capture.cases.filter(entry=>entry.case_id===item.case_id);
|
|
77
|
+
ensure(observed.length===1,'existing-ui-baseline 缺少或重复 capture case');
|
|
78
|
+
for(const key of ['route','state','viewport','source_refs','actions','image','api'])ensure(digest(observed[0][key])===digest(item[key]),`existing-ui-baseline 截图动作绑定不一致: ${item.case_id}/${key}`);
|
|
79
|
+
}
|
|
80
|
+
ensure(capture.cases.length===ids.size,'existing-ui-baseline capture 含未登记用例');
|
|
81
|
+
ensure(allFiles.every(ref=>consumed.has(ref)),'existing-ui-baseline 包含未绑定原始文件');
|
|
82
|
+
} catch(error) { errors.push(error.message); }
|
|
83
|
+
return {errors};
|
|
84
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { uiBaselineRef, uiBaselineCaseIds, uiBaselineKind, hasConsumerRoutes } from './ui-baseline.mjs';
|
|
1
2
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
@@ -6,17 +7,18 @@ import { openBundle } from './strategic-handoff.mjs';
|
|
|
6
7
|
import { verifyConsumption } from './strategic-handoff-consumption.mjs';
|
|
7
8
|
import { read, safe, ensure, hash, schema, project, ROOT } from './strategic-handoff-io.mjs';
|
|
8
9
|
|
|
9
|
-
const ACCEPTANCE_SCHEMAS=new Map([[1,'docs/process/schemas/frontend-delivery-acceptance-v1.schema.json'],[2,'docs/process/schemas/frontend-delivery-acceptance.schema.json']]);
|
|
10
|
+
const ACCEPTANCE_SCHEMAS=new Map([[1,'docs/process/schemas/frontend-delivery-acceptance-v1.schema.json'],[2,'docs/process/schemas/frontend-delivery-acceptance.schema.json'],[3,'docs/process/schemas/frontend-delivery-acceptance-v3.schema.json']]);
|
|
10
11
|
|
|
11
12
|
function acceptanceSchema(version) {
|
|
12
|
-
ensure(ACCEPTANCE_SCHEMAS.has(version),`Frontend Delivery Acceptance 未知 schema_version: ${String(version)};支持版本: 1, 2
|
|
13
|
+
ensure(ACCEPTANCE_SCHEMAS.has(version),`Frontend Delivery Acceptance 未知 schema_version: ${String(version)};支持版本: 1, 2, 3;既有 UI 接收请使用 3`);
|
|
13
14
|
return ACCEPTANCE_SCHEMAS.get(version);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export async function verifyFrontendStrategicPreflight({root=process.cwd(),preflightRef,expectedDigest}={}) {
|
|
17
|
+
export async function verifyFrontendStrategicPreflight({root=process.cwd(),preflightRef,expectedDigest,readOnly=false}={}) {
|
|
17
18
|
project(root);
|
|
18
19
|
const file=safe(root,preflightRef),preflight=read(file);
|
|
19
|
-
|
|
20
|
+
ensure([1,2].includes(preflight.schema_version),'Frontend Strategic Preflight 未知 schema_version');
|
|
21
|
+
schema(preflight,preflight.schema_version===2?'docs/process/schemas/frontend-strategic-preflight-v2.schema.json':'docs/process/schemas/frontend-strategic-preflight.schema.json');
|
|
20
22
|
const preflightDigest=hash(readFileSync(file));
|
|
21
23
|
if(expectedDigest)ensure(expectedDigest===preflightDigest,'前端战略预检摘要已变化,需重编译合同');
|
|
22
24
|
ensure(preflight.status==='verified','前端战略预检尚未 verified');
|
|
@@ -30,19 +32,21 @@ export async function verifyFrontendStrategicPreflight({root=process.cwd(),prefl
|
|
|
30
32
|
const reconciliation=spawnSync(process.execPath,[path.join(ROOT,'scripts/verify-context-reconciliation'),'--root',root,safe(root,preflight.context_reconciliation_ref)],{encoding:'utf8'});
|
|
31
33
|
ensure(reconciliation.status===0,`前端战略预检 Context Reconciliation 未通过: ${reconciliation.error?.message||reconciliation.stderr}`);
|
|
32
34
|
return openBundle(safe(root,receipt.package_ref),bundle=>{
|
|
33
|
-
ensure(bundle.handoff
|
|
35
|
+
ensure(hasConsumerRoutes(bundle.handoff)&&bundle.manifest.bundle_digest===receipt.bundle_digest,'前端战略预检必须绑定支持消费者路由的当前交接包');
|
|
34
36
|
const frontendRoute=bundle.handoff.consumer_routes.find(item=>item.route_id===preflight.route_id&&item.capability==='frontend-engineering-design');
|
|
35
37
|
const backendRoute=bundle.handoff.consumer_routes.find(item=>item.capability==='backend-technical-design');
|
|
36
38
|
ensure(frontendRoute&&frontendRoute.activation!=='not-applicable','前端路由未启用');
|
|
37
39
|
const known=[...bundle.indexes.rules.map(item=>item.rule_id),...bundle.indexes.scenarios.filter(item=>item.critical).map(item=>item.scenario_id)].sort();
|
|
38
40
|
ensure(JSON.stringify([...preflight.source_rule_refs].sort())===JSON.stringify(known),'前端战略预检未完整绑定源规则/关键场景');
|
|
39
|
-
|
|
40
|
-
ensure(preflight.
|
|
41
|
+
ensure((preflight.schema_version===2)===(bundle.handoff.schema_version===5),'前端预检与 Handoff 协议版本不匹配,禁止降级');
|
|
42
|
+
if(preflight.schema_version===2)ensure(preflight.ui_baseline_kind===uiBaselineKind(bundle.handoff),'前端预检基线类型不一致');
|
|
43
|
+
const expectedVisual=`${base}/package/payload/files/${uiBaselineRef(bundle.handoff).persisted_ref}/${uiBaselineRef(bundle.handoff).manifest_ref}`;
|
|
44
|
+
ensure((preflight.schema_version===2?preflight.ui_baseline_ref:preflight.visual_baseline_ref)===expectedVisual,'前端战略预检 Visual Baseline 引用不一致');
|
|
41
45
|
const expectedMode=backendRoute.activation==='not-applicable'?'not-applicable':'required';
|
|
42
46
|
ensure(preflight.backend_dependency.mode===expectedMode&&preflight.backend_dependency.route_id===backendRoute.route_id,'前端战略预检后端依赖与消费者路由不一致');
|
|
43
47
|
if(expectedMode==='not-applicable')ensure(preflight.backend_dependency.reason===backendRoute.reason&&JSON.stringify(preflight.backend_dependency.impact_refs)===JSON.stringify(backendRoute.impact_refs)&&JSON.stringify(preflight.backend_dependency.evidence_refs)===JSON.stringify(backendRoute.evidence_refs),'backend-not-applicable 依据与战略路由不一致');
|
|
44
|
-
return {result:'preflight-verified',ready_for_agent:false,preflight_ref:preflightRef,preflight_digest:preflightDigest,strategic_bundle_digest:receipt.bundle_digest,backend_dependency:expectedMode,next_action:'prepare-frontend-engineering-design-draft'};
|
|
45
|
-
});
|
|
48
|
+
return {result:'preflight-verified',ready_for_agent:false,preflight_ref:preflightRef,preflight_digest:preflightDigest,strategic_bundle_digest:receipt.bundle_digest,ui_baseline_kind:uiBaselineKind(bundle.handoff),preflight_schema_version:preflight.schema_version,backend_dependency:expectedMode,next_action:'prepare-frontend-engineering-design-draft'};
|
|
49
|
+
},{readOnly});
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
function pointer(value, ref) {
|
|
@@ -79,8 +83,10 @@ export async function verifyFrontendDelivery({root=process.cwd(),acceptanceRef,s
|
|
|
79
83
|
const acceptanceDigest=hash(readFileSync(file));
|
|
80
84
|
if(expectedDigest)ensure(expectedDigest===acceptanceDigest,'前端接收记录摘要已变化,需重编译合同');
|
|
81
85
|
ensure(sliceRef&&acceptance.slice_id===sliceRef,'前端接收记录与执行切片不匹配');
|
|
82
|
-
if(acceptance.schema_version
|
|
86
|
+
if(acceptance.schema_version>=2) {
|
|
83
87
|
const preflight=await verifyFrontendStrategicPreflight({root,preflightRef:acceptance.strategic_preflight.ref,expectedDigest:acceptance.strategic_preflight.digest});
|
|
88
|
+
ensure((acceptance.schema_version===3)===(preflight.preflight_schema_version===2),'前端接收与预检协议版本不匹配,禁止降级');
|
|
89
|
+
if(acceptance.schema_version===3)ensure(acceptance.ui_baseline_kind===preflight.ui_baseline_kind,'前端接收基线类型不一致');
|
|
84
90
|
ensure(preflight.strategic_bundle_digest===acceptance.strategic_handoff.bundle_digest,'前端接收与战略预检版本不一致');
|
|
85
91
|
if(acceptance.backend_dependency.mode==='not-applicable') {
|
|
86
92
|
ensure(!acceptance.backend_delivery,'backend-not-applicable 不得绑定后端交付收据');
|
|
@@ -88,13 +94,14 @@ export async function verifyFrontendDelivery({root=process.cwd(),acceptanceRef,s
|
|
|
88
94
|
ensure(strategic.result==='verified',`战略承接阻断: ${JSON.stringify(strategic.issues)}`);
|
|
89
95
|
const strategicReceipt=read(safe(root,acceptance.strategic_handoff.import_receipt_ref));
|
|
90
96
|
return openBundle(safe(root,strategicReceipt.package_ref),bundle=>{
|
|
91
|
-
|
|
97
|
+
ensure(acceptance.schema_version===3||bundle.handoff.schema_version!==5,'Handoff v5 必须使用前端接收 v3');
|
|
98
|
+
const visualCases=uiBaselineCaseIds(bundle.handoff);
|
|
92
99
|
const sourceIds=new Set([...bundle.indexes.rules.map(item=>item.rule_id),...bundle.indexes.scenarios.filter(item=>item.critical).map(item=>item.scenario_id)]);
|
|
93
100
|
const ids=acceptance.frontend_cases.map(item=>item.case_id);ensure(new Set(ids).size===ids.length,'前端验收用例 ID 重复');
|
|
94
101
|
for(const item of acceptance.frontend_cases){
|
|
95
102
|
ensure(item.operation_ids.length===0,'backend-not-applicable 前端用例不得绑定 operation_ids');
|
|
96
103
|
ensure(item.source_ids.every(id=>sourceIds.has(id)),'前端用例引用未知规则/场景');
|
|
97
|
-
ensure(item.visual_case_ids.every(id=>visualCases.includes(id)),'前端用例视觉基线悬空');
|
|
104
|
+
ensure((acceptance.schema_version===3?item.baseline_case_ids:item.visual_case_ids).every(id=>visualCases.includes(id)),'前端用例视觉基线悬空');
|
|
98
105
|
const evidence=readFileSync(safe(root,item.evidence_ref));ensure(evidence.length>0&&hash(evidence)===item.evidence_digest,'前端承接用例说明为空或摘要漂移');
|
|
99
106
|
}
|
|
100
107
|
return {result:'inputs-verified',ready_for_agent:false,slice_id:sliceRef,acceptance_ref:acceptanceRef,acceptance_digest:acceptanceDigest,strategic_bundle_digest:strategic.consumed_bundle_digest,backend_dependency:'not-applicable',next_action:'prepare-or-revalidate-frontend-plan-and-slice-contract'};
|
|
@@ -117,12 +124,13 @@ export async function verifyFrontendDelivery({root=process.cwd(),acceptanceRef,s
|
|
|
117
124
|
const ids=acceptance.frontend_cases.map(item=>item.case_id);
|
|
118
125
|
ensure(new Set(ids).size===ids.length,'前端验收用例 ID 重复');
|
|
119
126
|
await openBundle(safe(backend.source,delivery.strategic_bundle_ref),bundle=>{
|
|
120
|
-
|
|
127
|
+
ensure(acceptance.schema_version===3||bundle.handoff.schema_version!==5,'Handoff v5 必须使用前端接收 v3');
|
|
128
|
+
const visualCases=uiBaselineCaseIds(bundle.handoff);
|
|
121
129
|
for(const item of acceptance.frontend_cases) {
|
|
122
|
-
if(acceptance.schema_version
|
|
130
|
+
if(acceptance.schema_version>=2)ensure(item.operation_ids.length>0,'有后端依赖时前端用例必须绑定已交付 operation_ids');
|
|
123
131
|
ensure(item.operation_ids.every(id=>delivery.scope.operation_ids.includes(id)),'前端用例依赖未交付接口');
|
|
124
132
|
ensure(item.source_ids.every(id=>delivery.scope.source_ids.includes(id)),'前端用例依赖未交付规则/场景');
|
|
125
|
-
ensure(item.visual_case_ids.every(id=>visualCases.includes(id)),'前端用例视觉基线悬空');
|
|
133
|
+
ensure((acceptance.schema_version===3?item.baseline_case_ids:item.visual_case_ids).every(id=>visualCases.includes(id)),'前端用例视觉基线悬空');
|
|
126
134
|
const evidence=readFileSync(safe(root,item.evidence_ref));
|
|
127
135
|
ensure(evidence.length>0&&hash(evidence)===item.evidence_digest,'前端承接用例说明为空或摘要漂移');
|
|
128
136
|
}
|