create-yss-spec 3.4.9 → 3.4.10
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/package.json +1 -1
- package/src/template/attach-planner.js +5 -0
- package/src/template/sync-planner.js +5 -0
- package/template/.agents/skills/.strategic-design-skills-manifest.json +1 -1
- package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.agents/skills/yss-openapi-draft-review/SKILL.md +2 -0
- package/template/.agents/skills/yss-openapi-governance/SKILL.md +4 -0
- package/template/.agents/skills/yss-product-lifecycle/SKILL.md +11 -5
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +63 -1
- package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.agents/skills/yss-prototype-stage/SKILL.md +2 -0
- package/template/.agents/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs +2 -2
- package/template/.agents/skills/yss-stage-decision/SKILL.md +2 -0
- package/template/.agents/skills/yss-tactical-design/SKILL.md +2 -0
- package/template/.agents/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.agents/skills/yss-technical-design/scripts/engineering-design.mjs +1 -1
- package/template/.agents/skills/yss-technical-design/scripts/validate-technical-design.mjs +11 -8
- package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.codex/skills/yss-openapi-draft-review/SKILL.md +2 -0
- package/template/.codex/skills/yss-openapi-governance/SKILL.md +4 -0
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +11 -5
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +63 -1
- package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.codex/skills/yss-prototype-stage/SKILL.md +2 -0
- package/template/.codex/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs +2 -2
- package/template/.codex/skills/yss-stage-decision/SKILL.md +2 -0
- package/template/.codex/skills/yss-tactical-design/SKILL.md +2 -0
- package/template/.codex/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.codex/skills/yss-technical-design/scripts/engineering-design.mjs +1 -1
- package/template/.codex/skills/yss-technical-design/scripts/validate-technical-design.mjs +11 -8
- package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.cursor/skills/yss-openapi-draft-review/SKILL.md +2 -0
- package/template/.cursor/skills/yss-openapi-governance/SKILL.md +4 -0
- package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +11 -5
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +63 -1
- package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.cursor/skills/yss-prototype-stage/SKILL.md +2 -0
- package/template/.cursor/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs +2 -2
- package/template/.cursor/skills/yss-stage-decision/SKILL.md +2 -0
- package/template/.cursor/skills/yss-tactical-design/SKILL.md +2 -0
- package/template/.cursor/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.cursor/skills/yss-technical-design/scripts/engineering-design.mjs +1 -1
- package/template/.cursor/skills/yss-technical-design/scripts/validate-technical-design.mjs +11 -8
- package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
- package/template/.pi/skills/yss-openapi-draft-review/SKILL.md +2 -0
- package/template/.pi/skills/yss-openapi-governance/SKILL.md +4 -0
- package/template/.pi/skills/yss-product-lifecycle/SKILL.md +11 -5
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +63 -1
- package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +2 -2
- package/template/.pi/skills/yss-prototype-stage/SKILL.md +2 -0
- package/template/.pi/skills/yss-prototype-stage/scripts/visual-baseline-contract.mjs +2 -2
- package/template/.pi/skills/yss-stage-decision/SKILL.md +2 -0
- package/template/.pi/skills/yss-tactical-design/SKILL.md +2 -0
- package/template/.pi/skills/yss-technical-design/SKILL.md +2 -0
- package/template/.pi/skills/yss-technical-design/scripts/engineering-design.mjs +1 -1
- package/template/.pi/skills/yss-technical-design/scripts/validate-technical-design.mjs +11 -8
- package/template/AGENTS.md +2 -0
- package/template/CONTEXT.md +2 -1
- package/template/README.md +1 -0
- package/template/docs/agents/issue-tracker.md +7 -0
- package/template/docs/process/contract-reading.md +46 -0
- package/template/docs/process/lifecycle-artifact-map.md +2 -2
- package/template/docs/process/lifecycle-registry-baseline.json +2 -1
- package/template/docs/process/lifecycle-registry.yaml +10 -4
- package/template/docs/process/schemas/api-contract-decision-v2.schema.json +198 -0
- package/template/docs/process/schemas/digital-human-task-package.schema.json +8 -0
- package/template/docs/process/schemas/lifecycle-checkpoint.schema.json +259 -0
- package/template/docs/process/schemas/stage-tracking.schema.json +259 -0
- package/template/docs/process/stage-tracking.md +62 -0
- package/template/docs/process/strategic-handoff-package.md +8 -0
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +2 -0
- package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214/347/264/242/345/274/225.md +1 -0
- package/template/docs/user-guide//351/230/266/346/256/265/346/216/245/345/205/245/344/270/216/344/272/247/347/211/251/345/244/215/347/224/250/344/275/277/347/224/250/346/211/213/345/206/214.md +243 -0
- package/template/scripts/api-contract-decision +16 -0
- package/template/scripts/complete-backend-delivery +17 -0
- package/template/scripts/contract +13 -0
- package/template/scripts/dispatch-slice-task +13 -0
- package/template/scripts/lib/api-contract-decision.mjs +45 -4
- package/template/scripts/lib/approval-record.mjs +1 -1
- package/template/scripts/lib/approved-execution-context.mjs +35 -7
- package/template/scripts/lib/backend-architecture.mjs +2 -2
- package/template/scripts/lib/backend-delivery-terminal.mjs +51 -0
- package/template/scripts/lib/backend-platform-provenance.mjs +1 -1
- package/template/scripts/lib/backend-platform.mjs +1 -1
- package/template/scripts/lib/backend-scaffold-prerequisites.mjs +1 -1
- package/template/scripts/lib/context-contract.mjs +1 -1
- package/template/scripts/lib/context-reconciliation.mjs +1 -1
- package/template/scripts/lib/contract-source.mjs +22 -0
- package/template/scripts/lib/contract-views.mjs +109 -0
- package/template/scripts/lib/delivery-preflight.mjs +3 -2
- package/template/scripts/lib/digital-human-roles.mjs +1 -1
- package/template/scripts/lib/existing-backend-architecture.mjs +4 -3
- package/template/scripts/lib/existing-ui-baseline.mjs +4 -2
- package/template/scripts/lib/frontend-delivery-boundary.mjs +1 -1
- package/template/scripts/lib/frontend-delivery.mjs +1 -1
- package/template/scripts/lib/harness-execution-scope.mjs +15 -1
- package/template/scripts/lib/implementation-contract-compiler.mjs +4 -4
- package/template/scripts/lib/json-schema.mjs +19 -4
- package/template/scripts/lib/lifecycle-context-query.mjs +5 -1
- package/template/scripts/lib/lifecycle-controls.mjs +1 -1
- package/template/scripts/lib/lifecycle-execution-scope.mjs +92 -0
- package/template/scripts/lib/lifecycle-registry.mjs +2 -1
- package/template/scripts/lib/lifecycle-transition.mjs +21 -3
- package/template/scripts/lib/openapi-draft-validation.mjs +2 -1
- package/template/scripts/lib/skill-registry.mjs +1 -1
- package/template/scripts/lib/slice-contract-preparation.mjs +16 -14
- package/template/scripts/lib/slice-contract-views.mjs +1 -1
- package/template/scripts/lib/slice-contract.mjs +18 -3
- package/template/scripts/lib/slice-execution-preflight.mjs +7 -3
- package/template/scripts/lib/slice-repositories.mjs +1 -1
- package/template/scripts/lib/slice-task-package.mjs +3 -2
- package/template/scripts/lib/stage-tracking-migration.mjs +176 -0
- package/template/scripts/lib/stage-tracking.mjs +184 -0
- package/template/scripts/lib/strategic-handoff-consumption.mjs +5 -4
- package/template/scripts/lib/strategic-handoff-io.mjs +5 -2
- package/template/scripts/lib/strategic-handoff-routing.mjs +31 -0
- package/template/scripts/lib/strategic-handoff.mjs +4 -2
- package/template/scripts/lib/task-package.mjs +2 -0
- package/template/scripts/lib/technical-design-boundary.mjs +4 -2
- package/template/scripts/lib/ui-baseline.mjs +1 -1
- package/template/scripts/lib/user-decision.mjs +1 -1
- package/template/scripts/lib/validation-phase.mjs +72 -0
- package/template/scripts/stage-tracking +25 -0
- package/template/scripts/strategic-consumer-entry +47 -0
- package/template/scripts/verify-context-reconciliation +4 -3
- package/template/scripts/verify-lifecycle-checkpoint +10 -0
- package/template/skills-lock.json +13 -13
- package/template.snapshot.json +4 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {readFileSync,withValidationPhase} from './validation-phase.mjs';
|
|
2
|
+
import {safe,hash,schema} from './strategic-handoff-io.mjs';
|
|
3
|
+
import {parseSliceYaml,readSliceContract} from './slice-contract.mjs';
|
|
4
|
+
import {renderSliceContractView,diffSliceContracts} from './slice-contract-views.mjs';
|
|
5
|
+
|
|
6
|
+
// This is a presentation adapter table, never a lifecycle or approval registry.
|
|
7
|
+
export const contractKinds=Object.freeze({
|
|
8
|
+
slice:null,plan:null,spec:null,'product-design':null,'prototype-confirmation':null,'visual-baseline':null,
|
|
9
|
+
'technical-design':'.agents/skills/yss-technical-design/references/technical-design.schema.json',
|
|
10
|
+
'api-decision':'docs/process/schemas/api-contract-decision.schema.json',
|
|
11
|
+
'data-decision':'docs/process/schemas/data-architecture-decision.schema.json',
|
|
12
|
+
scaffold:'docs/process/schemas/project-scaffold-contract.schema.json',
|
|
13
|
+
'scaffold-decision':'docs/process/schemas/scaffold-architecture-decisions.schema.json',
|
|
14
|
+
handoff:null,'backend-delivery':'docs/process/schemas/backend-delivery.schema.json',
|
|
15
|
+
'frontend-acceptance':'docs/process/schemas/frontend-delivery-acceptance.schema.json',approval:null,'user-decision':'docs/process/schemas/user-decision.schema.json',
|
|
16
|
+
'context-reconciliation':'docs/process/schemas/context-reconciliation.schema.json'
|
|
17
|
+
});
|
|
18
|
+
const object=v=>v&&typeof v==='object'&&!Array.isArray(v);
|
|
19
|
+
const stringify=v=>typeof v==='string'?v:JSON.stringify(v,null,2);
|
|
20
|
+
const metadata=/^(schema_version|kind|digest|sha256|subject_digest|.*_digest|.*_sha256|resolution|basis|inputs|sources|artifact_bindings|current_version)$/;
|
|
21
|
+
function display(value){
|
|
22
|
+
if(Array.isArray(value))return value.map(display);
|
|
23
|
+
if(!object(value))return value;
|
|
24
|
+
return Object.fromEntries(Object.entries(value).filter(([key])=>!metadata.test(key)).map(([k,v])=>[k,display(v)]));
|
|
25
|
+
}
|
|
26
|
+
function validateKind(kind){if(!Object.hasOwn(contractKinds,kind))throw new TypeError(`未知合同类型 ${kind};可用:${Object.keys(contractKinds).join(', ')}`);}
|
|
27
|
+
function load(ref,root){
|
|
28
|
+
const bytes=readFileSync(safe(root,ref));
|
|
29
|
+
if(/\.md$/i.test(ref)){
|
|
30
|
+
const text=bytes.toString('utf8'),front=/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
|
|
31
|
+
return{bytes,raw:{...(front?parseSliceYaml(front[1]):{}),正文:front?text.slice(front[0].length):text}};
|
|
32
|
+
}
|
|
33
|
+
const raw=parseSliceYaml(bytes);if(!object(raw))throw new TypeError('合同必须是结构化对象或 Markdown 文档');return{bytes,raw};
|
|
34
|
+
}
|
|
35
|
+
function references(value,root,rows=[],seen=new Set()){
|
|
36
|
+
if(!value||typeof value!=='object')return rows;
|
|
37
|
+
if(object(value)&&typeof value.ref==='string'&&typeof value.digest==='string'){
|
|
38
|
+
const key=`${value.ref}\0${value.digest}`;
|
|
39
|
+
if(!seen.has(key)){seen.add(key);try{rows.push({ref:value.ref,current:hash(readFileSync(safe(root,value.ref)))===value.digest});}catch(error){rows.push({ref:value.ref,current:false,error:error.message});}}
|
|
40
|
+
}
|
|
41
|
+
if(object(value))for(const [key,ref]of Object.entries(value))if(key.endsWith('_ref')&&typeof ref==='string'&&typeof value[key.replace(/_ref$/,'_digest')]==='string')references({ref,digest:value[key.replace(/_ref$/,'_digest')]},root,rows,seen);
|
|
42
|
+
for(const child of Object.values(value))references(child,root,rows,seen);
|
|
43
|
+
return rows;
|
|
44
|
+
}
|
|
45
|
+
function declaredBlockers(value,prefix=''){
|
|
46
|
+
if(!value||typeof value!=='object')return[];
|
|
47
|
+
const rows=[];
|
|
48
|
+
for(const [key,child]of Object.entries(value)){
|
|
49
|
+
const field=prefix?`${prefix}.${key}`:key;
|
|
50
|
+
if(['blockers','blocking_findings','stale_inputs','readiness_blockers'].includes(key)&&Array.isArray(child))for(const item of child)rows.push(`${field}: ${stringify(item)}`);
|
|
51
|
+
else rows.push(...declaredBlockers(child,field));
|
|
52
|
+
}
|
|
53
|
+
return rows;
|
|
54
|
+
}
|
|
55
|
+
function markdown(view){
|
|
56
|
+
const lines=[`# ${view.kind} · ${view.binding.id||view.binding.ref}`,`版本:${view.binding.version||'源资产未声明'};权威文件:${view.binding.ref}`,`摘要:${view.binding.digest}`,`检查:${view.checks.join(';')}。批准有效性未核验;本视图不授予执行权限。`];
|
|
57
|
+
for(const [name,value]of Object.entries(view.content))if(value!==undefined)lines.push(`## ${name}`,stringify(value));
|
|
58
|
+
if(view.blockers.length)lines.push('## 阻断与未决检查',...view.blockers.map(x=>`- ${x}`));
|
|
59
|
+
return lines.join('\n\n')+'\n';
|
|
60
|
+
}
|
|
61
|
+
export function viewContract(ref,options={}){return withValidationPhase({root:options.root,purpose:'contract-view',slice_id:ref,work_unit_id:options.unit_id,readOnly:true},()=>view(ref,options));}
|
|
62
|
+
function view(ref,{root=process.cwd(),kind,profile='review',unit_id}={}){
|
|
63
|
+
validateKind(kind);if(!['review','task','full'].includes(profile))throw new TypeError(`未知阅读档位 ${profile}`);
|
|
64
|
+
let binding,content,checks=['可读取'],blockers=[],sourceChecks=[];
|
|
65
|
+
if(kind==='slice'){
|
|
66
|
+
const loaded=readSliceContract(ref,{root,diagnostic:true});binding=loaded.binding;blockers.push(...declaredBlockers(loaded.raw));
|
|
67
|
+
const legacy=renderSliceContractView(ref,{root,unit_id:profile==='task'?unit_id:undefined});
|
|
68
|
+
checks.push('Slice 结构及绑定来源');
|
|
69
|
+
if(legacy.diagnostic_only){content={诊断:legacy.blockers};blockers=legacy.blockers.map(b=>b.reason);}
|
|
70
|
+
else if(profile==='full')content={权威合同:loaded.raw,完整视图:legacy};
|
|
71
|
+
else if(profile==='review'){
|
|
72
|
+
content={...legacy.summary,验收:legacy.acceptance,实施范围:{工程:loaded.raw.scope?.project_roots||legacy.constraints.project_roots,允许写入:loaded.raw.scope?.allowed_write_paths||legacy.constraints.allowed_write_paths},关键约束:loaded.raw.scope?.forbidden_patterns||legacy.constraints.forbidden_patterns,停止条件:'来源或批准过期、越界、缺证据、验证失败、drift / violation / new_impacts 时停止并回交。'};
|
|
73
|
+
for(const key of ['risks','risk_acceptance','blockers','pending_decisions','constraints'])if(loaded.raw[key])content[key]=loaded.raw[key];
|
|
74
|
+
for(const [label,pattern]of [['风险',/风险/],['关键取舍',/取舍/],['待决定事项',/待|决定/],['变化',/变化|变更/]])if(!Object.keys(content).some(k=>pattern.test(k)))content[label]='来源未说明,审阅时核实。';
|
|
75
|
+
const extensions=loaded.raw.extensions||{};
|
|
76
|
+
for(const [name,extension]of Object.entries(extensions))for(const [key,value]of Object.entries(extension))if(/risk|constraint|decision|block|exception|unknown/.test(key))content[`${name}.${key}`]=value;
|
|
77
|
+
}else{
|
|
78
|
+
if(!unit_id)throw new TypeError('task 视图必须指定 --unit');
|
|
79
|
+
const units=loaded.contract.work_units.filter(u=>u.id===unit_id);if(units.length!==1)throw new TypeError(`工作单元缺失或不唯一: ${unit_id}`);
|
|
80
|
+
content={当前任务:units[0].work_unit||units[0],全局约束:legacy.constraints,专项约束:legacy.extensions,编译约束:loaded.raw.resolution,适用性:loaded.raw.applicability,验收:legacy.acceptance,上游引用:Object.fromEntries(Object.entries(loaded.sources).map(([k,v])=>[k,{ref:v.ref,version:v.version}])),停止条件:'来源或批准过期、越界、缺证据、验证失败、drift / violation / new_impacts 时停止并回交。'};
|
|
81
|
+
const known=new Set(['schema_version','contract_id','contract_version','slice_id','status','basis','scope','applicability','resolution','acceptance','verification','work_units','extensions','ticket_policy']);
|
|
82
|
+
content.其他约束=Object.fromEntries(Object.entries(loaded.raw).filter(([k])=>!known.has(k)));
|
|
83
|
+
}
|
|
84
|
+
}else{
|
|
85
|
+
const {bytes,raw}=load(ref,root);binding={ref,id:raw.contract_id||raw.technical_design_id||raw.decision_id||raw.request?.request_id||raw.handoff_id||raw.baseline_id||raw.spec_id||raw.plan_id||raw.id||null,version:raw.contract_version||raw.decision_version||raw.handoff_version||raw.version||null,digest:hash(bytes)};
|
|
86
|
+
blockers.push(...declaredBlockers(raw));
|
|
87
|
+
let schemaRef=contractKinds[kind];if(kind==='api-decision'&&raw.schema_version===2)schemaRef='docs/process/schemas/api-contract-decision-v2.schema.json';
|
|
88
|
+
if(kind==='frontend-acceptance'&&[1,3].includes(raw.schema_version))schemaRef=`docs/process/schemas/frontend-delivery-acceptance${raw.schema_version===1?'-v1':'-v3'}.schema.json`;
|
|
89
|
+
if(schemaRef)try{schema(raw,schemaRef);checks.push('结构校验');}catch(error){blockers.push(error.message);}
|
|
90
|
+
else checks.push('结构与批准未核验,按资产所有者规则继续');
|
|
91
|
+
sourceChecks=references(raw,root);if(sourceChecks.length)checks.push(`核对 ${sourceChecks.length} 项可识别来源摘要;其余语义由资产所有者核验`);for(const source of sourceChecks)if(!source.current)blockers.push(`来源不可读或过期:${source.ref}`);
|
|
92
|
+
// Unknown business fields remain visible. Only well-known machine metadata is hidden.
|
|
93
|
+
content=profile==='full'?{权威合同:raw,来源核对:sourceChecks}:display(raw);
|
|
94
|
+
if(kind==='api-decision'&&raw.schema_version===2&&raw.draft_review){try{const review=load(raw.draft_review.ref,root).raw;content.实际审查记录=profile==='full'?review:display(review);if(review.result!=='approved'||!Array.isArray(review.blocking_findings)||review.blocking_findings.length)blockers.push('OpenAPI Draft Review 实际记录未批准或有阻断');if(review.draft_ref!==raw.openapi?.ref||review.draft_digest!==raw.openapi?.digest)blockers.push('Draft Review 未绑定当前 OpenAPI');}catch(error){blockers.push(error.message);}}
|
|
95
|
+
if(profile==='task'){
|
|
96
|
+
if(raw.work_units){if(!unit_id)throw new TypeError('含工作单元的合同必须指定 --unit');const matches=raw.work_units.filter(x=>x.id===unit_id);if(matches.length!==1)throw new TypeError(`工作单元缺失或不唯一: ${unit_id}`);content.work_units=matches;}
|
|
97
|
+
content.机器绑定中的约束=Object.fromEntries(['resolution','basis','inputs','sources','artifact_bindings'].filter(k=>raw[k]!==undefined).map(k=>[k,raw[k]]));
|
|
98
|
+
content.约束分类='保留未分类字段;本视图只允许阅读,执行影响由资产所有者核验。';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const view={read_only:true,execution_allowed:false,kind,profile,binding,checks,approval_validity:'not-checked',content,blockers};view.markdown=markdown(view);return view;
|
|
102
|
+
}
|
|
103
|
+
export function diffContract(beforeRef,afterRef,{root=process.cwd(),beforeRoot=root,kind}={}){
|
|
104
|
+
validateKind(kind);if(kind==='slice')return diffSliceContracts(beforeRef,afterRef,{root,beforeRoot});
|
|
105
|
+
const before=load(beforeRef,beforeRoot),after=load(afterRef,root),changes=[];
|
|
106
|
+
const walk=(a,b,p)=>{if(JSON.stringify(a)===JSON.stringify(b))return;if(object(a)&&object(b)){for(const key of new Set([...Object.keys(a),...Object.keys(b)]))walk(a[key],b[key],p?`${p}.${key}`:key);}else changes.push({path:p,before:a??null,after:b??null,category:metadata.test(p.split('.').at(-1))?'binding-change':'requires-review'});};
|
|
107
|
+
walk(before.raw,after.raw,'');const source_checks=[...references(before.raw,beforeRoot).map(x=>({...x,side:'before'})),...references(after.raw,root).map(x=>({...x,side:'after'}))];
|
|
108
|
+
return{read_only:true,approval_reusable:false,execution_allowed:false,before:{ref:beforeRef,digest:hash(before.bytes)},after:{ref:afterRef,digest:hash(after.bytes)},bytes_changed:!before.bytes.equals(after.bytes),changes,source_checks,decision_impact:'必须按当前差异和授权延续协议核验;无法读取旧来源时影响未知。'};
|
|
109
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { normalizeSliceContract, sourceSliceContract, parseSliceYaml } from './slice-contract.mjs';
|
|
2
2
|
import { assertExistingSliceStructure, createApprovedExecutionContext, verifySliceContractApproval } from './approved-execution-context.mjs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { readFileSync, existsSync } from '
|
|
4
|
+
import { readFileSync, existsSync, withValidationPhase } from './validation-phase.mjs';
|
|
5
5
|
import { ROOT, read, parse, safe, hash, digest, ensure, schema, sourceApprovalPolicy } from './strategic-handoff-io.mjs';
|
|
6
6
|
import { parseContextContract } from './context-contract.mjs';
|
|
7
7
|
import { validateApprovalRecord } from './approval-record.mjs';
|
|
@@ -47,7 +47,8 @@ function requireBackendCapability(key) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Read-only: never executes input commands, starts services, unpacks archives or writes receipts. */
|
|
50
|
-
export async function preflightDelivery(input,{
|
|
50
|
+
export async function preflightDelivery(input,options={}) {return withValidationPhase({root:path.resolve(options.root||process.cwd(),typeof input?.governance_root==='string'?input.governance_root:'.'),purpose:`delivery-${options.stage}`,slice_id:input?.scope?.slice_id,work_unit_id:options.work_unit_id,readOnly:true,signal:options.signal},()=>preflight(input,options));}
|
|
51
|
+
async function preflight(input,{stage,root=process.cwd(),work_unit_id}={}) {
|
|
51
52
|
try { ensure(STAGES.includes(stage),'未知阶段;必须是 prepare|build|export|accept');schema(input,'docs/process/schemas/delivery-preflight-input.schema.json'); }
|
|
52
53
|
catch(error) { return invalidPreflightInput(error.message); }
|
|
53
54
|
root=path.resolve(root,input.governance_root||'.');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {assertApprovedExecutionContext} from './approved-execution-context.mjs';
|
|
2
2
|
import {createHash} from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, lstatSync, readdirSync } from './validation-phase.mjs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import {execFileSync} from 'node:child_process';
|
|
6
7
|
import {parseDocument} from '../vendor/yaml.mjs';
|
|
@@ -35,7 +36,7 @@ function inspectMaven(root,units){
|
|
|
35
36
|
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))`;
|
|
36
37
|
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}`);}
|
|
37
38
|
}
|
|
38
|
-
export function verifyExistingArchitecture(identity,bindings,{root,registry,execution}={}){
|
|
39
|
+
export function verifyExistingArchitecture(identity,bindings,{root,registry,execution,readOnly=false}={}){
|
|
39
40
|
const profile=validateExistingArchitecture(identity,registry);
|
|
40
41
|
requireThat(text(root),'ARCH_EVIDENCE_REF','需要治理 root');
|
|
41
42
|
const baseline=bound(root,bindings?.engineering_baseline),registration=bound(root,bindings?.repository_registration),manifest=bound(root,bindings?.manifest);
|
|
@@ -132,7 +133,7 @@ export function verifyExistingArchitecture(identity,bindings,{root,registry,exec
|
|
|
132
133
|
}
|
|
133
134
|
if(changes.length){
|
|
134
135
|
requireThat(execution,'ARCH_SOURCE_STALE',`固定源码改变: ${changes.join(', ')}`);
|
|
135
|
-
const permission=assertApprovedExecutionContext(execution,{root,architectureIdentity:identity,architectureEvidence:bindings});
|
|
136
|
+
const permission=assertApprovedExecutionContext(execution,{root,architectureIdentity:identity,architectureEvidence:bindings,readOnly});
|
|
136
137
|
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);});
|
|
137
138
|
requireThat(changes.every(ref=>allowed.some(scope=>beneath(ref,scope))&®istration.allowed_write_paths.some(scope=>beneath(ref,scope))),'ARCH_SOURCE_OUT_OF_SCOPE','输出增量超出批准写范围');
|
|
138
139
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, withValidationPhase, inValidationPhase } from './validation-phase.mjs';
|
|
2
3
|
import { ensure, hash, digest, read, safe, files, schema, treeDigest } from './strategic-handoff-io.mjs';
|
|
3
4
|
|
|
4
5
|
// This is an observation bundle, not a prototype or an approval generator.
|
|
5
|
-
export function validateExistingUiBaseline(data,
|
|
6
|
+
export function validateExistingUiBaseline(data,options={}) {return inValidationPhase()?validate(data,options):withValidationPhase({root:options.bundleRoot,purpose:'existing-ui-baseline',readOnly:true},()=>validate(data,options));}
|
|
7
|
+
function validate(data, { bundleRoot } = {}) {
|
|
6
8
|
const errors=[];
|
|
7
9
|
try {
|
|
8
10
|
schema(data,'docs/process/schemas/existing-ui-baseline.schema.json');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeSliceContract, sourceSliceContract, parseSliceYaml } from './slice-contract.mjs';
|
|
2
2
|
import { loadDeliveryProfile } from './harness-execution-scope.mjs';
|
|
3
|
-
import { existsSync } from '
|
|
3
|
+
import { existsSync } from './validation-phase.mjs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import { ROOT, read, safe, ensure, digest } from './strategic-handoff-io.mjs';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { uiBaselineRef, uiBaselineCaseIds, uiBaselineKind, hasConsumerRoutes } from './ui-baseline.mjs';
|
|
2
|
-
import { existsSync, readdirSync, readFileSync } from '
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from './validation-phase.mjs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { openBackendDelivery } from './backend-delivery.mjs';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadExecutionScope, assertScopeWorkUnit } from './lifecycle-execution-scope.mjs';
|
|
2
|
+
import { existsSync } from './validation-phase.mjs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { ROOT, read, safe, ensure } from './strategic-handoff-io.mjs';
|
|
4
5
|
|
|
@@ -30,6 +31,12 @@ function activeProfile(root) {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function enforceHarnessTaskScope(task,{root=ROOT}={}) {
|
|
34
|
+
const scope=loadExecutionScope(root);
|
|
35
|
+
if(scope) {
|
|
36
|
+
assertScopeWorkUnit(task.contract?.kind==='slice-implementation'?'work-unit.slice-implementation':task.work_unit_id,{root});
|
|
37
|
+
ensure(task.role_id!=='role.frontend-engineer','后端职责不允许派发前端实现');
|
|
38
|
+
ensure(task.allowed_write_paths.every(ref=>!/^apps\/frontend(?:\/|$)/.test(ref)),'禁止写入前端工程');
|
|
39
|
+
}
|
|
33
40
|
const profile=activeProfile(root);
|
|
34
41
|
if (!profile) return;
|
|
35
42
|
ensure(task.contract?.kind!=='template-maintenance','专职产品项目不得派发模板维护任务');
|
|
@@ -41,6 +48,13 @@ export function enforceHarnessTaskScope(task,{root=ROOT}={}) {
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
export function enforceHarnessSkillScope(skills,registry,{root=ROOT}={}) {
|
|
51
|
+
if(loadExecutionScope(root)) {
|
|
52
|
+
assertScopeWorkUnit('work-unit.slice-implementation',{root});
|
|
53
|
+
for(const id of skills) {
|
|
54
|
+
const impacts=registry.skills.find(skill=>skill.id===id)?.impacts||[];
|
|
55
|
+
ensure(!impacts.includes('frontend')||impacts.includes('backend')||impacts.includes('lifecycle'),`后端职责禁止编译生产前端技能: ${id}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
44
58
|
const profile=activeProfile(root);
|
|
45
59
|
if (!profile) return;
|
|
46
60
|
const side=profile.profile_id==='harness.frontend-delivery'?'frontend':'backend';
|
|
@@ -5,7 +5,7 @@ import { createApprovedExecutionContext, createApprovedRecompilationContext, ass
|
|
|
5
5
|
import { enforceTechnicalDesign } from './technical-design-boundary.mjs';
|
|
6
6
|
import { enforceHarnessSkillScope } from "./harness-execution-scope.mjs";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { readFileSync } from
|
|
8
|
+
import { readFileSync } from './validation-phase.mjs';
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { parseDocument } from "../vendor/yaml.mjs";
|
|
11
11
|
import { DEFAULT_REGISTRY, loadSkillRegistry, resolveSkillForNewUse } from "./skill-registry.mjs";
|
|
@@ -276,8 +276,8 @@ export function evaluateContractFreshness(contract, { registry, compilerContract
|
|
|
276
276
|
const reasons = [];
|
|
277
277
|
let trustedExecution;
|
|
278
278
|
try {
|
|
279
|
-
trustedExecution=approved_slice?createApprovedExecutionContext(approved_slice,{root,contract:all,work_unit_id}):execution;
|
|
280
|
-
if(trustedExecution)assertApprovedExecutionContext(trustedExecution,{root,contract});
|
|
279
|
+
trustedExecution=approved_slice?createApprovedExecutionContext(approved_slice,{root,contract:all,work_unit_id,readOnly}):execution;
|
|
280
|
+
if(trustedExecution)assertApprovedExecutionContext(trustedExecution,{root,contract,readOnly});
|
|
281
281
|
} catch(error) { reasons.push(error.code||'EXECUTION_APPROVAL_INVALID');trustedExecution=undefined; }
|
|
282
282
|
try { enforceTechnicalDesign({ ...resolution, slice_id: contract.slice_id ?? resolution.slice_id }, { root,execution:trustedExecution,readOnly }); }
|
|
283
283
|
catch { reasons.push("technical-design-stale-or-unavailable"); }
|
|
@@ -289,7 +289,7 @@ export function evaluateContractFreshness(contract, { registry, compilerContract
|
|
|
289
289
|
try { validateArchitectureIdentity(resolution.architecture_identity, registry); }
|
|
290
290
|
catch { reasons.push("architecture-identity-invalid"); }
|
|
291
291
|
if (resolution.architecture_identity.schema_version === 2) {
|
|
292
|
-
try { verifyArchitectureEvidence(resolution.architecture_identity, resolution.architecture_evidence, { root, registry, execution:trustedExecution }); }
|
|
292
|
+
try { verifyArchitectureEvidence(resolution.architecture_identity, resolution.architecture_evidence, { root, registry, execution:trustedExecution, readOnly }); }
|
|
293
293
|
catch (error) { reasons.push(error.code ?? "architecture-evidence-stale-or-unavailable"); }
|
|
294
294
|
}
|
|
295
295
|
if (resolution.architecture_identity_digest !== architectureDigest(resolution.architecture_identity)) reasons.push("architecture-identity-digest-changed");
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync } from
|
|
1
|
+
import { existsSync, readFileSync, validationCache } from './validation-phase.mjs';
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
const schemaCache=Symbol('schema-results');
|
|
5
6
|
const validator = String.raw`
|
|
6
7
|
import json
|
|
7
8
|
import sys
|
|
@@ -21,8 +22,7 @@ for item in jobs:
|
|
|
21
22
|
try:
|
|
22
23
|
key = (item['schemaPath'], item['formatChecker'])
|
|
23
24
|
if key not in validators:
|
|
24
|
-
|
|
25
|
-
schema = json.load(source)
|
|
25
|
+
schema = json.loads(item['schemaText'])
|
|
26
26
|
validators[key] = Draft202012Validator(schema, format_checker=FormatChecker() if key[1] else None)
|
|
27
27
|
errors = list(validators[key].iter_errors(item['value']))
|
|
28
28
|
if item['errorStyle'] == 'verbose':
|
|
@@ -45,10 +45,11 @@ export function validateJsonSchemas(items, { cwd = process.cwd(), timeoutMs = 30
|
|
|
45
45
|
const effectiveSchema = path.resolve(schemaPath);
|
|
46
46
|
if (!existsSync(effectiveSchema)) throw new TypeError(`缺少 ${label}: ${effectiveSchema}`);
|
|
47
47
|
if (!["compact", "verbose"].includes(errorStyle)) throw new TypeError(`未知 schema errorStyle: ${errorStyle}`);
|
|
48
|
-
return { value, schemaPath: effectiveSchema, formatChecker, errorStyle };
|
|
48
|
+
return { value, schemaPath: effectiveSchema, schemaText: readFileSync(effectiveSchema, 'utf8'), formatChecker, errorStyle };
|
|
49
49
|
});
|
|
50
50
|
// readall waits for EOF even after the complete JSON has arrived. A known UTF-8
|
|
51
51
|
// byte frame lets the validator finish when a transport keeps its write end open.
|
|
52
|
+
const run = jobs => {
|
|
52
53
|
const input = Buffer.from(JSON.stringify(jobs), "utf8");
|
|
53
54
|
const result = spawnSync("python3", ["-c", validator, String(input.length)], { cwd, encoding: "utf8", input, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 });
|
|
54
55
|
if (result.error?.code === "ETIMEDOUT") throw new TypeError(`JSON_SCHEMA_TIMEOUT: schema validation exceeded ${timeoutMs}ms`);
|
|
@@ -58,6 +59,20 @@ export function validateJsonSchemas(items, { cwd = process.cwd(), timeoutMs = 30
|
|
|
58
59
|
if (!Array.isArray(results) || results.some(item => !item || typeof item.valid !== "boolean" || typeof item.error !== "string")) throw new TypeError("JSON_SCHEMA_PROTOCOL: validator returned invalid result records");
|
|
59
60
|
if (results.length !== jobs.length) throw new TypeError("schema batch 返回数量不匹配");
|
|
60
61
|
return results;
|
|
62
|
+
};
|
|
63
|
+
// Batch identical jobs once and reuse only deterministic checks within this operation.
|
|
64
|
+
// External references retain the uncached path: their dependency closure is not local here.
|
|
65
|
+
const keys=jobs.map(job=>JSON.stringify([job.schemaPath,job.schemaText,job.value,job.formatChecker,job.errorStyle,path.resolve(cwd),timeoutMs]));
|
|
66
|
+
const pending=new Map(),cached=new Map();
|
|
67
|
+
for(let i=0;i<jobs.length;i++){
|
|
68
|
+
if(/"\$(?:ref|dynamicRef)"\s*:\s*"(?!#)/.test(jobs[i].schemaText)){pending.set(`${i}:${keys[i]}`,{job:jobs[i],indices:[i],cache:false});continue;}
|
|
69
|
+
const existing=validationCache(schemaCache,keys[i]);
|
|
70
|
+
if(existing!==undefined)cached.set(i,existing);
|
|
71
|
+
else {const item=pending.get(keys[i])||{job:jobs[i],indices:[],key:keys[i],cache:true};item.indices.push(i);pending.set(keys[i],item);}
|
|
72
|
+
}
|
|
73
|
+
const entries=[...pending.values()],computed=entries.length?run(entries.map(x=>x.job)):[];
|
|
74
|
+
entries.forEach((entry,index)=>{const result=computed[index];if(entry.cache&&!result.error.includes('Traceback (most recent call last)'))validationCache(schemaCache,entry.key,result);for(const i of entry.indices)cached.set(i,result);});
|
|
75
|
+
return jobs.map((_,i)=>structuredClone(cached.get(i)));
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
export function validateJsonSchema(value, schemaPath, options = {}) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadExecutionScope, assertScopeWorkUnit, scopedNextRoutes } from './lifecycle-execution-scope.mjs';
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -67,6 +68,8 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
|
|
|
67
68
|
throw new TypeError("至少提供 --mode、--stage、--work-unit 或 --include 之一");
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
const scope = loadExecutionScope();
|
|
72
|
+
if (workUnitId) assertScopeWorkUnit(workUnitId, { readOnly: true });
|
|
70
73
|
const lifecycle = validateRegistry(loadRegistry());
|
|
71
74
|
const orchestration = loadYaml(ORCHESTRATION_CONTRACT_REF, "生命周期编排合同");
|
|
72
75
|
const skillRegistry = loadSkillRegistry();
|
|
@@ -91,7 +94,7 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
|
|
|
91
94
|
const evidenceIds = new Set([...gates, ...checks].flatMap((gate) => gate.evidence ?? []));
|
|
92
95
|
const selected = Object.fromEntries(normalizedIncludes.map((key) => [key, orchestration[key]]));
|
|
93
96
|
const transition = workUnitId ? {
|
|
94
|
-
next: orchestration.transition_graph?.routes?.[workUnitId] ?? [],
|
|
97
|
+
next: scopedNextRoutes(workUnitId, orchestration.transition_graph?.routes?.[workUnitId] ?? []),
|
|
95
98
|
forbidden_shortcuts: (orchestration.transition_graph?.forbidden_shortcuts ?? [])
|
|
96
99
|
.filter((edge) => edge.from === workUnitId || edge.to === workUnitId),
|
|
97
100
|
} : null;
|
|
@@ -129,6 +132,7 @@ export function queryLifecycleContext({ mode, stageId, workUnitId, include = []
|
|
|
129
132
|
work_unit: workUnit,
|
|
130
133
|
},
|
|
131
134
|
execution: {
|
|
135
|
+
...(scope ? { responsibility_scope: scope } : {}),
|
|
132
136
|
mode: mode ? orchestration.modes[mode] : null,
|
|
133
137
|
selected,
|
|
134
138
|
...(loadPlan ? { plan_checks: lifecycle.stages.find(stage => stage.id === 'stage.plan').spec_entry.required_checks.map(id => ({ id, status: 'pending', evidence_refs: [] })) } : {}),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from './validation-phase.mjs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { safe } from './strategic-handoff-io.mjs';
|
|
5
|
+
import { ROOT } from './lifecycle-registry.mjs';
|
|
6
|
+
import { parseDocument } from '../vendor/yaml.mjs';
|
|
7
|
+
|
|
8
|
+
export const SCOPE_REF = '.yss-execution-scope.yaml';
|
|
9
|
+
export const TERMINAL_REF = '.yss-backend-delivery.json';
|
|
10
|
+
const read = (root, ref) => {
|
|
11
|
+
const file = safe(root, ref);
|
|
12
|
+
const doc = parseDocument(readFileSync(file, 'utf8'), { uniqueKeys: true });
|
|
13
|
+
if (doc.errors.length) throw new TypeError(doc.errors[0].message);
|
|
14
|
+
return doc.toJS({ maxAliasCount: 0 });
|
|
15
|
+
};
|
|
16
|
+
const check = (ok, message) => { if (!ok) throw new TypeError(`execution-scope-blocked: ${message}`); };
|
|
17
|
+
|
|
18
|
+
/** The project opt-in selects a policy; callers cannot supply a replacement policy. */
|
|
19
|
+
export function loadExecutionScope(root = ROOT) {
|
|
20
|
+
const marker = path.join(root, '.yss-plugin.json');
|
|
21
|
+
const plugin = existsSync(marker) ? read(root, '.yss-plugin.json') : null;
|
|
22
|
+
const file = path.join(root, SCOPE_REF);
|
|
23
|
+
if (!existsSync(file)) {
|
|
24
|
+
check(!['yss-plan-to-backend', 'yss-backend-delivery'].includes(plugin?.plugin), '插件项目缺少职责范围;迁移后才能继续');
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const config = read(root, SCOPE_REF), identity = read(root, 'yss-project.yaml');
|
|
28
|
+
check(identity.schema_version === 1 && identity.repository_mode === 'project-instance', '职责范围仅适用于项目实例');
|
|
29
|
+
check(config.schema_version === 1 && config.scope_id === 'plan-to-backend'
|
|
30
|
+
&& Object.keys(config).every(key => ['schema_version', 'scope_id'].includes(key)), '未知或被扩大的职责配置');
|
|
31
|
+
if (plugin) check(['yss-plan-to-backend', 'yss-backend-delivery'].includes(plugin.plugin) && plugin.execution_scope === config.scope_id, '插件与项目职责绑定不一致');
|
|
32
|
+
const contract = read(root, '.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml');
|
|
33
|
+
const policy = contract.execution_scopes?.[config.scope_id];
|
|
34
|
+
check(policy?.terminal_work_unit === 'work-unit.backend-delivery' && Array.isArray(policy.allowed_work_units), '匹配版本的主控合同缺少职责策略');
|
|
35
|
+
return { ...policy, scope_id: config.scope_id };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function verifyDeliveryTerminal(root = ROOT) {
|
|
39
|
+
check(existsSync(path.join(root, TERMINAL_REF)), '缺少正式后端交付终点记录');
|
|
40
|
+
const result = spawnSync(process.execPath, [path.join(root, 'scripts/complete-backend-delivery'), 'verify', '--root', root], {
|
|
41
|
+
cwd: root, encoding: 'utf8', input: '', timeout: 120000, maxBuffer: 8 * 1024 * 1024,
|
|
42
|
+
env: { ...process.env, NODE_OPTIONS: '', NODE_PATH: '' },
|
|
43
|
+
});
|
|
44
|
+
check(!result.error && result.status === 0, result.error?.message || result.stderr || '后端交付终点复验失败');
|
|
45
|
+
return JSON.parse(result.stdout);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function assertScopeWorkUnit(workUnit, { root = ROOT, readOnly = false } = {}) {
|
|
49
|
+
const scope = loadExecutionScope(root);
|
|
50
|
+
if (!scope) { check(workUnit !== 'work-unit.backend-delivery', '后端终点需要显式职责范围'); return null; }
|
|
51
|
+
check(scope.allowed_work_units.includes(workUnit), `职责范围不允许 ${workUnit}`);
|
|
52
|
+
if (existsSync(path.join(root, TERMINAL_REF))) {
|
|
53
|
+
verifyDeliveryTerminal(root);
|
|
54
|
+
check(readOnly, '后端交付已完成;不能恢复实现或自动发布');
|
|
55
|
+
}
|
|
56
|
+
return scope;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function scopedNextRoutes(workUnit, routes, { root = ROOT } = {}) {
|
|
60
|
+
const scope = loadExecutionScope(root);
|
|
61
|
+
if (!scope) return routes.filter(id => id !== 'work-unit.backend-delivery');
|
|
62
|
+
assertScopeWorkUnit(workUnit, { root, readOnly: true });
|
|
63
|
+
if (existsSync(path.join(root, TERMINAL_REF))) return [];
|
|
64
|
+
return (scope.route_overrides[workUnit] || routes).filter(id => scope.allowed_work_units.includes(id));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function assertScopeTransition(current, next, state, { root = ROOT } = {}) {
|
|
68
|
+
const scope = loadExecutionScope(root);
|
|
69
|
+
if (!scope) { check(current !== 'work-unit.backend-delivery' && next !== 'work-unit.backend-delivery', '后端终点需要显式职责范围'); return; }
|
|
70
|
+
if (current === scope.terminal_work_unit && next === null) { verifyDeliveryTerminal(root); return; }
|
|
71
|
+
assertScopeWorkUnit(current, { root });
|
|
72
|
+
if (next) assertScopeWorkUnit(next, { root });
|
|
73
|
+
check(next !== null, '不能把中间阶段声明为后端交付完成');
|
|
74
|
+
assertScopeImpacts(state, { root });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function assertScopeImpacts(state, { root = ROOT } = {}) {
|
|
78
|
+
if (!loadExecutionScope(root)) return;
|
|
79
|
+
check(state?.delivery_impacts?.frontend !== true, '产品 UI 设计保留,生产前端实现必须交给下游');
|
|
80
|
+
if (state?.ui_impact === true) {
|
|
81
|
+
const deferred = state.downstream_frontend;
|
|
82
|
+
check(deferred && ['owner', 'ticket_ref', 'verification_plan', 'target_version'].every(key => typeof deferred[key] === 'string' && deferred[key].trim()), 'UI 产品须明确下游前端责任、Ticket、验证计划和目标版本');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function assertScopeSlice(contract, { root = ROOT } = {}) {
|
|
87
|
+
if (!loadExecutionScope(root)) return;
|
|
88
|
+
check(contract.backend?.status === 'required' && contract.frontend?.status === 'not-applicable', '本职责仅允许后端 Slice;产品 UI 设计不能混作前端实现批准');
|
|
89
|
+
check(!(contract.common?.impacted_areas || []).some(x => ['ui', 'frontend'].includes(x)), 'Slice 仍包含生产前端实现影响');
|
|
90
|
+
const roles = (contract.work_units || []).map(unit => unit.role_id);
|
|
91
|
+
check(!roles.includes('role.frontend-engineer'), '禁止派发前端实现');
|
|
92
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { readFileSync } from './validation-phase.mjs';
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { parseDocument } from "../vendor/yaml.mjs";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assertTrackingTransition } from './stage-tracking.mjs';
|
|
2
|
+
import { assertScopeTransition, assertScopeImpacts, assertScopeWorkUnit, scopedNextRoutes } from './lifecycle-execution-scope.mjs';
|
|
1
3
|
// Wire capability marker: profile-local readiness rules are not interchangeable.
|
|
2
4
|
export const SLICE_REPOSITORY_PREPARATION_PROTOCOL = 1;
|
|
3
5
|
import { createHash } from "node:crypto";
|
|
@@ -43,7 +45,8 @@ const NEXT_ROUTES = deepFreeze({
|
|
|
43
45
|
[TICKET_DECOMPOSITION_WORK_UNIT]: [IMPLEMENTATION_WORK_UNIT],
|
|
44
46
|
[IMPLEMENTATION_WORK_UNIT]: ["work-unit.frontend-implementation-verification", "work-unit.code-review"],
|
|
45
47
|
"work-unit.frontend-implementation-verification": ["work-unit.code-review"],
|
|
46
|
-
"work-unit.code-review": ["work-unit.release-and-retrospective", IMPLEMENTATION_WORK_UNIT],
|
|
48
|
+
"work-unit.code-review": ["work-unit.release-and-retrospective", IMPLEMENTATION_WORK_UNIT, "work-unit.backend-delivery"],
|
|
49
|
+
"work-unit.backend-delivery": [],
|
|
47
50
|
"work-unit.release-and-retrospective": [],
|
|
48
51
|
});
|
|
49
52
|
|
|
@@ -249,7 +252,11 @@ function validateTicketReference(ref, trackerKind) {
|
|
|
249
252
|
* by `validateWorkflowExecutionResult` before this function is called.
|
|
250
253
|
*/
|
|
251
254
|
export function validateNextRoute(currentWorkUnit, nextRoute, decisionState, options = {}) {
|
|
252
|
-
|
|
255
|
+
try { assertTrackingTransition(currentWorkUnit, nextRoute, decisionState, { root: options.root || ROOT }); }
|
|
256
|
+
catch (error) { return blockedResult(['stage-tracking-blocked'], [error.message]); }
|
|
257
|
+
try { assertScopeTransition(currentWorkUnit, nextRoute, decisionState, options); }
|
|
258
|
+
catch (error) { return blockedResult(['execution-scope-blocked'], [error.message]); }
|
|
259
|
+
if (currentWorkUnit === "work-unit.code-review" && ["work-unit.release-and-retrospective", "work-unit.backend-delivery"].includes(nextRoute)) {
|
|
253
260
|
try {
|
|
254
261
|
if (decisionState?.review_input?.scope_kind !== "change") throw new Error("只读基线审计不能关闭实现审查");
|
|
255
262
|
validateBackendReview(decisionState, { root: options.root || ROOT });
|
|
@@ -262,7 +269,7 @@ export function validateNextRoute(currentWorkUnit, nextRoute, decisionState, opt
|
|
|
262
269
|
try { enforceFrontendDelivery(decisionState, { root: options.root, phase: nextRoute === IMPLEMENTATION_WORK_UNIT ? "implementation" : "inputs" }); }
|
|
263
270
|
catch (error) { return blockedResult(["frontend-delivery-blocked"], [error.message]); }
|
|
264
271
|
}
|
|
265
|
-
const routes = NEXT_ROUTES[currentWorkUnit];
|
|
272
|
+
const routes = NEXT_ROUTES[currentWorkUnit] && scopedNextRoutes(currentWorkUnit, NEXT_ROUTES[currentWorkUnit], options);
|
|
266
273
|
if (!routes) return blockedResult([BLOCKING_SIGNALS.invalidRoute], ["known_current_work_unit"]);
|
|
267
274
|
if (nextRoute === REPOSITORY_PREPARATION_WORK_UNIT && currentWorkUnit !== "work-unit.technical-analysis") {
|
|
268
275
|
return blockedResult([BLOCKING_SIGNALS.technicalAnalysisRequired], ["work-unit.technical-analysis predecessor"]);
|
|
@@ -299,6 +306,8 @@ export function validateNextRoute(currentWorkUnit, nextRoute, decisionState, opt
|
|
|
299
306
|
* generated and verified scaffold. Unaffected roles need an explicit reason.
|
|
300
307
|
*/
|
|
301
308
|
export function validateImplementationRepositoriesReady(state, { exists = existsSync, read = (ref) => readFileSync(ref, "utf8"), root = ROOT } = {}) {
|
|
309
|
+
try { assertScopeImpacts(state, { root }); }
|
|
310
|
+
catch (error) { return blockedResult(['execution-scope-blocked'], [error.message]); }
|
|
302
311
|
const preparation = state?.implementation_repository_preparation;
|
|
303
312
|
const impacts = state?.delivery_impacts;
|
|
304
313
|
const signals = [];
|
|
@@ -411,6 +420,13 @@ function validateDecisionBoundary(workUnit, state, options) {
|
|
|
411
420
|
* `exists` is injectable so external adapters can resolve their own tracker refs.
|
|
412
421
|
*/
|
|
413
422
|
export function validateTicketFormalization(state, { exists = existsSync, read = (ref) => readFileSync(ref, "utf8"), ...decisionOptions } = {}) {
|
|
423
|
+
if ((state?.tracker_kind ?? "local-markdown") === "local-markdown" && state?.vertical_slice_ticket?.ref && isReadable(state.vertical_slice_ticket.ref, exists)) {
|
|
424
|
+
try {
|
|
425
|
+
const header = read(state.vertical_slice_ticket.ref).match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
426
|
+
const kind = header ? parseDocument(header[1], { uniqueKeys: true }).toJS({ maxAliasCount: 0 })?.kind : null;
|
|
427
|
+
if (kind === "stage-work-item") return blockedResult(["stage-work-item-not-implementable"], ["vertical-slice-ticket required"]);
|
|
428
|
+
} catch { return blockedResult(["ticket-content-unreadable"], ["readable vertical slice content"]); }
|
|
429
|
+
}
|
|
414
430
|
try { enforceFrontendDelivery(state, { root: decisionOptions.root, phase: "implementation" }); }
|
|
415
431
|
catch (error) { return blockedResult(["frontend-delivery-blocked"], [error.message]); }
|
|
416
432
|
const repositoryResult = validateImplementationRepositoriesReady(state, { exists, read, ...decisionOptions });
|
|
@@ -532,6 +548,8 @@ export function validateTicketFormalization(state, { exists = existsSync, read =
|
|
|
532
548
|
* Validate the complete implementation entry seam after ready-for-agent promotion.
|
|
533
549
|
*/
|
|
534
550
|
export function validateImplementationEntry(state, options = {}) {
|
|
551
|
+
try { assertScopeWorkUnit(IMPLEMENTATION_WORK_UNIT, options); assertScopeImpacts(state, options); }
|
|
552
|
+
catch (error) { return blockedResult(['execution-scope-blocked'], [error.message]); }
|
|
535
553
|
const ticketResult = validateTicketFormalization(state, options);
|
|
536
554
|
if (ticketResult.result === "blocked") return ticketResult;
|
|
537
555
|
if (state?.predecessor_work_unit !== TICKET_DECOMPOSITION_WORK_UNIT) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { existsSync, lstatSync, readFileSync } from "./validation-phase.mjs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { parseAllDocuments, parseDocument } from "../vendor/yaml.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from
|
|
1
|
+
import { existsSync, readFileSync } from './validation-phase.mjs';
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseDocument } from "../vendor/yaml.mjs";
|
|
4
4
|
import { nestedSkillPaths, OBSOLETE, PROJECTION_ROOTS, ROOT, unregisteredNestedSkillPaths } from "./skill-supply-chain.mjs";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import {readContractSource,contractSourceMetadata} from './contract-source.mjs';
|
|
1
2
|
import {sliceRepositories} from './slice-repositories.mjs';
|
|
2
3
|
import { sliceCheckApplicability } from './slice-applicability.mjs';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import fs from '
|
|
5
|
+
import fs, {withValidationPhase} from './validation-phase.mjs';
|
|
5
6
|
import { stringify } from '../vendor/yaml.mjs';
|
|
6
7
|
import { safe, hash, digest, write } from './strategic-handoff-io.mjs';
|
|
7
8
|
import { compileDefaultImplementationContract, digestDocument, loadCompilerContract } from './implementation-contract-compiler.mjs';
|
|
@@ -9,11 +10,7 @@ import { normalizeSliceContract, parseSliceYaml, readSliceSources, resolveSliceB
|
|
|
9
10
|
|
|
10
11
|
const object=value=>value && typeof value==='object' && !Array.isArray(value);
|
|
11
12
|
const same=(a,b)=>digest(a)===digest(b);
|
|
12
|
-
|
|
13
|
-
const front=/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
|
|
14
|
-
if(front)return parseSliceYaml(front[1]);
|
|
15
|
-
return (/^\s*[{[]/.test(text)||/^[A-Za-z_][\w-]*:\s/m.test(text)&&!/^#/m.test(text))?parseSliceYaml(text):{};
|
|
16
|
-
}
|
|
13
|
+
const metadata=contractSourceMetadata;
|
|
17
14
|
function mergeFacts(left,right,path,report) {
|
|
18
15
|
if(left===undefined)return structuredClone(right);
|
|
19
16
|
if(right===undefined)return structuredClone(left);
|
|
@@ -43,7 +40,15 @@ function lifecycleSources(checkpoint,report) {
|
|
|
43
40
|
return result;
|
|
44
41
|
}
|
|
45
42
|
/** sources select existing assets; refinements contain ONLY new slice facts, never an approved contract. */
|
|
46
|
-
export function prepareSliceImplementationContract(
|
|
43
|
+
export function prepareSliceImplementationContract(options={}) {
|
|
44
|
+
let result;
|
|
45
|
+
try{return withValidationPhase({root:options.root,purpose:'slice-prepare',slice_id:options.ticket_ref,readOnly:true,signal:options.signal},()=>result=prepare(options));}
|
|
46
|
+
catch(error){
|
|
47
|
+
if(!result||!['VALIDATION_INPUT_CHANGED','VALIDATION_CANCELLED'].includes(error.code))throw error;
|
|
48
|
+
result.slice_contract.status='blocked';result.report.blockers.push({code:error.code==='VALIDATION_INPUT_CHANGED'?'SLICE_INPUT_CONFLICT':error.code,field:'slice',source_ref:null,reason:`${error.message.includes('compiler-contract.yaml')?'编译规则来源冲突':'生成期间来源变化'}: ${error.message}`,responsibility:'professional-review',recovery:'重新读取当前来源并准备新草案'});return result;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function prepare({root=process.cwd(),ticket_ref,checkpoint_ref,sources={},refinements={},approved_slice}={}) {
|
|
47
52
|
const started=performance.now();
|
|
48
53
|
const report={read_only:true,blockers:[],sources:{},provenance:[],reason_chains:{},checks:[]};
|
|
49
54
|
const snapshots=new Map();
|
|
@@ -52,6 +57,7 @@ export function prepareSliceImplementationContract({root=process.cwd(),ticket_re
|
|
|
52
57
|
try {
|
|
53
58
|
let ticketText='',ticket={},saved={};
|
|
54
59
|
try {ticketText=read(ticket_ref).toString('utf8');ticket=metadata(ticketText);}catch(error){report.blockers.push({code:'SLICE_SOURCE_UNREADABLE',field:'basis.ticket',source_ref:ticket_ref,reason:error.message});}
|
|
60
|
+
if (ticket.kind === 'stage-work-item' || /\/work-items\//.test(ticket_ref || '')) report.blockers.push({code:'STAGE_WORK_ITEM_NOT_IMPLEMENTABLE',field:'basis.ticket',reason:'stage-work-item 不能作为实现 Ticket'});
|
|
55
61
|
if(checkpoint_ref)try {saved=parseSliceYaml(read(checkpoint_ref));}catch(error){report.blockers.push({code:'SLICE_SOURCE_UNREADABLE',field:'checkpoint',source_ref:checkpoint_ref,reason:error.message});}
|
|
56
62
|
const fromTicket=ticket.slice_implementation || {};
|
|
57
63
|
const facts=mergeFacts(saved.slice_implementation || {},fromTicket,'slice',report);
|
|
@@ -73,13 +79,9 @@ export function prepareSliceImplementationContract({root=process.cwd(),ticket_re
|
|
|
73
79
|
try {
|
|
74
80
|
if(typeof value==='string' && Object.hasOwn(selected,value)) {basis[key]=value;continue;}
|
|
75
81
|
const binding=typeof value==='string'?{ref:value}:value;
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const sourceVersion=data.requirement_version??data.version??data.contract_version;
|
|
80
|
-
if(binding.version!==undefined&&sourceVersion!==undefined&&String(binding.version)!==String(sourceVersion))throw new TypeError(`来源版本冲突: ${key}`);
|
|
81
|
-
const version=binding.version||data.requirement_version||data.version||data.contract_version;
|
|
82
|
-
basis[key]={ref:binding.ref,digest:rawDigest,...(version?{version:String(version)}:{}),...(binding.approval_ref?{approval_ref:binding.approval_ref}:{})};
|
|
82
|
+
const source=readContractSource(binding,{root,read});
|
|
83
|
+
const rawDigest=source.binding.digest;
|
|
84
|
+
basis[key]=source.binding;
|
|
83
85
|
if(seen.has(binding.ref)) {
|
|
84
86
|
const previous=seen.get(binding.ref);
|
|
85
87
|
if(!same(basis[key],basis[previous]))throw new TypeError(`来源绑定冲突: ${key}/${previous}`);
|