@birdie_moblie/open_spec 2.1.7
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/.agents/plugins/marketplace.json +13 -0
- package/.claude-plugin/marketplace.json +12 -0
- package/.claude-plugin/plugin.json +9 -0
- package/.codex-plugin/plugin.json +9 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/openspec.js +3 -0
- package/dist/cli/commands/change.d.ts +3 -0
- package/dist/cli/commands/change.js +37 -0
- package/dist/cli/commands/ext.d.ts +3 -0
- package/dist/cli/commands/ext.js +196 -0
- package/dist/cli/commands/figma.d.ts +3 -0
- package/dist/cli/commands/figma.js +73 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +71 -0
- package/dist/cli/commands/opsx.d.ts +3 -0
- package/dist/cli/commands/opsx.js +324 -0
- package/dist/cli/commands/usage.d.ts +3 -0
- package/dist/cli/commands/usage.js +21 -0
- package/dist/cli/output.d.ts +8 -0
- package/dist/cli/output.js +17 -0
- package/dist/cli/program.d.ts +4 -0
- package/dist/cli/program.js +42 -0
- package/dist/cli/register.d.ts +3 -0
- package/dist/cli/register.js +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.js +55 -0
- package/dist/config/presets.d.ts +129 -0
- package/dist/config/presets.js +125 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/schema.js +178 -0
- package/dist/core/archive.d.ts +5 -0
- package/dist/core/archive.js +33 -0
- package/dist/core/change.d.ts +12 -0
- package/dist/core/change.js +69 -0
- package/dist/core/code-changes.d.ts +9 -0
- package/dist/core/code-changes.js +31 -0
- package/dist/core/flow.d.ts +6 -0
- package/dist/core/flow.js +99 -0
- package/dist/core/gates.d.ts +5 -0
- package/dist/core/gates.js +71 -0
- package/dist/core/instructions.d.ts +21 -0
- package/dist/core/instructions.js +102 -0
- package/dist/core/source-operations.d.ts +25 -0
- package/dist/core/source-operations.js +94 -0
- package/dist/core/sources.d.ts +33 -0
- package/dist/core/sources.js +234 -0
- package/dist/core/state.d.ts +15 -0
- package/dist/core/state.js +75 -0
- package/dist/core/status.d.ts +31 -0
- package/dist/core/status.js +22 -0
- package/dist/core/tasks.d.ts +15 -0
- package/dist/core/tasks.js +143 -0
- package/dist/core/todo.d.ts +31 -0
- package/dist/core/todo.js +114 -0
- package/dist/core/types.d.ts +297 -0
- package/dist/core/types.js +227 -0
- package/dist/core/verify.d.ts +229 -0
- package/dist/core/verify.js +391 -0
- package/dist/core/worker-evidence.d.ts +71 -0
- package/dist/core/worker-evidence.js +253 -0
- package/dist/core/worker.d.ts +22 -0
- package/dist/core/worker.js +478 -0
- package/dist/delivery/claude-install.d.ts +4 -0
- package/dist/delivery/claude-install.js +44 -0
- package/dist/delivery/distribution.d.ts +3 -0
- package/dist/delivery/distribution.js +53 -0
- package/dist/delivery/skills.d.ts +36 -0
- package/dist/delivery/skills.js +178 -0
- package/dist/figma/cache.d.ts +19 -0
- package/dist/figma/cache.js +115 -0
- package/dist/figma/claude.d.ts +14 -0
- package/dist/figma/claude.js +121 -0
- package/dist/figma/collect.d.ts +3 -0
- package/dist/figma/collect.js +57 -0
- package/dist/figma/index.d.ts +35 -0
- package/dist/figma/index.js +178 -0
- package/dist/figma/media.d.ts +5 -0
- package/dist/figma/media.js +77 -0
- package/dist/figma/response.d.ts +9 -0
- package/dist/figma/response.js +27 -0
- package/dist/hooks/command-context.d.ts +19 -0
- package/dist/hooks/command-context.js +33 -0
- package/dist/hooks/engine.d.ts +61 -0
- package/dist/hooks/engine.js +314 -0
- package/dist/hooks/plugin-runtime.d.ts +188 -0
- package/dist/hooks/plugin-runtime.js +198 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +185 -0
- package/dist/marketplace/adapters.d.ts +202 -0
- package/dist/marketplace/adapters.js +203 -0
- package/dist/marketplace/builtin.d.ts +9 -0
- package/dist/marketplace/builtin.js +9 -0
- package/dist/marketplace/install.d.ts +38 -0
- package/dist/marketplace/install.js +108 -0
- package/dist/marketplace/registry.d.ts +71 -0
- package/dist/marketplace/registry.js +342 -0
- package/dist/marketplace/upgrade.d.ts +39 -0
- package/dist/marketplace/upgrade.js +104 -0
- package/dist/shared/command-log.d.ts +9 -0
- package/dist/shared/command-log.js +55 -0
- package/dist/shared/errors.d.ts +7 -0
- package/dist/shared/errors.js +14 -0
- package/dist/shared/exec.d.ts +16 -0
- package/dist/shared/exec.js +86 -0
- package/dist/shared/fs.d.ts +18 -0
- package/dist/shared/fs.js +93 -0
- package/dist/shared/git.d.ts +22 -0
- package/dist/shared/git.js +132 -0
- package/dist/shared/hash.d.ts +5 -0
- package/dist/shared/hash.js +29 -0
- package/dist/shared/paths.d.ts +16 -0
- package/dist/shared/paths.js +97 -0
- package/dist/shared/pkg.d.ts +3 -0
- package/dist/shared/pkg.js +23 -0
- package/dist/shared/version.d.ts +9 -0
- package/dist/shared/version.js +34 -0
- package/dist/usage/analyze.d.ts +5 -0
- package/dist/usage/analyze.js +212 -0
- package/dist/usage/claude.d.ts +9 -0
- package/dist/usage/claude.js +59 -0
- package/dist/usage/collect.d.ts +11 -0
- package/dist/usage/collect.js +71 -0
- package/dist/usage/doctor.d.ts +72 -0
- package/dist/usage/doctor.js +121 -0
- package/dist/usage/parse.d.ts +28 -0
- package/dist/usage/parse.js +345 -0
- package/dist/usage/types.d.ts +103 -0
- package/dist/usage/types.js +2 -0
- package/docs/getting-started.md +20 -0
- package/docs/guides/configuration.md +33 -0
- package/docs/guides/cost-benchmark.md +88 -0
- package/docs/guides/plugin-migration.md +65 -0
- package/docs/guides/smart-figma.md +51 -0
- package/docs/guides/source-reading.md +35 -0
- package/docs/guides/ui-conventions-template.md +39 -0
- package/docs/guides/worker-evidence.md +73 -0
- package/docs/guides/workflow.md +74 -0
- package/docs/maintainers/architecture.md +27 -0
- package/docs/maintainers/plugin-runtime-contract.md +55 -0
- package/package.json +78 -0
- package/plugin.json +9 -0
- package/presets/backend-service.yaml +19 -0
- package/presets/flutter-mobile/plugin-hooks.json +68 -0
- package/presets/flutter-mobile/ui-conventions.md +122 -0
- package/presets/flutter-mobile.yaml +205 -0
- package/presets/web-product.yaml +23 -0
- package/skills/opsx-apply/SKILL.md +35 -0
- package/skills/opsx-apply/nodes/blocked.md +7 -0
- package/skills/opsx-apply/nodes/code-task.md +29 -0
- package/skills/opsx-apply/nodes/gate-a.md +9 -0
- package/skills/opsx-apply/nodes/stop.md +5 -0
- package/skills/opsx-apply/nodes/task-build.md +50 -0
- package/skills/opsx-apply/nodes/task-repair.md +20 -0
- package/skills/opsx-apply/nodes/task-scout.md +28 -0
- package/skills/opsx-apply/nodes/task-verify.md +38 -0
- package/skills/opsx-archive/SKILL.md +24 -0
- package/skills/opsx-archive/nodes/archive.md +15 -0
- package/skills/opsx-explore/SKILL.md +20 -0
- package/skills/opsx-explore/nodes/explore.md +3 -0
- package/skills/opsx-fix-verify/SKILL.md +30 -0
- package/skills/opsx-fix-verify/nodes/repair.md +11 -0
- package/skills/opsx-fix-verify/nodes/reverify.md +3 -0
- package/skills/opsx-fix-verify/nodes/stop.md +3 -0
- package/skills/opsx-gate-decision/SKILL.md +20 -0
- package/skills/opsx-gate-decision/nodes/decide.md +15 -0
- package/skills/opsx-propose/SKILL.md +36 -0
- package/skills/opsx-propose/nodes/blocked.md +13 -0
- package/skills/opsx-propose/nodes/collect.md +35 -0
- package/skills/opsx-propose/nodes/parse.md +72 -0
- package/skills/opsx-propose/nodes/plan.md +65 -0
- package/skills/opsx-propose/nodes/stop.md +17 -0
- package/skills/opsx-propose/nodes/tech.md +77 -0
- package/skills/opsx-propose/scripts/read-json-source.mjs +22 -0
- package/skills/opsx-resume/SKILL.md +21 -0
- package/skills/opsx-resume/nodes/route.md +12 -0
- package/skills/opsx-source-update/SKILL.md +23 -0
- package/skills/opsx-source-update/nodes/update.md +14 -0
- package/skills/opsx-verify/SKILL.md +32 -0
- package/skills/opsx-verify/nodes/blocked.md +3 -0
- package/skills/opsx-verify/nodes/failed.md +3 -0
- package/skills/opsx-verify/nodes/gate-b.md +7 -0
- package/skills/opsx-verify/nodes/stop.md +5 -0
- package/skills/opsx-verify/nodes/verify.md +10 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { sha256File } from '../shared/hash.js';
|
|
3
|
+
import { statSync } from 'node:fs';
|
|
4
|
+
import { copyPath, ensureDir, exists, listFilesRecursive, readJson, removePath, writeJson } from '../shared/fs.js';
|
|
5
|
+
import { AppError } from '../shared/errors.js';
|
|
6
|
+
import { collectSources } from '../hooks/engine.js';
|
|
7
|
+
import { artifactPath, requireChange } from './change.js';
|
|
8
|
+
import { appendEvent, saveState } from './state.js';
|
|
9
|
+
import { ChangeStateSchema, NODES, SourcesLedgerSchema, } from './types.js';
|
|
10
|
+
import { normalizeRel, resolveInside } from '../shared/paths.js';
|
|
11
|
+
const CAPABILITIES = ['requirements', 'interfaces', 'design'];
|
|
12
|
+
const BACKUP_REL = '.opsx/source-update-backup';
|
|
13
|
+
const MUTABLE_ARTIFACTS = ['spec.md', 'tech.md', 'plan', 'parsed', 'reports'];
|
|
14
|
+
export function addSources(projectRoot, changeName, input) {
|
|
15
|
+
const state = requireChange(projectRoot, changeName);
|
|
16
|
+
if (state.activeWorker)
|
|
17
|
+
throw new AppError('worker_active', 'active Worker 存在时不能更新 source');
|
|
18
|
+
// Validate the entire request before writing a revision or creating an update
|
|
19
|
+
// backup. An invalid append-only locator cannot be repaired by appending again.
|
|
20
|
+
for (const source of input.sources ?? []) {
|
|
21
|
+
if (!isCapability(source.capability))
|
|
22
|
+
throw new AppError('invalid_capability', `不支持的 source capability: ${source.capability}`);
|
|
23
|
+
if (source.kind !== 'file')
|
|
24
|
+
continue;
|
|
25
|
+
let file;
|
|
26
|
+
try {
|
|
27
|
+
file = resolveInside(projectRoot, source.locator, 'source_file_outside_project');
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (!(error instanceof AppError))
|
|
31
|
+
throw error;
|
|
32
|
+
throw new AppError(error.code, `${error.message};请先将获授权的文件复制到项目内,再用相对路径登记。此次未写入来源账本。`);
|
|
33
|
+
}
|
|
34
|
+
if (!exists(file) || !statSync(file).isFile())
|
|
35
|
+
throw new AppError('source_file_missing', `来源文件不存在或不是普通文件: ${source.locator};请先创建文件,再登记。此次未写入来源账本。`);
|
|
36
|
+
}
|
|
37
|
+
const current = loadLedger(projectRoot, changeName, state.sourceRevision);
|
|
38
|
+
const pastGateA = ['code', 'gate_b', 'verify', 'gate_c', 'completed'].includes(state.node)
|
|
39
|
+
|| state.gates['gate-A'].status === 'approved';
|
|
40
|
+
const updatingApprovedChange = pastGateA || Boolean(state.sourceUpdate);
|
|
41
|
+
if (pastGateA && !state.sourceUpdate) {
|
|
42
|
+
createSourceUpdateBackup(projectRoot, changeName, state);
|
|
43
|
+
const completedTaskIds = state.tasks.filter((task) => task.status === 'completed').map((task) => task.id);
|
|
44
|
+
state.sourceUpdate = {
|
|
45
|
+
fromRevision: state.sourceRevision,
|
|
46
|
+
startedAt: new Date().toISOString(),
|
|
47
|
+
previousNode: state.node,
|
|
48
|
+
hadCompletedTasks: state.tasks.some((task) => task.status === 'completed'),
|
|
49
|
+
backupPath: BACKUP_REL,
|
|
50
|
+
completedTaskIds,
|
|
51
|
+
changedSourceIds: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const items = [...current.items];
|
|
55
|
+
const addedIds = [];
|
|
56
|
+
for (const source of input.sources ?? []) {
|
|
57
|
+
// Narrowed again for the public TypeScript input type; validation above is
|
|
58
|
+
// intentionally before any source-update side effect.
|
|
59
|
+
if (!isCapability(source.capability))
|
|
60
|
+
throw new AppError('invalid_capability', `不支持的 source capability: ${source.capability}`);
|
|
61
|
+
let nextIndex = items.length + 1;
|
|
62
|
+
while (items.some((item) => item.id === `src-${nextIndex}`))
|
|
63
|
+
nextIndex += 1;
|
|
64
|
+
const id = `src-${nextIndex}`;
|
|
65
|
+
items.push({
|
|
66
|
+
id,
|
|
67
|
+
capability: source.capability,
|
|
68
|
+
kind: source.kind,
|
|
69
|
+
locator: source.locator,
|
|
70
|
+
});
|
|
71
|
+
addedIds.push(id);
|
|
72
|
+
}
|
|
73
|
+
if (items.length === 0 && !(input.notes ?? current.notes)) {
|
|
74
|
+
throw new AppError('empty_sources', '至少需要 notes 或一条 source');
|
|
75
|
+
}
|
|
76
|
+
const ledger = {
|
|
77
|
+
revision: state.sourceRevision + 1,
|
|
78
|
+
notes: input.notes ?? current.notes,
|
|
79
|
+
items,
|
|
80
|
+
};
|
|
81
|
+
writeJson(artifactPath(projectRoot, changeName, `sources/sources-r${ledger.revision}.json`), ledger);
|
|
82
|
+
state.sourceRevision = ledger.revision;
|
|
83
|
+
if (state.sourceUpdate) {
|
|
84
|
+
state.sourceUpdate.changedSourceIds = [...new Set([...state.sourceUpdate.changedSourceIds, ...addedIds])];
|
|
85
|
+
}
|
|
86
|
+
state.node = NODES.COLLECT;
|
|
87
|
+
state.tasks = updatingApprovedChange ? state.tasks.map((task) => ({ ...task, status: 'pending' })) : [];
|
|
88
|
+
state.lastIssue = null;
|
|
89
|
+
state.verifyRunId = null;
|
|
90
|
+
state.repair = null;
|
|
91
|
+
state.gates['gate-A'] = { status: 'pending' };
|
|
92
|
+
if (updatingApprovedChange) {
|
|
93
|
+
state.gates['gate-B'] = { status: 'pending' };
|
|
94
|
+
state.gates['gate-C'] = { status: 'pending' };
|
|
95
|
+
}
|
|
96
|
+
saveState(projectRoot, state);
|
|
97
|
+
appendEvent(projectRoot, changeName, 'sources.updated', { revision: ledger.revision });
|
|
98
|
+
return ledger;
|
|
99
|
+
}
|
|
100
|
+
export function collect(projectRoot, changeName, config, options = {}) {
|
|
101
|
+
const state = requireChange(projectRoot, changeName);
|
|
102
|
+
if (state.activeWorker)
|
|
103
|
+
throw new AppError('worker_active', 'active Worker 存在时不能重新采集');
|
|
104
|
+
if (state.node !== NODES.COLLECT)
|
|
105
|
+
throw new AppError('collect_not_pending', '采集已完成;更新来源后再采集,不能回退当前阶段');
|
|
106
|
+
if (state.sourceRevision === 0)
|
|
107
|
+
throw new AppError('no_sources', '请先运行 openspec sources add');
|
|
108
|
+
const ledger = loadLedger(projectRoot, changeName, state.sourceRevision);
|
|
109
|
+
const ledgerPath = artifactPath(projectRoot, changeName, `sources/sources-r${ledger.revision}.json`);
|
|
110
|
+
const outcome = collectSources(projectRoot, changeName, config, ledger.items, {
|
|
111
|
+
refresh: options.refresh,
|
|
112
|
+
checkpoint: (sourceId, artifact, collection) => {
|
|
113
|
+
const item = ledger.items.find((source) => source.id === sourceId);
|
|
114
|
+
item.artifact = artifact;
|
|
115
|
+
item.collection = collection;
|
|
116
|
+
writeJson(ledgerPath, ledger);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
if (outcome.deferred.length > 0) {
|
|
120
|
+
throw new AppError('sources_incomplete', `source 尚未完成采集: ${outcome.deferred.join(', ')}`, outcome);
|
|
121
|
+
}
|
|
122
|
+
const bySource = new Map(outcome.sourceArtifacts.map((item) => [item.sourceId, item.path]));
|
|
123
|
+
ledger.items = ledger.items.map(({ collection: _checkpoint, ...item }) => {
|
|
124
|
+
const artifact = bySource.get(item.id) ?? item.artifact;
|
|
125
|
+
return { ...item, artifact, ...(artifact ? { artifactSha256: sha256File(artifactPath(projectRoot, changeName, artifact)) } : {}) };
|
|
126
|
+
});
|
|
127
|
+
writeJson(artifactPath(projectRoot, changeName, `sources/sources-r${ledger.revision}.json`), ledger);
|
|
128
|
+
state.collectedRevision = state.sourceRevision;
|
|
129
|
+
state.node = NODES.PARSE;
|
|
130
|
+
state.activeWorker = null;
|
|
131
|
+
saveState(projectRoot, state);
|
|
132
|
+
appendEvent(projectRoot, changeName, 'sources.collected', {
|
|
133
|
+
revision: ledger.revision,
|
|
134
|
+
artifacts: outcome.artifacts,
|
|
135
|
+
covered: outcome.covered,
|
|
136
|
+
});
|
|
137
|
+
return { ledger, ...outcome };
|
|
138
|
+
}
|
|
139
|
+
export function abortUpdate(projectRoot, changeName, reason) {
|
|
140
|
+
const state = requireChange(projectRoot, changeName);
|
|
141
|
+
if (!state.sourceUpdate)
|
|
142
|
+
throw new AppError('no_source_update', '当前没有可中止的 source update');
|
|
143
|
+
if (state.gates['gate-A'].status === 'approved' && state.node !== NODES.GATE_A) {
|
|
144
|
+
throw new AppError('source_update_committed', '新版 Gate-A 已通过,不能中止');
|
|
145
|
+
}
|
|
146
|
+
const from = state.sourceUpdate.fromRevision;
|
|
147
|
+
const ledger = loadLedger(projectRoot, changeName, from);
|
|
148
|
+
const backupRoot = artifactPath(projectRoot, changeName, state.sourceUpdate.backupPath);
|
|
149
|
+
const parsed = ChangeStateSchema.safeParse(readJson(resolveInside(backupRoot, 'state.json', 'source_backup_outside_change')));
|
|
150
|
+
if (!parsed.success)
|
|
151
|
+
throw new AppError('source_update_backup_invalid', 'source update backup 状态不合法', parsed.error.issues);
|
|
152
|
+
if (parsed.data.changeName !== changeName)
|
|
153
|
+
throw new AppError('source_update_backup_mismatch', 'source update backup 不属于当前 change');
|
|
154
|
+
restoreMutableArtifacts(projectRoot, changeName, backupRoot);
|
|
155
|
+
removeNewSourceRevisions(projectRoot, changeName, from);
|
|
156
|
+
removePath(backupRoot);
|
|
157
|
+
const restored = parsed.data;
|
|
158
|
+
restored.sourceUpdate = null;
|
|
159
|
+
saveState(projectRoot, restored);
|
|
160
|
+
appendEvent(projectRoot, changeName, 'source_update.aborted', { reason, revision: from });
|
|
161
|
+
return ledger;
|
|
162
|
+
}
|
|
163
|
+
export function commitSourceUpdate(projectRoot, state) {
|
|
164
|
+
if (!state.sourceUpdate)
|
|
165
|
+
return;
|
|
166
|
+
removePath(artifactPath(projectRoot, state.changeName, state.sourceUpdate.backupPath));
|
|
167
|
+
state.sourceUpdate = null;
|
|
168
|
+
}
|
|
169
|
+
export function loadLedger(projectRoot, changeName, revision) {
|
|
170
|
+
if (revision <= 0)
|
|
171
|
+
return { revision: 0, notes: '', items: [] };
|
|
172
|
+
const file = artifactPath(projectRoot, changeName, `sources/sources-r${revision}.json`);
|
|
173
|
+
if (!exists(file))
|
|
174
|
+
throw new AppError('sources_ledger_missing', `缺少 source ledger: r${revision}`);
|
|
175
|
+
const parsed = SourcesLedgerSchema.safeParse(readJson(file));
|
|
176
|
+
if (!parsed.success)
|
|
177
|
+
throw new AppError('invalid_sources_ledger', `source ledger 不合法: r${revision}`, parsed.error.issues);
|
|
178
|
+
if (parsed.data.revision !== revision)
|
|
179
|
+
throw new AppError('sources_revision_mismatch', `source ledger revision 不匹配: 期望 r${revision}`);
|
|
180
|
+
return parsed.data;
|
|
181
|
+
}
|
|
182
|
+
function isCapability(value) {
|
|
183
|
+
return CAPABILITIES.includes(value);
|
|
184
|
+
}
|
|
185
|
+
export function parseSourceArg(raw) {
|
|
186
|
+
const separator = raw.indexOf('=');
|
|
187
|
+
const capabilityRaw = separator >= 0 ? raw.slice(0, separator) : '';
|
|
188
|
+
const rest = separator >= 0 ? raw.slice(separator + 1) : undefined;
|
|
189
|
+
if (!capabilityRaw || rest === undefined || !rest) {
|
|
190
|
+
throw new AppError('invalid_source', '格式应为 capability=locator 或 capability:text=...');
|
|
191
|
+
}
|
|
192
|
+
const [capabilityPart, kindPart] = capabilityRaw.split(':');
|
|
193
|
+
const capability = capabilityPart ?? '';
|
|
194
|
+
if (!isCapability(capability))
|
|
195
|
+
throw new AppError('invalid_capability', `不支持的 capability: ${capability}`);
|
|
196
|
+
if (kindPart === 'text')
|
|
197
|
+
return { capability, kind: 'text', locator: rest };
|
|
198
|
+
if (kindPart === 'file')
|
|
199
|
+
return { capability, kind: 'file', locator: rest };
|
|
200
|
+
return { capability, kind: 'url', locator: rest };
|
|
201
|
+
}
|
|
202
|
+
function createSourceUpdateBackup(projectRoot, changeName, state) {
|
|
203
|
+
const backupRoot = artifactPath(projectRoot, changeName, BACKUP_REL);
|
|
204
|
+
removePath(backupRoot);
|
|
205
|
+
ensureDir(resolveInside(backupRoot, 'artifacts', 'source_backup_outside_change'));
|
|
206
|
+
writeJson(resolveInside(backupRoot, 'state.json', 'source_backup_outside_change'), state);
|
|
207
|
+
for (const relative of MUTABLE_ARTIFACTS) {
|
|
208
|
+
const source = artifactPath(projectRoot, changeName, relative);
|
|
209
|
+
if (exists(source))
|
|
210
|
+
copyPath(source, resolveInside(backupRoot, path.posix.join('artifacts', relative), 'source_backup_outside_change'));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function restoreMutableArtifacts(projectRoot, changeName, backupRoot) {
|
|
214
|
+
for (const relative of MUTABLE_ARTIFACTS) {
|
|
215
|
+
const target = artifactPath(projectRoot, changeName, relative);
|
|
216
|
+
removePath(target);
|
|
217
|
+
const backup = resolveInside(backupRoot, path.posix.join('artifacts', relative), 'source_backup_outside_change');
|
|
218
|
+
if (exists(backup))
|
|
219
|
+
copyPath(backup, target);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function removeNewSourceRevisions(projectRoot, changeName, fromRevision) {
|
|
223
|
+
const sourcesDir = artifactPath(projectRoot, changeName, 'sources');
|
|
224
|
+
for (const file of listFilesRecursive(sourcesDir)) {
|
|
225
|
+
const match = path.basename(file).match(/^sources-r(\d+)\.json$/);
|
|
226
|
+
if (match && Number(match[1]) > fromRevision)
|
|
227
|
+
removePath(file);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export function parseSourceFileArg(raw) {
|
|
231
|
+
const parsed = parseSourceArg(raw);
|
|
232
|
+
return { capability: parsed.capability, kind: 'file', locator: normalizeRel(parsed.locator) };
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=sources.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ChangeState, type GateId } from './types.js';
|
|
2
|
+
export declare function opsxDir(projectRoot: string, changeName: string): string;
|
|
3
|
+
export declare function statePath(projectRoot: string, changeName: string): string;
|
|
4
|
+
export declare function eventsPath(projectRoot: string, changeName: string): string;
|
|
5
|
+
export declare function workersDir(projectRoot: string, changeName: string): string;
|
|
6
|
+
export declare function emptyGates(): Record<GateId, {
|
|
7
|
+
status: 'pending';
|
|
8
|
+
}>;
|
|
9
|
+
export declare function createState(changeName: string, now?: string): ChangeState;
|
|
10
|
+
export declare function loadState(projectRoot: string, changeName: string): ChangeState;
|
|
11
|
+
export declare function saveState(projectRoot: string, state: ChangeState): void;
|
|
12
|
+
export declare function hasState(projectRoot: string, changeName: string): boolean;
|
|
13
|
+
export declare function appendEvent(projectRoot: string, changeName: string, type: string, payload?: Record<string, unknown>): void;
|
|
14
|
+
export declare function isKnownGate(value: string): value is GateId;
|
|
15
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { appendText, exists, readJson, writeJson } from '../shared/fs.js';
|
|
3
|
+
import { changeDir, OPSX_DIR, resolveInside } from '../shared/paths.js';
|
|
4
|
+
import { ChangeStateSchema, GATE_IDS, NODES, STATE_VERSION } from './types.js';
|
|
5
|
+
import { AppError } from '../shared/errors.js';
|
|
6
|
+
export function opsxDir(projectRoot, changeName) {
|
|
7
|
+
return resolveInside(projectRoot, path.posix.join(changeDir(changeName), OPSX_DIR), 'opsx_path_outside_project');
|
|
8
|
+
}
|
|
9
|
+
export function statePath(projectRoot, changeName) {
|
|
10
|
+
return resolveInside(opsxDir(projectRoot, changeName), 'state.json', 'state_path_outside_change');
|
|
11
|
+
}
|
|
12
|
+
export function eventsPath(projectRoot, changeName) {
|
|
13
|
+
return resolveInside(opsxDir(projectRoot, changeName), 'events.jsonl', 'events_path_outside_change');
|
|
14
|
+
}
|
|
15
|
+
export function workersDir(projectRoot, changeName) {
|
|
16
|
+
return resolveInside(opsxDir(projectRoot, changeName), 'workers', 'workers_path_outside_change');
|
|
17
|
+
}
|
|
18
|
+
export function emptyGates() {
|
|
19
|
+
return {
|
|
20
|
+
'gate-A': { status: 'pending' },
|
|
21
|
+
'gate-B': { status: 'pending' },
|
|
22
|
+
'gate-C': { status: 'pending' },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function createState(changeName, now = new Date().toISOString()) {
|
|
26
|
+
return {
|
|
27
|
+
version: STATE_VERSION,
|
|
28
|
+
changeName,
|
|
29
|
+
node: NODES.COLLECT,
|
|
30
|
+
sourceRevision: 0,
|
|
31
|
+
collectedRevision: 0,
|
|
32
|
+
gates: emptyGates(),
|
|
33
|
+
tasks: [],
|
|
34
|
+
engineChangedFiles: [],
|
|
35
|
+
activeWorker: null,
|
|
36
|
+
techSkipped: false,
|
|
37
|
+
lastIssue: null,
|
|
38
|
+
sourceUpdate: null,
|
|
39
|
+
verifyRunId: null,
|
|
40
|
+
repair: null,
|
|
41
|
+
createdAt: now,
|
|
42
|
+
updatedAt: now,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function loadState(projectRoot, changeName) {
|
|
46
|
+
const result = ChangeStateSchema.safeParse(readJson(statePath(projectRoot, changeName)));
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
throw new AppError('invalid_state', `change 状态不合法: ${changeName}`, result.error.issues);
|
|
49
|
+
}
|
|
50
|
+
return result.data;
|
|
51
|
+
}
|
|
52
|
+
export function saveState(projectRoot, state) {
|
|
53
|
+
state.updatedAt = new Date().toISOString();
|
|
54
|
+
const result = ChangeStateSchema.safeParse(state);
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
throw new AppError('invalid_state', `拒绝写入不合法的 change 状态: ${state.changeName}`, result.error.issues);
|
|
57
|
+
}
|
|
58
|
+
writeJson(statePath(projectRoot, state.changeName), result.data);
|
|
59
|
+
}
|
|
60
|
+
export function hasState(projectRoot, changeName) {
|
|
61
|
+
return exists(statePath(projectRoot, changeName));
|
|
62
|
+
}
|
|
63
|
+
export function appendEvent(projectRoot, changeName, type, payload = {}) {
|
|
64
|
+
const line = JSON.stringify({
|
|
65
|
+
type,
|
|
66
|
+
at: new Date().toISOString(),
|
|
67
|
+
changeName,
|
|
68
|
+
payload,
|
|
69
|
+
});
|
|
70
|
+
appendText(eventsPath(projectRoot, changeName), `${line}\n`);
|
|
71
|
+
}
|
|
72
|
+
export function isKnownGate(value) {
|
|
73
|
+
return GATE_IDS.includes(value);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ProjectConfig } from '../config/schema.js';
|
|
2
|
+
export declare function changeStatus(projectRoot: string, changeName: string, config: ProjectConfig): {
|
|
3
|
+
next: import("./types.js").NextAction;
|
|
4
|
+
progress: {
|
|
5
|
+
total: number;
|
|
6
|
+
completed: number;
|
|
7
|
+
};
|
|
8
|
+
uiTasksWithoutDesignRefs: string[];
|
|
9
|
+
version: number;
|
|
10
|
+
changeName: string;
|
|
11
|
+
node: import("./types.js").Node;
|
|
12
|
+
sourceRevision: number;
|
|
13
|
+
collectedRevision: number;
|
|
14
|
+
gates: Record<import("./types.js").GateId, import("./types.js").GateState>;
|
|
15
|
+
tasks: import("./types.js").TaskState[];
|
|
16
|
+
engineChangedFiles: string[];
|
|
17
|
+
codeChanges?: {
|
|
18
|
+
baseline: string;
|
|
19
|
+
filesByStep: Record<string, string[]>;
|
|
20
|
+
pendingSteps?: string[];
|
|
21
|
+
};
|
|
22
|
+
activeWorker: import("./types.js").ActiveWorker | null;
|
|
23
|
+
techSkipped: boolean;
|
|
24
|
+
lastIssue: import("./types.js").LastIssue | null;
|
|
25
|
+
sourceUpdate: import("./types.js").SourceUpdate | null;
|
|
26
|
+
verifyRunId: string | null;
|
|
27
|
+
repair: import("./types.js").VerifyRepair | null;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { effectiveTaskCategories } from '../config/schema.js';
|
|
2
|
+
import { readJson } from '../shared/fs.js';
|
|
3
|
+
import { artifactPath, requireChange } from './change.js';
|
|
4
|
+
import { nextAction } from './flow.js';
|
|
5
|
+
import { loadLedger } from './sources.js';
|
|
6
|
+
import { requireValidTaskPlan, taskProgress } from './tasks.js';
|
|
7
|
+
export function changeStatus(projectRoot, changeName, config) {
|
|
8
|
+
const state = requireChange(projectRoot, changeName);
|
|
9
|
+
const hasDesign = loadLedger(projectRoot, changeName, state.sourceRevision).items.some((source) => source.capability === 'design');
|
|
10
|
+
const missing = [];
|
|
11
|
+
if (hasDesign && state.tasks.some((task) => task.category === 'ui')) {
|
|
12
|
+
// 用已确认任务分类校验计划,status 不因项目刚移除某分类而失去可读性。
|
|
13
|
+
const categories = [...new Set([...Object.keys(effectiveTaskCategories(config)), ...state.tasks.map((task) => task.category)])];
|
|
14
|
+
const tasks = requireValidTaskPlan(readJson(artifactPath(projectRoot, changeName, 'plan/task-plan.json')), categories);
|
|
15
|
+
for (const task of state.tasks.filter((item) => item.category === 'ui')) {
|
|
16
|
+
if (!tasks.find((entry) => entry.id === task.id)?.designRefs?.length)
|
|
17
|
+
missing.push(task.id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { ...state, next: nextAction(state), progress: taskProgress(state), uiTasksWithoutDesignRefs: missing };
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChangeState, TaskState, TaskPlanTask } from './types.js';
|
|
2
|
+
export declare function nextPendingTask(state: Pick<ChangeState, 'tasks'>): TaskState | undefined;
|
|
3
|
+
export declare function taskProgress(state: Pick<ChangeState, 'tasks'>): {
|
|
4
|
+
total: number;
|
|
5
|
+
completed: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function validateTaskPlan(plan: unknown, allowedCategories: string[], knownSourceRefs?: string[]): {
|
|
8
|
+
tasks: TaskPlanTask[];
|
|
9
|
+
failure: {
|
|
10
|
+
code: string;
|
|
11
|
+
failures: string[];
|
|
12
|
+
} | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function requireValidTaskPlan(plan: unknown, allowedCategories: string[], knownSourceRefs?: string[]): TaskPlanTask[];
|
|
15
|
+
//# sourceMappingURL=tasks.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { AppError } from '../shared/errors.js';
|
|
2
|
+
import { assertPathToken, normalizeRel, resolveInside } from '../shared/paths.js';
|
|
3
|
+
export function nextPendingTask(state) {
|
|
4
|
+
return state.tasks.find((task) => task.status !== 'completed');
|
|
5
|
+
}
|
|
6
|
+
export function taskProgress(state) {
|
|
7
|
+
return {
|
|
8
|
+
total: state.tasks.length,
|
|
9
|
+
completed: state.tasks.filter((task) => task.status === 'completed').length,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function validateTaskPlan(plan, allowedCategories, knownSourceRefs) {
|
|
13
|
+
const failures = [];
|
|
14
|
+
if (!isRecord(plan) || !Array.isArray(plan.tasks)) {
|
|
15
|
+
return { tasks: [], failure: { code: 'invalid_task_plan', failures: ['task-plan.json 必须包含 tasks[]'] } };
|
|
16
|
+
}
|
|
17
|
+
if (plan.tasks.length === 0) {
|
|
18
|
+
return { tasks: [], failure: { code: 'invalid_task_plan', failures: ['task-plan.json 至少需要一个 task'] } };
|
|
19
|
+
}
|
|
20
|
+
const categories = new Set(allowedCategories);
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const tasks = [];
|
|
23
|
+
for (const [index, raw] of plan.tasks.entries()) {
|
|
24
|
+
if (!isRecord(raw)) {
|
|
25
|
+
failures.push(`tasks[${index}] 必须是对象`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const id = typeof raw.id === 'string' ? raw.id.trim() : '';
|
|
29
|
+
const label = id ? `Task ${id}` : `tasks[${index}]`;
|
|
30
|
+
if (!id)
|
|
31
|
+
failures.push(`${label} 需要 id`);
|
|
32
|
+
else {
|
|
33
|
+
try {
|
|
34
|
+
assertPathToken(id, `${label}.id`);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
38
|
+
}
|
|
39
|
+
if (/^gate-/i.test(id))
|
|
40
|
+
failures.push(`Gate 不能写成 task: ${id}`);
|
|
41
|
+
else if (seen.has(id))
|
|
42
|
+
failures.push(`重复的 task id: ${id}`);
|
|
43
|
+
}
|
|
44
|
+
const title = typeof raw.title === 'string' ? raw.title.trim() : typeof raw.subject === 'string' ? raw.subject.trim() : '';
|
|
45
|
+
if (!title)
|
|
46
|
+
failures.push(`${label} 需要 title`);
|
|
47
|
+
const category = typeof raw.category === 'string' ? raw.category.trim() : '';
|
|
48
|
+
if (!category)
|
|
49
|
+
failures.push(`${label} 需要 category`);
|
|
50
|
+
else if (!categories.has(category))
|
|
51
|
+
failures.push(`${label} 的 category 不在允许集合中: ${category}`);
|
|
52
|
+
if (raw.type !== undefined && raw.type !== 'code' && raw.type !== 'interface-codegen') {
|
|
53
|
+
failures.push(`${label}.type 必须是 code 或 interface-codegen`);
|
|
54
|
+
}
|
|
55
|
+
const type = raw.type === 'interface-codegen' || category === 'api-codegen' ? 'interface-codegen' : 'code';
|
|
56
|
+
if ('contextRefs' in raw)
|
|
57
|
+
failures.push(`${label}.contextRefs 已移除,请按基线契约重新生成计划`);
|
|
58
|
+
if ('files' in raw)
|
|
59
|
+
failures.push(`${label}.files 已移除,请使用 refScope`);
|
|
60
|
+
if ('allowedDirs' in raw)
|
|
61
|
+
failures.push(`${label}.allowedDirs 已移除,请使用 refScope`);
|
|
62
|
+
const declared = asStringArray(raw.refScope, `${label}.refScope`, failures);
|
|
63
|
+
const refScope = [...new Set((declared ?? []).map(normalizeRel))];
|
|
64
|
+
for (const entry of refScope) {
|
|
65
|
+
try {
|
|
66
|
+
resolveInside('/__openspec_ref_scope__', entry, 'invalid_ref_scope');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const dependsOn = asStringArray(raw.dependsOn, `${label}.dependsOn`, failures) ?? [];
|
|
73
|
+
for (const dep of dependsOn) {
|
|
74
|
+
if (dep === id)
|
|
75
|
+
failures.push(`${label} 不能依赖自己`);
|
|
76
|
+
else if (!seen.has(dep))
|
|
77
|
+
failures.push(`${label} dependsOn 必须指向更早的 task: ${dep}`);
|
|
78
|
+
}
|
|
79
|
+
if (type === 'interface-codegen') {
|
|
80
|
+
for (const key of ['capability', 'operation', 'provider']) {
|
|
81
|
+
if (typeof raw[key] !== 'string' || !String(raw[key]).trim()) {
|
|
82
|
+
failures.push(`${label}.${key} 在 api-codegen 任务中必填`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (id)
|
|
87
|
+
seen.add(id);
|
|
88
|
+
let designRefs;
|
|
89
|
+
if (raw.designRefs !== undefined) {
|
|
90
|
+
if (!Array.isArray(raw.designRefs) || raw.designRefs.some((ref) => typeof ref !== 'string' || !ref.trim())) {
|
|
91
|
+
failures.push(`${label}.designRefs 必须是非空字符串组成的数组(可为空数组)`);
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
designRefs = [...new Set(raw.designRefs.map((ref) => ref.trim()))];
|
|
95
|
+
}
|
|
96
|
+
const sourceRefs = asStringArray(raw.sourceRefs, `${label}.sourceRefs`, failures);
|
|
97
|
+
if (knownSourceRefs) {
|
|
98
|
+
const known = new Set(knownSourceRefs);
|
|
99
|
+
for (const sourceRef of sourceRefs ?? []) {
|
|
100
|
+
if (!known.has(sourceRef))
|
|
101
|
+
failures.push(`${label} 引用了未知 sourceRef: ${sourceRef}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
tasks.push({
|
|
105
|
+
id,
|
|
106
|
+
designRefs,
|
|
107
|
+
title,
|
|
108
|
+
category,
|
|
109
|
+
type,
|
|
110
|
+
refScope: refScope.length > 0 ? refScope : undefined,
|
|
111
|
+
dependsOn,
|
|
112
|
+
notes: typeof raw.notes === 'string' ? raw.notes : undefined,
|
|
113
|
+
status: 'pending',
|
|
114
|
+
capability: typeof raw.capability === 'string' ? raw.capability : undefined,
|
|
115
|
+
operation: typeof raw.operation === 'string' ? raw.operation : undefined,
|
|
116
|
+
provider: typeof raw.provider === 'string' ? raw.provider : undefined,
|
|
117
|
+
sourceRefs: sourceRefs && sourceRefs.length > 0 ? sourceRefs : undefined,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return failures.length > 0
|
|
121
|
+
? { tasks: [], failure: { code: 'invalid_task_plan', failures } }
|
|
122
|
+
: { tasks, failure: null };
|
|
123
|
+
}
|
|
124
|
+
export function requireValidTaskPlan(plan, allowedCategories, knownSourceRefs) {
|
|
125
|
+
const result = validateTaskPlan(plan, allowedCategories, knownSourceRefs);
|
|
126
|
+
if (result.failure) {
|
|
127
|
+
throw new AppError('invalid_task_plan', result.failure.failures.join('; '), result.failure);
|
|
128
|
+
}
|
|
129
|
+
return result.tasks;
|
|
130
|
+
}
|
|
131
|
+
function asStringArray(value, field, failures) {
|
|
132
|
+
if (value === undefined)
|
|
133
|
+
return [];
|
|
134
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
135
|
+
failures.push(`${field} 必须是字符串数组`);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return value.map((item) => item.trim()).filter(Boolean);
|
|
139
|
+
}
|
|
140
|
+
function isRecord(value) {
|
|
141
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=tasks.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ProjectConfig } from '../config/schema.js';
|
|
3
|
+
export declare const PendingWorkSchema: z.ZodObject<{
|
|
4
|
+
changeName: z.ZodString;
|
|
5
|
+
items: z.ZodArray<z.ZodObject<{
|
|
6
|
+
category: z.ZodString;
|
|
7
|
+
source: z.ZodEnum<{
|
|
8
|
+
code: "code";
|
|
9
|
+
worker: "worker";
|
|
10
|
+
}>;
|
|
11
|
+
file: z.ZodString;
|
|
12
|
+
line: z.ZodNumber;
|
|
13
|
+
text: z.ZodString;
|
|
14
|
+
marker: z.ZodOptional<z.ZodEnum<{
|
|
15
|
+
TODO: "TODO";
|
|
16
|
+
FIXME: "FIXME";
|
|
17
|
+
}>>;
|
|
18
|
+
workerId: z.ZodOptional<z.ZodString>;
|
|
19
|
+
stepId: z.ZodOptional<z.ZodString>;
|
|
20
|
+
}, z.core.$strict>>;
|
|
21
|
+
skippedFiles: z.ZodArray<z.ZodObject<{
|
|
22
|
+
file: z.ZodString;
|
|
23
|
+
reason: z.ZodString;
|
|
24
|
+
}, z.core.$strict>>;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
export type PendingWork = z.infer<typeof PendingWorkSchema>;
|
|
27
|
+
export declare function reviewScopedFiles(config: ProjectConfig, files: string[]): string[];
|
|
28
|
+
/** 只读当前 changed-set;历史 Worker 声明单独展示,不推断其已解决。 */
|
|
29
|
+
export declare function collectPendingWork(projectRoot: string, changeName: string, config: ProjectConfig): PendingWork;
|
|
30
|
+
export declare function renderPendingWork(pending: PendingWork): string;
|
|
31
|
+
//# sourceMappingURL=todo.d.ts.map
|