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/approvals.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
-
import { artifactMap, safeRead, hasLocator, sections } from './documents.mjs';
|
|
2
|
+
import { artifactMap, safeRead, hasLocator, sections, definitions } from './documents.mjs';
|
|
3
3
|
import { fail, hash } from './io.mjs';
|
|
4
|
+
import {evaluateRuns} from './domains.mjs';
|
|
4
5
|
|
|
5
6
|
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
6
7
|
const reject = message => fail('REVIEW_CHECK', message, 'Read review status. Preserve history, self-check the current document, and record only an actual explicit user decision.');
|
|
@@ -33,7 +34,7 @@ export async function reviewFingerprint(directory, state, doc) {
|
|
|
33
34
|
}
|
|
34
35
|
const assets = [];
|
|
35
36
|
for (const ref of doc.assetRefs ?? []) { const bytes = await safeRead(directory,state,ref,null); if (bytes === null) return null; assets.push([ref,hash(bytes)]); }
|
|
36
|
-
return hash(assets.length ? JSON.stringify([hash(body),assets]) : body);
|
|
37
|
+
return hash(assets.length||doc.coverageRefs ? JSON.stringify([hash(body),assets,doc.coverageRefs??null]) : body);
|
|
37
38
|
}
|
|
38
39
|
function decisionFor(state, doc, digest) {
|
|
39
40
|
let approved = null, checked = null, latest = null;
|
|
@@ -49,49 +50,6 @@ function decisionFor(state, doc, digest) {
|
|
|
49
50
|
if (approved?.version !== doc.version || approved?.digest !== digest) approved = null;
|
|
50
51
|
return { approved, checked, latest };
|
|
51
52
|
}
|
|
52
|
-
// A corrective review may consume a failed conclusion without first pretending
|
|
53
|
-
// it passed. Its recorded coverage must include the object it supersedes.
|
|
54
|
-
function reviewsResult(later, earlier) {
|
|
55
|
-
return later.stage === 'review' && earlier.stage === 'review' && text(earlier.resultRef)
|
|
56
|
-
&& later.historyResultRefs?.includes(earlier.resultRef)
|
|
57
|
-
&& (!earlier.overall || later.overall === true)
|
|
58
|
-
&& (earlier.reviewOf?.length
|
|
59
|
-
? earlier.reviewOf.every(id => later.reviewOf?.includes(id))
|
|
60
|
-
: !later.reviewOf?.length && later.scope === earlier.scope);
|
|
61
|
-
}
|
|
62
|
-
// Required edges for standard delivery are derived from the actual work, not
|
|
63
|
-
// optional inputRefs/dependsOn. Limited commissions retain their selected graph.
|
|
64
|
-
function standardEntry(state, run, index, completed) {
|
|
65
|
-
const before=state.stageRuns.slice(0,index), dependsOn=[], issues=[];
|
|
66
|
-
const status=id=>completed.find(r=>r.id===id);
|
|
67
|
-
const implementations=before.filter(r=>r.stage==='implement');
|
|
68
|
-
if(run.stage==='review') {
|
|
69
|
-
const covered=run.reviewOf??[];
|
|
70
|
-
if(!covered.length) issues.push('entry:implementation-coverage');
|
|
71
|
-
dependsOn.push(...covered);
|
|
72
|
-
if(run.overall && implementations.some(r=>!covered.includes(r.id))) issues.push('entry:overall-coverage');
|
|
73
|
-
for(const id of covered) {
|
|
74
|
-
const implementation=state.stageRuns.find(r=>r.id===id);
|
|
75
|
-
if(['failed','mixed','unknown','not-run'].includes(implementation?.result)) issues.push(`entry:implementation-result:${id}`);
|
|
76
|
-
}
|
|
77
|
-
} else if(['verify','handoff'].includes(run.stage)) {
|
|
78
|
-
if(!implementations.length) issues.push('entry:implementation');
|
|
79
|
-
dependsOn.push(...implementations.map(r=>r.id),...before.filter(r=>r.stage==='review').map(r=>r.id));
|
|
80
|
-
const overall=before.findLast(r=>r.stage==='review' && r.overall && r.result==='passed' && status(r.id)?.releaseReady && implementations.length && implementations.every(b=>r.reviewOf?.includes(b.id) && before.indexOf(b)<before.indexOf(r)));
|
|
81
|
-
if(!overall) issues.push('entry:passing-overall-review');
|
|
82
|
-
if(run.stage==='handoff') {
|
|
83
|
-
const checks=before.filter(r=>r.stage==='verify');
|
|
84
|
-
const effective=checks.filter((r,i)=>!checks.slice(i+1).some(later=>status(later.id)?.releaseReady && later.result==='passed' && later.historyResultRefs?.includes(r.resultRef)));
|
|
85
|
-
if(!effective.length) issues.push('entry:verification');
|
|
86
|
-
dependsOn.push(...checks.map(r=>r.id));
|
|
87
|
-
for(const check of effective) {
|
|
88
|
-
if(check.result!=='passed') issues.push(`entry:verification-result:${check.id}`);
|
|
89
|
-
if(overall && before.indexOf(check)<before.indexOf(overall)) issues.push(`entry:verification-before-review:${check.id}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return {dependsOn,issues};
|
|
94
|
-
}
|
|
95
53
|
export async function reviewStatus(directory, state) {
|
|
96
54
|
const map = await artifactMap(), documents = [], evidenceIssues = [], evidenceCache = new Map();
|
|
97
55
|
for (const r of state.reviewHistory ?? []) {
|
|
@@ -113,64 +71,16 @@ export async function reviewStatus(directory, state) {
|
|
|
113
71
|
try { await evidence(directory,state,state.deliveryPolicy.partialRef); limitedScope=true; }
|
|
114
72
|
catch { /* Report an unavailable scope basis; never silently grant an exception. */ }
|
|
115
73
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const kind = spec.kind === 'requirement' ? requirement : spec.kind;
|
|
122
|
-
required = (state.documents ?? []).filter(d => d.kind === kind || (run.stage === 'contract' && d.kind === 'sql')).map(d => d.path);
|
|
123
|
-
}
|
|
124
|
-
const declared = run.dependsOn ?? (i ? [state.stageRuns[i - 1].id] : []);
|
|
125
|
-
const producers = state.stageRuns.slice(0, i).filter(r => r.outputRefs.some(out => run.inputRefs.some(input => out.split('#')[0] === input.split('#')[0]))).map(r => r.id);
|
|
126
|
-
const development = ['requirements','tech-design','contract','test-design','plan','implement'];
|
|
127
|
-
const prerequisites = development.includes(run.stage) ? state.stageRuns.slice(0,i).filter(r=>development.includes(r.stage) && development.indexOf(r.stage)<development.indexOf(run.stage)).map(r=>r.id) : [];
|
|
128
|
-
const gated=state.deliveryPolicy?.version===1 && ['review','verify','handoff'].includes(run.stage);
|
|
129
|
-
const entrance=gated && !limitedScope ? standardEntry(state,run,i,runs) : {dependsOn:[],issues:[]};
|
|
130
|
-
if(gated && state.deliveryPolicy.partialRef && !limitedScope) entrance.issues.push('entry:limited-scope-evidence');
|
|
131
|
-
const dependsOn = [...new Set([...declared, ...producers, ...prerequisites,...entrance.dependsOn])];
|
|
132
|
-
const blockedBy = dependsOn.filter(id => {
|
|
133
|
-
const dependency = runs.find(r => r.id === id);
|
|
134
|
-
const original = state.stageRuns.find(r => r.id === id);
|
|
135
|
-
return !dependency?.releaseReady && !(dependency?.ready && reviewsResult(run, original));
|
|
136
|
-
});
|
|
137
|
-
blockedBy.push(...entrance.issues);
|
|
138
|
-
// An omitted producer run must not turn an unapproved input into permission.
|
|
139
|
-
// Schema 4 keeps prerequisite approvals even when producer runs are omitted.
|
|
140
|
-
if (state.schemaVersion === 4) {
|
|
141
|
-
const needs = [];
|
|
142
|
-
if (['prototype','tech-design','contract','test-design','plan','implement'].includes(run.stage)) needs.push(requirement);
|
|
143
|
-
if (['contract','test-design','plan','implement'].includes(run.stage)) needs.push('tech-design');
|
|
144
|
-
if (['plan','implement'].includes(run.stage) && !state.deliveryPolicy?.partialRef) needs.push('testing');
|
|
145
|
-
if (run.stage === 'implement') needs.push('plan');
|
|
146
|
-
if (['test-design','plan','implement'].includes(run.stage)) {
|
|
147
|
-
if (state.documentPolicy.interface !== null && state.documentPolicy.interface !== 'none') needs.push('contract');
|
|
148
|
-
if (state.documentPolicy.databases?.length) needs.push('sql');
|
|
149
|
-
}
|
|
150
|
-
if (run.stage === 'implement' && state.stageRuns.some(r=>r.stage==='prototype')) needs.push('prototype');
|
|
151
|
-
for (const kind of needs) {
|
|
152
|
-
const inputs = (state.documents ?? []).filter(d=>d.kind === kind);
|
|
153
|
-
if (!inputs.length || inputs.some(d=>!['approved','adopted'].includes(documents.find(r=>r.path===d.path)?.status))) blockedBy.push(`approval:${kind}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const waiting = required.filter(file => !['approved','adopted'].includes(documents.find(d => d.path === file)?.status));
|
|
157
|
-
const ready = run.progress === 'done' && !['needs-review','unknown'].includes(run.applicability) && !blockedBy.length && !waiting.length && (!spec?.humanReview || required.length > 0);
|
|
158
|
-
const releaseReady = ready && (run.stage !== 'review' || run.result === 'passed');
|
|
159
|
-
runs.push({ id: run.id, stage: run.stage, progress: run.progress, applicability: run.applicability, documents: required, waiting, dependsOn, blockedBy, ready, releaseReady });
|
|
160
|
-
if (releaseReady && run.stage === 'review') {
|
|
161
|
-
for (const earlier of runs.slice(0,-1)) {
|
|
162
|
-
if (earlier.ready && reviewsResult(run, state.stageRuns.find(r=>r.id===earlier.id))) {
|
|
163
|
-
earlier.releaseReady = true;
|
|
164
|
-
earlier.resolvedBy = run.id;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
74
|
+
let expectedCoverage=[];
|
|
75
|
+
const testing=state.documents.find(d=>d.kind==='testing');
|
|
76
|
+
if(testing){const body=await safeRead(directory,state,testing.path).catch(()=>null);expectedCoverage=testing.mode==='new'&&body?definitions(body,testing.path,map.documents.testing).filter(d=>d.id.startsWith('TC-')).map(d=>testing.path+'#'+d.id):(testing.coverageRefs??[]);}
|
|
77
|
+
const execution=evaluateRuns(state,documents,map,{limitedScope,expectedCoverage,evidenceIssues});
|
|
78
|
+
const runs=execution.runs;
|
|
169
79
|
const current = runs.find(r => r.id === state.currentRun);
|
|
170
80
|
const pending = runs.filter(r => r.progress !== 'planned').flatMap(r => r.waiting.map(document => ({ run: r.id, document, status: documents.find(d => d.path === document)?.status })));
|
|
171
81
|
const nextRun = runs.find(r => !r.ready);
|
|
172
82
|
const nextStage = current ? runs[runs.indexOf(current) + 1]?.stage ?? null : nextRun?.stage ?? null;
|
|
173
|
-
return { compatibility: 'current', documents, runs, pending, evidenceIssues, currentRun: current?.id ?? null, nextRun: nextRun?.id ?? null, nextStage, canProceed: !state.next.questionRef && (current ? !current.blockedBy.length && !(current.progress === 'done' && !current.releaseReady) && current.progress !== 'blocked' && !['needs-review','unknown'].includes(current.applicability) : !pending.length && !runs.some(r=>r.progress==='blocked' || (r.progress === 'done' && !r.releaseReady) || (r.progress !== 'planned' && r.blockedBy.length) || ['needs-review','unknown'].includes(r.applicability))), identity: 'Record validation only; user identity, intent and source authenticity are not authenticated.' };
|
|
83
|
+
return { compatibility: 'current', execution, documents, runs, pending, evidenceIssues, currentRun: current?.id ?? null, nextRun: nextRun?.id ?? null, nextStage, canProceed: !state.next.questionRef && (current ? !current.blockedBy.length && !(current.progress === 'done' && !current.releaseReady) && current.progress !== 'blocked' && !['needs-review','unknown'].includes(current.applicability) : !pending.length && !runs.some(r=>r.progress==='blocked' || (r.progress === 'done' && !r.releaseReady) || (r.progress !== 'planned' && r.blockedBy.length) || ['needs-review','unknown'].includes(r.applicability))), identity: 'Record validation only; user identity, intent and source authenticity are not authenticated.' };
|
|
174
84
|
}
|
|
175
85
|
async function evidence(directory, state, ref) {
|
|
176
86
|
if (!text(ref) || ref.includes('\\') || ref.split('#')[0].split('/').includes('..') || ref.startsWith('/') || (/^[a-zA-Z]:/.test(ref) && !ref.startsWith('repo:'))) reject('Review evidence must stay inside a registered root.');
|
|
@@ -214,34 +124,45 @@ export async function enforceReviews(directory, state, previous) {
|
|
|
214
124
|
const a = first(order[i]), b = first(order[j]);
|
|
215
125
|
if (a >= 0 && b >= 0 && a > b && !previous?.stageRuns.some(r => r.id === state.stageRuns[b].id && r.progress !== 'planned')) reject(`${order[i]} must precede ${order[j]} in the initial development arrangement.`);
|
|
216
126
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
if
|
|
230
|
-
if
|
|
231
|
-
if (
|
|
232
|
-
for (const r of status.runs.filter(r => r.documents.includes(doc.path) && r.progress !== 'planned')) affected.add(r.id);
|
|
127
|
+
let status=await reviewStatus(directory,state);
|
|
128
|
+
const addedChanges=state.openChanges.filter(c=>!previous?.openChanges.some(old=>JSON.stringify(old)===JSON.stringify(c)));
|
|
129
|
+
const affected=new Set();
|
|
130
|
+
for(const doc of state.documents){
|
|
131
|
+
const priorDoc=previous?.documents.find(d=>d.path===doc.path),oldApproval=oldHistory.findLast(r=>r.document===doc.path&&(['approved','editorial'].includes(r.kind)||(priorDoc?.mode==='input'&&r.kind==='self-check'&&r.result==='passed')));
|
|
132
|
+
const now=status.documents.find(d=>d.path===doc.path);if(!priorDoc||!oldApproval||!now)continue;
|
|
133
|
+
const changed=oldApproval.digest!==now.digest||oldApproval.version!==doc.version;
|
|
134
|
+
const previousContentChanged=previous.documentDigests?.[doc.path]!==state.documentDigests?.[doc.path]||priorDoc.version!==doc.version;
|
|
135
|
+
const editorial=state.reviewHistory.slice(oldHistory.length).some(r=>r.document===doc.path&&r.kind==='editorial');
|
|
136
|
+
if(!changed||editorial)continue;
|
|
137
|
+
const change=(previousContentChanged?addedChanges:state.openChanges).find(c=>c.impactRef&&Array.isArray(c.affectedRefs));
|
|
138
|
+
if(!change)reject(`${doc.path}: record the material change and scoped impact, or reconcile a true editorial change.`);
|
|
139
|
+
if(['approved','adopted'].includes(now.status)&&doc.version===oldApproval.version)reject(`${doc.path}: material content needs a new version.`);
|
|
140
|
+
if(!previousContentChanged)continue;
|
|
141
|
+
for(const r of state.stageRuns){if(r.stage==='verify'){const refs=(r.coverageRefs??[]).filter(ref=>change.affectedRefs.includes(ref)||change.affectedRefs.includes(doc.path));r.invalidCoverageRefs=[...new Set([...(r.invalidCoverageRefs??[]),...refs])];}if(r.progress!=='planned'&&(r.outputRefs.includes(doc.path)&&!['implement','review','verify'].includes(r.stage)||change.affectedRefs.some(ref=>r.inputRefs.includes(ref)||r.outputRefs.includes(ref)||r.resultRef===ref||state.batches.some(b=>b.id===r.batchId&&(b.definitionRef===ref||b.taskRefs.includes(ref))))))affected.add(r.id);}
|
|
233
142
|
}
|
|
234
|
-
|
|
235
|
-
for
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
143
|
+
// A new authority or an explicit scoped impact need not edit the same file.
|
|
144
|
+
for(const change of addedChanges){const old=previous?.openChanges.find(c=>c.id===change.id);if(!change.impactRef||!change.affectedRefs||old&&old.impactRef===change.impactRef&&JSON.stringify(old.affectedRefs)===JSON.stringify(change.affectedRefs))continue;
|
|
145
|
+
for(const r of state.stageRuns){if(r.progress==='planned')continue;const refs=change.affectedRefs;if(r.stage==='verify'){const invalid=(r.coverageRefs??[]).filter(ref=>refs.includes(ref)||refs.includes(ref.split('#')[0]));r.invalidCoverageRefs=[...new Set([...(r.invalidCoverageRefs??[]),...invalid])];}
|
|
146
|
+
if(refs.some(ref=>r.inputRefs.includes(ref)||r.outputRefs.includes(ref)||r.resultRef===ref||state.batches.some(b=>b.id===r.batchId&&(b.definitionRef===ref||b.taskRefs.includes(ref)))))affected.add(r.id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
let changed=true;while(changed){const size=affected.size,affectedB=new Set(state.stageRuns.filter(r=>affected.has(r.id)&&r.batchId).map(r=>r.batchId));for(const b of state.batches)if(b.dependsOn.some(id=>affectedB.has(id)))for(const r of state.stageRuns.filter(r=>r.batchId===b.id&&r.progress!=='planned'))affected.add(r.id);for(const r of state.stageRuns)if([...(r.dependsOn??[]),...(r.reviewOf??[])].some(id=>affected.has(id)))affected.add(r.id);changed=affected.size!==size;}
|
|
150
|
+
for(const r of state.stageRuns)if(affected.has(r.id))r.applicability='needs-review';
|
|
151
|
+
status=await reviewStatus(directory,state);
|
|
152
|
+
for(const r of state.stageRuns)if(r.applicability==='needs-review'&&!['implement','review','verify','handoff'].includes(r.stage)&&state.reviewHistory.slice(oldHistory.length).some(h=>r.outputRefs.includes(h.document)&&['approved','editorial','self-check'].includes(h.kind))&&r.outputRefs.length&&r.outputRefs.every(ref=>['approved','adopted'].includes(status.documents.find(d=>d.path===ref)?.status)))r.applicability='current';
|
|
153
|
+
status=await reviewStatus(directory,state);
|
|
154
|
+
for(const run of state.stageRuns){
|
|
155
|
+
const old=previous?.stageRuns.find(r=>r.id===run.id);
|
|
156
|
+
const progressing=['active','done'].includes(run.progress)&&(!old||old.progress!==run.progress||old.resultRef!==run.resultRef||old.applicability==='needs-review'&&run.applicability==='current');
|
|
157
|
+
const recovering=old&&['needs-review','unknown'].includes(old.applicability)&&run.applicability==='current';
|
|
158
|
+
const currentEntry=recovering?await reviewStatus(directory,{...state,stageRuns:state.stageRuns.map(r=>r.id===run.id?{...r,approvalBasis:undefined}:r)}):status;
|
|
159
|
+
if((progressing||recovering)&¤tEntry.runs.find(r=>r.id===run.id).blockedBy.length)reject(`${run.id}: prerequisite completion, approval or affected work is unresolved.`);
|
|
160
|
+
const rs=status.runs.find(r=>r.id===run.id);
|
|
161
|
+
if(run.progress==='done'&&rs.ready&&!run.approvalBasis?.length){
|
|
162
|
+
const relevant=status.documents.filter(d=>['approved','adopted'].includes(d.status));
|
|
163
|
+
run.approvalBasis=relevant.map(d=>{const event=state.reviewHistory.findLast(h=>h.document===d.path&&h.digest===d.digest&&['approved','editorial','self-check'].includes(h.kind));return {document:d.path,digest:d.digest,recordId:event.id};});
|
|
164
|
+
}
|
|
244
165
|
}
|
|
245
|
-
if (state.outcome.goal === 'met' && previous?.outcome.goal !== 'met' && status.runs.some(r => !r.ready)) reject('Resolve selected stages and document approvals before declaring the goal met.');
|
|
166
|
+
if (state.outcome.goal === 'met' && previous?.outcome.goal !== 'met' && status.runs.some(r => !r.ready&&!r.resolvedBy&&r.stage!=='verify')) reject('Resolve selected stages and document approvals before declaring the goal met.');
|
|
246
167
|
return status;
|
|
247
168
|
}
|
package/lib/cli.mjs
CHANGED
|
@@ -19,7 +19,7 @@ const HELP = {
|
|
|
19
19
|
bind: 'Resolve active installation once. Returns immutable release/core/stage/CLI paths plus configured dataRoot, configPath and customPath. Source mode returns the default data root with null config/custom paths. No directories are created. Optional --home is for isolated testing.',
|
|
20
20
|
init: 'init <dir> --input <json-file|-> --rules-version <version>. Create state once; input requires goal.summary, language (en or zh-CN) and saved .aimship/init.md; directory must be a child of bound dataRoot unless storage has an explicit local decisionRef; endpoint defaults to standard delivery; artifact conditions may remain null (unknown). Does not create templates.',
|
|
21
21
|
read: 'read <dir> [--rules-version <version>] [--delivery-id <known-id>]. Read current delivery state without executing work.',
|
|
22
|
-
update: 'update <dir> --revision <N> --rules-version <version> --input <json-file|->.
|
|
22
|
+
update: 'update <dir> --revision <N> --rules-version <version> --input <json-file|->. Merge ordinary metadata; record controlled facts with operations. Identity/history fields are managed. null is a literal value, not deletion.',
|
|
23
23
|
check: 'check <dir> [--rules-version <version>] [--all]. Check structure and current input references (or every registered reference). Checks ordinary Markdown sections and searchable numbers in new local Markdown; reused and unsupported fragments are explicitly not checked. Does not prove semantics, remote access or validation results.',
|
|
24
24
|
resume: 'resume <dir> [--rules-version <version>] [--delivery-id <known-id>]. Read state and return fixed rule paths, required inputs, warnings, and continuation text; never execute work or rewrite state.',
|
|
25
25
|
recover: 'recover <dir> --revision <backup-revision> --rules-version <version>. Restore state.previous.json only over missing/corrupt state; preserve corrupted bytes. Inspect actual artifacts afterward; nothing reruns.',
|
|
@@ -32,7 +32,9 @@ const HELP = {
|
|
|
32
32
|
protocolVersion: PROTOCOL_VERSION,
|
|
33
33
|
documentReview: {reviewPolicy:1, reviewHistory:'Append-only {id,kind,document,version,digest,sourceRef,summary}. self-check requires result: passed|failed; approved requires decision: explicit-approval; editorial requires previousDigest. Read review.documents for current digests. Tools validate records, not user identity. See docs/cli.md#document-review-protocol.'},
|
|
34
34
|
deliveryPolicy: 'New states default to {version:1}; partialRef identifies an explicitly limited scope, null restores standard scope with a new goal decision. Completion is derived separately from goal/acceptance.',
|
|
35
|
-
|
|
35
|
+
implementationWork: 'Started implementation requires workType batch|repair; business work has registered batchId. Repairs retain B identity and prior result references. Completed implement/review/verify records require objectKey SHA-256 for the actual object. reviewScope is batch|overall; reviewOf covers actual work. Formal verify declares coverageRefs and optional per-coverage results; historyResultRefs links rechecks/retests.',
|
|
36
|
+
operations: ['register-batches','record-run','remove-planned-run','record-review','record-change','record-outcome','set-current','set-applicability'],
|
|
37
|
+
sessions: 'One batch includes automatic batch review and authorized repair/recheck before stopping. After the last batch recommend a fresh session for the existing overall review, unless explicitly authorized here. Overall review stops before formal verification. Host delegation is conditional and adds no stage or business batch. Session advice executes nothing and grants no approval.',
|
|
36
38
|
stages: STAGES,
|
|
37
39
|
progress: ['planned', 'active', 'blocked', 'done'],
|
|
38
40
|
result: ['passed', 'failed', 'not-run', 'unknown', 'mixed'],
|
|
@@ -41,15 +43,16 @@ const HELP = {
|
|
|
41
43
|
stageInstance: { id: 'stable-instance-id', stage: '<stage>', scope: 'batch or whole-delivery scope', progress: 'planned', inputRefs: [], outputRefs: [], resultRef: 'review.md#REV-001 (required when done)' },
|
|
42
44
|
references: 'delivery-relative/path#optional-fragment, repo:<registered-id>/<repo-relative-path>#fragment, or HTTP(S) URL. Local paths cannot escape their root. Repository registration: {id, path: absolute-path, role}.',
|
|
43
45
|
topLevel: {
|
|
44
|
-
managed: 'schemaVersion, protocolVersion, revision, deliveryId, rulesVersion, createdAt, updatedAt, goalHistory, documentDigests, writeResult. Do not supply these in init/update. rulesVersion records the last saving tool; protocolVersion identifies compatible delivery semantics. Legacy 17/18 without it map precisely to protocol 1. Reads never adopt metadata;
|
|
46
|
+
managed: 'schemaVersion, protocolVersion, revision, deliveryId, rulesVersion, createdAt, updatedAt, goalHistory, documentDigests, writeResult. Do not supply these in init/update. rulesVersion records the last saving tool; protocolVersion identifies compatible delivery semantics. Legacy 17/18 without it map precisely to protocol 1. Reads never adopt metadata; protocol 2 does not continue schema4/protocol1 deliveries. No migration or version-field rewrite.',
|
|
45
47
|
requiredInit: 'language: en|zh-CN and goal: {summary, endpoint?}; save nonempty initialization notes in .aimship/init.md before init. Defaults: endpoint standard delivery, goalRef .aimship/init.md, empty documents, and null documentPolicy conditions/authority. Register PRD and switch goalRef when drafted; no empty templates.',
|
|
46
48
|
language: 'en or zh-CN from explicit user preference, otherwise this delivery conversation. Required in current state; retained on update/render/resume.',
|
|
47
49
|
storage: 'Init-only {mode: explicit, decisionRef: delivery-relative-readable-file} for a user-selected location. Otherwise a strict child of configured dataRoot. Returned dataRoot is managed; ordinary workspace/material location is not a choice.',
|
|
48
|
-
stageRuns: 'Ordered
|
|
49
|
-
|
|
50
|
+
stageRuns: 'Ordered execution history, default []; update through record-run, not raw replacement. Every ID is unique; currentRun must match one of these IDs or be null. Stage types may repeat with distinct IDs.',
|
|
51
|
+
batches: 'Stable registered B definitions {id,definitionRef,taskRefs,dependsOn}; register-batches requires the current approved plan. Separate from implementation/repair runs.',
|
|
52
|
+
currentRun: 'Instance ID or null (default); update through set-current. Do not invent an ID without registering its instance in stageRuns.',
|
|
50
53
|
repositories: 'Array of {id, path: absolute-path, role, ...optional-version-facts}, default []. No repository is required for a design-only goal.',
|
|
51
|
-
openChanges: 'Array of {id,
|
|
52
|
-
outcome: '
|
|
54
|
+
openChanges: 'Array of {id,ref,status:open|closed,impactRef?,affectedRefs?}, default []; use record-change; close records rather than deleting history.',
|
|
55
|
+
outcome: 'Managed via record-outcome; defaults to {goal: in-progress, acceptance: not-recorded}. A met goal needs goalConclusionRef; recorded acceptance needs acceptanceRef. Optional handoffRef, mergeRef, deploymentRef retain independent facts.',
|
|
53
56
|
next: '{action, inputRefs?: []}; defaults to continuing the confirmed arrangement.',
|
|
54
57
|
cycle: 'Positive integer, default 1. A reopening increments by one, appends a closureRef and resets current acceptance.',
|
|
55
58
|
closureRefs: 'Historical closure references, default []. Preserve them across reopenings.'
|
package/lib/delivery.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { text, completionMessage } from './language.mjs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { unknownConditions } from './documents.mjs';
|
|
4
4
|
import { fail } from './io.mjs';
|
|
5
|
+
import { implementationReviewScope, implementationType } from './execution.mjs';
|
|
5
6
|
|
|
6
7
|
// Current delivery policy and completion checks.
|
|
7
8
|
export function validateDelivery(state) {
|
|
@@ -9,6 +10,8 @@ export function validateDelivery(state) {
|
|
|
9
10
|
if (!p || p.version !== 1) fail('INVALID_STATE', 'Unsupported delivery policy.', 'Preserve the state and use a compatible tool.');
|
|
10
11
|
if (p.partialRef != null && (typeof p.partialRef !== 'string' || !p.partialRef.trim())) fail('INVALID_STATE', 'partialRef must identify the explicit scope decision.', 'Record the actual decision in the existing requirement or change record.');
|
|
11
12
|
for (const r of state.stageRuns) {
|
|
13
|
+
if (r.workType !== undefined && (r.stage !== 'implement' || !['batch','repair'].includes(r.workType))) fail('INVALID_STATE','workType is batch or repair on implementation runs only.','Record the actual work purpose.');
|
|
14
|
+
if (r.repairOf !== undefined && (r.workType !== 'repair' || !Array.isArray(r.repairOf) || !r.repairOf.length || new Set(r.repairOf).size !== r.repairOf.length || !r.repairOf.every(id=>state.stageRuns.slice(0,state.stageRuns.indexOf(r)).some(prior=>prior.id===id)))) fail('INVALID_STATE','repairOf must name preceding work for an implementation repair.','Retain the actual repair basis; this does not replace review coverage.');
|
|
12
15
|
if (r.overall !== undefined && (r.stage !== 'review' || typeof r.overall !== 'boolean')) fail('INVALID_STATE', 'overall is a review scope flag.', 'Identify the actual overall review.');
|
|
13
16
|
if (r.sessionGroup !== undefined && (typeof r.sessionGroup !== 'string' || !r.sessionGroup.trim())) fail('INVALID_STATE', 'Invalid sessionGroup.', 'Use a nonempty advisory group ID.');
|
|
14
17
|
if (r.reviewOf !== undefined && (r.stage !== 'review' || !Array.isArray(r.reviewOf) || !r.reviewOf.length || new Set(r.reviewOf).size !== r.reviewOf.length || r.reviewOf.some(id => !state.stageRuns.some(x => x.id === id && x.stage === 'implement')))) fail('INVALID_STATE', 'reviewOf must identify implementation runs.', 'Record the actual reviewed batches.');
|
|
@@ -19,42 +22,28 @@ export function arrangeSessions(state) {
|
|
|
19
22
|
for (const r of state.stageRuns) {
|
|
20
23
|
if (r.sessionGroup) continue;
|
|
21
24
|
const category = ['frame','requirements'].includes(r.stage) ? 'requirements' : ['tech-design','contract','prototype'].includes(r.stage) ? 'technical' : r.stage;
|
|
22
|
-
const batch = r.stage === 'review' &&
|
|
25
|
+
const batch = r.stage === 'review' && r.reviewScope==='batch' && r.reviewOf?.length === 1 ? state.stageRuns.find(x=>x.id===r.reviewOf[0]) : null;
|
|
23
26
|
r.sessionGroup = batch ? (batch.sessionGroup ?? `implement:${batch.id}`) : ['requirements','technical','test-design','plan'].includes(category) ? category : `${category}:${r.id}`;
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
|
-
export function completionStatus(state,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for (const [i, batch] of implementations.entries()) {
|
|
45
|
-
if (!reviews.some(r=>r.reviewOf?.includes(batch.id) && runs.indexOf(r)>runs.indexOf(batch) && (!implementations[i+1] || runs.indexOf(r)<runs.indexOf(implementations[i+1])))) gaps.push(`Missing passing batch review for ${batch.id}.`);
|
|
46
|
-
}
|
|
47
|
-
const overall=reviews.findLast(r=>r.overall === true && implementations.length && implementations.every(b=>r.reviewOf?.includes(b.id) && runs.indexOf(r)>runs.indexOf(b)));
|
|
48
|
-
if (!overall) gaps.push('Missing passing overall implementation review.');
|
|
49
|
-
const allChecks=runs.filter(r=>r.stage==='verify');
|
|
50
|
-
const checks=allChecks.filter((r,i)=>!allChecks.slice(i+1).some(later=>ready(later) && later.result==='passed' && later.historyResultRefs?.includes(r.resultRef)));
|
|
51
|
-
if (!checks.length || checks.some(r=>!ready(r) || r.result!=='passed' || !overall || runs.indexOf(r)<runs.indexOf(overall))) gaps.push('Independent verification is not fully recorded as passed after overall review.');
|
|
52
|
-
const handoff=runs.findLast(r=>r.stage==='handoff' && ready(r));
|
|
53
|
-
if (handoff && checks.some(r=>runs.indexOf(r)>runs.indexOf(handoff))) gaps.push('Handoff precedes verification.');
|
|
54
|
-
if (state.openChanges.some(c=>c.status==='open')) gaps.push('Unresolved changes remain.');
|
|
55
|
-
const partial=!!state.deliveryPolicy.partialRef;
|
|
56
|
-
const readyForAcceptance=!partial && !gaps.length;
|
|
57
|
-
return {status:partial?'partial-scope':readyForAcceptance && state.outcome.goal==='met' && state.outcome.acceptance==='accepted'?'standard-complete':readyForAcceptance?'ready-for-acceptance':'incomplete',standardComplete:readyForAcceptance && state.outcome.goal==='met' && state.outcome.acceptance==='accepted',readyForAcceptance,gaps:gaps.map(g=>completionMessage(state,g)),partialRef:state.deliveryPolicy.partialRef??null,scope:text(state,'Recorded structure, approvals and results only; not independent proof of test execution or semantic quality.','仅评估已记录的结构、审批和结果;不独立证明测试已执行或内容语义正确。')};
|
|
29
|
+
export function completionStatus(state,review,{changed=[]}={}) {
|
|
30
|
+
const gaps=[],model=review.execution;
|
|
31
|
+
const has=stage=>state.stageRuns.some(r=>r.stage===stage&&review.runs.find(s=>s.id===r.id)?.ready);
|
|
32
|
+
if(state.goal.endpoint===null)gaps.push('Delivery endpoint remains unknown.');
|
|
33
|
+
if(state.documentPolicy.authority===null||unknownConditions(state.documentPolicy).length)gaps.push('Artifact conditions remain unknown.');
|
|
34
|
+
if(changed.length)gaps.push('Changed or missing registered documents require reconciliation.');
|
|
35
|
+
for(const stage of ['requirements','tech-design','test-design','plan','implement','verify','handoff'])if(!has(stage))gaps.push(`Missing current completed ${stage}.`);
|
|
36
|
+
if((state.documentPolicy.interface!==null&&state.documentPolicy.interface!=='none'||state.documentPolicy.databases?.length)&&!has('contract'))gaps.push('Missing applicable interface/SQL contract.');
|
|
37
|
+
if(state.stageRuns.some(r=>r.stage==='prototype')&&!has('prototype'))gaps.push('Missing applicable interaction prototype.');
|
|
38
|
+
if(!model?.batches.length||model.batches.some(b=>b.status!=='complete'))gaps.push('Business batches or their reviews remain unresolved.');
|
|
39
|
+
if(!model?.overall)gaps.push('Missing passing overall implementation review.');
|
|
40
|
+
const handoff=state.stageRuns.findLast(r=>r.stage==='handoff'&&review.runs.find(s=>s.id===r.id)?.ready);
|
|
41
|
+
if(handoff&&model?.verification.runIds.some(id=>state.stageRuns.findIndex(r=>r.id===id)>state.stageRuns.indexOf(handoff)))gaps.push('Handoff precedes verification.');
|
|
42
|
+
if(!model?.verification.passed)gaps.push('Independent verification is not fully recorded as passed after overall review.');
|
|
43
|
+
if(review.runs.some(s=>!s.ready&&!s.resolvedBy&&s.stage!=='verify'))gaps.push('Selected work or document approvals remain unresolved.');
|
|
44
|
+
if(state.openChanges.some(c=>c.status==='open'))gaps.push('Unresolved changes remain.');
|
|
45
|
+
const partial=!!state.deliveryPolicy.partialRef,readyForAcceptance=!partial&&!gaps.length;
|
|
46
|
+
return {status:partial?'partial-scope':readyForAcceptance&&state.outcome.goal==='met'&&state.outcome.acceptance==='accepted'?'standard-complete':readyForAcceptance?'ready-for-acceptance':'incomplete',standardComplete:readyForAcceptance&&state.outcome.goal==='met'&&state.outcome.acceptance==='accepted',readyForAcceptance,gaps:gaps.map(g=>completionMessage(state,g)),partialRef:state.deliveryPolicy.partialRef??null,scope:text(state,'Recorded facts and current dependencies only; not proof of semantic quality.','仅评估记录与当前依赖;不证明业务质量。')};
|
|
58
47
|
}
|
|
59
48
|
// A display suggestion, never a scheduled run or authorization.
|
|
60
49
|
export function suggestedNextStage(state) {
|
|
@@ -64,7 +53,7 @@ export function suggestedNextStage(state) {
|
|
|
64
53
|
if(state.deliveryPolicy.partialRef)return null;
|
|
65
54
|
if(!current)return 'requirements';
|
|
66
55
|
const contracts=(state.documentPolicy.interface!=null && state.documentPolicy.interface!=='none') || state.documentPolicy.databases?.length;
|
|
67
|
-
return ({frame:'requirements',requirements:'tech-design',prototype:'tech-design','tech-design':contracts?'contract':'test-design',contract:'test-design','test-design':'plan',plan:'implement',implement:'review',review:current.overall?'verify':'review',verify:'handoff'})[current.stage]??null;
|
|
56
|
+
return ({frame:'requirements',requirements:'tech-design',prototype:'tech-design','tech-design':contracts?'contract':'test-design',contract:'test-design','test-design':'plan',plan:'implement',implement:'review',review:(current.reviewScope==='overall')?'verify':'review',verify:'handoff'})[current.stage]??null;
|
|
68
57
|
}
|
|
69
58
|
function majorSessionBoundary(state,current) {
|
|
70
59
|
if (!current || current.progress !== 'done') return false;
|
|
@@ -74,7 +63,7 @@ function majorSessionBoundary(state,current) {
|
|
|
74
63
|
if (['tech-design','contract'].includes(current.stage)) return next==='test-design';
|
|
75
64
|
if (current.stage==='plan') return next==='implement';
|
|
76
65
|
if (current.stage==='review' && current.result==='passed') {
|
|
77
|
-
return current.overall ? next==='verify' : !!current.reviewOf?.length && ['implement','review'].includes(next);
|
|
66
|
+
return (current.reviewScope==='overall') ? next==='verify' : !!current.reviewOf?.length && ['implement','review'].includes(next);
|
|
78
67
|
}
|
|
79
68
|
return false;
|
|
80
69
|
}
|
|
@@ -98,10 +87,23 @@ export function sessionAdvice(directory,state,review,{saved=true}={}) {
|
|
|
98
87
|
else if(!current && state.stageRuns.length && review.runs.every(r=>r.ready)) action=concluded?endAction:review.runs.some(r=>r.releaseReady===false)?'resolve-current':endAction;
|
|
99
88
|
else if(current && ((!status?.resolvedBy && (['failed','mixed','unknown','not-run'].includes(current.result) || (current.stage==='review' && current.progress==='done' && !status?.releaseReady))) || current.progress==='blocked' || ['needs-review','unknown'].includes(current.applicability))) action=concluded && !next?'finished':'resolve-current';
|
|
100
89
|
else if(status?.ready) action=!next?endAction:next.progress!=='planned'?'inspect-existing-work':'await-continue';
|
|
101
|
-
|
|
90
|
+
// Internal checkpoints execute nothing and never release another batch.
|
|
91
|
+
let reviewScope=null;
|
|
92
|
+
if (saved && !concluded && !pending && !state.next.questionRef && status?.ready && current?.progress==='done' && !['needs-review','unknown'].includes(current.applicability)) {
|
|
93
|
+
if(current.stage==='implement' && !['failed','mixed','unknown','not-run'].includes(current.result)) {
|
|
94
|
+
reviewScope=implementationReviewScope(state,current);
|
|
95
|
+
const reviewed=state.stageRuns.some(r=>r.stage==='review' && state.stageRuns.indexOf(r)>state.stageRuns.indexOf(current) && r.reviewOf?.includes(current.id) && r.result==='passed' && review.runs.find(s=>s.id===r.id)?.releaseReady);
|
|
96
|
+
const selectedReview=next?.stage==='review' && next.reviewOf?.includes(current.id);
|
|
97
|
+
if(reviewScope && !reviewed && (!state.deliveryPolicy.partialRef || selectedReview))action='continue-implementation-review';
|
|
98
|
+
else if(!reviewScope && !reviewed && implementationType(state,current)==='repair')action='inspect-review-scope';
|
|
99
|
+
} else if(current.stage==='review' && !status.resolvedBy && (current.reviewScope==='overall')!==true && current.reviewOf?.length && ['failed','mixed'].includes(current.result)) action='continue-batch-repair';
|
|
100
|
+
}
|
|
102
101
|
const suggestNewSession=['await-review','await-continue'].includes(action) && majorSessionBoundary(state,current);
|
|
103
102
|
let instruction=action==='await-clarification'?text(state,'Please answer the question listed below so this work can continue.','请回答下一步所列的具体问题,以便继续当前工作。'):action==='await-continue'?text(state,'This stage is saved. Say “continue” when ready.','本阶段已保存。准备好后可说“继续”。'):action==='assess-outcome'?text(state,'Review the handoff against the agreed goal and record the actual goal conclusion and acceptance; completed handoff alone does not end the delivery.','请对照约定目标审阅交付材料,记录实际目标结论和用户接受结果;完成交付文档不等于交付已结束。'):null;
|
|
103
|
+
if(action==='continue-implementation-review')instruction=text(state,'Review the saved implementation and repair findings within the authorized work unit before reporting its stop.','继续审查已保存的实现,并在已授权工作单元内修复、复查后报告停点。');
|
|
104
|
+
if(action==='inspect-review-scope')instruction=text(state,'Read the existing plan and original review to establish this repair’s batch or overall scope before selecting its recheck. The next scheduled review alone does not establish that scope.','先读取已有计划及原审查,核对本次修复对应的批次或整体范围,再选择复查;不能仅凭下一条已排审查推定范围。');
|
|
105
|
+
if(action==='continue-batch-repair')instruction=text(state,'Repair and recheck this batch within the existing authorization; stop for a scope, approval or permission dependency.','在已有授权内修复并复查本批;遇范围、审批或权限依赖时停在实际问题。');
|
|
104
106
|
if(suggestNewSession) instruction=[instruction,action==='await-review'?text(state,'A new session is recommended to review this document; you can also review it here.','建议新开会话审阅本份文档,也可留在这里审核。'):text(state,'A new session is recommended for the next work; you can also continue here.','建议新开会话接续下一步,也可留在这里继续。')].filter(Boolean).join(' ');
|
|
105
107
|
const continuation=['reconcile-save','await-review','await-clarification','assess-outcome','finished'].includes(action)?null:text(state,`Use Aimship to continue delivery ${JSON.stringify(path.resolve(directory))}; first read its current state and relevant formal documents, then continue the recorded work.${instruction?' '+instruction:''}`,`使用 Aimship 继续交付「${path.resolve(directory)}」,先读取当前状态和相关正式文档再接续。${instruction??''}`);
|
|
106
|
-
return {action,stop:!['continue-current','inspect-existing-work'].includes(action),suggestNewSession,currentRun:current?.id??null,nextRun:next?.id??null,groups,continuation,instruction,advisory:true,note:text(state,'A session boundary is not a blocker or approval. Continue here when requested. Resolve the delivery identity, current approvals and unfinished effects on every resume; this text grants no approval.','会话边界不是阻塞或批准。用户要求时可在当前会话继续。每次续接都需核对交付身份、当前审批和未完成事项;本提示不授予权限。')};
|
|
108
|
+
return {action,stop:!['continue-current','inspect-existing-work','continue-implementation-review','continue-batch-repair','inspect-review-scope'].includes(action),suggestNewSession,reviewScope,currentRun:current?.id??null,nextRun:action==='continue-implementation-review'?(next?.stage==='review' && next.reviewOf?.includes(current.id) && (reviewScope==='overall-recheck' ? next.reviewScope==='overall' : next.reviewScope==='batch')?next.id:null):['continue-batch-repair','inspect-review-scope'].includes(action)?null:next?.id??null,groups,continuation,instruction,advisory:true,note:text(state,'A session boundary is not a blocker or approval. Continue here when requested. Resolve the delivery identity, current approvals and unfinished effects on every resume; this text grants no approval.','会话边界不是阻塞或批准。用户要求时可在当前会话继续。每次续接都需核对交付身份、当前审批和未完成事项;本提示不授予权限。')};
|
|
107
109
|
}
|
package/lib/documents.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { hash, inside, fail } from './io.mjs';
|
|
4
|
+
import {strippedGenerated} from './projections.mjs';
|
|
4
5
|
|
|
5
6
|
export async function artifactMap() {
|
|
6
7
|
return JSON.parse(await fs.readFile(new URL('../skills/aimship/references/artifact-map.json', import.meta.url), 'utf8'));
|
|
@@ -86,7 +87,8 @@ export function definitions(text, file, spec) {
|
|
|
86
87
|
}
|
|
87
88
|
const structured=/^\s*(?:\||#{1,6}\s+|[-*+]\s+)/.test(line);
|
|
88
89
|
if(!structured && !new RegExp('^'+ID+'[::]').test(line)) continue;
|
|
89
|
-
|
|
90
|
+
let candidate=line.trim().replace(/^\|\s*/, '').replace(/^(?:#{1,6}\s+|[-*+]\s+)/,'').replace(/^[*`]+/,'');
|
|
91
|
+
if(heading)candidate=candidate.replace(/^\d+(?:\.\d+)*[.)、]?\s+/, '');
|
|
90
92
|
const match=new RegExp('^('+ID+')(?![A-Za-z0-9_-])(?:[*`]*)(?:\\s*[|::—–-]\\s*|\\s+|$)(.*)$').exec(candidate);
|
|
91
93
|
if(!match) continue;
|
|
92
94
|
let body=match[2];
|
|
@@ -129,14 +131,14 @@ export async function inspectDocuments(directory, state) {
|
|
|
129
131
|
for (const kind of new Set(docs.map(d=>d.kind))) if (!['sql','source'].includes(kind) && docs.filter(d=>d.kind===kind).length>1) issues.push(`Register one current artifact for ${kind}; keep history through references.`);
|
|
130
132
|
const requirementKind = 'prd';
|
|
131
133
|
const find = kind => docs.find(d => d.kind === (kind === 'requirement' ? requirementKind : kind));
|
|
132
|
-
if (state.schemaVersion ===
|
|
134
|
+
if (state.schemaVersion === 5) {
|
|
133
135
|
try {
|
|
134
136
|
const request = await safeRead(directory,state,'.aimship/init.md');
|
|
135
137
|
if (!request?.trim()) issues.push('Missing or empty .aimship/init.md.');
|
|
136
138
|
else digests['.aimship/init.md'] = hash(request);
|
|
137
139
|
} catch { issues.push('Cannot read .aimship/init.md inside this delivery.'); }
|
|
138
140
|
}
|
|
139
|
-
const preparing = state.schemaVersion ===
|
|
141
|
+
const preparing = state.schemaVersion === 5 && !find('requirement');
|
|
140
142
|
if (preparing && state.goalRef !== '.aimship/init.md') issues.push('Before requirements exist, goalRef must point to .aimship/init.md.');
|
|
141
143
|
if (!preparing && !find('requirement')) issues.push(`Register ${requirementKind} as the requirement entry.`);
|
|
142
144
|
if (!preparing && policy.authority === null) issues.push('Select the requirement authority when registering PRD.');
|
|
@@ -153,7 +155,7 @@ export async function inspectDocuments(directory, state) {
|
|
|
153
155
|
try { text = await safeRead(directory, state, doc.path); }
|
|
154
156
|
catch (error) { issues.push(`Cannot read ${doc.path}: ${error.code}`); continue; }
|
|
155
157
|
if (text === null) { unchecked.push(`${doc.path}: remote or unavailable repository; inspect adopted version using host tools (${doc.basisRef ?? 'missing basisRef'}).`); continue; }
|
|
156
|
-
digests[doc.path] = hash(text);
|
|
158
|
+
digests[doc.path] = hash(doc.mode==='new'?strippedGenerated(text,doc.kind):text);
|
|
157
159
|
if (!text.trim()) issues.push(`${doc.path}: empty document.`);
|
|
158
160
|
if (doc.mode !== 'new') {
|
|
159
161
|
// Reuse is a current input option, not a historical document protocol.
|
|
@@ -177,7 +179,7 @@ export async function inspectDocuments(directory, state) {
|
|
|
177
179
|
const runs = state.stageRuns;
|
|
178
180
|
for (const [i, run] of state.stageRuns.entries()) {
|
|
179
181
|
if (run.progress === 'planned') continue;
|
|
180
|
-
const contract = map.stages[run.stage], doc = find(contract.kind) ?? (run.stage === 'contract' && policy.interface === 'none' ? find('sql') : null);
|
|
182
|
+
const contract = map.stages[run.stage], formerAuthority=run.stage==='requirements'&&run.approvalBasis?.length&&state.goalHistory?.some(h=>h.goalRef?.split('#')[0]===run.resultRef?.split('#')[0])?docs.find(d=>d.kind==='source'&&d.path===run.resultRef?.split('#')[0]):null, doc = formerAuthority ?? find(contract.kind) ?? (run.stage === 'contract' && policy.interface === 'none' ? find('sql') : null);
|
|
181
183
|
if (!doc && preparing && ['frame','requirements'].includes(run.stage) && run.progress !== 'done') continue;
|
|
182
184
|
if (!doc) { issues.push(`${run.id}: missing registered ${contract.kind} artifact.`); continue; }
|
|
183
185
|
if (run.stage === 'implement' && !state.stageRuns.slice(0,i).some(r => r.stage === 'plan' && r.progress === 'done')) issues.push(`${run.id}: complete plan before implementation.`);
|
|
@@ -206,7 +208,7 @@ export async function inspectDocuments(directory, state) {
|
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
|
-
if (state.schemaVersion ===
|
|
211
|
+
if (state.schemaVersion === 5) {
|
|
210
212
|
const unknown = unknownConditions(policy);
|
|
211
213
|
const dependent = runs.filter(r=>r.progress !== 'planned' && !['frame','requirements','diagnose'].includes(r.stage));
|
|
212
214
|
if (preparing && dependent.length) issues.push('Prepare and review formal requirements before dependent work.');
|
|
@@ -262,7 +264,7 @@ export async function inspectDocuments(directory, state) {
|
|
|
262
264
|
export async function documentChanges(directory, state) {
|
|
263
265
|
const changes=[];
|
|
264
266
|
for (const [ref, digest] of Object.entries(state.documentDigests ?? {})) {
|
|
265
|
-
try { const text = await safeRead(directory,state,ref); if (text === null || hash(text) !== digest) changes.push(ref); }
|
|
267
|
+
try { const text = await safeRead(directory,state,ref); const doc=state.documents.find(d=>d.path===ref);if (text === null || hash(doc?.mode==='new'?strippedGenerated(text,doc.kind):text) !== digest) changes.push(ref); }
|
|
266
268
|
catch { changes.push(ref); }
|
|
267
269
|
}
|
|
268
270
|
return changes;
|