aimship 0.1.0-preview.11 → 0.1.0-preview.13

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 (46) hide show
  1. package/CONTRIBUTING.md +3 -3
  2. package/README.md +6 -10
  3. package/README.zh-CN.md +6 -10
  4. package/docs/cli.md +23 -25
  5. package/docs/guide.md +314 -148
  6. package/docs/guide.zh-CN.md +314 -148
  7. package/lib/approvals.mjs +77 -13
  8. package/lib/cli.mjs +27 -21
  9. package/lib/delivery.mjs +15 -12
  10. package/lib/discovery.mjs +25 -0
  11. package/lib/documents.mjs +159 -65
  12. package/lib/install.mjs +1 -1
  13. package/lib/language.mjs +41 -0
  14. package/lib/location.mjs +65 -0
  15. package/lib/state.mjs +54 -87
  16. package/lib/view.mjs +36 -34
  17. package/package.json +1 -1
  18. package/skills/aimship/SKILL.md +3 -3
  19. package/skills/aimship/references/artifact-map.json +165 -9
  20. package/skills/aimship/references/core.md +17 -13
  21. package/skills/aimship/references/document-patterns.md +17 -11
  22. package/skills/aimship/references/document-review.md +1 -3
  23. package/skills/aimship/references/hosts.md +11 -9
  24. package/skills/aimship/references/recovery.md +9 -13
  25. package/skills/aimship/references/sources-and-changes.md +11 -4
  26. package/skills/aimship/references/stages/diagnose.md +1 -1
  27. package/skills/aimship/references/stages/frame.md +1 -1
  28. package/skills/aimship/references/stages/handoff.md +1 -1
  29. package/skills/aimship/references/stages/implement.md +2 -2
  30. package/skills/aimship/references/stages/plan.md +1 -1
  31. package/skills/aimship/references/stages/requirements.md +1 -1
  32. package/skills/aimship/references/stages/review.md +1 -1
  33. package/skills/aimship/references/stages/scope.md +1 -1
  34. package/skills/aimship/references/stages/tech-design.md +1 -1
  35. package/skills/aimship/references/stages/test-design.md +1 -1
  36. package/skills/aimship/references/stages/verify.md +2 -2
  37. package/skills/aimship/references/templates/change-log.md +0 -2
  38. package/skills/aimship/references/templates/diagnosis.md +0 -7
  39. package/skills/aimship/references/templates/handoff.md +0 -6
  40. package/skills/aimship/references/templates/plan.md +2 -8
  41. package/skills/aimship/references/templates/prd.md +0 -7
  42. package/skills/aimship/references/templates/review.md +0 -3
  43. package/skills/aimship/references/templates/scope.md +0 -7
  44. package/skills/aimship/references/templates/tech-design.md +0 -7
  45. package/skills/aimship/references/templates/testing.md +2 -8
  46. package/lib/migrate.mjs +0 -77
package/lib/state.mjs CHANGED
@@ -1,13 +1,15 @@
1
+ import { validateInitLocation } from './location.mjs';
2
+ import { text } from './language.mjs';
1
3
  import { validateDelivery, arrangeSessions, completionStatus, sessionAdvice } from './delivery.mjs';
2
4
  import fs from 'node:fs/promises';
3
5
  import { validateReviewState, enforceReviews, reviewStatus } from './approvals.mjs';
4
6
  import path from 'node:path';
5
- import { inspectDocuments, documentChanges, anchors, preservedLegacyRuns } from './documents.mjs';
7
+ import { inspectDocuments, documentChanges, hasLocator, artifactMap, safeRead } from './documents.mjs';
6
8
  import { savedResult, viewStatus, generateView } from './view.mjs';
7
9
  import { randomUUID } from 'node:crypto';
8
- import { atomicWrite, exists, fail, inside, json, noSymlinks, readJSON, withLock } from './io.mjs';
10
+ import { atomicWrite, exists, fail, hash, inside, json, noSymlinks, readJSON, withLock } from './io.mjs';
9
11
 
10
- export const SCHEMA_VERSION = 3;
12
+ export const SCHEMA_VERSION = 4;
11
13
  export const STAGES = ['frame', 'requirements', 'prototype', 'scope', 'diagnose', 'tech-design', 'contract', 'test-design', 'plan', 'implement', 'review', 'verify', 'handoff'];
12
14
  const PROGRESS = ['planned', 'active', 'blocked', 'done'];
13
15
  const RESULTS = ['passed', 'failed', 'not-run', 'unknown', 'mixed'];
