aimship 0.1.0-preview.19 → 0.1.0-preview.21
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/CONTRIBUTING.md +3 -3
- package/README.md +6 -4
- package/README.zh-CN.md +6 -4
- package/docs/cli.md +41 -12
- package/docs/examples.md +4 -0
- package/docs/guide.md +16 -12
- package/docs/guide.zh-CN.md +16 -12
- package/lib/approvals.mjs +47 -126
- package/lib/cli.mjs +10 -7
- package/lib/delivery.mjs +39 -37
- package/lib/documents.mjs +9 -7
- package/lib/domains.mjs +123 -0
- package/lib/execution.mjs +17 -0
- package/lib/language.mjs +2 -1
- package/lib/operations.mjs +30 -0
- package/lib/projections.mjs +73 -0
- package/lib/protocol.mjs +5 -5
- package/lib/state.mjs +30 -11
- package/lib/view.mjs +160 -92
- package/package.json +2 -2
- package/skills/aimship/SKILL.md +2 -2
- package/skills/aimship/references/core.md +21 -7
- package/skills/aimship/references/document-patterns.md +16 -2
- package/skills/aimship/references/document-review.md +2 -2
- package/skills/aimship/references/recovery.md +4 -4
- package/skills/aimship/references/review-method.md +7 -0
- package/skills/aimship/references/reviews/batch.md +21 -0
- package/skills/aimship/references/reviews/overall.md +21 -0
- package/skills/aimship/references/reviews/recheck.md +21 -0
- package/skills/aimship/references/routing.md +16 -0
- package/skills/aimship/references/sources-and-changes.md +1 -1
- package/skills/aimship/references/stages/contract.md +12 -14
- package/skills/aimship/references/stages/diagnose.md +12 -14
- package/skills/aimship/references/stages/frame.md +11 -11
- package/skills/aimship/references/stages/handoff.md +12 -12
- package/skills/aimship/references/stages/implement.md +14 -14
- package/skills/aimship/references/stages/plan.md +10 -12
- package/skills/aimship/references/stages/prototype.md +10 -14
- package/skills/aimship/references/stages/requirements.md +10 -14
- package/skills/aimship/references/stages/review.md +14 -16
- package/skills/aimship/references/stages/tech-design.md +15 -17
- package/skills/aimship/references/stages/test-design.md +10 -12
- package/skills/aimship/references/stages/verify.md +13 -13
- package/skills/aimship/references/templates/change-log.md +2 -2
- package/skills/aimship/references/templates/diagnosis.md +7 -7
- package/skills/aimship/references/templates/handoff.md +6 -6
- package/skills/aimship/references/templates/plan.md +13 -7
- package/skills/aimship/references/templates/prd.md +7 -7
- package/skills/aimship/references/templates/review.md +11 -6
- package/skills/aimship/references/templates/tech-design.md +7 -7
- package/skills/aimship/references/templates/testing.md +14 -8
package/lib/domains.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Delivery domains share facts and dependencies; they do not share one status enum.
|
|
2
|
+
import {fail} from './io.mjs';
|
|
3
|
+
const present=x=>typeof x==='string'&&x.trim().length>0;
|
|
4
|
+
const ok=r=>r?.progress==='done'&&!['needs-review','unknown'].includes(r.applicability);
|
|
5
|
+
const passing=r=>ok(r)&&r.result==='passed';
|
|
6
|
+
const development=['requirements','tech-design','contract','test-design','plan','implement'];
|
|
7
|
+
const execution=['implement','review','verify','handoff'];
|
|
8
|
+
const reject=message=>fail('INVALID_STATE',message,'Read the current domain facts and record the actual relationship.');
|
|
9
|
+
export const phaseOf=run=>run.stage==='review'&&run.reviewScope==='batch'?'implement':run.stage;
|
|
10
|
+
export function phaseOrder(state){
|
|
11
|
+
const selected=[...new Set(state.stageRuns.map(phaseOf))];if(state.deliveryPolicy.partialRef)return selected;
|
|
12
|
+
const order=['requirements','tech-design','contract','test-design','plan','implement','review','verify','handoff'];
|
|
13
|
+
if(state.documentPolicy.interface==='none'&&state.documentPolicy.databases?.length===0&&!selected.includes('contract'))order.splice(2,1);
|
|
14
|
+
for(const stage of selected.filter(s=>!order.includes(s))){const at=state.stageRuns.findIndex(r=>phaseOf(r)===stage),next=state.stageRuns.slice(at+1).map(phaseOf).find(s=>order.includes(s)),prior=state.stageRuns.slice(0,at).map(phaseOf).findLast(s=>order.includes(s));order.splice(next?order.indexOf(next):prior?order.indexOf(prior)+1:0,0,stage);}return order;
|
|
15
|
+
}
|
|
16
|
+
export function validateDomains(state){
|
|
17
|
+
if(!Array.isArray(state.batches))reject('batches must be an array.');
|
|
18
|
+
const ids=new Set();for(const b of state.batches){if(!/^B-\d{3,}$/.test(b.id)||ids.has(b.id)||!present(b.definitionRef)||!Array.isArray(b.taskRefs)||!Array.isArray(b.dependsOn))reject('Each batch needs a unique B id, definitionRef, taskRefs and dependsOn.');ids.add(b.id);}
|
|
19
|
+
const visiting=new Set(),visited=new Set();const visit=id=>{if(visiting.has(id))reject('Batch dependencies must not contain a cycle.');if(visited.has(id))return;visiting.add(id);const b=state.batches.find(x=>x.id===id);for(const dep of b.dependsOn){if(!ids.has(dep))reject('Unknown batch dependency '+dep);visit(dep);}visiting.delete(id);visited.add(id);};ids.forEach(visit);
|
|
20
|
+
for(const [i,r] of state.stageRuns.entries()){
|
|
21
|
+
if(r.reviewScope!==undefined&&(r.stage!=='review'||!['batch','overall'].includes(r.reviewScope)))reject('reviewScope belongs to reviews.');
|
|
22
|
+
if(r.overall!==undefined)reject('Use reviewScope, not the former overall flag.');
|
|
23
|
+
if(r.batchId!==undefined&&!ids.has(r.batchId))reject('Unknown batchId on '+r.id);
|
|
24
|
+
if(r.stage==='implement'&&r.progress!=='planned'){
|
|
25
|
+
if(!['batch','repair'].includes(r.workType))reject('Implementation needs its actual workType.');
|
|
26
|
+
if(r.workType==='batch'&&!r.batchId)reject('Business implementation needs batchId.');
|
|
27
|
+
}
|
|
28
|
+
if(r.stage==='review'&&r.progress!=='planned'){
|
|
29
|
+
if(!['batch','overall'].includes(r.reviewScope))reject('Review needs explicit batch/overall scope.');
|
|
30
|
+
if(r.reviewScope==='batch'&&!r.batchId)reject('Batch review needs batchId.');
|
|
31
|
+
if((!state.deliveryPolicy.partialRef&&!r.reviewOf?.length)||(r.reviewOf??[]).some(id=>!state.stageRuns.slice(0,i).some(p=>p.id===id&&p.stage==='implement')))reject('Review must cover preceding implementation records.');
|
|
32
|
+
}
|
|
33
|
+
if(r.progress==='done'&&['implement','review','verify'].includes(r.stage)&&!/^[a-f0-9]{64}$/.test(r.objectKey??''))reject('Completed engineering records need their actual objectKey digest.');
|
|
34
|
+
if(r.stage==='verify'&&r.progress!=='planned'&&(!r.coverageRefs?.length||!r.coverageRefs.every(present)))reject('Formal execution needs coverageRefs.');
|
|
35
|
+
if(Array.isArray(r.coverageResults)&&r.coverageResults.some(x=>r.result==='passed'&&x.result!=='passed'||r.result==='failed'&&x.result==='passed'))reject('Run and coverage results conflict; record mixed results explicitly.');
|
|
36
|
+
if(r.coverageResults!==undefined&&(!Array.isArray(r.coverageResults)||r.coverageResults.some(x=>!r.coverageRefs?.includes(x.ref)||!['passed','failed','mixed','unknown','not-run'].includes(x.result))||new Set(r.coverageResults.map(x=>x.ref)).size!==r.coverageResults.length))reject('coverageResults must identify distinct declared coverage and actual results.');
|
|
37
|
+
if(r.invalidCoverageRefs!==undefined&&(!Array.isArray(r.invalidCoverageRefs)||r.invalidCoverageRefs.some(ref=>!r.coverageRefs?.includes(ref))))reject('Invalid coverage applicability.');
|
|
38
|
+
if(r.assessment?.coverageRefs&&(!Array.isArray(r.assessment.coverageRefs)||r.assessment.coverageRefs.some(ref=>!r.coverageRefs?.includes(ref))))reject('Assessment coverage must belong to its RUN.');
|
|
39
|
+
if(r.assessment&&(!/^[a-f0-9]{64}$/.test(r.assessment.forObjectKey??'')||!present(r.assessment.ref)))reject('An applicability assessment needs its target object and evidence ref.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function coversReview(later,earlier){return later.stage==='review'&&earlier.stage==='review'&&later.historyResultRefs?.includes(earlier.resultRef)&&later.reviewScope===earlier.reviewScope&&(later.reviewScope==='overall'||later.batchId===earlier.batchId)&&((earlier.reviewOf?.length)?earlier.reviewOf.every(id=>later.reviewOf?.includes(id)):later.objectKey===earlier.objectKey&&later.scope===earlier.scope);}
|
|
43
|
+
function resolvesImplementation(later,earlier){return later.stage==='implement'&&earlier.stage==='implement'&&later.workType==='repair'&&later.batchId===earlier.batchId&&later.historyResultRefs?.includes(earlier.resultRef);}
|
|
44
|
+
const matchObject=(run,key,ref)=>!!key&&(run.objectKey===key||(run.assessment?.forObjectKey===key&&(!ref||!run.assessment.coverageRefs||run.assessment.coverageRefs.includes(ref))));
|
|
45
|
+
export function batchStates(state,statuses){
|
|
46
|
+
const ready=id=>statuses.find(s=>s.id===id)?.releaseReady;
|
|
47
|
+
return state.batches.map(b=>{
|
|
48
|
+
const work=state.stageRuns.filter(r=>r.stage==='implement'&&r.batchId===b.id&&r.progress!=='planned');
|
|
49
|
+
const reviews=state.stageRuns.filter(r=>r.stage==='review'&&r.reviewScope==='batch'&&r.batchId===b.id&&r.progress!=='planned');
|
|
50
|
+
const review=reviews.findLast(r=>passing(r)&&ready(r.id)&&work.length&&work.every(w=>r.reviewOf?.includes(w.id)&&state.stageRuns.indexOf(r)>state.stageRuns.indexOf(w)));
|
|
51
|
+
const unresolved=reviews.some(r=>!statuses.find(s=>s.id===r.id)?.resolvedBy&&(!ready(r.id)||r.result!=='passed'));
|
|
52
|
+
const implReady=work.length>0&&work.every(r=>ready(r.id));
|
|
53
|
+
const status=work.some(r=>['needs-review','unknown'].includes(r.applicability))?'needs-review':work.some(r=>r.progress==='blocked')?'blocked':review&&!unresolved&&implReady?'complete':unresolved?'needs-repair':implReady?'await-review':work.length?'active':'planned';
|
|
54
|
+
return {...b,status,implementationIds:work.map(r=>r.id),reviewIds:reviews.map(r=>r.id),reviewId:review?.id??null};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export function overallReview(state,statuses){
|
|
58
|
+
const work=state.stageRuns.filter(r=>r.stage==='implement'&&r.progress!=='planned');
|
|
59
|
+
if(state.stageRuns.some(r=>r.stage==='review'&&r.progress!=='planned'&&!statuses.find(s=>s.id===r.id)?.resolvedBy&&(!statuses.find(s=>s.id===r.id)?.releaseReady||r.result!=='passed')))return null;
|
|
60
|
+
return state.stageRuns.findLast(r=>r.stage==='review'&&r.reviewScope==='overall'&&passing(r)&&statuses.find(s=>s.id===r.id)?.releaseReady&&work.length&&work.every(w=>r.reviewOf?.includes(w.id)&&state.stageRuns.indexOf(r)>state.stageRuns.indexOf(w)))??null;
|
|
61
|
+
}
|
|
62
|
+
export function verificationState(state,statuses,expected=[]){
|
|
63
|
+
const overall=overallReview(state,statuses),target=overall?.objectKey;
|
|
64
|
+
const all=state.stageRuns.filter(r=>r.stage==='verify'&&r.progress!=='planned');
|
|
65
|
+
const units=new Map();for(const r of all){for(const ref of (r.coverageRefs??[]).filter(ref=>expected.includes(ref))){let list=units.get(ref);if(!list)units.set(ref,list=[]);list.push(r);}}
|
|
66
|
+
for(const ref of expected)if(!units.has(ref))units.set(ref,[]);
|
|
67
|
+
const coverage=[...units].map(([ref,records])=>{
|
|
68
|
+
let effective=null;const pending=new Map();
|
|
69
|
+
for(const r of records){
|
|
70
|
+
const result=r.coverageResults?.find(x=>x.ref===ref)?.result??r.result??'unknown';
|
|
71
|
+
if(result!=='passed')pending.set(r.resultRef,{run:r,result});
|
|
72
|
+
if(!ok(r)||r.invalidCoverageRefs?.includes(ref)||!expected.includes(ref)||!statuses.find(s=>s.id===r.id)?.ready||!matchObject(r,target,ref))continue;
|
|
73
|
+
if(result==='passed')for(const old of r.historyResultRefs??[])pending.delete(old);
|
|
74
|
+
effective={run:r,result};
|
|
75
|
+
}
|
|
76
|
+
if(pending.size)effective=[...pending.values()].at(-1);
|
|
77
|
+
if(effective?.result==='passed'&&(effective.run.invalidCoverageRefs?.includes(ref)||!ok(effective.run)||!matchObject(effective.run,target,ref)))effective=null;
|
|
78
|
+
return {ref,runId:effective?.run.id??null,resultRef:effective?.run.resultRef??null,result:effective?.result??'not-run'};
|
|
79
|
+
});
|
|
80
|
+
return {overallId:overall?.id??null,objectKey:target??null,coverage,passed:!!overall&&coverage.length>0&&coverage.every(c=>c.result==='passed'),runIds:[...new Set(coverage.map(c=>c.runId).filter(Boolean))]};
|
|
81
|
+
}
|
|
82
|
+
export function evaluateRuns(state,documents,map,{limitedScope=false,expectedCoverage=[],evidenceIssues=[]}={}){
|
|
83
|
+
const statuses=[];
|
|
84
|
+
const approved=kind=>{const docs=state.documents.filter(d=>d.kind===kind);return docs.length>0&&docs.every(d=>['approved','adopted'].includes(documents.find(x=>x.path===d.path)?.status));};
|
|
85
|
+
for(const [i,run] of state.stageRuns.entries()){
|
|
86
|
+
const before=state.stageRuns.slice(0,i),spec=map.stages[run.stage];
|
|
87
|
+
const historical=(!spec?.humanReview||state.documents.filter(d=>run.outputRefs.includes(d.path)&&d.kind===(spec.kind==='requirement'?'prd':spec.kind)).every(d=>['approved','adopted'].includes(documents.find(x=>x.path===d.path)?.status)))&&run.progress==='done'&&run.approvalBasis?.length&&run.approvalBasis.every(b=>state.reviewHistory.some(h=>h.id===b.recordId&&h.document===b.document&&h.digest===b.digest&&(['approved','editorial'].includes(h.kind)||(h.kind==='self-check'&&h.result==='passed'&&(state.documents.find(d=>d.path===h.document)?.mode==='input'||state.goalHistory?.some(g=>g.goalRef?.split('#')[0]===h.document))))&&!evidenceIssues.includes(h.id)));
|
|
88
|
+
const required=spec?.humanReview?state.documents.filter(d=>d.kind===(spec.kind==='requirement'?'prd':spec.kind)||(run.stage==='contract'&&d.kind==='sql')).map(d=>d.path):[];
|
|
89
|
+
const waiting=historical?[]:required.filter(p=>!['approved','adopted'].includes(documents.find(d=>d.path===p)?.status));
|
|
90
|
+
const designOrder=['requirements','prototype','tech-design','contract','test-design','plan'],rank=designOrder.indexOf(run.stage),predecessor=rank>0?before.findLast(r=>designOrder.indexOf(r.stage)>=0&&designOrder.indexOf(r.stage)<rank):null;
|
|
91
|
+
const deps=run.dependsOn??(predecessor?[predecessor.id]:[]),blocked=[];
|
|
92
|
+
for(const id of deps){const prior=before.find(r=>r.id===id),s=statuses.find(s=>s.id===id);if(!historical&&!s?.releaseReady&&!coversReview(run,prior??{})&&!resolvesImplementation(run,prior??{}))blocked.push(id);}
|
|
93
|
+
const needs=[];if(!historical){if(['prototype','tech-design','contract','test-design','plan','implement'].includes(run.stage))needs.push('prd');if(['contract','test-design','plan','implement'].includes(run.stage))needs.push('tech-design');if(['plan','implement'].includes(run.stage)&&!limitedScope)needs.push('testing');if(run.stage==='implement')needs.push('plan');if(run.stage==='verify'&&!limitedScope)needs.push('testing');if(run.stage==='review'&&!limitedScope)needs.push('prd','tech-design','plan');const prototype=before.findLast(r=>r.stage==='prototype')??state.stageRuns.find(r=>r.stage==='prototype'&&run.dependsOn?.includes(r.id));if(['tech-design','contract','test-design','plan','implement'].includes(run.stage)&&prototype){needs.push('prototype');if(!statuses.find(s=>s.id===prototype.id)?.releaseReady)blocked.push('entry:selected-prototype');}if(['test-design','plan','implement'].includes(run.stage)){if(state.documentPolicy.interface!==null&&state.documentPolicy.interface!=='none')needs.push('contract');if(state.documentPolicy.databases?.length)needs.push('sql');}for(const kind of needs)if(!approved(kind))blocked.push('approval:'+kind);}
|
|
94
|
+
if(!limitedScope){
|
|
95
|
+
const subset={...state,stageRuns:before};const batches=batchStates(subset,statuses);
|
|
96
|
+
if(run.stage==='implement'){
|
|
97
|
+
const plan=before.findLast(r=>r.stage==='plan')??state.stageRuns.find(r=>r.stage==='plan');if(!historical&&plan&&!statuses.find(s=>s.id===plan.id)?.releaseReady)blocked.push('entry:selected-plan');
|
|
98
|
+
const b=state.batches.find(b=>b.id===run.batchId);if(b)for(const id of b.dependsOn)if(batches.find(b=>b.id===id)?.status!=='complete')blocked.push('batch:'+id);
|
|
99
|
+
if(run.workType==='repair'&&!run.batchId&&!run.repairOf?.length)blocked.push('repair:scope');
|
|
100
|
+
}
|
|
101
|
+
if(run.stage==='review'){
|
|
102
|
+
const work=before.filter(r=>r.stage==='implement'&&r.progress!=='planned'&&(run.reviewScope==='overall'||r.batchId===run.batchId));
|
|
103
|
+
if(!work.length||work.some(r=>!run.reviewOf?.includes(r.id)))blocked.push('entry:implementation-coverage');
|
|
104
|
+
for(const id of run.reviewOf??[])if(!statuses.find(s=>s.id===id)?.releaseReady)blocked.push('implementation:'+id);
|
|
105
|
+
if(run.reviewScope==='overall'&&batches.some(b=>b.status!=='complete'))blocked.push('entry:batch-reviews');
|
|
106
|
+
if(run.reviewScope==='batch'&&(run.reviewOf??[]).some(id=>before.find(r=>r.id===id)?.batchId!==run.batchId))blocked.push('entry:batch-scope');
|
|
107
|
+
}
|
|
108
|
+
if(['verify','handoff'].includes(run.stage)&&!historical){
|
|
109
|
+
const overall=overallReview(subset,statuses);if(!overall)blocked.push('entry:passing-overall-review');
|
|
110
|
+
if(run.stage==='verify'&&run.progress!=='planned'&&(run.coverageRefs??[]).some(ref=>!expectedCoverage.includes(ref)))blocked.push('entry:declared-test-coverage');
|
|
111
|
+
if(run.stage==='verify'&&run.progress!=='planned'&&overall&&!matchObject(run,overall.objectKey))blocked.push('entry:reviewed-object');
|
|
112
|
+
if(run.stage==='handoff'&&!verificationState(subset,statuses,expectedCoverage).passed)blocked.push('entry:verification');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const ready=ok(run)&&!blocked.length&&!waiting.length&&(!spec?.humanReview||required.length>0);
|
|
116
|
+
const releaseReady=ready&&(!['implement','review'].includes(run.stage)||run.result==='passed');
|
|
117
|
+
const status={id:run.id,stage:run.stage,progress:run.progress,applicability:run.applicability,documents:required,waiting,dependsOn:deps,blockedBy:blocked,ready,releaseReady};statuses.push(status);
|
|
118
|
+
if(releaseReady)for(const older of statuses.slice(0,-1)){const old=state.stageRuns.find(r=>r.id===older.id);if(coversReview(run,old)||resolvesImplementation(run,old)){older.resolvedBy=run.id;older.releaseReady=true;}}
|
|
119
|
+
}
|
|
120
|
+
const batches=batchStates(state,statuses),verification=verificationState(state,statuses,expectedCoverage),overall=overallReview(state,statuses);
|
|
121
|
+
for(const r of state.stageRuns.filter(r=>r.stage==='verify'&&r.progress==='done')){const effective=verification.coverage.filter(c=>(r.coverageRefs??[]).includes(c.ref));if(effective.length===(r.coverageRefs??[]).length&&effective.length&&effective.every(c=>c.result==='passed'&&c.runId!==r.id&&state.stageRuns.find(x=>x.id===c.runId)?.historyResultRefs?.includes(r.resultRef)))statuses.find(s=>s.id===r.id).resolvedBy=[...new Set(effective.map(c=>c.runId))];}
|
|
122
|
+
return {runs:statuses,batches,verification,overall:overall?{id:overall.id,objectKey:overall.objectKey,resultRef:overall.resultRef}:null};
|
|
123
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Advisory work classification. Every implementation still participates in review gates.
|
|
2
|
+
export function implementationType(state,run){return run.stage==='implement'?(run.workType??'unclassified'):null;}
|
|
3
|
+
export function implementationSummary(state) {
|
|
4
|
+
const runs=state.stageRuns.filter(r=>r.stage==='implement');
|
|
5
|
+
return Object.fromEntries(['batch','repair','unclassified'].map(kind=>[kind,runs.filter(r=>implementationType(state,r)===kind)]));
|
|
6
|
+
}
|
|
7
|
+
export function resolvedRun(state,review,run){return review.runs.find(r=>r.id===run.id)?.resolvedBy??null;}
|
|
8
|
+
export function implementationReviewScope(state,run) {
|
|
9
|
+
if(implementationType(state,run)==='batch')return 'batch';
|
|
10
|
+
if(implementationType(state,run)==='repair') {
|
|
11
|
+
const basis=(run.repairOf??[]).map(id=>state.stageRuns.find(r=>r.id===id));
|
|
12
|
+
if(basis.some(r=>r?.stage==='review' && r.reviewScope==='overall'))return 'overall-recheck';
|
|
13
|
+
if(run.batchId)return 'recheck';
|
|
14
|
+
if(basis.length && basis.every(r=>r?.stage==='review' && r.reviewOf?.length))return 'recheck';
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
package/lib/language.mjs
CHANGED
|
@@ -4,7 +4,7 @@ export const text = (state, english, chinese) => languageOf(state) === 'zh-CN' ?
|
|
|
4
4
|
const labels = {
|
|
5
5
|
'not recorded':'未记录', unknown:'未知', null:'未知', true:'是', false:'否', none:'不适用', http:'HTTP', other:'其他',
|
|
6
6
|
technical:'技术与契约', unrecorded:'未分组', unavailable:'不可用', 'assess-outcome':'核对交付结论与接受结果', frame:'目标梳理', requirements:'需求', prototype:'原型', 'tech-design':'技术设计', contract:'接口与 SQL', 'test-design':'测试设计', plan:'实施计划', implement:'实施', review:'审查', verify:'验证', handoff:'交付验收', diagnose:'调查',
|
|
7
|
-
planned:'待开始', active:'进行中', blocked:'受阻', done:'已完成记录', passed:'通过', failed:'失败', 'not-run':'未执行', mixed:'部分通过', current:'当前有效', 'needs-review':'待复核',
|
|
7
|
+
complete:'本批闭环', 'needs-repair':'待修复复查', 'await-review':'待批审', planned:'待开始', active:'进行中', blocked:'受阻', done:'已完成记录', passed:'通过', failed:'失败', 'not-run':'未执行', mixed:'部分通过', current:'当前有效', 'needs-review':'待复核',
|
|
8
8
|
'in-progress':'进行中', met:'已达成', 'not-met':'未达成', 'not-recorded':'未记录', accepted:'已接受', declined:'未接受',
|
|
9
9
|
adopted:'已采用输入', 'input-basis-required':'采用依据待明确', approved:'已批准', pending:'待审核', 'self-check-required':'待自检', 'self-check-failed':'自检失败', 'changes-requested':'需修改', 'evidence-changed':'审批依据已变化',
|
|
10
10
|
'partial-scope':'明确限定范围', 'standard-complete':'标准交付已完成', 'ready-for-acceptance':'待验收', incomplete:'未完成',
|
|
@@ -29,6 +29,7 @@ const explanations = {
|
|
|
29
29
|
'Missing passing overall implementation review.':'缺少通过的整体实施审查。',
|
|
30
30
|
'Independent verification is not fully recorded as passed after overall review.':'整体审查之后的独立验证尚未完整记录为通过。',
|
|
31
31
|
'Handoff precedes verification.':'交付记录早于验证。',
|
|
32
|
+
'Business batches or their reviews remain unresolved.':'业务批次或其审查尚未闭环。',
|
|
32
33
|
'Unresolved changes remain.':'仍有未处理的变更。'
|
|
33
34
|
};
|
|
34
35
|
export function completionMessage(state,message) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {fail} from './io.mjs';
|
|
2
|
+
import {reviewStatus} from './approvals.mjs';
|
|
3
|
+
const controlled=['stageRuns','batches','reviewHistory','openChanges','outcome','currentRun'];
|
|
4
|
+
export async function applyOperations(directory,state,patch){
|
|
5
|
+
for(const key of controlled)if(Object.hasOwn(patch,key))fail('DOMAIN_OPERATION_REQUIRED',`${key} is managed by domain operations.`,'Submit the actual record through operations; do not replace domain arrays.');
|
|
6
|
+
if(patch.operations!==undefined&&!Array.isArray(patch.operations))fail('INVALID_STATE','operations must be an array.','Use the documented domain actions.');
|
|
7
|
+
for(const op of patch.operations??[]){
|
|
8
|
+
if(!op||typeof op!=='object')fail('INVALID_STATE','Invalid domain operation.','Read current state before updating.');
|
|
9
|
+
if(op.type==='register-batches'){
|
|
10
|
+
const status=await reviewStatus(directory,state),plan=status.documents.find(d=>state.documents.find(x=>x.path===d.path)?.kind==='plan');
|
|
11
|
+
if(plan?.status!=='approved')fail('REVIEW_CHECK','Approve the current plan before registering its batches.','Review the actual plan content.');
|
|
12
|
+
if(!Array.isArray(op.batches))fail('INVALID_STATE','register-batches needs batches.','Record stable B definitions.');
|
|
13
|
+
for(const b of op.batches){const old=state.batches.find(x=>x.id===b.id);if(old&&JSON.stringify({...old,planBasis:undefined})!==JSON.stringify({...b,planBasis:undefined})&&state.stageRuns.some(r=>r.batchId===b.id&&r.progress!=='planned')&&(!(old.planBasis?.digest!==plan.digest||old.planBasis?.version!==plan.version)||!state.openChanges.some(c=>c.impactRef&&c.affectedRefs?.some(ref=>[old.definitionRef,...old.taskRefs,b.definitionRef,...b.taskRefs].includes(ref)))))fail('REVIEW_CHECK','Started batch changes need a revised plan and impact record.','Preserve the batch identity and assess affected work.');const next={...b,planBasis:{document:plan.path,version:plan.version,digest:plan.digest}};state.batches=old?state.batches.map(x=>x.id===b.id?next:x):[...state.batches,next];}
|
|
14
|
+
}else if(op.type==='record-run'){
|
|
15
|
+
const run=structuredClone(op.run);if(!run||(run.approvalBasis!==undefined||run.invalidCoverageRefs!==undefined))fail('INVALID_STATE','record-run needs a run; approvalBasis is tool-managed.','Submit actual run facts.');
|
|
16
|
+
const old=state.stageRuns.find(x=>x.id===run.id);if(old?.progress==='done'&&JSON.stringify(old)!==JSON.stringify(run))fail('HISTORY_PROTECTED','Completed facts cannot be rewritten.','Record a new repair, recheck or retest; use set-applicability for current validity.');
|
|
17
|
+
if(old?.progress==='planned'&&run.progress!=='planned'&&run.dependsOn===undefined&&!['implement','review','verify','handoff'].includes(run.stage)){const prior=state.stageRuns.slice(0,state.stageRuns.indexOf(old)).filter(r=>!['implement','review','verify','handoff'].includes(r.stage)).at(-1);if(prior)run.dependsOn=[prior.id];}
|
|
18
|
+
state.stageRuns=old?state.stageRuns.map(x=>x.id===run.id?run:x):[...state.stageRuns,run];state.stageRuns=[...state.stageRuns.filter(r=>r.progress!=='planned'),...state.stageRuns.filter(r=>r.progress==='planned')];state.currentRun=run.id;
|
|
19
|
+
}else if(op.type==='remove-planned-run'){const r=state.stageRuns.find(r=>r.id===op.runId);if(!r||r.progress!=='planned'||state.stageRuns.some(x=>x.dependsOn?.includes(r.id)||x.reviewOf?.includes(r.id)))fail('HISTORY_PROTECTED','Only unstarted, unreferenced arrangements may be removed.','Preserve actual execution and dependencies.');state.stageRuns=state.stageRuns.filter(x=>x.id!==r.id);if(state.currentRun===r.id)state.currentRun=null;}else if(op.type==='record-review')state.reviewHistory.push(structuredClone(op.record));
|
|
20
|
+
else if(op.type==='record-change'){
|
|
21
|
+
const c=structuredClone(op.change),old=state.openChanges.find(x=>x.id===c?.id);if(old&&old.ref!==c.ref)fail('HISTORY_PROTECTED','Preserve the original change reference.','Append its actual disposition.');state.openChanges=old?state.openChanges.map(x=>x.id===c.id?c:x):[...state.openChanges,c];
|
|
22
|
+
}else if(op.type==='record-outcome')state.outcome={...state.outcome,...structuredClone(op.outcome)};
|
|
23
|
+
else if(op.type==='set-current')state.currentRun=op.runId;
|
|
24
|
+
else if(op.type==='set-applicability'){
|
|
25
|
+
const r=state.stageRuns.find(r=>r.id===op.runId);if(!r)fail('INVALID_STATE','Unknown run for assessment.','Read current work.');r.applicability=op.applicability;
|
|
26
|
+
if(op.applicability==='current'){if(!op.assessmentRef||!op.forObjectKey)fail('INVALID_STATE','An applicability recovery needs its target object and assessmentRef.','Record the actual assessment.');r.assessment={forObjectKey:op.forObjectKey,ref:op.assessmentRef,...(op.coverageRefs?{coverageRefs:structuredClone(op.coverageRefs)}:{})};if(r.stage==='verify'&&op.coverageRefs){if(!Array.isArray(op.coverageRefs)||op.coverageRefs.some(ref=>!r.coverageRefs.includes(ref)))fail('INVALID_STATE','Assessment must name declared coverage.','Assess the actual affected coverage.');r.invalidCoverageRefs=(r.invalidCoverageRefs??[]).filter(ref=>!op.coverageRefs.includes(ref));}}
|
|
27
|
+
}else fail('INVALID_STATE','Unknown domain operation '+op.type,'Use register-batches, record-run, remove-planned-run, record-review, record-change, record-outcome, set-current or set-applicability.');
|
|
28
|
+
}
|
|
29
|
+
return state;
|
|
30
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Finite generated summaries are views of delivery facts, never another ledger.
|
|
2
|
+
import {text, displayValue} from './language.mjs';
|
|
3
|
+
|
|
4
|
+
const titles={
|
|
5
|
+
plan:['5.1 Current batch progress (generated)','5.1 当前批次进度(由状态生成)'],
|
|
6
|
+
testing:['5.1 Current verification (generated)','5.1 当前验证结果(由状态生成)'],
|
|
7
|
+
review:['1.1 Current review (generated)','1.1 当前审查结论(由状态生成)']
|
|
8
|
+
};
|
|
9
|
+
const containers={
|
|
10
|
+
plan:['Task status and execution records','执行记录'],
|
|
11
|
+
testing:['Execution and retest records','执行记录'],
|
|
12
|
+
review:['Review basis and scope','审查依据']
|
|
13
|
+
};
|
|
14
|
+
const headingKey=value=>value.replace(/^\d+(?:\.\d+)*[.)、]?\s*/, '').trim().toLowerCase();
|
|
15
|
+
export const generatedKinds=Object.keys(titles);
|
|
16
|
+
// Ignore fenced examples and comments when locating an owned heading. Preserve
|
|
17
|
+
// byte offsets so stripping or replacing the body leaves AI prose untouched.
|
|
18
|
+
function headings(raw){
|
|
19
|
+
let fence=null,offset=0;const result=[];
|
|
20
|
+
const visible=raw.replace(/<!--[^]*?-->/g,m=>m.replace(/[^\n]/g,' '));
|
|
21
|
+
for(const line of visible.split('\n')){
|
|
22
|
+
const mark=/^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
23
|
+
if(mark){if(!fence)fence=mark[1];else if(mark[1][0]===fence[0]&&mark[1].length>=fence.length)fence=null;}
|
|
24
|
+
else if(!fence){const h=/^ {0,3}(#{1,6})[ \t]+(.+?)\s*#*$/.exec(line);if(h)result.push({level:h[1].length,title:h[2],start:offset,end:offset+line.length});}
|
|
25
|
+
offset+=line.length+1;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
export function generatedArea(raw,kind){
|
|
30
|
+
const all=headings(raw),matches=all.filter(h=>h.level===3&&titles[kind]?.includes(h.title));
|
|
31
|
+
if(matches.length!==1)return {count:matches.length};
|
|
32
|
+
const h=matches[0],parents=all.filter(n=>n.level===2&&containers[kind]?.some(title=>headingKey(title)===headingKey(n.title))),parent=all.filter(n=>n.start<h.start&&n.level<=2).at(-1);
|
|
33
|
+
if(parents.length!==1||parent!==parents[0])return {count:0,reason:'GENERATED_SECTION_MISPLACED'};
|
|
34
|
+
const end=all.find(n=>n.start>h.start&&n.level<=3)?.start??raw.length;
|
|
35
|
+
return {count:1,start:h.start,bodyStart:h.end,end,body:raw.slice(h.end,end),title:h.title};
|
|
36
|
+
}
|
|
37
|
+
export function strippedGenerated(raw,kind){
|
|
38
|
+
const area=generatedArea(raw,kind);return area.count===1?raw.slice(0,area.bodyStart)+'\n\n'+raw.slice(area.end):raw;
|
|
39
|
+
}
|
|
40
|
+
export function initialGeneratedBody(body){return ['', 'Pending state generation.','等待状态生成。'].includes(body.trim());}
|
|
41
|
+
const cell=x=>String(x??'—').replaceAll('|','\\|').replaceAll('\n',' ');
|
|
42
|
+
const reference=ref=>ref?`[${cell(ref).replaceAll('[','\\[').replaceAll(']','\\]')}](${ref.split('#')[0].replaceAll(' ','%20').replaceAll('(','%28').replaceAll(')','%29')})`:'—';
|
|
43
|
+
const list=refs=>refs?.length?refs.map(reference).join('; '):'—';
|
|
44
|
+
export function projectionText(state,review,kind){
|
|
45
|
+
const t=(en,zh)=>text(state,en,zh),value=x=>cell(displayValue(state,x));
|
|
46
|
+
const model=review.execution,run=id=>state.stageRuns.find(r=>r.id===id),status=id=>review.runs.find(r=>r.id===id);
|
|
47
|
+
const lines=[t(`Saved state revision: ${state.revision}.`,`保存状态修订:${state.revision}。`),''];
|
|
48
|
+
if(kind==='plan'){
|
|
49
|
+
lines.push(t('| Batch / TASK scope | Implementation | Batch review / repair | Result references | Remaining work or blocker |','| 批次/TASK范围 | 实施 | 批审/修复 | 结果引用 | 剩余工作或阻塞 |'),'| --- | --- | --- | --- | --- |');
|
|
50
|
+
for(const b of model.batches){
|
|
51
|
+
const work=b.implementationIds.map(run),reviews=b.reviewIds.map(run),last=reviews.filter(r=>!status(r.id)?.resolvedBy).at(-1),repairs=work.filter(r=>r.workType==='repair');
|
|
52
|
+
const implementation=!work.length?t('Not started','尚未开始'):work.every(r=>status(r.id)?.releaseReady)?t('Recorded implementation complete','实现已记录完成'):t('In progress or needs checking','进行中或待核对');
|
|
53
|
+
const detail=(last?`${value(last.result??'unknown')} · ${reference(last.resultRef)}`:t('Batch review pending','待批次审查'))+(repairs.length?t(`; repairs: ${repairs.length}`,`;修复工作:${repairs.length}`):'');
|
|
54
|
+
const blocked=work.flatMap(r=>status(r.id)?.blockedBy??[]);
|
|
55
|
+
lines.push(`| ${reference(b.definitionRef)} · ${list(b.taskRefs)} | ${implementation} | ${detail} | ${list(work.map(r=>r.resultRef).filter(Boolean))} | ${value(b.status)}${blocked.length?' · '+blocked.map(cell).join(', '):''} |`);
|
|
56
|
+
}
|
|
57
|
+
if(!model.batches.length)lines.push(t('| No approved batches registered | Not started | Not started | — | Register the approved plan batches when applicable. |','| 尚未登记已批准批次 | 尚未开始 | 尚未开始 | — | 适用时登记已批准计划中的批次。 |'));
|
|
58
|
+
}else if(kind==='testing'){
|
|
59
|
+
lines.push(t(`Current reviewed object: ${model.verification.objectKey??'not recorded'}.`,`当前已审对象:${model.verification.objectKey??'尚未记录'}。`),'',t('| Coverage | Current result | Effective RUN / evidence |','| 覆盖范围 | 当前结果 | 有效RUN/依据 |'),'| --- | --- | --- |');
|
|
60
|
+
for(const c of model.verification.coverage)lines.push(`| ${reference(c.ref)} | ${value(c.result)} | ${reference(c.resultRef)} |`);
|
|
61
|
+
if(!model.verification.coverage.length)lines.push(t('| No current coverage registered | Not run | — |','| 尚未登记当前覆盖 | 未执行 | — |'));
|
|
62
|
+
lines.push('',model.verification.passed?t('Current recorded coverage passed. Historical RUNs remain below.','当前已记录覆盖通过;历史RUN保留在下方。'):t('Verification is incomplete; unresolved or unexecuted coverage remains visible above.','验证尚未完成;未解决或未执行的覆盖保留在上表。'));
|
|
63
|
+
}else if(kind==='review'){
|
|
64
|
+
lines.push(t('| Scope | Current review | Result / applicability | Recheck or remaining scope |','| 范围 | 当前审查 | 结果/适用性 | 复查或剩余范围 |'),'| --- | --- | --- | --- |');
|
|
65
|
+
const groups=[...model.batches.map(b=>({key:b.id,batch:b,records:state.stageRuns.filter(r=>r.stage==='review'&&r.reviewScope==='batch'&&r.batchId===b.id&&r.progress!=='planned')})),{key:state.deliveryPolicy.partialRef&&!model.batches.length?t('Review','审查'):t('Overall review','整体审查'),records:state.stageRuns.filter(r=>r.stage==='review'&&r.reviewScope==='overall'&&r.progress!=='planned')}];
|
|
66
|
+
for(const group of groups){const r=group.records.filter(r=>!status(r.id)?.resolvedBy).at(-1);if(!r){lines.push(`| ${cell(group.key)} | — | ${t('Not started','尚未开始')} | — |`);continue;}
|
|
67
|
+
const effective=r.reviewScope==='overall'?(state.deliveryPolicy.partialRef&&!model.batches.length?status(r.id)?.releaseReady&&r.result==='passed':model.overall?.id===r.id):group.batch?.status==='complete'&&group.batch.reviewId===r.id;
|
|
68
|
+
lines.push(`| ${cell(group.key)} | ${reference(r.resultRef)} | ${value(r.result??'unknown')} · ${value(r.applicability??'current')} | ${effective?t('Current passing review','当前有效通过审查'):t('Inspect the cited review and affected scope.','核对所引审查及受影响范围。')} |`);
|
|
69
|
+
}
|
|
70
|
+
lines.push('',t('Findings, severity, disposition and review-object details stay in the REV records below. Review is separate from formal testing and user acceptance.','问题严重度、处置及审查对象详情留在下方REV记录中;审查与正式测试、用户验收分别记录。'));
|
|
71
|
+
}
|
|
72
|
+
return '\n\n'+lines.join('\n')+'\n\n';
|
|
73
|
+
}
|
package/lib/protocol.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Change this when state, approval, or stage semantics become incompatible.
|
|
2
|
-
export const PROTOCOL_VERSION =
|
|
2
|
+
export const PROTOCOL_VERSION = 2;
|
|
3
3
|
// Historical facts remain fixed when PROTOCOL_VERSION advances.
|
|
4
4
|
const legacy = new Map([['0.1.0-preview.17', 1], ['0.1.0-preview.18', 1]]);
|
|
5
5
|
|
|
@@ -9,10 +9,10 @@ export function packageProtocol(pkg) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function stateProtocol(state) {
|
|
12
|
-
|
|
13
|
-
if
|
|
14
|
-
if
|
|
15
|
-
return
|
|
12
|
+
if(state.reviewPolicy!==1||state.deliveryPolicy?.version!==1)return null;
|
|
13
|
+
if(state.schemaVersion===5)return state.protocolVersion===2?2:null;
|
|
14
|
+
if(state.schemaVersion===4)return Object.hasOwn(state,'protocolVersion')?state.protocolVersion:legacy.get(state.rulesVersion)??null;
|
|
15
|
+
return null;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function upgradeCompatibility(from, to) {
|
package/lib/state.mjs
CHANGED
|
@@ -7,10 +7,12 @@ import path from 'node:path';
|
|
|
7
7
|
import { inspectDocuments, documentChanges, hasLocator, artifactMap } from './documents.mjs';
|
|
8
8
|
import { savedResult, viewStatus, generateView } from './view.mjs';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import {applyOperations} from './operations.mjs';
|
|
11
|
+
import {validateDomains} from './domains.mjs';
|
|
10
12
|
import { PROTOCOL_VERSION, stateProtocol } from './protocol.mjs';
|
|
11
13
|
import { atomicWrite, exists, fail, inside, json, noSymlinks, readJSON, withLock } from './io.mjs';
|
|
12
14
|
|
|
13
|
-
export const SCHEMA_VERSION =
|
|
15
|
+
export const SCHEMA_VERSION = 5;
|
|
14
16
|
export const STAGES = ['frame', 'requirements', 'prototype', 'diagnose', 'tech-design', 'contract', 'test-design', 'plan', 'implement', 'review', 'verify', 'handoff'];
|
|
15
17
|
const PROGRESS = ['planned', 'active', 'blocked', 'done'];
|
|
16
18
|
const RESULTS = ['passed', 'failed', 'not-run', 'unknown', 'mixed'];
|
|
@@ -32,14 +34,14 @@ function unique(items, label) { requireThat(items.every(object), `${label} entri
|
|
|
32
34
|
export function validateState(state) {
|
|
33
35
|
requireThat(object(state), 'State must be an object.');
|
|
34
36
|
requireThat(['en','zh-CN'].includes(state.language), 'language must be en or zh-CN.');
|
|
35
|
-
|
|
37
|
+
if(state.schemaVersion!==SCHEMA_VERSION||stateProtocol(state)!==PROTOCOL_VERSION)fail('PROTOCOL_MISMATCH','This tool requires schema 5 / delivery protocol 2.','Preserve existing files. Complete the old delivery with its supported tool; no migration or version-field edits are performed.');
|
|
36
38
|
if (Object.hasOwn(state,'protocolVersion')) requireThat(Number.isSafeInteger(state.protocolVersion) && state.protocolVersion > 0, 'Invalid protocolVersion.');
|
|
37
39
|
requireThat(state.reviewPolicy===1 && state.deliveryPolicy?.version===1,'Current document and delivery approval policies are required.');
|
|
38
40
|
if (state.schemaVersion === SCHEMA_VERSION) {
|
|
39
|
-
requireThat(object(state.documentPolicy) && (['authored','adopted','editable'].includes(state.documentPolicy.authority) || (state.schemaVersion ===
|
|
41
|
+
requireThat(object(state.documentPolicy) && (['authored','adopted','editable'].includes(state.documentPolicy.authority) || (state.schemaVersion === 5 && state.documentPolicy.authority === null)), 'documentPolicy.authority is required.');
|
|
40
42
|
{
|
|
41
43
|
const p = state.documentPolicy;
|
|
42
|
-
const unknown = value => state.schemaVersion ===
|
|
44
|
+
const unknown = value => state.schemaVersion === 5 && value === null;
|
|
43
45
|
requireThat((unknown(p.interaction) || typeof p.interaction === 'boolean') && (unknown(p.prototypeCovered) || typeof p.prototypeCovered === 'boolean') && (unknown(p.interface) || ['none','http','other'].includes(p.interface)) && (unknown(p.databases) || (Array.isArray(p.databases) && p.databases.every(x => typeof x === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(x)) && new Set(p.databases).size === p.databases.length)), 'Invalid documentPolicy conditions.');
|
|
44
46
|
requireThat(Array.isArray(state.documents), 'documents must be an array.');
|
|
45
47
|
const paths = new Set();
|
|
@@ -47,6 +49,7 @@ export function validateState(state) {
|
|
|
47
49
|
requireThat(object(d) && ['prd','tech-design','plan','testing','review','diagnosis','handoff','change-log','prototype','contract','sql','source'].includes(d.kind) && ['new','reused','input'].includes(d.mode) && string(d.version), 'Invalid document registration.');
|
|
48
50
|
ref(d.path, 'documents.path'); requireThat(!d.path.includes('#') && !paths.has(d.path), 'Document paths must be unique and have no fragment.'); paths.add(d.path);
|
|
49
51
|
if (d.assetRefs !== undefined) refs(d.assetRefs, 'documents.assetRefs');
|
|
52
|
+
if(d.coverageRefs!==undefined)refs(d.coverageRefs,'documents.coverageRefs');
|
|
50
53
|
if (d.mode !== 'new') { ref(d.basisRef, 'documents.basisRef'); }
|
|
51
54
|
if (d.mode === 'input') requireThat(['prd','prototype'].includes(d.kind) && (d.kind !== 'prd' || p.authority === 'adopted'), 'Input mode is for an unchanged supplied PRD or prototype; an input PRD uses adopted authority.');
|
|
52
55
|
}
|
|
@@ -54,7 +57,7 @@ export function validateState(state) {
|
|
|
54
57
|
}
|
|
55
58
|
requireThat(Number.isSafeInteger(state.revision) && state.revision >= 0, 'revision must be a nonnegative integer.');
|
|
56
59
|
requireThat(string(state.deliveryId) && string(state.rulesVersion), 'deliveryId and rulesVersion are required.');
|
|
57
|
-
requireThat(object(state.goal) && string(state.goal.summary) && (string(state.goal.endpoint) || (state.schemaVersion ===
|
|
60
|
+
requireThat(object(state.goal) && string(state.goal.summary) && (string(state.goal.endpoint) || (state.schemaVersion === 5 && state.goal.endpoint === null)), 'goal.summary and goal.endpoint are required.');
|
|
58
61
|
ref(state.goalRef, 'goalRef');
|
|
59
62
|
if (state.goal.decisionRef !== undefined) ref(state.goal.decisionRef, 'goal.decisionRef');
|
|
60
63
|
requireThat(Number.isSafeInteger(state.cycle) && state.cycle >= 1, 'cycle must be a positive integer.');
|
|
@@ -66,13 +69,15 @@ export function validateState(state) {
|
|
|
66
69
|
refs(run.inputRefs, `${run.id}.inputRefs`); refs(run.outputRefs, `${run.id}.outputRefs`);
|
|
67
70
|
for (const key of ['resultRef', 'priorResultRef']) if (run[key] !== undefined) ref(run[key], `${run.id}.${key}`);
|
|
68
71
|
if (run.historyResultRefs !== undefined) refs(run.historyResultRefs, `${run.id}.historyResultRefs`);
|
|
72
|
+
if(run.coverageRefs!==undefined)refs(run.coverageRefs,`${run.id}.coverageRefs`);
|
|
73
|
+
if(run.assessment)ref(run.assessment.ref,`${run.id}.assessment`);
|
|
69
74
|
if (run.result !== undefined) requireThat(RESULTS.includes(run.result), `Invalid result in ${run.id}.`);
|
|
70
75
|
if (run.applicability !== undefined) requireThat(['current', 'needs-review', 'unknown'].includes(run.applicability), `Invalid applicability in ${run.id}.`);
|
|
71
76
|
if (run.progress === 'done') requireThat(string(run.resultRef), `Completed instance ${run.id} requires resultRef; file existence does not establish semantic completion.`);
|
|
72
77
|
}
|
|
73
78
|
requireThat(state.currentRun === null || state.stageRuns.some(x => x.id === state.currentRun), 'currentRun must refer to a stage instance or be null.');
|
|
74
79
|
requireThat(Array.isArray(state.openChanges), 'openChanges must be an array.'); unique(state.openChanges, 'Change');
|
|
75
|
-
for (const change of state.openChanges) { ref(change.ref, 'change.ref'); requireThat(['open', 'closed'].includes(change.status), 'Invalid change status.'); }
|
|
80
|
+
for (const change of state.openChanges) { if(change.impactRef)ref(change.impactRef,'change.impactRef');if(change.affectedRefs!==undefined)refs(change.affectedRefs,'change.affectedRefs');ref(change.ref, 'change.ref'); requireThat(['open', 'closed'].includes(change.status), 'Invalid change status.'); }
|
|
76
81
|
requireThat(Array.isArray(state.repositories), 'repositories must be an array.'); unique(state.repositories, 'Repository');
|
|
77
82
|
for (const repo of state.repositories) requireThat(path.isAbsolute(repo.path ?? '') && string(repo.role), 'Repository path must be absolute and role nonempty.');
|
|
78
83
|
requireThat(object(state.outcome) && ['in-progress', 'met', 'not-met', 'unknown'].includes(state.outcome.goal), 'Invalid outcome.goal.');
|
|
@@ -87,6 +92,8 @@ export function validateState(state) {
|
|
|
87
92
|
requireThat(state.currentRun === null || state.stageRuns.find(r=>r.id===state.currentRun).progress === 'blocked', 'A current question keeps its stage blocked until clarified.');
|
|
88
93
|
}
|
|
89
94
|
if (state.goalHistory !== undefined) requireThat(Array.isArray(state.goalHistory), 'goalHistory must be an array.');
|
|
95
|
+
for(const b of state.batches??[]){ref(b.definitionRef,'batch.definitionRef');refs(b.taskRefs,'batch.taskRefs');}
|
|
96
|
+
validateDomains(state);
|
|
90
97
|
validateDelivery(state);
|
|
91
98
|
if (state.deliveryPolicy?.partialRef) ref(state.deliveryPolicy.partialRef, 'deliveryPolicy.partialRef');
|
|
92
99
|
validateReviewState(state);
|
|
@@ -107,7 +114,7 @@ export async function readState(directory) {
|
|
|
107
114
|
requireThat(object(state),'State must be an object.');
|
|
108
115
|
validateState(state);
|
|
109
116
|
const review=await reviewStatus(directory,state), view=await viewStatus(directory,state), changes=await documentChanges(directory,state);
|
|
110
|
-
return {...paths,state,readOnly:false,review,completion:completionStatus(state,review,{changed:changes}),session:sessionAdvice(directory,state,review,{saved:view.status==='current' && !changes.length}),view,documentChanges:changes};
|
|
117
|
+
return {...paths,state,readOnly:false,review,execution:review.execution,completion:completionStatus(state,review,{changed:changes}),session:sessionAdvice(directory,state,review,{saved:view.status==='current' && !changes.length}),view,documentChanges:changes};
|
|
111
118
|
}
|
|
112
119
|
async function inspectForWrite(directory,state,previous) {
|
|
113
120
|
const inspected=await inspectDocuments(directory,state);
|
|
@@ -117,6 +124,11 @@ async function inspectForWrite(directory,state,previous) {
|
|
|
117
124
|
const results=await checkReferences(directory,{...state,goalRef:run.resultRef,next:{inputRefs:[]},stageRuns:[],currentRun:null});
|
|
118
125
|
if (results.some(r=>!['exists','not-checked'].includes(r.status))) fail('DOCUMENT_CHECK',`Cannot locate result for ${run.id}.`,'Fix the result reference.',{references:results});
|
|
119
126
|
}
|
|
127
|
+
for(const b of state.batches){for(const value of [b.definitionRef,...b.taskRefs]){const [file,locator]=value.split('#'),doc=state.documents.find(d=>d.kind==='plan'&&d.path===file);if(!doc||!locator||!hasLocator(await fs.readFile(path.join(directory,file),'utf8'),locator,(await artifactMap()).documents.plan))fail('DOCUMENT_CHECK','Cannot locate batch/task definition '+value,'Keep approved definitions in the plan.');}}
|
|
128
|
+
const addedRefs=[];
|
|
129
|
+
for(const r of state.stageRuns)if(r.assessment&&JSON.stringify(r.assessment)!==JSON.stringify(previous?.stageRuns.find(x=>x.id===r.id)?.assessment))addedRefs.push(r.assessment.ref);
|
|
130
|
+
for(const c of state.openChanges)if(c.impactRef&&JSON.stringify(c)!==JSON.stringify(previous?.openChanges.find(x=>x.id===c.id)))addedRefs.push(c.impactRef,...c.affectedRefs);
|
|
131
|
+
for(const value of addedRefs){const results=await checkReferences(directory,{...state,goalRef:value,next:{inputRefs:[]},stageRuns:[],batches:[],openChanges:[],currentRun:null});if(results.some(r=>!['exists','not-checked'].includes(r.status)))fail('DOCUMENT_CHECK','Cannot locate current assessment or impact evidence '+value,'Preserve reachable evidence before updating.',{references:results});}
|
|
120
132
|
state.documentDigests=inspected.digests;
|
|
121
133
|
}
|
|
122
134
|
export async function renderState(directory,revision,rulesVersion,runtime,preserveEdits=false) {
|
|
@@ -143,6 +155,7 @@ export async function initState(directory, input, rulesVersion, runtime) {
|
|
|
143
155
|
const p = statePaths(directory); await noSymlinks(p.internal);
|
|
144
156
|
if (await exists(p.statePath) || await exists(p.backup)) fail('ALREADY_EXISTS','Existing delivery or recovery material found.','Read/resume the existing delivery.');
|
|
145
157
|
requireThat(object(input) && ['en','zh-CN'].includes(input.language), 'New delivery input requires language en or zh-CN, from the explicit preference or current user conversation.');
|
|
158
|
+
requireThat(Array.isArray(input.stageRuns??[])&&Array.isArray(input.batches??[])&&Array.isArray(input.reviewHistory??[])&&Array.isArray(input.openChanges??[])&&(input.stageRuns??[]).every(r=>(r.progress==='planned'||(['requirements','frame'].includes(r.stage)&&r.progress==='active'))&&!r.approvalBasis&&!r.assessment) && !(input.batches??[]).length && !(input.reviewHistory??[]).length && !(input.openChanges??[]).length && input.outcome===undefined, 'Initialize empty domain facts or planned runs; record executions and decisions through domain operations.');
|
|
146
159
|
const storage = await validateInitLocation(directory,input,runtime);
|
|
147
160
|
await fs.mkdir(p.internal, { recursive: true });
|
|
148
161
|
return withLock(p.lock, async () => {
|
|
@@ -150,7 +163,7 @@ export async function initState(directory, input, rulesVersion, runtime) {
|
|
|
150
163
|
const forbidden = ['schemaVersion', 'protocolVersion', 'revision', 'deliveryId', 'rulesVersion', 'createdAt', 'updatedAt', 'goalHistory', 'documentDigests', 'writeResult'];
|
|
151
164
|
requireThat(object(input) && !forbidden.some(x => Object.hasOwn(input, x)), 'init input must not set tool-managed identity/version fields.');
|
|
152
165
|
const now = new Date().toISOString();
|
|
153
|
-
const state = { goalRef:'.aimship/init.md', documentPolicy:{authority:null,interaction:null,prototypeCovered:null,interface:null,databases:null}, documents:[], deliveryPolicy: {version:1}, reviewPolicy: 1, reviewHistory: [], cycle: 1, closureRefs: [], stageRuns: [], currentRun: null, openChanges: [], repositories: [], outcome: { goal: 'in-progress', acceptance: 'not-recorded' }, next: { action: text(input,'Clarify the request and prepare requirements for review.','澄清诉求并准备需求审核。'), inputRefs:['.aimship/init.md'] }, ...input, storage, schemaVersion: SCHEMA_VERSION, protocolVersion: PROTOCOL_VERSION, revision: 0, deliveryId: randomUUID(), rulesVersion, createdAt: now, updatedAt: now };
|
|
166
|
+
const state = { goalRef:'.aimship/init.md', documentPolicy:{authority:null,interaction:null,prototypeCovered:null,interface:null,databases:null}, documents:[], deliveryPolicy: {version:1}, reviewPolicy: 1, reviewHistory: [], cycle: 1, closureRefs: [], batches: [], stageRuns: [], currentRun: null, openChanges: [], repositories: [], outcome: { goal: 'in-progress', acceptance: 'not-recorded' }, next: { action: text(input,'Clarify the request and prepare requirements for review.','澄清诉求并准备需求审核。'), inputRefs:['.aimship/init.md'] }, ...input, storage, schemaVersion: SCHEMA_VERSION, protocolVersion: PROTOCOL_VERSION, revision: 0, deliveryId: randomUUID(), rulesVersion, createdAt: now, updatedAt: now };
|
|
154
167
|
if (object(input.documentPolicy)) state.documentPolicy = {authority:null,interaction:null,prototypeCovered:null,interface:null,databases:null,...input.documentPolicy};
|
|
155
168
|
if (object(state.goal) && state.goal.endpoint == null) state.goal.endpoint = text(state,'Implementation, review, independent verification and delivery acceptance.','完成实现、评审、独立验证和交付验收。');
|
|
156
169
|
arrangeSessions(state); validateState(state); requireThat(state.deliveryPolicy?.version === 1, 'New deliveries require deliveryPolicy version 1.'); await inspectForWrite(directory,state); await enforceReviews(directory,state); await enforceCompletion(directory,state); await atomicWrite(p.statePath, json(state)); return savedResult(directory,state,{initialized:true});
|
|
@@ -190,16 +203,19 @@ export async function updateState(directory, patch, revision, rulesVersion, runt
|
|
|
190
203
|
assertRules(previous, rulesVersion, runtime);
|
|
191
204
|
const immutable = ['schemaVersion', 'protocolVersion', 'revision', 'deliveryId', 'rulesVersion', 'createdAt', 'updatedAt', 'goalHistory', 'documentDigests', 'writeResult', 'storage'];
|
|
192
205
|
requireThat(!immutable.some(x => Object.hasOwn(patch, x)), 'Identity, revision, and historical metadata are tool-managed.');
|
|
193
|
-
const
|
|
206
|
+
const metadata={...patch};delete metadata.operations;
|
|
207
|
+
const next=merge(previous,metadata);await applyOperations(directory,next,patch);
|
|
194
208
|
next.protocolVersion = PROTOCOL_VERSION; next.rulesVersion = runtime.version;
|
|
195
209
|
requireThat(previous.deliveryPolicy === undefined || next.deliveryPolicy?.version === previous.deliveryPolicy.version, 'Cannot remove or downgrade deliveryPolicy.');
|
|
196
210
|
arrangeSessions(next); validateState(next);
|
|
197
211
|
// Registering the first formal basis is not a new user scope decision.
|
|
198
|
-
const formalizing = previous.schemaVersion ===
|
|
212
|
+
const formalizing = previous.schemaVersion === 5 && previous.goalRef === '.aimship/init.md' && !previous.documents.some(d=>d.kind==='prd') && next.documents.some(d=>d.kind==='prd' && d.path === next.goalRef.split('#')[0]);
|
|
199
213
|
if (JSON.stringify(previous.goal) !== JSON.stringify(next.goal) || (previous.goalRef !== next.goalRef && !formalizing)) {
|
|
200
214
|
requireThat(string(next.goal.decisionRef) && next.goal.decisionRef !== previous.goal.decisionRef, 'Goal revision requires a new explicit decisionRef; viewing results does not revise a goal.');
|
|
201
215
|
next.goalHistory = [...(previous.goalHistory ?? []), { goal: previous.goal, goalRef: previous.goalRef, deliveryPolicy: previous.deliveryPolicy, outcome: previous.outcome, revision: previous.revision }];
|
|
202
216
|
}
|
|
217
|
+
const newEngineering=next.stageRuns.some(r=>['implement','review','verify'].includes(r.stage)&&r.progress!=='planned'&&!previous.stageRuns.some(old=>old.id===r.id&&old.progress!=='planned'));
|
|
218
|
+
requireThat(previous.outcome.acceptance!=='accepted'||!newEngineering||(next.cycle===previous.cycle+1&&next.outcome.acceptance==='not-recorded'&&next.closureRefs.length>previous.closureRefs.length),'Reopen accepted work with its prior closure and reset acceptance before recording new engineering work.');
|
|
203
219
|
await inspectForWrite(directory,next,previous); await enforceReviews(directory,next,previous);
|
|
204
220
|
if (previous.deliveryPolicy?.partialRef !== next.deliveryPolicy?.partialRef && previous.deliveryPolicy !== undefined) requireThat(string(next.goal.decisionRef) && next.goal.decisionRef !== previous.goal.decisionRef, 'Scope changes need a new explicit goal decision.');
|
|
205
221
|
if (next.deliveryPolicy && previous.deliveryPolicy && JSON.stringify(previous.outcome)!==JSON.stringify(next.outcome)) await enforceCompletion(directory,next);
|
|
@@ -244,6 +260,9 @@ export async function checkReferences(directory, state, all = false) {
|
|
|
244
260
|
add(history?.goalRef); add(history?.goal?.decisionRef); addOutcome(history?.outcome);
|
|
245
261
|
}
|
|
246
262
|
for (const r of state.reviewHistory ?? []) add(r.sourceRef);
|
|
263
|
+
for (const b of state.batches??[])for(const ref of [b.definitionRef,...b.taskRefs])add(ref);
|
|
264
|
+
for(const r of state.stageRuns){for(const ref of r.coverageRefs??[])add(ref);if(r.assessment)add(r.assessment.ref);}
|
|
265
|
+
for(const c of state.openChanges){if(c.impactRef)add(c.impactRef);}
|
|
247
266
|
for (const d of state.documents ?? []) { for (const r of d.assetRefs ?? []) add(r); if(d.mode==='input') add(d.basisRef); }
|
|
248
267
|
}
|
|
249
268
|
const results = [];
|
|
@@ -276,7 +295,7 @@ export async function checkReferences(directory, state, all = false) {
|
|
|
276
295
|
|
|
277
296
|
async function enforceCompletion(directory,state) {
|
|
278
297
|
if (state.deliveryPolicy?.version !== 1 || state.outcome.goal !== 'met') return;
|
|
279
|
-
if (state.schemaVersion ===
|
|
298
|
+
if (state.schemaVersion === 5 && (state.goal.endpoint === null || state.goalRef === '.aimship/init.md')) fail('DELIVERY_INCOMPLETE','Initialization is not a completed delivery.','Resolve the endpoint and formal requirements before claiming completion.');
|
|
280
299
|
const completion=completionStatus(state,await reviewStatus(directory,state));
|
|
281
300
|
if (!state.deliveryPolicy.partialRef && !completion.readyForAcceptance) fail('DELIVERY_INCOMPLETE','Standard delivery evidence is incomplete.','Keep the goal in progress, or record the explicitly limited scope without claiming standard completion.',completion);
|
|
282
301
|
const refs=await checkReferences(directory,state,true);
|