aimship 0.1.0-preview.7 → 0.1.0-preview.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +38 -1
- package/README.md +120 -58
- package/README.zh-CN.md +120 -58
- package/docs/cli.md +18 -2
- package/docs/guide.md +134 -201
- package/docs/guide.zh-CN.md +134 -201
- package/docs/installation.md +12 -8
- package/docs/reference.md +78 -0
- package/docs/reference.zh-CN.md +78 -0
- package/lib/approvals.mjs +153 -0
- package/lib/cli.mjs +4 -3
- package/lib/documents.mjs +5 -4
- package/lib/state.mjs +13 -7
- package/lib/view.mjs +6 -0
- package/package.json +1 -1
- package/skills/aimship/SKILL.md +1 -1
- package/skills/aimship/references/artifact-map.json +21 -12
- package/skills/aimship/references/core.md +5 -3
- package/skills/aimship/references/document-patterns.md +4 -2
- package/skills/aimship/references/document-review.md +28 -0
- package/skills/aimship/references/recovery.md +2 -0
- package/skills/aimship/references/sources-and-changes.md +2 -0
- package/skills/aimship/references/stages/contract.md +7 -3
- package/skills/aimship/references/stages/diagnose.md +4 -0
- package/skills/aimship/references/stages/plan.md +5 -1
- package/skills/aimship/references/stages/prototype.md +4 -0
- package/skills/aimship/references/stages/requirements.md +4 -0
- package/skills/aimship/references/stages/scope.md +4 -0
- package/skills/aimship/references/stages/tech-design.md +6 -2
- package/skills/aimship/references/stages/test-design.md +5 -1
- package/skills/aimship/references/templates/plan.md +1 -0
- package/skills/aimship/references/templates/tech-design.md +1 -0
- package/skills/aimship/references/templates/testing.md +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Aimship 交付文档参考
|
|
2
|
+
|
|
3
|
+
[English](reference.md) · [使用指南](guide.zh-CN.md) · [README](../README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
本文供需要查阅文档生成条件、阶段产物及编号规则时使用;日常操作请先阅读使用指南。
|
|
6
|
+
|
|
7
|
+
目录位置以 [使用指南的注释目录树](guide.zh-CN.md#delivery-structure)为准:正式文档直接放在需求目录,新原型放在 `prototype/`,原始资料放在 `source/`,内部状态和证据放在 `.aimship/`。交付内不再增加 `docs/` 或 `sql/` 层级。
|
|
8
|
+
|
|
9
|
+
## 需求依据与输入条件
|
|
10
|
+
|
|
11
|
+
| 输入条件 | 需求文档 | 原型处理 |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| 提供权威 PRD/原型供本次采用 | 用 `scope.md` 记录采用版本、本次范围、缺口、决定和验收,保留原件 | 已覆盖的交互复用;委托范围内有未覆盖的交互变化时补原型 |
|
|
14
|
+
| 只有想法、讨论、截图或零散参考 | 自编 `prd.md`,其中包含范围和验收 | 委托范围内涉及交互变化、且现有材料未覆盖时生成原型 |
|
|
15
|
+
| 技术重构或已知原因的修复 | 自编 `prd.md`,说明技术目标、边界、行为约束和验收 | 不涉及交互变化就不生成原型 |
|
|
16
|
+
| 独立调查、只做 review 或只做验证 | PRD/Scope 只定义本次委托及终点 | 不虚构后续实施需求或原型 |
|
|
17
|
+
|
|
18
|
+
`prd.md` 和 `scope.md` 是需求依据的两种选择,不应默认同时创建两份重复描述。如果你明确委托维护一份已有、可编辑的权威 PRD,且它完整表达本次范围、授权和终点,可以登记复用。仅凭“以前由我们编写”不能认定采用的原件可以修改。
|
|
19
|
+
|
|
20
|
+
外来的 PRD、原型、图片等按采用版本保留在 `source/`,自己生成的正式文档按上述位置存放。需求变化时不改写外部原件。单独一个链接未必能保留资料版本,Agent 应记录可用来源、版本和访问缺口。提取文本、查看视觉内容、操作原型和编辑原格式,是不同能力。
|
|
21
|
+
|
|
22
|
+
## 阶段产物与内容
|
|
23
|
+
|
|
24
|
+
阶段名称是稳定标识。下表是阶段目录,不是强制顺序;实际选用哪些阶段及其依赖,由接受后的执行安排决定。即使无需单独执行需求阶段,每次交付仍保留原始委托、需求/范围依据和状态记录。共用 `plan.md` 或 `testing.md` 不代表两个阶段同时完成,各阶段分别记录状态和结果。文档在对应阶段开始时形成草稿,文件已创建不等于阶段已完成。
|
|
25
|
+
|
|
26
|
+
| 阶段 | 使用条件 | 产物及内容 |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `frame` 问题定义 | 问题或预期结果仍需澄清 | PRD/Scope 的目标与背景章节:问题、目标、非目标和假设 |
|
|
29
|
+
| `requirements` 需求 | 需要编写或采用需求 | `prd.md`:目标、范围、需求、状态、验收和执行安排;或 `scope.md`:资料采用、本次范围、排除项、决定、验收和执行安排 |
|
|
30
|
+
| `scope` 范围确认 | 需要明确本次委托边界 | 更新 PRD 的范围或 Scope 的目标/边界,不再新建另一份需求说明 |
|
|
31
|
+
| `prototype` 原型 | 所需交互变化未被现有资料覆盖 | 新 HTML 使用 `prototype/index.html`,相关资源放在同目录;其他约定格式或复用入口单独登记 |
|
|
32
|
+
| `diagnose` 调查定位 | 明确安排了独立调查 | `diagnosis.md`:目标、现象、环境、证据、原因、未知项和结论 |
|
|
33
|
+
| `tech-design` 技术方案 | 需要设计技术实现 | `tech-design.md`:依据、架构、模块、数据/契约、异常、风险和需求覆盖;通过后在 contract 生成并审核所需 SQL |
|
|
34
|
+
| `contract` 契约及 SQL | 接口或数据库变化需要正式产物 | HTTP 使用 `openapi.yaml` 或已登记的工程权威契约;非 HTTP 使用约定格式;每个目标库的独立 SQL 逐份审核 |
|
|
35
|
+
| `test-design` 测试设计 | 需要编写测试用例 | `testing.md`:依据、环境、用例/断言,以及需求和验收覆盖 |
|
|
36
|
+
| `plan` 实施计划 | 任何实施开始前,包括单个小批次 | `plan.md`:依据、批次、任务、验证安排、执行记录章节和剩余工作 |
|
|
37
|
+
| `implement` 实施 | 执行已确定的计划 | 在代码仓库实施;同一份 `plan.md` 记录实际任务状态、代码对象和偏差 |
|
|
38
|
+
| `review` 审核 | 审核某批、整体成果,或单独受托 review | `review.md`:审核依据、各次审核范围、发现与处理、结论 |
|
|
39
|
+
| `verify` 验证 | 必要 review 后执行带版本用例,或单独受托验证 | 在同一份 `testing.md` 追加执行批次/RUN、实际结果、证据、重测和结论 |
|
|
40
|
+
| `handoff` 交接 | 交付约定终点的成果 | `handoff.md`:目标、产物、使用方式、审核/验证结果、限制,以及完成/验收/集成状态 |
|
|
41
|
+
|
|
42
|
+
开发流程中,test-design 先于 plan;文档按[审核流程](guide.zh-CN.md#逐份审核与自动衔接)处理。`plan` 必须先于首次实施完成。每批实施后 review,全部完成后整体 review,再独立验证。实施时可以编写测试代码,但不额外设置逐批全量测试门禁。只有一批时,一次 review 可以明确覆盖批次和整体范围。单独委托验证时,可以采用已有、明确版本的用例,不声称重新执行过 `test-design`。
|
|
43
|
+
|
|
44
|
+
普通开发调试、已知原因的 Bug 不增加 `diagnose`。实现错误回到实施,设计错误回到技术方案,需求缺口回到 PRD/Scope,错误的测试预期回到带版本的用例。出现需要独立调查的重大未知项时,应记录阶段调整,相关依赖工作暂停。
|
|
45
|
+
|
|
46
|
+
## SQL、变更和进度
|
|
47
|
+
|
|
48
|
+
| 产物 | 生成规则与内容 |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `database.sql` | 数据库结构或数据发生变化时需要;一个数据库目标在需求根目录使用这一份文件 |
|
|
51
|
+
| `database-<id>.sql` | 多个独立数据库目标各一份,`id` 使用稳定的目标标识 |
|
|
52
|
+
| `change-log.md` | 有重要变更时记录原因、旧/新含义、决定、影响文档与工作,以及剩余动作 |
|
|
53
|
+
| `status.md` | 自动生成的阅读入口,显示目标、进度、阻塞、下一步和文档链接 |
|
|
54
|
+
| `.aimship/state.json` | 工具维护的状态,包含目标/阶段、文档引用、版本绑定及关联仓库/worktree/代码位置 |
|
|
55
|
+
|
|
56
|
+
SQL 标明目标、方言、版本/结构依据、执行条件,按顺序组织语句并关联需求和设计,在技术方案通过后的 contract 阶段、测试用例之前准备并逐份审核。Markdown 中的 SQL 片段或迁移文件引用不能替代它。生成 SQL 不代表获准执行数据库操作或修改工程迁移机制。
|
|
57
|
+
|
|
58
|
+
已有工程 HTTP 契约继续作为权威来源,避免维护另一份相互冲突的副本。项目当前版本和目标版本在 PRD/Scope 中记录,并注明工程来源和观察依据;未设置版本时如实说明。`plan.md` 记录实际改动,审核与测试结果定位准确的 commit、构建或未提交内容。多仓库分别记录版本和对象引用,与 Aimship 规则版本、状态格式及保存 revision 区分。交接记录最终代码对象和真实合并/发布状态,目标版本不代表已发布版本。
|
|
59
|
+
|
|
60
|
+
## 固定大纲和可追溯编号
|
|
61
|
+
|
|
62
|
+
新文档遵循[统一模板和规则](../skills/aimship/references/document-patterns.md),保留章节顺序和稳定锚点,可翻译展示标题。不适用章节说明原因,小需求使用相同大纲、缩短内容。未选择的阶段不生成空文档,也不按“小需求”随意合并文件。
|
|
63
|
+
|
|
64
|
+
| 编号 | 含义 |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `REQ-001`、`AC-001` | 需求、验收标准 |
|
|
67
|
+
| `B-001`、`TASK-001` | 实施批次、任务 |
|
|
68
|
+
| `PAGE-001`、`Q-001` | 原型页面、待决问题 |
|
|
69
|
+
| `TC-001`、`RUN-001` | 测试用例、实际验证执行 |
|
|
70
|
+
| `REV-001`、`CHG-001` | 审核、重要变更 |
|
|
71
|
+
|
|
72
|
+
各类编号从 `001` 开始,在包含已废弃条目的历史最大值后递增,三位是最小补零宽度。旧编号不用于另一项义务。同一命名空间和适用版本内,一个编号只有一处有效定义,其他文档引用该定义。分类或排版变化保留编号,拆分/合并则创建新编号并关联原编号。
|
|
73
|
+
|
|
74
|
+
需求/验收通过引用关联到设计、任务、代码对象、审核、用例版本和实际执行,允许多对多关联;不能因数字相同就认定 `REQ-001` 对应 `TC-001`。可独立测试的公共规则、权限和质量要求应具有独立身份。外部编号保留原样,并携带来源/版本命名空间;无原编号时建立指向原文位置的本地映射。用例含义改变后产生新版本,先前失败及当时的用例和代码版本仍可追溯。
|
|
75
|
+
|
|
76
|
+
例如,`REQ-001 → AC-002 → TC-004@1 → RUN-003` 表示明确的需求到结果关联。RUN 同时记录被测代码/构建及证据,仅存在用例编号不能证明已执行。
|
|
77
|
+
|
|
78
|
+
工具检查结构、引用和基础覆盖;Agent 仍需判断正文和断言是否真正满足需求。元信息合法不等于内容正确。
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { artifactMap, safeRead, anchors } from './documents.mjs';
|
|
3
|
+
import { fail, hash } from './io.mjs';
|
|
4
|
+
|
|
5
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
6
|
+
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.');
|
|
7
|
+
const kinds = ['self-check', 'approved', 'changes-requested', 'editorial'];
|
|
8
|
+
export function validateReviewState(state) {
|
|
9
|
+
if (state.reviewPolicy === undefined) return;
|
|
10
|
+
if (state.reviewPolicy !== 1 || !Array.isArray(state.reviewHistory)) reject('Unsupported review policy or missing reviewHistory.');
|
|
11
|
+
const ids = new Set();
|
|
12
|
+
for (const r of state.reviewHistory) {
|
|
13
|
+
if (!r || !text(r.id) || ids.has(r.id) || !kinds.includes(r.kind) || !text(r.document) || !text(r.version) || !/^[a-f0-9]{64}$/.test(r.digest ?? '') || !text(r.sourceRef) || !text(r.summary)) reject('Review records need unique ID, kind, document, version, digest, sourceRef and summary.');
|
|
14
|
+
ids.add(r.id);
|
|
15
|
+
if (r.kind === 'self-check' && !['passed','failed'].includes(r.result)) reject('Self-check records must distinguish passed from failed.');
|
|
16
|
+
if (r.kind === 'approved' && r.decision !== 'explicit-approval') reject('Approval requires an explicit user approval decision; questions, silence and requests to see results are not approval.');
|
|
17
|
+
if (r.kind === 'editorial' && !/^[a-f0-9]{64}$/.test(r.previousDigest ?? '')) reject('Editorial reconciliation needs the previous approved digest.');
|
|
18
|
+
}
|
|
19
|
+
for (const [index, run] of state.stageRuns.entries()) {
|
|
20
|
+
if (run.dependsOn !== undefined && (!Array.isArray(run.dependsOn) || new Set(run.dependsOn).size !== run.dependsOn.length || !run.dependsOn.every(id => state.stageRuns.slice(0, index).some(r => r.id === id)))) reject(`${run.id}: dependencies must be unique preceding run IDs.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Shared execution/result sections are outside the approved design surface.
|
|
24
|
+
// Reused formats have no inferred boundary: the entire pinned file is reviewed.
|
|
25
|
+
export async function reviewFingerprint(directory, state, doc) {
|
|
26
|
+
const stop = doc.mode === 'new' ? { plan: 'execution', testing: 'runs' }[doc.kind] : null;
|
|
27
|
+
let body = await safeRead(directory, state, doc.path, stop ? 'utf8' : null);
|
|
28
|
+
if (body === null) return null;
|
|
29
|
+
if (stop) {
|
|
30
|
+
const at = body.search(new RegExp(`<a\\s+id=["']${stop}["']\\s*><\\/a>`));
|
|
31
|
+
if (at < 0) return null;
|
|
32
|
+
body = body.slice(0, at);
|
|
33
|
+
}
|
|
34
|
+
const assets = [];
|
|
35
|
+
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
|
+
}
|
|
38
|
+
function decisionFor(state, doc, digest) {
|
|
39
|
+
let approved = null, checked = null, latest = null;
|
|
40
|
+
for (const r of state.reviewHistory ?? []) {
|
|
41
|
+
if (r.document !== doc.path) continue;
|
|
42
|
+
if (r.kind === 'changes-requested') { approved = null; checked = null; }
|
|
43
|
+
if (['approved','editorial'].includes(r.kind)) approved = r;
|
|
44
|
+
if (r.version === doc.version && r.digest === digest) {
|
|
45
|
+
latest = r;
|
|
46
|
+
if (r.kind === 'self-check') { checked = r.result === 'passed' ? r : null; if (r.result === 'failed') approved = null; }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (approved?.version !== doc.version || approved?.digest !== digest) approved = null;
|
|
50
|
+
return { approved, checked, latest };
|
|
51
|
+
}
|
|
52
|
+
export async function reviewStatus(directory, state) {
|
|
53
|
+
if (state.reviewPolicy !== 1) return { compatibility: 'review-adoption-required', canProceed: false, pending: [], runs: [], documents: [], instruction: 'Preserve old results. Enable reviewPolicy: 1 with an empty reviewHistory after compatibility assessment; no historical approval is inferred.' };
|
|
54
|
+
const map = await artifactMap(), documents = [], evidenceIssues = [], evidenceCache = new Map();
|
|
55
|
+
for (const r of state.reviewHistory ?? []) {
|
|
56
|
+
if (!evidenceCache.has(r.sourceRef)) { try { evidenceCache.set(r.sourceRef,await evidence(directory,state,r.sourceRef)); } catch { evidenceCache.set(r.sourceRef,null); } }
|
|
57
|
+
if (!r.sourceDigest || evidenceCache.get(r.sourceRef) !== r.sourceDigest) evidenceIssues.push(r.id);
|
|
58
|
+
}
|
|
59
|
+
for (const doc of state.documents ?? []) {
|
|
60
|
+
if (['source','change-log','review','handoff'].includes(doc.kind)) continue;
|
|
61
|
+
let digest = null;
|
|
62
|
+
try { digest = await reviewFingerprint(directory, state, doc); } catch { /* Missing input remains blocked. */ }
|
|
63
|
+
const { approved, checked, latest } = decisionFor(state, doc, digest);
|
|
64
|
+
documents.push({ path: doc.path, version: doc.version, digest, selfCheck: checked?.id ?? null, approval: approved?.id ?? null, status: !digest ? 'unavailable' : approved && (evidenceIssues.includes(approved.id) || (checked && evidenceIssues.includes(checked.id))) ? 'evidence-changed' : approved ? 'approved' : latest?.kind === 'changes-requested' ? 'changes-requested' : latest?.kind === 'self-check' && latest.result === 'failed' ? 'self-check-failed' : checked ? 'pending' : 'self-check-required' });
|
|
65
|
+
}
|
|
66
|
+
const runs = [], requirement = state.documentPolicy?.authority === 'adopted' ? 'scope' : 'prd';
|
|
67
|
+
for (const [i, run] of state.stageRuns.entries()) {
|
|
68
|
+
const spec = map.stages[run.stage];
|
|
69
|
+
let required = [];
|
|
70
|
+
if (spec?.humanReview) {
|
|
71
|
+
const kind = spec.kind === 'requirement' ? requirement : spec.kind;
|
|
72
|
+
required = (state.documents ?? []).filter(d => d.kind === kind || (run.stage === 'contract' && d.kind === 'sql')).map(d => d.path);
|
|
73
|
+
}
|
|
74
|
+
const declared = run.dependsOn ?? (i ? [state.stageRuns[i - 1].id] : []);
|
|
75
|
+
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);
|
|
76
|
+
const development = ['requirements','scope','tech-design','contract','test-design','plan','implement'];
|
|
77
|
+
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) : [];
|
|
78
|
+
const dependsOn = [...new Set([...declared, ...producers, ...prerequisites])];
|
|
79
|
+
const blockedBy = dependsOn.filter(id => !runs.find(r => r.id === id)?.ready);
|
|
80
|
+
const waiting = required.filter(file => documents.find(d => d.path === file)?.status !== 'approved');
|
|
81
|
+
const ready = run.progress === 'done' && !['needs-review','unknown'].includes(run.applicability) && !blockedBy.length && !waiting.length && (!spec?.humanReview || required.length > 0);
|
|
82
|
+
runs.push({ id: run.id, stage: run.stage, progress: run.progress, applicability: run.applicability, documents: required, waiting, dependsOn, blockedBy, ready });
|
|
83
|
+
}
|
|
84
|
+
const current = runs.find(r => r.id === state.currentRun);
|
|
85
|
+
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 })));
|
|
86
|
+
const nextRun = runs.find(r => !r.ready);
|
|
87
|
+
const nextStage = current ? runs[runs.indexOf(current) + 1]?.stage ?? null : nextRun?.stage ?? null;
|
|
88
|
+
return { compatibility: 'current', documents, runs, pending, evidenceIssues, currentRun: current?.id ?? null, nextRun: nextRun?.id ?? null, nextStage, canProceed: current ? !current.blockedBy.length && !(current.progress === 'done' && !current.ready) && current.progress !== 'blocked' && !['needs-review','unknown'].includes(current.applicability) : !pending.length && !runs.some(r=>r.progress==='blocked' || ['needs-review','unknown'].includes(r.applicability)), identity: 'Record validation only; user identity, intent and source authenticity are not authenticated.' };
|
|
89
|
+
}
|
|
90
|
+
async function evidence(directory, state, ref) {
|
|
91
|
+
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.');
|
|
92
|
+
let body;
|
|
93
|
+
try { body = await safeRead(directory, state, ref); } catch { reject(`Cannot read review evidence ${ref}.`); }
|
|
94
|
+
if (!text(body)) reject(`Preserve locally readable review evidence for ${ref}.`);
|
|
95
|
+
const fragment = ref.split('#')[1];
|
|
96
|
+
if (fragment && !anchors(body).includes(fragment)) reject(`Review evidence anchor not found: ${ref}.`);
|
|
97
|
+
return hash(body);
|
|
98
|
+
}
|
|
99
|
+
export async function enforceReviews(directory, state, previous) {
|
|
100
|
+
validateReviewState(state);
|
|
101
|
+
if (state.reviewPolicy !== 1) reject('Adopt the review protocol before workflow updates; old results remain readable but are not approvals.');
|
|
102
|
+
const oldHistory = previous?.reviewHistory ?? [];
|
|
103
|
+
for (const old of previous?.documents ?? []) if (oldHistory.some(r=>r.document===old.path) && !state.documents.some(d=>d.path===old.path && d.kind===old.kind)) reject(`Preserve reviewed document registration ${old.path}.`);
|
|
104
|
+
if (state.reviewHistory.length < oldHistory.length || !oldHistory.every((r, i) => isDeepStrictEqual(r, state.reviewHistory[i]))) reject('Preserve review history unchanged and in order.');
|
|
105
|
+
const prefix = { ...state, reviewHistory: [...oldHistory] };
|
|
106
|
+
for (const r of state.reviewHistory.slice(oldHistory.length)) {
|
|
107
|
+
const doc = state.documents.find(d => d.path === r.document);
|
|
108
|
+
if (!doc || r.version !== doc.version || r.digest !== await reviewFingerprint(directory, state, doc)) reject(`${r.id}: review must bind the current registered document version and content.`);
|
|
109
|
+
const prior = decisionFor(prefix, doc, r.digest);
|
|
110
|
+
if (r.kind === 'approved' && !prior.checked) reject(`${r.id}: self-check the same content before recording approval.`);
|
|
111
|
+
if (r.kind === 'editorial' && (!prior.checked || !decisionFor(prefix, doc, r.previousDigest).approved)) reject(`${r.id}: editorial reconciliation needs a self-check and the previous approval at the same version.`);
|
|
112
|
+
r.sourceDigest = await evidence(directory, state, r.sourceRef);
|
|
113
|
+
prefix.reviewHistory.push(r);
|
|
114
|
+
}
|
|
115
|
+
// Only the initial selected development sequence is ordered; later rework can recur.
|
|
116
|
+
const order = ['requirements','scope','tech-design','contract','test-design','plan','implement'];
|
|
117
|
+
const first = stage => state.stageRuns.findIndex(r => r.stage === stage);
|
|
118
|
+
for (let i = 0; i < order.length; i++) for (let j = i + 1; j < order.length; j++) {
|
|
119
|
+
const a = first(order[i]), b = first(order[j]);
|
|
120
|
+
if (order[i] === 'requirements' && order[j] === 'scope') continue;
|
|
121
|
+
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.`);
|
|
122
|
+
}
|
|
123
|
+
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));
|
|
124
|
+
let status = await reviewStatus(directory, state);
|
|
125
|
+
// Invalidate only registered producer/dependency paths. Independent branches retain their facts.
|
|
126
|
+
const affected = new Set();
|
|
127
|
+
for (const doc of state.documents) {
|
|
128
|
+
const oldDoc = previous?.documents?.find(d => d.path === doc.path);
|
|
129
|
+
const latest = oldHistory.filter(r => r.document === doc.path && ['approved','editorial'].includes(r.kind)).at(-1);
|
|
130
|
+
const now = status.documents.find(d => d.path === doc.path);
|
|
131
|
+
if (!oldDoc || !latest || !now) continue;
|
|
132
|
+
const changed = latest.digest !== now.digest || latest.version !== doc.version;
|
|
133
|
+
const editorial = state.reviewHistory.slice(oldHistory.length).some(r=>r.document===doc.path && r.kind==='editorial' && r.digest===now.digest);
|
|
134
|
+
const requested = state.reviewHistory.slice(oldHistory.length).some(r=>r.document===doc.path && (r.kind==='changes-requested' || (r.kind==='self-check' && r.result==='failed')));
|
|
135
|
+
if ((!changed && !requested) || editorial) continue;
|
|
136
|
+
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.`);
|
|
137
|
+
if (changed && now.status === 'approved' && doc.version === latest.version) reject(`${doc.path}: a material revision needs a new document version; use editorial only for unchanged meaning.`);
|
|
138
|
+
for (const r of status.runs.filter(r => r.documents.includes(doc.path) && r.progress !== 'planned')) affected.add(r.id);
|
|
139
|
+
}
|
|
140
|
+
for (const r of status.runs) if (r.dependsOn.some(id => affected.has(id))) affected.add(r.id);
|
|
141
|
+
for (const r of state.stageRuns) if (affected.has(r.id) && r.progress !== 'planned') r.applicability = 'needs-review';
|
|
142
|
+
status = await reviewStatus(directory, state);
|
|
143
|
+
const previousStatus = previous?.reviewPolicy === 1 ? await reviewStatus(directory, previous) : null;
|
|
144
|
+
for (const run of state.stageRuns) {
|
|
145
|
+
const old = previous?.stageRuns.find(r => r.id === run.id);
|
|
146
|
+
const progressing = ['active','done'].includes(run.progress) && (!old || old.progress !== run.progress || old.resultRef !== run.resultRef || restoring.has(run.id));
|
|
147
|
+
if (restoring.has(run.id) && run.applicability === 'needs-review') reject(`${run.id}: current document or dependency still needs review.`);
|
|
148
|
+
if (progressing && status.runs.find(r => r.id === run.id).blockedBy.length) reject(`${run.id}: prerequisite completion, approval or affected dependency is unresolved.`);
|
|
149
|
+
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.`);
|
|
150
|
+
}
|
|
151
|
+
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.');
|
|
152
|
+
return status;
|
|
153
|
+
}
|
package/lib/cli.mjs
CHANGED
|
@@ -29,6 +29,7 @@ const HELP = {
|
|
|
29
29
|
},
|
|
30
30
|
stateProtocol: {
|
|
31
31
|
schemaVersion: SCHEMA_VERSION,
|
|
32
|
+
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.'},
|
|
32
33
|
stages: STAGES,
|
|
33
34
|
progress: ['planned', 'active', 'blocked', 'done'],
|
|
34
35
|
result: ['passed', 'failed', 'not-run', 'unknown', 'mixed'],
|
|
@@ -154,7 +155,7 @@ export async function run(argv, context) {
|
|
|
154
155
|
const read = await readState(directory);
|
|
155
156
|
let mismatch = null;
|
|
156
157
|
if (!read.readOnly) { try { assertRules(read.state, rulesVersion, runtime); } catch (error) { mismatch = { code: error.code, ...error.details, recovery: error.recovery }; } }
|
|
157
|
-
if (command === 'read') return { command, data: { state: read.state, readOnly: read.readOnly, versionMismatch: mismatch, statePath: read.statePath, view: read.view, documentChanges: read.documentChanges, layoutConflict:read.conflict??false, layoutMismatch:read.layoutMismatch??false, candidates:read.candidates, migration:read.migration?.status??null } };
|
|
158
|
+
if (command === 'read') return { command, data: { state: read.state, readOnly: read.readOnly, versionMismatch: mismatch, statePath: read.statePath, review:read.review, view: read.view, documentChanges: read.documentChanges, layoutConflict:read.conflict??false, layoutMismatch:read.layoutMismatch??false, candidates:read.candidates, migration:read.migration?.status??null } };
|
|
158
159
|
if (read.readOnly) return { command, data: { state: read.state, readOnly: true, canProceed: false, reason: read.conflict?'Conflicting state layouts require explicit reconciliation.':'Unknown schema: no structure checking, mutation, or automatic work is permitted.', candidates:read.candidates } };
|
|
159
160
|
validateState(read.state); const references = await checkReferences(directory, read.state, options.all);
|
|
160
161
|
const documents=await inspectDocuments(directory,read.state);
|
|
@@ -163,9 +164,9 @@ export async function run(argv, context) {
|
|
|
163
164
|
if (missing.length) fail('REFERENCE_CHECK', 'Required references are missing or inaccessible.', 'Repair the specific registered paths; do not scan the disk or overwrite source materials.', { references, versionMismatch: mismatch });
|
|
164
165
|
if (mismatch) fail('VERSION_MISMATCH', 'State is structurally valid, but rules are not bound consistently.', mismatch.recovery, mismatch);
|
|
165
166
|
if (documents.issues.length) fail('DOCUMENT_CHECK','Document mapping or identities need repair.','Inspect listed issues.',documents);
|
|
166
|
-
return { command, data: { structure: 'valid', references, documents, view:read.view, documentChanges:read.documentChanges, semantics: 'not-checked' } };
|
|
167
|
+
return { command, data: { structure: 'valid', review:read.review, references, documents, view:read.view, documentChanges:read.documentChanges, semantics: 'not-checked' } };
|
|
167
168
|
}
|
|
168
169
|
const currentRun = read.state.stageRuns.find(x => x.id === read.state.currentRun) ?? null;
|
|
169
170
|
const blocked = currentRun?.progress === 'blocked';
|
|
170
|
-
return { command, data: { state: read.state, binding: await binding(runtime), readOnly: false, canProceed: read.state.documentPolicy?.authority !== 'legacy' && !mismatch && missing.length === 0 && !blocked && !documents.issues.length && !read.documentChanges.length && read.view.status === 'current' && (!read.migration || read.migration.status === 'complete'), statePath:read.statePath, view:read.view, documentChanges:read.documentChanges, documents, versionMismatch: mismatch, references, currentRun, continuation: `Use Aimship to continue ${directory}; first read ${read.statePath} and the current inputs it references, then follow the confirmed arrangement. Inspect unfinished effects and related artifacts before repeating any action.`, warnings: ['Resume does not rerun work, tests, migrations, or other side effects.', 'Structural readiness does not establish authorization, business readiness, or goal completion.', ...(blocked ? [`Current run ${currentRun.id} is recorded as blocked. Resolve its documented blocker before progressing this work.`] : []), ...(missing.length ? ['Some required paths must be repaired.'] : []), ...(read.state.checkpoint ? ['Inspect the saved checkpoint and current artifacts.'] : [])] } };
|
|
171
|
+
return { command, data: { state: read.state, binding: await binding(runtime), readOnly: false, review:read.review, canProceed: read.review.canProceed && read.state.documentPolicy?.authority !== 'legacy' && !mismatch && missing.length === 0 && !blocked && !documents.issues.length && !read.documentChanges.length && read.view.status === 'current' && (!read.migration || read.migration.status === 'complete'), statePath:read.statePath, view:read.view, documentChanges:read.documentChanges, documents, versionMismatch: mismatch, references, currentRun, continuation: `Use Aimship to continue ${directory}; first read ${read.statePath} and the current inputs it references, then follow the confirmed arrangement. Inspect unfinished effects and related artifacts before repeating any action.`, warnings: ['Resume does not rerun work, tests, migrations, or other side effects.', 'Structural readiness does not establish authorization, business readiness, or goal completion.', ...(blocked ? [`Current run ${currentRun.id} is recorded as blocked. Resolve its documented blocker before progressing this work.`] : []), ...(missing.length ? ['Some required paths must be repaired.'] : []), ...(read.state.checkpoint ? ['Inspect the saved checkpoint and current artifacts.'] : [])] } };
|
|
171
172
|
}
|
package/lib/documents.mjs
CHANGED
|
@@ -19,13 +19,13 @@ export function resolveRef(directory, state, ref) {
|
|
|
19
19
|
if (!inside(base, file)) fail('INVALID_REFERENCE', 'Reference escapes its root.', 'Repair the registered reference.');
|
|
20
20
|
return file;
|
|
21
21
|
}
|
|
22
|
-
export async function safeRead(directory, state, ref) {
|
|
22
|
+
export async function safeRead(directory, state, ref, encoding = 'utf8') {
|
|
23
23
|
const file = resolveRef(directory, state, ref);
|
|
24
24
|
if (!file) return null;
|
|
25
25
|
const real = await fs.realpath(file);
|
|
26
26
|
const base = ref.startsWith('repo:') ? state.repositories.find(r => r.id === /^repo:([^/]+)/.exec(ref)[1]).path : directory;
|
|
27
27
|
if (!inside(await fs.realpath(base), real)) fail('INVALID_REFERENCE', 'Reference resolves outside its root.', 'Repair the symbolic link or reference.');
|
|
28
|
-
return fs.readFile(real,
|
|
28
|
+
return fs.readFile(real, encoding);
|
|
29
29
|
}
|
|
30
30
|
export const anchors = text => [...text.matchAll(/<a\s+id=["']([^"']+)["']\s*><\/a>/g)].map(m => m[1]);
|
|
31
31
|
const identityKinds = ['REQ','AC','B','TASK','TC','REV','RUN','CHG','Q','PAGE'];
|
|
@@ -94,7 +94,7 @@ export async function inspectDocuments(directory, state) {
|
|
|
94
94
|
const runs = state.stageRuns.filter(r => !legacyRuns.has(r.id));
|
|
95
95
|
for (const [i, run] of state.stageRuns.entries()) {
|
|
96
96
|
if (!runs.includes(run) || run.progress === 'planned') continue;
|
|
97
|
-
const contract = map.stages[run.stage], doc = find(contract.kind);
|
|
97
|
+
const contract = map.stages[run.stage], doc = find(contract.kind) ?? (run.stage === 'contract' && policy.interface === 'none' ? find('sql') : null);
|
|
98
98
|
if (!doc) { issues.push(`${run.id}: missing registered ${contract.kind} artifact.`); continue; }
|
|
99
99
|
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.`);
|
|
100
100
|
if (run.progress === 'done') {
|
|
@@ -108,7 +108,7 @@ export async function inspectDocuments(directory, state) {
|
|
|
108
108
|
const prefix=run.stage==='review'?'REV-':'RUN-';
|
|
109
109
|
if (!defs.some(d=>d.file===doc.path && d.namespace==='local' && d.id.startsWith(prefix) && d.run===run.id && `${doc.path}#${d.id.toLowerCase()}-v${d.version}`===run.resultRef)) issues.push(`${run.id}: result must locate its own versioned REV/RUN definition with matching run field.`);
|
|
110
110
|
}
|
|
111
|
-
if (run.stage === '
|
|
111
|
+
if (run.stage === 'contract') {
|
|
112
112
|
for (const db of policy.databases) {
|
|
113
113
|
const file = policy.databases.length === 1 ? 'database.sql' : `database-${db}.sql`;
|
|
114
114
|
if (!docs.some(d => d.kind === 'sql' && d.path === file) || !await exists(path.join(directory,file))) issues.push(`${run.id}: missing independent ${file}.`);
|
|
@@ -116,6 +116,7 @@ export async function inspectDocuments(directory, state) {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
if (state.reviewPolicy === 1 && (policy.interface !== 'none' || policy.databases.length) && runs.some(r=>['test-design','plan','implement'].includes(r.stage) && r.progress !== 'planned') && !runs.some(r=>r.stage==='contract' && r.progress==='done')) issues.push('Complete the applicable interface/SQL contract before test design, plan or implementation.');
|
|
119
120
|
if (policy.interface === 'http') {
|
|
120
121
|
const doc=find('contract');
|
|
121
122
|
if (doc?.mode === 'new' && doc.path !== 'openapi.yaml') issues.push('New HTTP contract must be openapi.yaml.');
|
package/lib/state.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { validateReviewState, enforceReviews, reviewStatus } from './approvals.mjs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { inspectDocuments, documentChanges, anchors, preservedLegacyRuns } from './documents.mjs';
|
|
4
5
|
import { savedResult, viewStatus, generateView } from './view.mjs';
|
|
@@ -6,7 +7,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
6
7
|
import { atomicWrite, exists, fail, inside, json, noSymlinks, readJSON, withLock } from './io.mjs';
|
|
7
8
|
|
|
8
9
|
export const SCHEMA_VERSION = 2;
|
|
9
|
-
export const STAGES = ['frame', 'requirements', 'prototype', 'scope', 'diagnose', 'tech-design', 'contract', '
|
|
10
|
+
export const STAGES = ['frame', 'requirements', 'prototype', 'scope', 'diagnose', 'tech-design', 'contract', 'test-design', 'plan', 'implement', 'review', 'verify', 'handoff'];
|
|
10
11
|
const PROGRESS = ['planned', 'active', 'blocked', 'done'];
|
|
11
12
|
const RESULTS = ['passed', 'failed', 'not-run', 'unknown', 'mixed'];
|
|
12
13
|
const OUTCOME_REFS = ['goalConclusionRef', 'acceptanceRef', 'mergeRef', 'deploymentRef', 'handoffRef'];
|
|
@@ -37,6 +38,7 @@ export function validateState(state) {
|
|
|
37
38
|
for (const d of state.documents) {
|
|
38
39
|
requireThat(object(d) && ['prd','scope','tech-design','plan','testing','review','diagnosis','handoff','change-log','prototype','contract','sql','source'].includes(d.kind) && ['new','reused'].includes(d.mode) && string(d.version), 'Invalid document registration.');
|
|
39
40
|
ref(d.path, 'documents.path'); requireThat(!d.path.includes('#') && !paths.has(d.path), 'Document paths must be unique and have no fragment.'); paths.add(d.path);
|
|
41
|
+
if (d.assetRefs !== undefined) refs(d.assetRefs, 'documents.assetRefs');
|
|
40
42
|
if (d.mode === 'reused') { ref(d.basisRef, 'documents.basisRef'); }
|
|
41
43
|
}
|
|
42
44
|
}
|
|
@@ -73,6 +75,7 @@ export function validateState(state) {
|
|
|
73
75
|
if (state.next.inputRefs !== undefined) refs(state.next.inputRefs, 'next.inputRefs');
|
|
74
76
|
if (state.goalHistory !== undefined) requireThat(Array.isArray(state.goalHistory), 'goalHistory must be an array.');
|
|
75
77
|
if (state.rulesHistory !== undefined) requireThat(Array.isArray(state.rulesHistory), 'rulesHistory must be an array.');
|
|
78
|
+
validateReviewState(state);
|
|
76
79
|
return state;
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -96,9 +99,9 @@ export async function readState(directory) {
|
|
|
96
99
|
const paths=await locateState(directory); const state=await readJSON(paths.statePath);
|
|
97
100
|
requireThat(object(state),'State must be an object.');
|
|
98
101
|
const layoutMismatch=(state.schemaVersion===1 && !paths.legacy)||(state.schemaVersion===2 && paths.legacy);
|
|
99
|
-
const readOnly=paths.conflict || layoutMismatch || ![1,SCHEMA_VERSION].includes(state.schemaVersion);
|
|
102
|
+
const readOnly=paths.conflict || layoutMismatch || ![1,SCHEMA_VERSION].includes(state.schemaVersion) || (state.reviewPolicy !== undefined && state.reviewPolicy !== 1);
|
|
100
103
|
if (!readOnly) validateState(state);
|
|
101
|
-
return {...paths,state,readOnly,layoutMismatch,view:await viewStatus(directory,state),documentChanges:await documentChanges(directory,state)};
|
|
104
|
+
return {...paths,state,readOnly,layoutMismatch,review:readOnly ? null : await reviewStatus(directory,state),view:await viewStatus(directory,state),documentChanges:await documentChanges(directory,state)};
|
|
102
105
|
}
|
|
103
106
|
async function inspectForWrite(directory,state,previous) {
|
|
104
107
|
const inspected=await inspectDocuments(directory,state);
|
|
@@ -142,8 +145,8 @@ export async function initState(directory, input, rulesVersion, runtime) {
|
|
|
142
145
|
const forbidden = ['schemaVersion', 'revision', 'deliveryId', 'rulesVersion', 'createdAt', 'updatedAt', 'goalHistory', 'rulesHistory', 'documentDigests', 'identityHistory', 'retiredIdentities', 'legacyRunIds', 'writeResult'];
|
|
143
146
|
requireThat(object(input) && !forbidden.some(x => Object.hasOwn(input, x)), 'init input must not set tool-managed identity/version fields.');
|
|
144
147
|
const now = new Date().toISOString();
|
|
145
|
-
const state = { cycle: 1, closureRefs: [], stageRuns: [], currentRun: null, openChanges: [], repositories: [], outcome: { goal: 'in-progress', acceptance: 'not-recorded' }, next: { action: 'Continue the confirmed arrangement.' }, ...input, schemaVersion: SCHEMA_VERSION, revision: 0, deliveryId: randomUUID(), rulesVersion, createdAt: now, updatedAt: now };
|
|
146
|
-
validateState(state); requireThat(state.documentPolicy.authority !== 'legacy', 'New deliveries cannot opt into legacy rules.'); await inspectForWrite(directory,state); await atomicWrite(p.statePath, json(state)); return savedResult(directory,state);
|
|
148
|
+
const state = { reviewPolicy: 1, reviewHistory: [], cycle: 1, closureRefs: [], stageRuns: [], currentRun: null, openChanges: [], repositories: [], outcome: { goal: 'in-progress', acceptance: 'not-recorded' }, next: { action: 'Continue the confirmed arrangement.' }, ...input, schemaVersion: SCHEMA_VERSION, revision: 0, deliveryId: randomUUID(), rulesVersion, createdAt: now, updatedAt: now };
|
|
149
|
+
validateState(state); requireThat(state.documentPolicy.authority !== 'legacy', 'New deliveries cannot opt into legacy rules.'); await inspectForWrite(directory,state); await enforceReviews(directory,state); await atomicWrite(p.statePath, json(state)); return savedResult(directory,state);
|
|
147
150
|
});
|
|
148
151
|
}
|
|
149
152
|
function merge(base, patch) {
|
|
@@ -207,7 +210,7 @@ export async function updateState(directory, patch, revision, rulesVersion, runt
|
|
|
207
210
|
}
|
|
208
211
|
requireThat(previous.documentPolicy?.authority === 'legacy' || next.documentPolicy?.authority !== 'legacy', 'Cannot downgrade a new delivery to legacy document rules.');
|
|
209
212
|
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.');
|
|
210
|
-
if (mode === 'update') await inspectForWrite(directory,next,previous);
|
|
213
|
+
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); }
|
|
211
214
|
retainHistory(previous, next); next.revision = previous.revision + 1; next.updatedAt = new Date().toISOString();
|
|
212
215
|
validateState(next); await atomicWrite(p.backup, json(previous)); await atomicWrite(p.statePath, json(next)); return next.schemaVersion === 2 ? savedResult(directory,next) : next;
|
|
213
216
|
});
|
|
@@ -221,9 +224,10 @@ export async function recoverState(directory, revision, rulesVersion, runtime) {
|
|
|
221
224
|
const raw = await fs.readFile(p.statePath, 'utf8');
|
|
222
225
|
let parsed; try { parsed = JSON.parse(raw); } catch { /* Corruption is preserved below. */ }
|
|
223
226
|
if (parsed) {
|
|
227
|
+
if (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.');
|
|
224
228
|
if (parsed.schemaVersion !== backup.schemaVersion) fail('UNKNOWN_SCHEMA', 'Cannot recover over an unknown schema.', 'Use a compatible tool; preserve the newer state.');
|
|
225
229
|
try { validateState(parsed); fail('STATE_VALID', 'Current state is valid; refusing rollback.', 'Use a normal revision-checked update to reconcile facts.'); }
|
|
226
|
-
catch (e) { if (e.code
|
|
230
|
+
catch (e) { if (!['INVALID_STATE','REVIEW_CHECK'].includes(e.code)) throw e; }
|
|
227
231
|
if (Number.isSafeInteger(parsed.revision)) corruptRevision = Math.max(corruptRevision, parsed.revision);
|
|
228
232
|
}
|
|
229
233
|
await atomicWrite(path.join(p.internal, `state.corrupt.${randomUUID()}.json`), raw);
|
|
@@ -246,6 +250,8 @@ export async function checkReferences(directory, state, all = false) {
|
|
|
246
250
|
}
|
|
247
251
|
for (const history of state.rulesHistory ?? []) add(history?.ref);
|
|
248
252
|
add(state.checkpoint?.ref);
|
|
253
|
+
for (const r of state.reviewHistory ?? []) add(r.sourceRef);
|
|
254
|
+
for (const d of state.documents ?? []) for (const r of d.assetRefs ?? []) add(r);
|
|
249
255
|
}
|
|
250
256
|
const results = [];
|
|
251
257
|
for (const value of values) {
|
package/lib/view.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { reviewStatus } from './approvals.mjs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { atomicWrite, exists, hash, json, readJSON, noSymlinks } from './io.mjs';
|
|
@@ -42,6 +43,11 @@ export async function renderText(directory,state) {
|
|
|
42
43
|
for (const r of state.stageRuns.filter(r=>r.progress==='blocked')) lines.push(`- Blocked: ${cell(r.id)} — ${cell(r.scope)}`);
|
|
43
44
|
for (const c of state.openChanges.filter(c=>c.status==='open')) lines.push(`- Open change/question: ${link(c.ref)}`);
|
|
44
45
|
if (state.checkpoint) lines.push('',`Unfinished facts: ${cell(state.checkpoint.summary)}`);
|
|
46
|
+
const review = await reviewStatus(directory,state);
|
|
47
|
+
lines.push('', '## Document review', '', `Review protocol: ${review.compatibility}`, `Next stage: ${cell(review.nextStage)}`, '', '| Document | Version | Review status | Approval record |', '| --- | --- | --- | --- |');
|
|
48
|
+
for (const d of review.documents) lines.push(`| ${artifactLink(d.path)} | ${cell(d.version)} | ${d.status} | ${cell(d.approval)} |`);
|
|
49
|
+
for (const p of review.pending) lines.push(`- Awaiting review: ${artifactLink(p.document)} (${cell(p.run)}; ${p.status})`);
|
|
50
|
+
lines.push('', 'Approval records bind content and user decision evidence; the tool does not authenticate the user.', '');
|
|
45
51
|
lines.push('','## Artifacts and plan','');
|
|
46
52
|
const refs=new Set([...(state.documents??[]).map(d=>d.path),...state.stageRuns.flatMap(r=>[...r.outputRefs,r.resultRef].filter(Boolean)),...(state.next.inputRefs??[])]);
|
|
47
53
|
for (const ref of refs) if (await available(directory,state,ref)) lines.push(`- ${artifactLink(ref)}`);
|
package/package.json
CHANGED
package/skills/aimship/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ Activate only when the user explicitly asks to start or resume Aimship. Ordinary
|
|
|
11
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.
|
|
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
13
|
3. For new work, use the binding’s configured `dataRoot` unless the user supplied a delivery location; do not infer it from the code root. Preserve the original request, identify the authorized endpoint and code locations, and propose the necessary ordered stages. After associating code scope, follow the core’s recorded-overlap check for independent deliveries. Existing explicit authorization and agreed arrangements remain valid. No answer is not acceptance. Create records only for an actual request.
|
|
14
|
-
4. Read the selected [stage reference](references/core.md#stage-references) when reaching it. Explain design choices and unresolved decisions,
|
|
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 document entry/short summary/next stage and pause. Explicit approval releases the next authorized stage without another start question; questions, silence and viewing results do not approve. Authorized implementation/review/repair/retest can then continue.
|
|
15
15
|
5. At stable results and before a manual session switch, save the state and give a short continuation prompt. 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
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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 2,
|
|
3
3
|
"documents": {
|
|
4
4
|
"prd": {
|
|
5
5
|
"path": "prd.md",
|
|
@@ -109,7 +109,8 @@
|
|
|
109
109
|
"sectionByKind": {
|
|
110
110
|
"prd": "requirements",
|
|
111
111
|
"scope": "adoption"
|
|
112
|
-
}
|
|
112
|
+
},
|
|
113
|
+
"humanReview": true
|
|
113
114
|
},
|
|
114
115
|
"scope": {
|
|
115
116
|
"kind": "requirement",
|
|
@@ -117,31 +118,39 @@
|
|
|
117
118
|
"sectionByKind": {
|
|
118
119
|
"prd": "scope",
|
|
119
120
|
"scope": "goals"
|
|
120
|
-
}
|
|
121
|
+
},
|
|
122
|
+
"humanReview": true
|
|
121
123
|
},
|
|
122
124
|
"prototype": {
|
|
123
125
|
"kind": "prototype",
|
|
124
|
-
"section": null
|
|
126
|
+
"section": null,
|
|
127
|
+
"humanReview": true
|
|
125
128
|
},
|
|
126
129
|
"diagnose": {
|
|
127
130
|
"kind": "diagnosis",
|
|
128
|
-
"section": "conclusion"
|
|
131
|
+
"section": "conclusion",
|
|
132
|
+
"humanReview": true
|
|
129
133
|
},
|
|
130
134
|
"tech-design": {
|
|
131
135
|
"kind": "tech-design",
|
|
132
|
-
"section": "coverage"
|
|
136
|
+
"section": "coverage",
|
|
137
|
+
"humanReview": true
|
|
133
138
|
},
|
|
134
139
|
"contract": {
|
|
135
140
|
"kind": "contract",
|
|
136
|
-
"section": null
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
"kind": "plan",
|
|
140
|
-
"section": "tasks"
|
|
141
|
+
"section": null,
|
|
142
|
+
"humanReview": true,
|
|
143
|
+
"additionalKind": "sql"
|
|
141
144
|
},
|
|
142
145
|
"test-design": {
|
|
143
146
|
"kind": "testing",
|
|
144
|
-
"section": "cases"
|
|
147
|
+
"section": "cases",
|
|
148
|
+
"humanReview": true
|
|
149
|
+
},
|
|
150
|
+
"plan": {
|
|
151
|
+
"kind": "plan",
|
|
152
|
+
"section": "tasks",
|
|
153
|
+
"humanReview": true
|
|
145
154
|
},
|
|
146
155
|
"implement": {
|
|
147
156
|
"kind": "plan",
|
|
@@ -37,7 +37,9 @@ Choose stages from the responsibilities below based on the goal and existing inp
|
|
|
37
37
|
|
|
38
38
|
Show the selected order and endpoint. If the user already specified or accepted this arrangement, record the decision and proceed. An unanswered proposal remains a proposal. Once agreed, follow the order; adding, dropping or reordering work needs a reason and an updated record. Important changes follow [change flow](sources-and-changes.md). A stage may recur as distinct instances with stable IDs and explicit scopes.
|
|
39
39
|
|
|
40
|
-
Explain each design's key decisions, why they fit the requirements, tradeoffs, artifact location and remaining questions. Ordinary internal implementation choices stay with the agent. Ask for material changes to goals, scope, external behavior, compatibility, significant cost, data handling or an explicit technical commitment.
|
|
40
|
+
Explain each design's key decisions, why they fit the requirements, tradeoffs, artifact location and remaining questions. Ordinary internal implementation choices stay with the agent. Ask for material changes to goals, scope, external behavior, compatibility, significant cost, data handling or an explicit technical commitment. Follow [document review](document-review.md): present each selected reviewable document after self-check and pause for explicit approval. An approval automatically starts the next authorized stage without a second start confirmation.
|
|
41
|
+
|
|
42
|
+
Default applicable development order: PRD/Scope → technical design → interface/SQL → test cases → plan → implementation/review/verification → handoff. Adapt to the actual endpoint and existing inputs as described in [document review](document-review.md); do not force all stages into specialist commissions.
|
|
41
43
|
|
|
42
44
|
## Stage references
|
|
43
45
|
|
|
@@ -59,7 +61,7 @@ Read the reference for the current responsibility, not all thirteen on every tur
|
|
|
59
61
|
| verify | Executing the agreed checks and recording evidence | [verify](stages/verify.md) |
|
|
60
62
|
| handoff | Delivering against the current goal or handing over incomplete work | [handoff](stages/handoff.md) |
|
|
61
63
|
|
|
62
|
-
Each run records its scope, required inputs, results and progress. A result may be a section in a shared document. Proceed when this run's requirements are met, the next inputs exist, authorization covers it and no relevant unresolved issue blocks it. An issue blocks only dependent work; changing the order to continue other work is still an explicit plan change.
|
|
64
|
+
Each run records its scope, required inputs, results and progress. A result may be a section in a shared document. A `done` run records production completion, not human approval. Proceed when required document approvals match current content, this run's requirements are met, the next inputs exist, authorization covers it and no relevant unresolved issue blocks it. An issue blocks only dependent work; changing the order to continue other work is still an explicit plan change.
|
|
63
65
|
|
|
64
66
|
## Implementation and evidence
|
|
65
67
|
|
|
@@ -77,7 +79,7 @@ Use the installed tool's documented state protocol. Read the current revision be
|
|
|
77
79
|
|
|
78
80
|
Important semantic changes retain historical review and test facts, mark affected conclusions for review and update the current editable authority. Do not overwrite external adopted sources. See [sources and change flow](sources-and-changes.md).
|
|
79
81
|
|
|
80
|
-
At stable results save the state, remaining issues and next
|
|
82
|
+
At stable results save the state, remaining issues, current review stop/pending document and next stage. Show the same facts in chat; an explicit approval releases the next authorized step without asking whether to begin again. On a session switch give: “Use Aimship to continue <absolute delivery directory>; read the actual statePath returned by read (new: .aimship/state.json), the current goal/Scope and next-step inputs; <specific blocker if any>.” A new session reloads the same delivery, not the whole transcript. Use [recovery](recovery.md) for mismatch or uncertainty. Keep one active writing session per delivery.
|
|
81
83
|
|
|
82
84
|
End when the current confirmed goal is met. For design work this means the agreed design, not code execution. For development it includes agreed review and verification. “Show me the result” does not shorten the goal. If the user explicitly changes the endpoint, preserve the old goal, decision and unfinished work, update the arrangement, and assess the revised goal. An incomplete handoff is allowed but is not goal completion.
|
|
83
85
|
|
|
@@ -10,9 +10,9 @@ Author current requirements in `prd.md`, including scope. Adopt an authoritative
|
|
|
10
10
|
|
|
11
11
|
Interaction changes require a prototype when existing adopted material does not cover them and that work lies within the commissioned endpoint. New HTML uses `prototype/index.html`; register other agreed formats or reused entries. No interaction change means no invented prototype. Investigation/clarification-only requirements describe that commission and leave future development undefined.
|
|
12
12
|
|
|
13
|
-
`plan` must finish before first implementation, even one small batch. `plan.md` holds both planned TASK/B records and actual execution, distinctly; state holds stage progress. `testing.md` holds cases/coverage and appended RUN records; verification follows overall review. Verify-only may register existing cases with version/coverage, without claiming test-design occurred. Review-only does not require implementation/tests. No arbitrary small-task merging, `implementation.md`, `test-plan.md`, `verification.md`, or `results.md` for new work.
|
|
13
|
+
`test-design` precedes the selected `plan`; both reviewable documents need explicit approval. `plan` must finish before first implementation, even one small batch. `plan.md` holds both planned TASK/B records and actual execution, distinctly; state holds stage progress. `testing.md` holds cases/coverage and appended RUN records; verification follows overall review. Verify-only may register existing cases with version/coverage, without claiming test-design occurred. Review-only does not require implementation/tests. No arbitrary small-task merging, `implementation.md`, `test-plan.md`, `verification.md`, or `results.md` for new work.
|
|
14
14
|
|
|
15
|
-
Use `tech-design.md`. HTTP changes use `openapi.yaml` or the registered engineering authority; no extra contract wrapper. Non-HTTP formats are explicitly registered. Database changes require independent `database.sql` (one target) or `database-<id>.sql` per target
|
|
15
|
+
Use `tech-design.md`. HTTP changes use `openapi.yaml` or the registered engineering authority; no extra contract wrapper. Non-HTTP formats are explicitly registered. Database changes require independent `database.sql` (one target) or `database-<id>.sql` per target in contract, after technical-design approval and before test cases. Technical design records the data approach and SQL plan without requiring finished SQL. SQL headers identify target/dialect/version/schema basis/conditions; ordered sections cite requirements/design. Never substitute a Markdown block or migration reference, or execute/change migration mechanisms without authorization.
|
|
16
16
|
|
|
17
17
|
`diagnosis.md` belongs only to an explicitly selected independent investigation. Known-cause bugs and ordinary development debugging do not add diagnose. Major unknowns require a recorded stage adjustment; hold dependent work. Implementation defects go to implementation, design errors to design, requirement gaps to PRD/Scope, incorrect expectations to versioned cases.
|
|
18
18
|
|
|
@@ -37,3 +37,5 @@ REQ/source → AC → design/operation/SQL, TASK → actual repository object
|
|
|
37
37
|
Register `documentPolicy` and `documents` per [CLI](../../../docs/cli.md). Draft at stage start; completion requires the mapped artifact and precise result anchor, not merely a filename. New Markdown anchors/metadata are machine-checkable; reused/remote/binary material needs recorded basis and explicit unchecked scope. Local references remain relative to delivery root even though JSON moved internally.
|
|
38
38
|
|
|
39
39
|
Save unfinished intent, update formal documents, inspect them, then revision-check state. A matching status revision does not prove documents unchanged. Read/check/resume diagnose only. A saved state with a failed view must be repaired using render at the returned revision, never replayed. Preserve hand-edited status before rebuilding and interpret intent against existing authority; it cannot confer acceptance or publishing permission.
|
|
40
|
+
|
|
41
|
+
Preserve the fixed outlines and identity rules during [document review](document-review.md). Plan/testing design approval excludes their later execution/result sections; keep actual execution there.
|