@@ -28,12 +30,14 @@ function unique(items, label) { requireThat(items.every(object), `${label} entri
28
30
 
29
31
  export function validateState(state) {
30
32
  requireThat(object(state), 'State must be an object.');
31
- if (![1, 2, SCHEMA_VERSION].includes(state.schemaVersion)) fail('UNKNOWN_SCHEMA', `Schema ${state.schemaVersion} is read-only for this tool.`, 'Preserve the file and use a compatible tool or an explicit, reviewed migration.');
32
- if (state.schemaVersion >= 2) {
33
- requireThat(object(state.documentPolicy) && (['authored','adopted','editable','legacy'].includes(state.documentPolicy.authority) || (state.schemaVersion === 3 && state.documentPolicy.authority === null)), 'documentPolicy.authority is required.');
34
- if (state.documentPolicy.authority !== 'legacy') {
33
+ requireThat(['en','zh-CN'].includes(state.language), 'language must be en or zh-CN.');
34
+ requireThat(state.schemaVersion === SCHEMA_VERSION, 'Invalid schemaVersion.');
35
+ requireThat(state.reviewPolicy===1 && state.deliveryPolicy?.version===1,'Current document and delivery approval policies are required.');
36
+ if (state.schemaVersion === SCHEMA_VERSION) {
37
+ requireThat(object(state.documentPolicy) && (['authored','adopted','editable'].includes(state.documentPolicy.authority) || (state.schemaVersion === 4 && state.documentPolicy.authority === null)), 'documentPolicy.authority is required.');
38
+ {
35
39
  const p = state.documentPolicy;
36
- const unknown = value => state.schemaVersion === 3 && value === null;
40
+ const unknown = value => state.schemaVersion === 4 && value === null;
37
41
  requireThat((unknown(p.interaction) || typeof p.interaction === 'boolean') && (unknown(p.prototypeCovered) || typeof p.prototypeCovered === 'boolean') && (unknown(p.interface) || ['none','http','other'].includes(p.interface)) && (unknown(p.databases) || (Array.isArray(p.databases) && p.databases.every(x => typeof x === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(x)) && new Set(p.databases).size === p.databases.length)), 'Invalid documentPolicy conditions.');
38
42
  requireThat(Array.isArray(state.documents), 'documents must be an array.');
39
43
  const paths = new Set();
@@ -47,9 +51,8 @@ export function validateState(state) {
47
51
  }
48
52
  requireThat(Number.isSafeInteger(state.revision) && state.revision >= 0, 'revision must be a nonnegative integer.');
49
53
  requireThat(string(state.deliveryId) && string(state.rulesVersion), 'deliveryId and rulesVersion are required.');
50
- requireThat(object(state.goal) && string(state.goal.summary) && (string(state.goal.endpoint) || (state.schemaVersion === 3 && state.goal.endpoint === null)), 'goal.summary and goal.endpoint are required.');
54
+ requireThat(object(state.goal) && string(state.goal.summary) && (string(state.goal.endpoint) || (state.schemaVersion === 4 && state.goal.endpoint === null)), 'goal.summary and goal.endpoint are required.');
51
55
  ref(state.goalRef, 'goalRef');
52
- if (state.compatibilityRef !== undefined) ref(state.compatibilityRef, 'compatibilityRef');
53
56
  if (state.goal.decisionRef !== undefined) ref(state.goal.decisionRef, 'goal.decisionRef');
54
57
  requireThat(Number.isSafeInteger(state.cycle) && state.cycle >= 1, 'cycle must be a positive integer.');
55
58
  refs(state.closureRefs, 'closureRefs');
@@ -77,58 +80,47 @@ export function validateState(state) {
77
80
  requireThat(object(state.next) && string(state.next.action), 'next.action is required.');
78
81
  if (state.next.inputRefs !== undefined) refs(state.next.inputRefs, 'next.inputRefs');
79
82
  if (state.goalHistory !== undefined) requireThat(Array.isArray(state.goalHistory), 'goalHistory must be an array.');
80
- if (state.rulesHistory !== undefined) requireThat(Array.isArray(state.rulesHistory), 'rulesHistory must be an array.');
81
83
  validateDelivery(state);
82
84
  if (state.deliveryPolicy?.partialRef) ref(state.deliveryPolicy.partialRef, 'deliveryPolicy.partialRef');
83
85
  validateReviewState(state);
84
86
  return state;
85
87
  }
86
88
 
87
- export function statePaths(directory, legacy = false) {
88
- const dir = path.resolve(directory), internal = legacy ? dir : path.join(dir,'.aimship');
89
- return { dir, internal, statePath:path.join(internal,'state.json'), backup:path.join(internal,'state.previous.json'), lock:path.join(internal,'.state.lock'), legacy };
89
+ export function statePaths(directory) {
90
+ const dir = path.resolve(directory), internal = path.join(dir,'.aimship');
91
+ return { dir, internal, statePath:path.join(internal,'state.json'), backup:path.join(internal,'state.previous.json'), lock:path.join(internal,'.state.lock') };
90
92
  }
91
- export async function locateState(directory, writing = false) {
92
- const modern=statePaths(directory), legacy=statePaths(directory,true);
93
- const hasNew=await exists(modern.statePath), hasOld=await exists(legacy.statePath);
94
- const journalPath=path.join(modern.internal,'migration.json');
95
- const journal=await exists(journalPath) ? await readJSON(journalPath) : null;
96
- if (hasNew && hasOld) {
97
- if (writing) fail('LAYOUT_CONFLICT','Both state layouts exist.','Inspect both copies; resume a recorded migration explicitly.');
98
- return {...modern,conflict:true,candidates:[legacy.statePath,modern.statePath],migration:journal};
99
- }
100
- if (writing && journal && journal.status !== 'complete') fail('MIGRATION_PENDING','Migration is incomplete.','Resume migrate with the original revision and compatibilityRef.');
101
- return {...(hasOld || (!hasNew && await exists(legacy.backup)) ? legacy : modern),migration:journal};
93
+ export async function locateState(directory) {
94
+ const current=statePaths(directory);
95
+ await noSymlinks(current.statePath);
96
+ return current;
102
97
  }
103
98
  export async function readState(directory) {
104
- const paths=await locateState(directory); const state=await readJSON(paths.statePath);
99
+ const paths=await locateState(directory), state=await readJSON(paths.statePath);
105
100
  requireThat(object(state),'State must be an object.');
106
- const layoutMismatch=(state.schemaVersion===1 && !paths.legacy)||([2,3].includes(state.schemaVersion) && paths.legacy);
107
- const readOnly=paths.conflict || layoutMismatch || ![1,2,SCHEMA_VERSION].includes(state.schemaVersion) || (state.reviewPolicy !== undefined && state.reviewPolicy !== 1) || (state.deliveryPolicy !== undefined && state.deliveryPolicy?.version !== 1);
108
- if (!readOnly) validateState(state);
109
- const review=readOnly ? null : await reviewStatus(directory,state);
110
- const view=await viewStatus(directory,state), changes=await documentChanges(directory,state);
111
- return {...paths,state,readOnly,layoutMismatch,review,completion:review ? completionStatus(state,review,{changed:changes}) : null,session:review ? sessionAdvice(directory,state,review,{saved:view.status==='current' && !changes.length}) : null,view,documentChanges:changes};
101
+ validateState(state);
102
+ const review=await reviewStatus(directory,state), view=await viewStatus(directory,state), changes=await documentChanges(directory,state);
103
+ return {...paths,state,readOnly:false,review,completion:completionStatus(state,review,{changed:changes}),session:sessionAdvice(directory,state,review,{saved:view.status==='current' && !changes.length}),view,documentChanges:changes};
112
104
  }
113
105
  async function inspectForWrite(directory,state,previous) {
114
106
  const inspected=await inspectDocuments(directory,state);
115
107
  if (inspected.issues.length) fail('DOCUMENT_CHECK','Document structure or traceability needs repair.','Repair the specific documents; file presence is not semantic completion.',inspected);
116
- // Historical migrated runs retain their old formats. New completed runs need resolvable results.
117
- const legacyRuns=await preservedLegacyRuns(directory,state);
118
- for (const run of state.stageRuns.filter(r=>r.progress==='done' && !legacyRuns.has(r.id))) {
108
+ for (const run of state.stageRuns.filter(r=>r.progress==='done')) {
119
109
  if (previous?.stageRuns.some(r=>r.id===run.id && JSON.stringify(r)===JSON.stringify(run))) continue;
120
110
  const results=await checkReferences(directory,{...state,goalRef:run.resultRef,next:{inputRefs:[]},stageRuns:[],currentRun:null});
121
111
  if (results.some(r=>!['exists','not-checked'].includes(r.status))) fail('DOCUMENT_CHECK',`Cannot locate result for ${run.id}.`,'Fix the result reference.',{references:results});
122
112
  }
113
+ const original=previous?.documentDigests?.['source/request.md'];
114
+ if (original && inspected.digests['source/request.md'] === undefined) {
115
+ try { inspected.digests['source/request.md']=hash(await safeRead(directory,state,'source/request.md')); } catch { inspected.digests['source/request.md']=null; }
116
+ }
117
+ if (original && inspected.digests['source/request.md'] !== original) fail('SOURCE_CHANGED','The preserved original request changed.','Restore source/request.md from the original evidence; record revised requirements in the current authority and change log. An unrelated save cannot adopt changed original input.');
123
118
  state.documentDigests=inspected.digests;
124
- state.retiredIdentities=[...new Set([...(previous?.retiredIdentities??[]),...inspected.retired??[]])];
125
- state.identityHistory=[...new Set([...(previous?.identityHistory??[]),...inspected.identities])];
126
119
  }
127
120
  export async function renderState(directory,revision,rulesVersion,runtime,preserveEdits=false) {
128
121
  const p=await locateState(directory,true);
129
122
  return withLock(p.lock,async()=>{
130
123
  const {state}=await readState(directory); validateState(state); checkRevision(state,revision); assertRules(state,rulesVersion,runtime);
131
- if (p.legacy) fail('MIGRATION_REQUIRED','Migrate before rendering.','Use migrate, then adopt.');
132
124
  return {revision:state.revision,stateSaved:false,view:await generateView(directory,state,preserveEdits)};
133
125
  });
134
126
  }
@@ -137,7 +129,7 @@ function checkRevision(state, revision) {
137
129
  }
138
130
  export function assertRules(state, rulesVersion, runtime) {
139
131
  assertInstallationComplete(runtime);
140
- if (!rulesVersion || rulesVersion !== state.rulesVersion || rulesVersion !== runtime.version || runtime.activeVersion !== runtime.version) fail('VERSION_MISMATCH', 'The session, state, tool, and active installation must use the same rules version.', 'Save only unfinished facts with checkpoint if available; bind the active version, reload its core and current stage, then adopt at a stable point.', { session: rulesVersion, state: state.rulesVersion, tool: runtime.version, active: runtime.activeVersion, launcher: runtime.launcher });
132
+ if (!rulesVersion || rulesVersion !== state.rulesVersion || rulesVersion !== runtime.version || runtime.activeVersion !== runtime.version) fail('VERSION_MISMATCH', 'The session, state, tool, and active installation must use the same rules version.', 'Bind the active version and reload its core and current stage; do not write with mismatched state, session or tool versions.', { session: rulesVersion, state: state.rulesVersion, tool: runtime.version, active: runtime.activeVersion, launcher: runtime.launcher });
141
133
  }
142
134
  export function assertInstallationComplete(runtime) {
143
135
  if (runtime.installPending) fail('INSTALL_PENDING', 'The installation is incomplete; new workflow writes and bindings are suspended.', 'Read state without progressing, then rerun install from the same exact pending package. After recovery, bind and reload its fixed core before continuing.', { pendingVersion: runtime.installPending.version, activeVersion: runtime.activeVersion, launcher: runtime.launcher });
@@ -145,17 +137,20 @@ export function assertInstallationComplete(runtime) {
145
137
  export async function initState(directory, input, rulesVersion, runtime) {
146
138
  assertInstallationComplete(runtime);
147
139
  if (rulesVersion !== runtime.version || runtime.activeVersion !== runtime.version) fail('VERSION_MISMATCH', 'New delivery requires the active tool and rules version.', 'Bind the current installation and use its returned version.');
148
- const p = statePaths(directory); await noSymlinks(p.internal); await fs.mkdir(p.internal, { recursive: true });
149
- if (await exists(path.join(p.dir,'state.json')) || await exists(path.join(p.dir,'state.previous.json')) || await exists(p.backup) || await exists(path.join(p.internal,'migration.json'))) fail('ALREADY_EXISTS','Existing delivery or recovery material found.','Read/resume the existing delivery.');
140
+ const p = statePaths(directory); await noSymlinks(p.internal);
141
+ if (await exists(p.statePath) || await exists(p.backup)) fail('ALREADY_EXISTS','Existing delivery or recovery material found.','Read/resume the existing delivery.');
142
+ requireThat(object(input) && ['en','zh-CN'].includes(input.language), 'New delivery input requires language en or zh-CN, from the explicit preference or current user conversation.');
143
+ const storage = await validateInitLocation(directory,input,runtime);
144
+ await fs.mkdir(p.internal, { recursive: true });
150
145
  return withLock(p.lock, async () => {
151
146
  if (await exists(p.statePath)) fail('ALREADY_EXISTS', 'A delivery already exists here.', 'Read/resume this delivery or choose a different folder.');
152
- const forbidden = ['schemaVersion', 'revision', 'deliveryId', 'rulesVersion', 'createdAt', 'updatedAt', 'goalHistory', 'rulesHistory', 'documentDigests', 'identityHistory', 'retiredIdentities', 'legacyRunIds', 'writeResult'];
147
+ const forbidden = ['schemaVersion', 'revision', 'deliveryId', 'rulesVersion', 'createdAt', 'updatedAt', 'goalHistory', 'documentDigests', 'writeResult'];
153
148
  requireThat(object(input) && !forbidden.some(x => Object.hasOwn(input, x)), 'init input must not set tool-managed identity/version fields.');
154
149
  const now = new Date().toISOString();
155
- const state = { goalRef:'source/request.md', documentPolicy:{authority:null,interaction:null,prototypeCovered:null,interface:null,databases:null}, documents:[], deliveryPolicy: {version:1}, reviewPolicy: 1, reviewHistory: [], cycle: 1, closureRefs: [], stageRuns: [], currentRun: null, openChanges: [], repositories: [], outcome: { goal: 'in-progress', acceptance: 'not-recorded' }, next: { action: 'Clarify the request and prepare requirements for review.', inputRefs:['source/request.md'] }, ...input, schemaVersion: SCHEMA_VERSION, revision: 0, deliveryId: randomUUID(), rulesVersion, createdAt: now, updatedAt: now };
150
+ const state = { goalRef:'source/request.md', documentPolicy:{authority:null,interaction:null,prototypeCovered:null,interface:null,databases:null}, documents:[], deliveryPolicy: {version:1}, reviewPolicy: 1, reviewHistory: [], cycle: 1, closureRefs: [], stageRuns: [], currentRun: null, openChanges: [], repositories: [], outcome: { goal: 'in-progress', acceptance: 'not-recorded' }, next: { action: text(input,'Clarify the request and prepare requirements for review.','澄清诉求并准备需求审核。'), inputRefs:['source/request.md'] }, ...input, storage, schemaVersion: SCHEMA_VERSION, revision: 0, deliveryId: randomUUID(), rulesVersion, createdAt: now, updatedAt: now };
156
151
  if (object(input.documentPolicy)) state.documentPolicy = {authority:null,interaction:null,prototypeCovered:null,interface:null,databases:null,...input.documentPolicy};
157
152
  if (object(state.goal) && state.goal.endpoint === undefined) state.goal.endpoint = null;
158
- arrangeSessions(state); validateState(state); requireThat(state.deliveryPolicy?.version === 1, 'New deliveries require deliveryPolicy version 1.'); requireThat(state.documentPolicy.authority !== 'legacy', 'New deliveries cannot opt into legacy rules.'); await inspectForWrite(directory,state); await enforceReviews(directory,state); await enforceCompletion(directory,state); await atomicWrite(p.statePath, json(state)); return savedResult(directory,state);
153
+ arrangeSessions(state); validateState(state); requireThat(state.deliveryPolicy?.version === 1, 'New deliveries require deliveryPolicy version 1.'); await inspectForWrite(directory,state); await enforceReviews(directory,state); await enforceCompletion(directory,state); await atomicWrite(p.statePath, json(state)); return savedResult(directory,state);
159
154
  });
160
155
  }
161
156
  function merge(base, patch) {
@@ -182,56 +177,30 @@ function retainHistory(previous, next) {
182
177
  requireThat(next.cycle >= previous.cycle && next.cycle <= previous.cycle + 1, 'cycle may stay unchanged or increase by one.');
183
178
  if (next.cycle > previous.cycle) requireThat(next.closureRefs.length > previous.closureRefs.length && next.outcome.acceptance === 'not-recorded', 'Reopening requires a previous closure reference and reset current acceptance.');
184
179
  }
185
- export async function updateState(directory, patch, revision, rulesVersion, runtime, mode = 'update') {
180
+ export async function updateState(directory, patch, revision, rulesVersion, runtime) {
186
181
  assertInstallationComplete(runtime);
187
182
  const p = await locateState(directory,true);
188
183
  return withLock(p.lock, async () => {
189
184
  const read = await readState(directory);
190
- if (read.layoutMismatch) fail('LAYOUT_CONFLICT','Schema and layout disagree.','Inspect the original layout; do not write through an inconsistent path.');
191
185
  const previous = read.state; validateState(previous); checkRevision(previous, revision);
192
- if (previous.schemaVersion === 1 && mode !== 'checkpoint') fail('MIGRATION_REQUIRED','Legacy layout/schema must migrate before rule adoption or updates.','Use migrate with a compatibility assessment, then adopt.');
193
186
  requireThat(object(patch), 'Update input must be an object.');
194
- if (mode === 'checkpoint') {
195
- if (rulesVersion !== previous.rulesVersion || runtime.version !== previous.rulesVersion) fail('VERSION_MISMATCH', 'Checkpoint must use the rules already bound to this state.', 'Use the compatible old tool or preserve facts outside state and resume with the active tool.');
196
- requireThat(Object.keys(patch).every(x => ['next', 'checkpoint'].includes(x)) && object(patch.checkpoint) && string(patch.checkpoint.summary), 'Checkpoint may save only next and checkpoint.summary/ref facts; it cannot complete stages or outcomes.');
197
- if (runtime.activeVersion === runtime.version) fail('CHECKPOINT_NOT_NEEDED', 'Rules match; use a normal update for unfinished facts.', 'Save current work with update.');
198
- if (previous.checkpoint?.activeVersion === runtime.activeVersion) fail('CHECKPOINT_ALREADY_SAVED', 'A checkpoint for this active version already exists.', 'Reload active rules and adopt at the recorded stable point.');
199
- if (patch.checkpoint.ref !== undefined) ref(patch.checkpoint.ref, 'checkpoint.ref');
200
- } else if (mode === 'adopt') {
201
- requireThat(runtime.version === runtime.activeVersion && rulesVersion === runtime.version, 'Adoption requires the active rules and tool.');
202
- requireThat(previous.rulesVersion !== rulesVersion, 'Rules are already adopted; no version change is needed.');
203
- requireThat(Object.keys(patch).length === 1 && string(patch.compatibilityRef), 'Adoption input must contain only compatibilityRef documenting the stable point and compatibility assessment.');
204
- ref(patch.compatibilityRef, 'compatibilityRef');
205
- } else {
206
- assertRules(previous, rulesVersion, runtime);
207
- }
208
- const immutable = ['schemaVersion', 'revision', 'deliveryId', 'rulesVersion', 'createdAt', 'updatedAt', 'goalHistory', 'rulesHistory', 'documentDigests', 'identityHistory', 'retiredIdentities', 'legacyRunIds', 'writeResult'];
187
+ assertRules(previous, rulesVersion, runtime);
188
+ const immutable = ['schemaVersion', 'revision', 'deliveryId', 'rulesVersion', 'createdAt', 'updatedAt', 'goalHistory', 'documentDigests', 'writeResult', 'storage'];
209
189
  requireThat(!immutable.some(x => Object.hasOwn(patch, x)), 'Identity, revision, and historical metadata are tool-managed.');
210
- let next;
211
- if (mode === 'adopt') {
212
- next = { ...previous, rulesVersion, rulesHistory: [...(previous.rulesHistory ?? []), { from: previous.rulesVersion, to: rulesVersion, ref: patch.compatibilityRef, at: new Date().toISOString() }] };
213
- } else next = merge(previous, patch);
214
- if (mode === 'checkpoint') next.checkpoint.activeVersion = runtime.activeVersion;
190
+ const next = merge(previous, patch);
215
191
  requireThat(previous.deliveryPolicy === undefined || next.deliveryPolicy?.version === previous.deliveryPolicy.version, 'Cannot remove or downgrade deliveryPolicy.');
216
- if (previous.deliveryPolicy === undefined && next.deliveryPolicy !== undefined) {
217
- requireThat(string(next.compatibilityRef) && patch.compatibilityRef !== undefined, 'Explicit delivery-policy adoption needs compatibilityRef; preserve historical outcomes.');
218
- const basis = await checkReferences(directory,{...next,goalRef:next.compatibilityRef,next:{inputRefs:[]},stageRuns:[],currentRun:null});
219
- requireThat(basis.every(r=>['exists','not-checked'].includes(r.status)), 'Compatibility assessment must be accessible before policy adoption.');
220
- }
221
192
  arrangeSessions(next); validateState(next);
222
193
  // Registering the first formal basis is not a new user scope decision.
223
- const formalizing = previous.schemaVersion === 3 && previous.goalRef === 'source/request.md' && !previous.documents.some(d=>['prd','scope'].includes(d.kind)) && next.documents.some(d=>['prd','scope'].includes(d.kind) && d.path === next.goalRef.split('#')[0]);
194
+ const formalizing = previous.schemaVersion === 4 && previous.goalRef === 'source/request.md' && !previous.documents.some(d=>['prd','scope'].includes(d.kind)) && next.documents.some(d=>['prd','scope'].includes(d.kind) && d.path === next.goalRef.split('#')[0]);
224
195
  if (JSON.stringify(previous.goal) !== JSON.stringify(next.goal) || (previous.goalRef !== next.goalRef && !formalizing)) {
225
196
  requireThat(string(next.goal.decisionRef) && next.goal.decisionRef !== previous.goal.decisionRef, 'Goal revision requires a new explicit decisionRef; viewing results does not revise a goal.');
226
197
  next.goalHistory = [...(previous.goalHistory ?? []), { goal: previous.goal, goalRef: previous.goalRef, deliveryPolicy: previous.deliveryPolicy, outcome: previous.outcome, revision: previous.revision }];
227
198
  }
228
- requireThat(previous.documentPolicy?.authority === 'legacy' || next.documentPolicy?.authority !== 'legacy', 'Cannot downgrade a new delivery to legacy document rules.');
229
- if (mode === 'update' && next.documentPolicy?.authority === 'legacy') requireThat(Object.keys(patch).every(k=>['next','checkpoint','repositories'].includes(k)), 'Reconcile legacy documents and set the current documentPolicy before advancing stages or outcomes.');
230
- if (mode === 'update') { await inspectForWrite(directory,next,previous); if (next.reviewPolicy === 1 || !Object.keys(patch).every(k=>['next','checkpoint','repositories'].includes(k))) await enforceReviews(directory,next,previous); }
199
+ await inspectForWrite(directory,next,previous); await enforceReviews(directory,next,previous);
231
200
  if (previous.deliveryPolicy?.partialRef !== next.deliveryPolicy?.partialRef && previous.deliveryPolicy !== undefined) requireThat(string(next.goal.decisionRef) && next.goal.decisionRef !== previous.goal.decisionRef, 'Scope changes need a new explicit goal decision.');
232
- if (mode === 'update' && next.deliveryPolicy && previous.deliveryPolicy && JSON.stringify(previous.outcome)!==JSON.stringify(next.outcome)) await enforceCompletion(directory,next);
201
+ if (next.deliveryPolicy && previous.deliveryPolicy && JSON.stringify(previous.outcome)!==JSON.stringify(next.outcome)) await enforceCompletion(directory,next);
233
202
  retainHistory(previous, next); next.revision = previous.revision + 1; next.updatedAt = new Date().toISOString();
234
- validateState(next); await atomicWrite(p.backup, json(previous)); await atomicWrite(p.statePath, json(next)); return next.schemaVersion >= 2 ? savedResult(directory,next) : next;
203
+ validateState(next); await atomicWrite(p.backup, json(previous)); await atomicWrite(p.statePath, json(next)); return savedResult(directory,next);
235
204
  });
236
205
  }
237
206
  export async function recoverState(directory, revision, rulesVersion, runtime) {
@@ -243,8 +212,8 @@ export async function recoverState(directory, revision, rulesVersion, runtime) {
243
212
  const raw = await fs.readFile(p.statePath, 'utf8');
244
213
  let parsed; try { parsed = JSON.parse(raw); } catch { /* Corruption is preserved below. */ }
245
214
  if (parsed) {
246
- if ((parsed.deliveryPolicy !== undefined && parsed.deliveryPolicy?.version !== 1) || (parsed.reviewPolicy !== undefined && parsed.reviewPolicy !== 1)) fail('UNKNOWN_SCHEMA','Cannot recover over an unknown review protocol.','Preserve the newer state and use a compatible tool.');
247
- if (parsed.schemaVersion !== backup.schemaVersion) fail('UNKNOWN_SCHEMA', 'Cannot recover over an unknown schema.', 'Use a compatible tool; preserve the newer state.');
215
+ requireThat(parsed.deliveryPolicy?.version === backup.deliveryPolicy.version && parsed.reviewPolicy === backup.reviewPolicy, 'State and recovery copy must use the same policies.');
216
+ requireThat(parsed.schemaVersion === backup.schemaVersion, 'State and recovery copy must use the same schemaVersion.');
248
217
  try { validateState(parsed); fail('STATE_VALID', 'Current state is valid; refusing rollback.', 'Use a normal revision-checked update to reconcile facts.'); }
249
218
  catch (e) { if (!['INVALID_STATE','REVIEW_CHECK'].includes(e.code)) throw e; }
250
219
  if (Number.isSafeInteger(parsed.revision)) corruptRevision = Math.max(corruptRevision, parsed.revision);
@@ -252,7 +221,7 @@ export async function recoverState(directory, revision, rulesVersion, runtime) {
252
221
  await atomicWrite(path.join(p.internal, `state.corrupt.${randomUUID()}.json`), raw);
253
222
  }
254
223
  const next = { ...backup, revision: corruptRevision + 1, updatedAt: new Date().toISOString(), recovery: { fromRevision: backup.revision, action: 'Inspect actual artifacts and unfinished effects before proceeding; nothing has been re-executed.' } };
255
- await atomicWrite(p.statePath, json(next)); return next.schemaVersion >= 2 ? savedResult(directory,next) : next;
224
+ await atomicWrite(p.statePath, json(next)); return savedResult(directory,next);
256
225
  });
257
226
  }
258
227
  export async function checkReferences(directory, state, all = false) {
@@ -264,13 +233,11 @@ export async function checkReferences(directory, state, all = false) {
264
233
  const add = value => { if (value !== undefined) { ref(value, 'registered reference'); values.add(value); } };
265
234
  const addOutcome = outcome => { for (const key of OUTCOME_REFS) add(outcome?.[key]); };
266
235
  if (state.deliveryPolicy?.partialRef != null) add(state.deliveryPolicy.partialRef);
267
- add(state.compatibilityRef);
236
+ add(state.storage?.decisionRef);
268
237
  add(state.goal.decisionRef); addOutcome(state.outcome);
269
238
  for (const history of state.goalHistory ?? []) {
270
239
  add(history?.goalRef); add(history?.goal?.decisionRef); addOutcome(history?.outcome);
271
240
  }
272
- for (const history of state.rulesHistory ?? []) add(history?.ref);
273
- add(state.checkpoint?.ref);
274
241
  for (const r of state.reviewHistory ?? []) add(r.sourceRef);
275
242
  for (const d of state.documents ?? []) for (const r of d.assetRefs ?? []) add(r);
276
243
  }
@@ -292,8 +259,8 @@ export async function checkReferences(directory, state, all = false) {
292
259
  await fs.access(real, fs.constants.R_OK);
293
260
  const fragment=value.split('#')[1], doc=state.documents?.find(d=>d.path===value.split('#')[0]);
294
261
  if (fragment && doc?.mode==='new' && /\.md$/.test(relative)) {
295
- const ids=anchors(await fs.readFile(real,'utf8'));
296
- results.push({ref:value,status:ids.filter(x=>x===fragment).length===1?'exists':'missing-fragment',path:target,fragmentChecked:true});
262
+ const body=await fs.readFile(real,'utf8');
263
+ results.push({ref:value,status:hasLocator(body,fragment,(await artifactMap()).documents[doc.kind])?'exists':'missing-fragment',path:target,fragmentChecked:true});
297
264
  } else if (fragment) results.push({ref:value,status:'not-checked',path:target,fragmentChecked:false,reason:'Nonstandard/reused fragment requires host inspection and recorded basis.'});
298
265
  else results.push({ref:value,status:'exists',path:target,fragmentChecked:false});
299
266
  }
@@ -304,7 +271,7 @@ export async function checkReferences(directory, state, all = false) {
304
271
 
305
272
  async function enforceCompletion(directory,state) {
306
273
  if (state.deliveryPolicy?.version !== 1 || state.outcome.goal !== 'met') return;
307
- if (state.schemaVersion === 3 && (state.goal.endpoint === null || state.goalRef === 'source/request.md')) fail('DELIVERY_INCOMPLETE','Initialization is not a completed delivery.','Resolve the endpoint and formal requirements before claiming completion.');
274
+ if (state.schemaVersion === 4 && (state.goal.endpoint === null || state.goalRef === 'source/request.md')) fail('DELIVERY_INCOMPLETE','Initialization is not a completed delivery.','Resolve the endpoint and formal requirements before claiming completion.');
308
275
  const completion=completionStatus(state,await reviewStatus(directory,state));
309
276
  if (!state.deliveryPolicy.partialRef && !completion.readyForAcceptance) fail('DELIVERY_INCOMPLETE','Standard delivery evidence is incomplete.','Keep the goal in progress, or record the explicitly limited scope without claiming standard completion.',completion);
310
277
  const refs=await checkReferences(directory,state,true);
package/lib/view.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { text, displayValue } from './language.mjs';
1
2
  import { completionStatus, sessionAdvice } from './delivery.mjs';
2
3
  import fs from 'node:fs/promises';
3
4
  import { reviewStatus } from './approvals.mjs';
@@ -8,55 +9,56 @@ import { resolveRef, documentChanges } from './documents.mjs';
8
9
 
9
10
  const paths = dir => ({ view: path.join(dir,'status.md'), meta: path.join(dir,'.aimship/view.json') });
10
11
  export async function viewStatus(directory, state) {
11
- if (![2,3].includes(state.schemaVersion)) return { status:'legacy', recovery:'Explicitly migrate this delivery before generating its view.' };
12
+ const t=(en,zh)=>text(state,en,zh);
12
13
  const p=paths(directory);
13
14
  try {
14
- if (!await exists(p.view)) return { status:'missing', recovery:'Run render at the current revision.' };
15
+ if (!await exists(p.view)) return { status:'missing', recovery:t('Run render at the current revision.','使用当前修订重新生成视图。') };
15
16
  await noSymlinks(p.view); await noSymlinks(p.meta);
16
- if (!await exists(p.meta)) return { status:'modified', recovery:'No trusted generation digest; preserve edits with render --preserve-edits.' };
17
+ if (!await exists(p.meta)) return { status:'modified', recovery:t('No trusted generation digest; preserve edits with render --preserve-edits.','没有可信的生成摘要;请保留手工修改后重新生成视图。') };
17
18
  const meta=await readJSON(p.meta), raw=await fs.readFile(p.view,'utf8');
18
- if (hash(raw)!==meta.hash) return { status:'modified', recovery:'Inspect the edits, then render --preserve-edits.' };
19
+ if (hash(raw)!==meta.hash) return { status:'modified', recovery:t('Inspect the edits, then render --preserve-edits.','先核对修改,再保留手工内容并重新生成视图。') };
19
20
  const current=meta.deliveryId===state.deliveryId && meta.revision===state.revision;
20
- return { status:current?'current':'stale', revision:meta.revision, recovery:current?null:'Run render; do not replay the saved business update.' };
21
- } catch(error) { return {status:'error',reason:error.code ?? error.message,recovery:'Inspect view/metadata paths; preserve untrusted content before render.'}; }
21
+ return { status:current?'current':'stale', revision:meta.revision, recovery:current?null:t('Run render; do not replay the saved business update.','重新生成视图,不要重放已保存的业务更新。') };
22
+ } catch(error) { return {status:'error',reason:error.code ?? error.message,recovery:t('Inspect view/metadata paths; preserve untrusted content before render.','核对视图和元数据路径,重新生成前保留现有内容。')}; }
22
23
  }
23
24
  const cell = x => String(x ?? 'not recorded').replaceAll('|','\\|').replaceAll('\n',' ');
24
- const link = ref => `[${ref.replaceAll('[','\\[').replaceAll(']','\\]')}](${ref.replaceAll(' ','%20').replaceAll('(','%28').replaceAll(')','%29')})`;
25
+ const link = ref => `[${ref.replaceAll('[','\\[').replaceAll(']','\\]')}](${ref.split('#')[0].replaceAll(' ','%20').replaceAll('(','%28').replaceAll(')','%29')})`;
25
26
  async function available(directory,state,ref) {
26
27
  if (/^https?:\/\//.test(ref)) return true;
27
28
  const p=resolveRef(directory,state,ref); return p && await exists(p);
28
29
  }
29
30
  export async function renderText(directory,state) {
31
+ const t=(en,zh)=>text(state,en,zh), v=x=>cell(displayValue(state,x)), identifier=x=>x==null?v(null):cell(x);
30
32
  const artifactLink=ref=> {
31
33
  if (!ref.startsWith('repo:')) return link(ref);
32
- const target=resolveRef(directory,state,ref);
33
- const fragment=ref.includes('#')?'#'+ref.split('#')[1]:'';
34
- return target ? `[${cell(ref)}](${encodeURI(target).replaceAll('(','%28').replaceAll(')','%29')}${fragment})` : cell(ref);
34
+ const target=resolveRef(directory,state,ref), fragment=ref.includes('#')?'#'+ref.split('#')[1]:'';
35
+ return target ? `[${cell(ref)}](${encodeURI(target).replaceAll('(','%28').replaceAll(')','%29')})` : cell(ref);
35
36
  };
36
- const lines=['# Delivery status','','Generated by Aimship. Edit formal documents and structured state, not this page.','', '## Goal and basis','', cell(state.goal.summary), '',`Endpoint: ${cell(state.goal.endpoint ?? 'Unknown — not yet agreed')}`,''];
37
- if (state.schemaVersion === 3) lines.push('Initialization: state saved; this page is the derived view.', `Requirements: ${state.documents.some(d=>['prd','scope'].includes(d.kind)) ? 'registered; see document review' : 'being clarified; no formal document yet'}`, `Artifact conditions: ${['interaction','prototypeCovered','interface','databases'].map(k=>`${k}=${state.documentPolicy[k] === null ? 'unknown' : JSON.stringify(state.documentPolicy[k])}`).join(', ')}`, '');
38
- if (await available(directory,state,state.goalRef)) lines.push(artifactLink(state.goalRef),'');
39
- lines.push('## Associated engineering objects','','| ID | Actual checkout/worktree | Role |','| --- | --- | --- |');
40
- for (const r of state.repositories) lines.push(`| ${cell(r.id)} | ${cell(r.path)} | ${cell(r.role)} |`);
41
- if (!state.repositories.length) lines.push('| — | No repository registered | — |');
42
- lines.push('','## Stage progress','','| Run / scope | Stage | Progress | Result | Applicability |','| --- | --- | --- | --- | --- |');
43
- for (const r of state.stageRuns) lines.push(`| ${cell(r.id)} / ${cell(r.scope)} | ${r.stage} | ${r.progress} | ${cell(r.result)} | ${cell(r.applicability)} |`);
44
- lines.push('','## Current work, blockers and next action','',`Current: ${cell(state.currentRun)}`, '',`Next: ${cell(state.next.action)}`);
45
- for (const r of state.stageRuns.filter(r=>r.progress==='blocked')) lines.push(`- Blocked: ${cell(r.id)} — ${cell(r.scope)}`);
46
- for (const c of state.openChanges.filter(c=>c.status==='open')) lines.push(`- Open change/question: ${link(c.ref)}`);
47
- if (state.checkpoint) lines.push('',`Unfinished facts: ${cell(state.checkpoint.summary)}`);
48
- const review = await reviewStatus(directory,state);
49
- lines.push('', '## Document review', '', `Review protocol: ${review.compatibility}`, `Next stage: ${cell(review.nextStage)}`, '', '| Document | Version | Review status | Approval record |', '| --- | --- | --- | --- |');
50
- for (const d of review.documents) lines.push(`| ${artifactLink(d.path)} | ${cell(d.version)} | ${d.status} | ${cell(d.approval)} |`);
51
- for (const p of review.pending) lines.push(`- Awaiting review: ${artifactLink(p.document)} (${cell(p.run)}; ${p.status})`);
52
- lines.push('', 'Approval records bind content and user decision evidence; the tool does not authenticate the user.', '');
53
- const completion=completionStatus(state,review,{changed:await documentChanges(directory,state)}), session=sessionAdvice(directory,state,review);
54
- lines.push('', '## Delivery completeness', '', `Assessment: ${completion.status}`, `Standard complete: ${completion.standardComplete}`, ...completion.gaps.map(g=>`- ${cell(g)}`), '', 'Historical goal/acceptance values are separate from this assessment.', '', '## Session suggestions', '', 'Requirements → technical/interface/SQL → test cases → plan → each implementation batch with its review → overall review → each verification batch. Only applicable work is grouped; these are context suggestions, not gates.', '', ...session.groups.map(g=>`- ${cell(g.id)}: ${g.runs.map(cell).join(', ')}`), '', `Next interaction: ${session.action}`);
55
- if (session.continuation) lines.push('',cell(session.continuation));
56
- lines.push('','## Artifacts and plan','');
37
+ const lines=[t('# Delivery status','# 交付状态'),'',t('Generated by Aimship. Edit formal documents and structured state, not this page.','由 Aimship 生成。请通过正式文档和结构化状态更新工作记录。'),'',t('## Goal and basis','## 目标与依据'),'',cell(state.goal.summary),'',`${t('Endpoint','交付终点')}: ${cell(state.goal.endpoint ?? t('Unknown — not yet agreed','未知,尚未约定'))}`,''];
38
+ lines.push(`${t('Display language','显示语言')}: ${state.language ?? v(null)}`,'');
39
+ if (state.schemaVersion===4) lines.push(t('Initialization: state saved; this page is the derived view.','初始化:状态已保存,本页为生成的视图。'),`${t('Requirements','需求')}: ${state.documents.some(d=>['prd','scope'].includes(d.kind))?t('registered; see document review','已登记,见文档审核'):t('being clarified; no formal document yet','正在澄清,尚无正式文档')}`,`${t('Artifact conditions','产物适用条件')}: ${['interaction','prototypeCovered','interface','databases'].map(k=>`${v(k)}=${state.documentPolicy[k]===null?v('unknown'):Array.isArray(state.documentPolicy[k])?(state.documentPolicy[k].length?state.documentPolicy[k].join(', '):v('none')):v(state.documentPolicy[k])}`).join(', ')}`,'');
40
+ if(await available(directory,state,state.goalRef))lines.push(artifactLink(state.goalRef),'');
41
+ lines.push(t('## Associated engineering objects','## 关联工程'),'',t('| ID | Actual checkout/worktree | Role |','| ID | 实际代码目录 | 用途 |'),'| --- | --- | --- |');
42
+ for(const r of state.repositories)lines.push(`| ${cell(r.id)} | ${cell(r.path)} | ${cell(r.role)} |`);
43
+ if(!state.repositories.length)lines.push(t('| — | No repository registered | — |','| — | 尚未关联代码仓库 | — |'));
44
+ lines.push('',t('## Stage progress','## 阶段进度'),'',t('| Run / scope | Stage | Progress | Result | Applicability |','| 工作记录 / 范围 | 阶段 | 进度 | 结果 | 有效性 |'),'| --- | --- | --- | --- | --- |');
45
+ for(const r of state.stageRuns)lines.push(`| ${cell(r.id)} / ${cell(r.scope)} | ${v(r.stage)} | ${v(r.progress)} | ${v(r.result)} | ${v(r.applicability)} |`);
46
+ lines.push('',t('## Current work, blockers and next action','## 当前工作、阻塞与下一步'),'',`${t('Current','当前工作')}: ${identifier(state.currentRun)}`,'',`${t('Next','下一步')}: ${cell(state.next.action)}`);
47
+ for(const r of state.stageRuns.filter(r=>r.progress==='blocked'))lines.push(`- ${t('Blocked','受阻')}: ${cell(r.id)} — ${cell(r.scope)}`);
48
+ for(const c of state.openChanges.filter(c=>c.status==='open'))lines.push(`- ${t('Open change/question','待处理变更或问题')}: ${link(c.ref)}`);
49
+ const review=await reviewStatus(directory,state);
50
+ lines.push('',t('## Document review','## 文档审核'),'',`${t('Review protocol','审核协议')}: ${v(review.compatibility)}`,`${t('Next stage','下一阶段')}: ${v(review.nextStage)}`,'',t('| Document | Version | Review status | Approval record |','| 文档 | 修订 | 审核状态 | 批准记录 |'),'| --- | --- | --- | --- |');
51
+ for(const d of review.documents)lines.push(`| ${artifactLink(d.path)} | ${cell(d.version)} | ${v(d.status)} | ${identifier(d.approval)} |`);
52
+ for(const p of review.pending)lines.push(`- ${t('Awaiting review','待审核')}: ${artifactLink(p.document)} (${cell(p.run)}; ${v(p.status)})`);
53
+ lines.push('',t('Approval records bind content and user decision evidence; the tool does not authenticate the user.','批准记录关联文档内容与用户决定依据;工具不认证用户身份。'),'');
54
+ const completion=completionStatus(state,review,{changed:await documentChanges(directory,state)}),session=sessionAdvice(directory,state,review);
55
+ lines.push('',t('## Delivery completeness','## 交付完整性'),'',`${t('Assessment','评估')}: ${v(completion.status)}`,`${t('Standard complete','标准交付完成')}: ${v(completion.standardComplete)}`,...completion.gaps.map(g=>`- ${cell(g)}`),'',t('Historical goal/acceptance values are separate from this assessment.','历史目标与接受记录独立保留,不替代本次完整性评估。'),'',t('## Session suggestions','## 会话建议'),'',t('Requirements → technical/interface/SQL → test cases → plan → each implementation batch with its review → overall review → each verification batch. Only applicable work is grouped; these are context suggestions, not gates.','需求 → 技术设计与适用的接口/SQL → 测试用例 → 实施计划 → 各实施批次及其审查 → 整体审查 → 各验证批次。仅分组适用工作;这是上下文建议,不是额外审批门槛。'),'',...session.groups.map(g=>`- ${cell(g.id)}: ${g.runs.map(cell).join(', ')}`),'',`${t('Next interaction','下一项操作')}: ${v(session.action)}`);
56
+ if(session.instruction)lines.push('',cell(session.instruction));
57
+ if(session.continuation)lines.push('',cell(session.continuation));
58
+ lines.push('',t('## Artifacts and plan','## 成果与计划'),'');
57
59
  const refs=new Set([...(state.documents??[]).map(d=>d.path),...state.stageRuns.flatMap(r=>[...r.outputRefs,r.resultRef].filter(Boolean)),...(state.next.inputRefs??[])]);
58
- for (const ref of refs) if (await available(directory,state,ref)) lines.push(`- ${artifactLink(ref)}`);
59
- lines.push('','## Independent outcomes and generation','',`Goal: ${state.outcome.goal}`,`Acceptance: ${state.outcome.acceptance}`,`Merge: ${state.outcome.mergeRef?link(state.outcome.mergeRef):'not recorded'}`,`Deployment: ${state.outcome.deploymentRef?link(state.outcome.deploymentRef):'not recorded'}`,'',`Delivery: ${state.deliveryId}`,`State revision: ${state.revision}`,`Rules: ${state.rulesVersion}`,`State saved: ${state.updatedAt}`,'','Matching revisions do not establish document semantics or authorization.','');
60
+ for(const ref of refs)if(await available(directory,state,ref))lines.push(`- ${artifactLink(ref)}`);
61
+ lines.push('',t('## Independent outcomes and generation','## 独立结果与生成信息'),'',`${t('Goal','目标')}: ${v(state.outcome.goal)}`,`${t('Acceptance','用户接受')}: ${v(state.outcome.acceptance)}`,`${t('Merge','合并')}: ${state.outcome.mergeRef?link(state.outcome.mergeRef):v(null)}`,`${t('Deployment','部署')}: ${state.outcome.deploymentRef?link(state.outcome.deploymentRef):v(null)}`,'',`${t('Delivery','交付')}: ${state.deliveryId}`,`${t('State revision','状态修订')}: ${state.revision}`,`${t('Rules','规则版本')}: ${state.rulesVersion}`,`${t('State saved','状态保存时间')}: ${state.updatedAt}`,'',t('Matching revisions do not establish document semantics or authorization.','修订一致不代表文档语义或授权已经核实。'),'');
60
62
  return lines.join('\n');
61
63
  }
62
64
  export async function generateView(directory,state,preserveEdits=false) {
@@ -73,7 +75,7 @@ export async function generateView(directory,state,preserveEdits=false) {
73
75
  await atomicWrite(p.view,text);
74
76
  await atomicWrite(p.meta,json({deliveryId:state.deliveryId,revision:state.revision,hash:hash(text)}));
75
77
  return {status:'current',revision:state.revision,...(preservedPath?{preservedPath}:{})};
76
- } catch(error) {return {status:'error',reason:error.code??error.message,...(preservedPath?{preservedPath}:{}),recovery:'State was saved. Repair the view paths, then render at the returned revision; do not replay update.'};}
78
+ } catch(error) {return {status:'error',reason:error.code??error.message,...(preservedPath?{preservedPath}:{}),recovery:text(state,'State was saved. Repair the view paths, then render at the returned revision; do not replay update.','状态已保存。请修复视图路径,并在返回的修订上重新生成视图;不要重复业务更新。')};}
77
79
  }
78
80
  export async function savedResult(directory,state) {
79
81
  const view=await generateView(directory,state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aimship",
3
- "version": "0.1.0-preview.11",
3
+ "version": "0.1.0-preview.13",
4
4
  "description": "Explicit, goal-driven delivery workflow for coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,12 +8,12 @@ disable-model-invocation: true
8
8
 
9
9
  Activate only when the user explicitly asks to start or resume Aimship. A clear request such as “use Aimship to start/continue” is explicit activation; a passing mention of its name or opening a project is not. In each new session, explicitly activate it again. Ordinary conversation does not create a delivery. Once activated, continue the same delivery across turns without asking the user to invoke every stage.
10
10
 
11
- 1. Run `"{{AIMSHIP_NODE}}" "{{AIMSHIP_LAUNCHER}}" bind` to resolve the installed version. Read the returned fixed-version core and CLI help. Use that fixed version's tools, carrying its rules version and the current state revision. If this is a source checkout rather than an installed entry, use `node bin/aimship.mjs bind` from the package root.
11
+ 1. Run `"{{AIMSHIP_NODE}}" "{{AIMSHIP_LAUNCHER}}" bind` to resolve the installed version. Read the returned fixed-version core and that CLI’s help before using its state protocol; cached old-tool schema metadata is not the newly bound protocol. Use that fixed version's tools, carrying its rules version and the current state revision. If this is a source checkout rather than an installed entry, use `node bin/aimship.mjs bind` from the package root.
12
12
  2. Read the core at the path returned by `bind`. Resolve all supporting links below against that same fixed release, even if a cached entry mentions an older path. [The shared core](references/core.md) is the source-checkout reference. For an existing delivery, read its state and the current goal/Scope plus only the next step's required references. Do not create a replacement delivery, reset its plan, or infer a new goal from “continue”.
13
- 3. Follow the core's [initialization flow](references/core.md#initialization) for new work or interrupted creation. A concrete problem or intended change is enough; no PRD, code or known technical conditions are required. Preserve the original request, resolve the same delivery and access, and use the existing init only when state does not exist. Unknown endpoint/conditions remain unknown, never an inferred authorization or limited scope. Report actual save location, current work and next step. Repeated activation resumes; later code association retains the same delivery ID.
13
+ 3. Follow the core's [initialization flow](references/core.md#initialization) for new work or interrupted creation. A concrete problem or intended change is enough; no PRD, code location, repository association or known technical conditions are required. Preserve the original request, resolve the same delivery and access, and use init only when state does not exist. New deliveries default to a separate folder under the bound dataRoot; the AI tool workspace or prototype location is not an explicit location choice. If access is needed, have the user add the actual delivery directory or grant access in the AI tool, without copying documents or claiming automatic folder addition. Save the delivery language from the user’s preference or current conversation, as the core specifies. Unknown endpoint/conditions remain unknown, never an inferred authorization or limited scope. Report actual save location, current work and next step. Repeated activation resumes; later code association retains the same delivery ID.
14
14
  4. Read the selected [stage reference](references/core.md#stage-references) when reaching it. Explain design choices and unresolved decisions. Follow [document review](references/document-review.md): generate, self-check, give the current document entry/status/review focus and wait; do not send a next-stage continuation prompt while review is pending. After explicit approval save and verify the record, then follow the core session interaction: continue within a group, suggest a new session at its boundary without executing the next group in that response; continuing here needs no new approval; questions, silence and viewing results do not approve. Authorized implementation/review/repair/retest can then continue.
15
15
  5. Use the core’s default session groups in existing state. At confirmed saved group boundaries give current results, next suggestion and one continuation line with the actual delivery directory; no extra prompt document. A requested full prompt is optional. On resume, uniquely verify the delivery and actual saved review/unfinished-work stop; never infer approval from the copied entry. Keep a compact binding: delivery ID, absolute delivery directory, state path, current run, rules version, and fixed core/tool paths. After compaction, reload these references if absent; do not assume the full skill survived.
16
16
 
17
- Read [recovery and updates](references/recovery.md) on interruption, version mismatch or missing references; [sources and change flow](references/sources-and-changes.md) when adopting external material or revising reviewed work; [host access](references/hosts.md) only when access or host setup needs attention. Read [document patterns](references/document-patterns.md) when establishing the artifact arrangement or producing a document. New deliveries keep formal documents at the root, source/request.md under source/, and structured state under .aimship/; status.md is the generated reading entry. On resume use the actual statePath returned by read, inspect view/document-change warnings, and explicitly repair or migrate as described in recovery. Do not treat a generated view as editable authority.
17
+ Read [recovery and updates](references/recovery.md) on interruption, version mismatch or missing references; [sources and change flow](references/sources-and-changes.md) when adopting external material or revising reviewed work; [AI tool access](references/hosts.md) only when access or tool setup needs attention. Read [document patterns](references/document-patterns.md) when establishing the artifact arrangement or producing a document. New deliveries keep formal documents at the delivery directory root, source/request.md under source/, and structured state under .aimship/; status.md is the generated reading entry. On resume use the actual statePath returned by read, inspect view/document-change warnings, and explicitly repair supported state as described in recovery. Do not treat a generated view as editable authority.
18
18
 
19
19
  The agent handles meaning, engineering and evidence. The local tools protect structure and writes; a successful command or a file's existence cannot establish goal completion. This skill does not schedule agents, open conversations, merge, deploy, or execute database changes by default.