aimship 0.1.0-preview.20 → 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.
Files changed (42) hide show
  1. package/CONTRIBUTING.md +3 -3
  2. package/README.md +6 -4
  3. package/README.zh-CN.md +6 -4
  4. package/docs/cli.md +36 -13
  5. package/docs/examples.md +4 -0
  6. package/docs/guide.md +15 -11
  7. package/docs/guide.zh-CN.md +15 -11
  8. package/lib/approvals.mjs +47 -126
  9. package/lib/cli.mjs +10 -8
  10. package/lib/delivery.mjs +23 -38
  11. package/lib/documents.mjs +7 -6
  12. package/lib/domains.mjs +123 -0
  13. package/lib/execution.mjs +4 -14
  14. package/lib/language.mjs +2 -1
  15. package/lib/operations.mjs +30 -0
  16. package/lib/projections.mjs +73 -0
  17. package/lib/protocol.mjs +5 -5
  18. package/lib/state.mjs +30 -11
  19. package/lib/view.mjs +136 -72
  20. package/package.json +2 -2
  21. package/skills/aimship/SKILL.md +1 -1
  22. package/skills/aimship/references/core.md +19 -5
  23. package/skills/aimship/references/document-patterns.md +15 -1
  24. package/skills/aimship/references/document-review.md +2 -2
  25. package/skills/aimship/references/recovery.md +4 -4
  26. package/skills/aimship/references/review-method.md +2 -2
  27. package/skills/aimship/references/reviews/batch.md +2 -2
  28. package/skills/aimship/references/reviews/overall.md +3 -3
  29. package/skills/aimship/references/reviews/recheck.md +1 -1
  30. package/skills/aimship/references/routing.md +1 -1
  31. package/skills/aimship/references/sources-and-changes.md +1 -1
  32. package/skills/aimship/references/stages/contract.md +2 -2
  33. package/skills/aimship/references/stages/diagnose.md +1 -1
  34. package/skills/aimship/references/stages/implement.md +4 -4
  35. package/skills/aimship/references/stages/plan.md +1 -1
  36. package/skills/aimship/references/stages/review.md +1 -1
  37. package/skills/aimship/references/stages/tech-design.md +3 -3
  38. package/skills/aimship/references/stages/test-design.md +1 -1
  39. package/skills/aimship/references/stages/verify.md +3 -3
  40. package/skills/aimship/references/templates/plan.md +7 -1
  41. package/skills/aimship/references/templates/review.md +7 -2
  42. package/skills/aimship/references/templates/testing.md +8 -2
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
- const runs = [], requirement = 'prd';
117
- for (const [i, run] of state.stageRuns.entries()) {
118
- const spec = map.stages[run.stage];
119
- let required = [];
120
- if (spec?.humanReview) {
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
- const restoring = new Set(state.stageRuns.filter(r=>r.applicability === 'current' && previous?.stageRuns.some(old=>old.id===r.id && old.applicability==='needs-review')).map(r=>r.id));
218
- let status = await reviewStatus(directory, state);
219
- // Invalidate only registered producer/dependency paths. Independent branches retain their facts.
220
- const affected = new Set(revisedApprovedPrd ? status.runs.filter(r=>r.stage==='requirements' && r.progress!=='planned').map(r=>r.id) : []);
221
- for (const doc of state.documents) {
222
- const oldDoc = previous?.documents?.find(d => d.path === doc.path);
223
- const latest = oldHistory.filter(r => r.document === doc.path && (['approved','editorial'].includes(r.kind) || (oldDoc?.mode==='input' && r.kind==='self-check' && r.result==='passed'))).at(-1);
224
- const now = status.documents.find(d => d.path === doc.path);
225
- if (!oldDoc || !latest || !now) continue;
226
- const changed = latest.digest !== now.digest || latest.version !== doc.version;
227
- const editorial = state.reviewHistory.slice(oldHistory.length).some(r=>r.document===doc.path && r.kind==='editorial' && r.digest===now.digest);
228
- const requested = state.reviewHistory.slice(oldHistory.length).some(r=>r.document===doc.path && (r.kind==='changes-requested' || (r.kind==='self-check' && r.result==='failed')));
229
- if ((!changed && !requested) || editorial) continue;
230
- if (changed && !state.openChanges.some(c => c.status === 'open')) reject(`${doc.path}: record the material change and impact in an open CHG, or self-check an editorial reconciliation.`);
231
- if (changed && ['approved','adopted'].includes(now.status) && doc.version === latest.version) reject(`${doc.path}: a material revision needs a new document version; use editorial only for unchanged meaning.`);
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
- for (const r of status.runs) if (r.dependsOn.some(id => affected.has(id))) affected.add(r.id);
235
- for (const r of state.stageRuns) if (affected.has(r.id) && r.progress !== 'planned') r.applicability = 'needs-review';
236
- status = await reviewStatus(directory, state);
237
- const previousStatus = previous?.reviewPolicy === 1 ? await reviewStatus(directory, previous) : null;
238
- for (const run of state.stageRuns) {
239
- const old = previous?.stageRuns.find(r => r.id === run.id);
240
- const progressing = ['active','done'].includes(run.progress) && (!old || old.progress !== run.progress || old.resultRef !== run.resultRef || restoring.has(run.id));
241
- if (restoring.has(run.id) && run.applicability === 'needs-review') reject(`${run.id}: current document or dependency still needs review.`);
242
- if (progressing && status.runs.find(r => r.id === run.id).blockedBy.length) reject(`${run.id}: prerequisite completion, approval or affected dependency is unresolved.`);
243
- if (old && old.progress !== 'planned' && previousStatus && !previousStatus.runs.find(r=>r.id===old.id).dependsOn.every(id=>status.runs.find(r=>r.id===run.id).dependsOn.includes(id))) reject(`${run.id}: preserve started dependencies; record revised work as a new run.`);
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)&&currentEntry.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|->. Recursively merge objects; arrays replace in full. Identity/history fields are managed. null is a literal value, not deletion.',
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,8 +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
- implementationWork: 'Optional implement-only workType: batch|repair and repairOf: preceding run IDs. They classify scope for display/advice, never omit implementation from review coverage. Without a purpose, pre-overall work retains legacy batch meaning; later work is unclassified until its actual basis is checked. No implicit state rewrite.',
36
- sessions: 'run.sessionGroup is an advisory context group, auto-filled for new runs; reviewOf names implementation runs and overall:true identifies overall review. read/check/resume expose session/completion. Supplied PRD/prototype mode input uses the actual request basisRef and self-check, not human approval. next.questionRef identifies a concrete question on a blocked stage (clear with null). Completed work units stop with await-review, await-clarification or await-continue. Implementation checkpoints can return continue-implementation-review, and failed batch reviews continue-batch-repair within existing authorization, without starting the next batch. Overall review stops before verify; suggestNewSession is advisory. Failed view saving returns reconcile-save. No new approval or automatic execution.',
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.',
37
38
  stages: STAGES,
38
39
  progress: ['planned', 'active', 'blocked', 'done'],
39
40
  result: ['passed', 'failed', 'not-run', 'unknown', 'mixed'],
@@ -42,15 +43,16 @@ const HELP = {
42
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)' },
43
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}.',
44
45
  topLevel: {
45
- 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; the next normal save records current tool/protocol without rewriting documents or revoking unchanged approvals.',
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.',
46
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.',
47
48
  language: 'en or zh-CN from explicit user preference, otherwise this delivery conversation. Required in current state; retained on update/render/resume.',
48
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.',
49
- stageRuns: 'Ordered array of stageInstance objects, default []. Every ID is unique; currentRun must match one of these IDs or be null. Stage types may repeat with distinct IDs.',
50
- currentRun: 'Instance ID or null (default). Do not invent an ID without registering its instance in stageRuns.',
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.',
51
53
  repositories: 'Array of {id, path: absolute-path, role, ...optional-version-facts}, default []. No repository is required for a design-only goal.',
52
- openChanges: 'Array of {id, ref, status: open|closed}, default []; close records rather than deleting history.',
53
- 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.',
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.',
54
56
  next: '{action, inputRefs?: []}; defaults to continuing the confirmed arrangement.',
55
57
  cycle: 'Positive integer, default 1. A reopening increments by one, appends a closureRef and resets current acceptance.',
56
58
  closureRefs: 'Historical closure references, default []. Preserve them across reopenings.'
package/lib/delivery.mjs CHANGED
@@ -22,42 +22,28 @@ export function arrangeSessions(state) {
22
22
  for (const r of state.stageRuns) {
23
23
  if (r.sessionGroup) continue;
24
24
  const category = ['frame','requirements'].includes(r.stage) ? 'requirements' : ['tech-design','contract','prototype'].includes(r.stage) ? 'technical' : r.stage;
25
- const batch = r.stage === 'review' && !r.overall && r.reviewOf?.length === 1 ? state.stageRuns.find(x=>x.id===r.reviewOf[0]) : null;
25
+ const batch = r.stage === 'review' && r.reviewScope==='batch' && r.reviewOf?.length === 1 ? state.stageRuns.find(x=>x.id===r.reviewOf[0]) : null;
26
26
  r.sessionGroup = batch ? (batch.sessionGroup ?? `implement:${batch.id}`) : ['requirements','technical','test-design','plan'].includes(category) ? category : `${category}:${r.id}`;
27
27
  }
28
28
  }
29
- export function completionStatus(state, review, {changed=[]}={}) {
30
- const runs=state.stageRuns, gaps=[];
31
- if (state.goal.endpoint === null) gaps.push('Delivery endpoint remains unknown.');
32
- if (state.documentPolicy.authority === null || unknownConditions(state.documentPolicy).length) gaps.push('Artifact conditions remain unknown.');
33
- if(changed.length) gaps.push('Changed or missing registered documents require reconciliation.');
34
- const ready=r=>review.runs.find(x=>x.id===r.id)?.ready;
35
- const has=stage=>runs.some(r=>r.stage===stage && ready(r));
36
- for (const stage of ['tech-design','test-design','plan','implement','verify','handoff']) if (!has(stage)) gaps.push(`Missing current completed ${stage}.`);
37
- if (!runs.some(r=>r.stage==='requirements' && ready(r))) gaps.push('Missing reviewed requirements.');
38
- if (((state.documentPolicy?.interface!=null && state.documentPolicy.interface!=='none') || state.documentPolicy?.databases?.length) && !has('contract')) gaps.push('Missing applicable interface/SQL contract.');
39
- if (runs.some(r=>r.stage==='prototype') && !has('prototype')) gaps.push('Missing applicable interaction prototype.');
40
- if (review.runs.some(r=>!r.ready)) gaps.push('Selected work or document approvals remain unresolved.');
41
- const implementations=runs.filter(r=>r.stage==='implement');
42
- if (implementations.some(r=>['failed','mixed','unknown','not-run'].includes(r.result))) gaps.push('Implementation results remain unresolved.');
43
- const allReviews=runs.filter(r=>r.stage==='review');
44
- const effectiveReviews=allReviews.filter(r=>!review.runs.find(x=>x.id===r.id)?.resolvedBy);
45
- if (effectiveReviews.some(r=>!ready(r) || r.result!=='passed')) gaps.push('Review results remain unresolved.');
46
- const reviews=allReviews.filter(r=>ready(r) && r.result==='passed');
47
- for (const [i, batch] of implementations.entries()) {
48
- 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}.`);
49
- }
50
- const overall=reviews.findLast(r=>r.overall === true && implementations.length && implementations.every(b=>r.reviewOf?.includes(b.id) && runs.indexOf(r)>runs.indexOf(b)));
51
- if (!overall) gaps.push('Missing passing overall implementation review.');
52
- const allChecks=runs.filter(r=>r.stage==='verify');
53
- const checks=allChecks.filter((r,i)=>!allChecks.slice(i+1).some(later=>ready(later) && later.result==='passed' && later.historyResultRefs?.includes(r.resultRef)));
54
- 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.');
55
- const handoff=runs.findLast(r=>r.stage==='handoff' && ready(r));
56
- if (handoff && checks.some(r=>runs.indexOf(r)>runs.indexOf(handoff))) gaps.push('Handoff precedes verification.');
57
- if (state.openChanges.some(c=>c.status==='open')) gaps.push('Unresolved changes remain.');
58
- const partial=!!state.deliveryPolicy.partialRef;
59
- const readyForAcceptance=!partial && !gaps.length;
60
- 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.','仅评估记录与当前依赖;不证明业务质量。')};
61
47
  }
62
48
  // A display suggestion, never a scheduled run or authorization.
63
49
  export function suggestedNextStage(state) {
@@ -67,7 +53,7 @@ export function suggestedNextStage(state) {
67
53
  if(state.deliveryPolicy.partialRef)return null;
68
54
  if(!current)return 'requirements';
69
55
  const contracts=(state.documentPolicy.interface!=null && state.documentPolicy.interface!=='none') || state.documentPolicy.databases?.length;
70
- 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;
71
57
  }
72
58
  function majorSessionBoundary(state,current) {
73
59
  if (!current || current.progress !== 'done') return false;
@@ -77,7 +63,7 @@ function majorSessionBoundary(state,current) {
77
63
  if (['tech-design','contract'].includes(current.stage)) return next==='test-design';
78
64
  if (current.stage==='plan') return next==='implement';
79
65
  if (current.stage==='review' && current.result==='passed') {
80
- 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);
81
67
  }
82
68
  return false;
83
69
  }
@@ -101,7 +87,6 @@ export function sessionAdvice(directory,state,review,{saved=true}={}) {
101
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;
102
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';
103
89
  else if(status?.ready) action=!next?endAction:next.progress!=='planned'?'inspect-existing-work':'await-continue';
104
- if (saved && !pending && state.deliveryPolicy?.version === 1 && status?.ready && next && review.runs.find(r=>r.id===next.id)?.blockedBy.length) action='resolve-current';
105
90
  // Internal checkpoints execute nothing and never release another batch.
106
91
  let reviewScope=null;
107
92
  if (saved && !concluded && !pending && !state.next.questionRef && status?.ready && current?.progress==='done' && !['needs-review','unknown'].includes(current.applicability)) {
@@ -111,7 +96,7 @@ export function sessionAdvice(directory,state,review,{saved=true}={}) {
111
96
  const selectedReview=next?.stage==='review' && next.reviewOf?.includes(current.id);
112
97
  if(reviewScope && !reviewed && (!state.deliveryPolicy.partialRef || selectedReview))action='continue-implementation-review';
113
98
  else if(!reviewScope && !reviewed && implementationType(state,current)==='repair')action='inspect-review-scope';
114
- } else if(current.stage==='review' && !status.resolvedBy && current.overall!==true && current.reviewOf?.length && ['failed','mixed'].includes(current.result)) action='continue-batch-repair';
99
+ } else if(current.stage==='review' && !status.resolvedBy && (current.reviewScope==='overall')!==true && current.reviewOf?.length && ['failed','mixed'].includes(current.result)) action='continue-batch-repair';
115
100
  }
116
101
  const suggestNewSession=['await-review','await-continue'].includes(action) && majorSessionBoundary(state,current);
117
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;
@@ -120,5 +105,5 @@ export function sessionAdvice(directory,state,review,{saved=true}={}) {
120
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.','在已有授权内修复并复查本批;遇范围、审批或权限依赖时停在实际问题。');
121
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(' ');
122
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??''}`);
123
- 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.overall===true : !next.overall)?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.','会话边界不是阻塞或批准。用户要求时可在当前会话继续。每次续接都需核对交付身份、当前审批和未完成事项;本提示不授予权限。')};
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.','会话边界不是阻塞或批准。用户要求时可在当前会话继续。每次续接都需核对交付身份、当前审批和未完成事项;本提示不授予权限。')};
124
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'));
@@ -130,14 +131,14 @@ export async function inspectDocuments(directory, state) {
130
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.`);
131
132
  const requirementKind = 'prd';
132
133
  const find = kind => docs.find(d => d.kind === (kind === 'requirement' ? requirementKind : kind));
133
- if (state.schemaVersion === 4) {
134
+ if (state.schemaVersion === 5) {
134
135
  try {
135
136
  const request = await safeRead(directory,state,'.aimship/init.md');
136
137
  if (!request?.trim()) issues.push('Missing or empty .aimship/init.md.');
137
138
  else digests['.aimship/init.md'] = hash(request);
138
139
  } catch { issues.push('Cannot read .aimship/init.md inside this delivery.'); }
139
140
  }
140
- const preparing = state.schemaVersion === 4 && !find('requirement');
141
+ const preparing = state.schemaVersion === 5 && !find('requirement');
141
142
  if (preparing && state.goalRef !== '.aimship/init.md') issues.push('Before requirements exist, goalRef must point to .aimship/init.md.');
142
143
  if (!preparing && !find('requirement')) issues.push(`Register ${requirementKind} as the requirement entry.`);
143
144
  if (!preparing && policy.authority === null) issues.push('Select the requirement authority when registering PRD.');
@@ -154,7 +155,7 @@ export async function inspectDocuments(directory, state) {
154
155
  try { text = await safeRead(directory, state, doc.path); }
155
156
  catch (error) { issues.push(`Cannot read ${doc.path}: ${error.code}`); continue; }
156
157
  if (text === null) { unchecked.push(`${doc.path}: remote or unavailable repository; inspect adopted version using host tools (${doc.basisRef ?? 'missing basisRef'}).`); continue; }
157
- digests[doc.path] = hash(text);
158
+ digests[doc.path] = hash(doc.mode==='new'?strippedGenerated(text,doc.kind):text);
158
159
  if (!text.trim()) issues.push(`${doc.path}: empty document.`);
159
160
  if (doc.mode !== 'new') {
160
161
  // Reuse is a current input option, not a historical document protocol.
@@ -178,7 +179,7 @@ export async function inspectDocuments(directory, state) {
178
179
  const runs = state.stageRuns;
179
180
  for (const [i, run] of state.stageRuns.entries()) {
180
181
  if (run.progress === 'planned') continue;
181
- 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);
182
183
  if (!doc && preparing && ['frame','requirements'].includes(run.stage) && run.progress !== 'done') continue;
183
184
  if (!doc) { issues.push(`${run.id}: missing registered ${contract.kind} artifact.`); continue; }
184
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.`);
@@ -207,7 +208,7 @@ export async function inspectDocuments(directory, state) {
207
208
  }
208
209
  }
209
210
  }
210
- if (state.schemaVersion === 4) {
211
+ if (state.schemaVersion === 5) {
211
212
  const unknown = unknownConditions(policy);
212
213
  const dependent = runs.filter(r=>r.progress !== 'planned' && !['frame','requirements','diagnose'].includes(r.stage));
213
214
  if (preparing && dependent.length) issues.push('Prepare and review formal requirements before dependent work.');
@@ -263,7 +264,7 @@ export async function inspectDocuments(directory, state) {
263
264
  export async function documentChanges(directory, state) {
264
265
  const changes=[];
265
266
  for (const [ref, digest] of Object.entries(state.documentDigests ?? {})) {
266
- 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); }
267
268
  catch { changes.push(ref); }
268
269
  }
269
270
  return changes;