@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,229 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ProjectConfig } from '../config/schema.js';
|
|
3
|
+
import { requireChange } from './change.js';
|
|
4
|
+
import { type ChangeState } from './types.js';
|
|
5
|
+
declare const CommandRunSchema: z.ZodObject<{
|
|
6
|
+
verifyRunId: z.ZodString;
|
|
7
|
+
id: z.ZodString;
|
|
8
|
+
command: z.ZodString;
|
|
9
|
+
code: z.ZodNumber;
|
|
10
|
+
ok: z.ZodBoolean;
|
|
11
|
+
skipped: z.ZodBoolean;
|
|
12
|
+
stdout: z.ZodString;
|
|
13
|
+
stderr: z.ZodString;
|
|
14
|
+
truncated: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
log: z.ZodOptional<z.ZodString>;
|
|
16
|
+
plugin: z.ZodOptional<z.ZodObject<{
|
|
17
|
+
path: z.ZodString;
|
|
18
|
+
sha256: z.ZodString;
|
|
19
|
+
binding: z.ZodObject<{
|
|
20
|
+
use: z.ZodString;
|
|
21
|
+
kind: z.ZodEnum<{
|
|
22
|
+
hook: "hook";
|
|
23
|
+
adapter: "adapter";
|
|
24
|
+
}>;
|
|
25
|
+
operation: z.ZodEnum<{
|
|
26
|
+
preview: "preview";
|
|
27
|
+
hook: "hook";
|
|
28
|
+
read: "read";
|
|
29
|
+
}>;
|
|
30
|
+
identity: z.ZodObject<{
|
|
31
|
+
name: z.ZodString;
|
|
32
|
+
marketplace: z.ZodString;
|
|
33
|
+
resolvedSha: z.ZodString;
|
|
34
|
+
digest: z.ZodString;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
37
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
38
|
+
inputDescription: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
workerId: z.ZodOptional<z.ZodString>;
|
|
41
|
+
stepId: z.ZodOptional<z.ZodString>;
|
|
42
|
+
}, z.core.$strict>>;
|
|
43
|
+
context: z.ZodOptional<z.ZodObject<{
|
|
44
|
+
path: z.ZodString;
|
|
45
|
+
digest: z.ZodString;
|
|
46
|
+
}, z.core.$strict>>;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
declare const EvidenceRecordSchema: z.ZodObject<{
|
|
49
|
+
verifyRunId: z.ZodString;
|
|
50
|
+
hookId: z.ZodString;
|
|
51
|
+
type: z.ZodEnum<{
|
|
52
|
+
skill: "skill";
|
|
53
|
+
file: "file";
|
|
54
|
+
check: "check";
|
|
55
|
+
}>;
|
|
56
|
+
status: z.ZodEnum<{
|
|
57
|
+
failed: "failed";
|
|
58
|
+
passed: "passed";
|
|
59
|
+
skipped: "skipped";
|
|
60
|
+
}>;
|
|
61
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
62
|
+
digest: z.ZodOptional<z.ZodString>;
|
|
63
|
+
plugin: z.ZodOptional<z.ZodObject<{
|
|
64
|
+
use: z.ZodString;
|
|
65
|
+
kind: z.ZodEnum<{
|
|
66
|
+
hook: "hook";
|
|
67
|
+
adapter: "adapter";
|
|
68
|
+
}>;
|
|
69
|
+
operation: z.ZodEnum<{
|
|
70
|
+
preview: "preview";
|
|
71
|
+
hook: "hook";
|
|
72
|
+
read: "read";
|
|
73
|
+
}>;
|
|
74
|
+
identity: z.ZodObject<{
|
|
75
|
+
name: z.ZodString;
|
|
76
|
+
marketplace: z.ZodString;
|
|
77
|
+
resolvedSha: z.ZodString;
|
|
78
|
+
digest: z.ZodString;
|
|
79
|
+
}, z.core.$strict>;
|
|
80
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
81
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
82
|
+
inputDescription: z.ZodOptional<z.ZodString>;
|
|
83
|
+
}, z.core.$strict>>;
|
|
84
|
+
}, z.core.$strict>;
|
|
85
|
+
declare const QaReportSchema: z.ZodObject<{
|
|
86
|
+
version: z.ZodLiteral<1>;
|
|
87
|
+
verifyRunId: z.ZodString;
|
|
88
|
+
status: z.ZodEnum<{
|
|
89
|
+
failed: "failed";
|
|
90
|
+
passed: "passed";
|
|
91
|
+
}>;
|
|
92
|
+
commands: z.ZodArray<z.ZodObject<{
|
|
93
|
+
verifyRunId: z.ZodString;
|
|
94
|
+
id: z.ZodString;
|
|
95
|
+
command: z.ZodString;
|
|
96
|
+
code: z.ZodNumber;
|
|
97
|
+
ok: z.ZodBoolean;
|
|
98
|
+
skipped: z.ZodBoolean;
|
|
99
|
+
stdout: z.ZodString;
|
|
100
|
+
stderr: z.ZodString;
|
|
101
|
+
truncated: z.ZodOptional<z.ZodBoolean>;
|
|
102
|
+
log: z.ZodOptional<z.ZodString>;
|
|
103
|
+
plugin: z.ZodOptional<z.ZodObject<{
|
|
104
|
+
path: z.ZodString;
|
|
105
|
+
sha256: z.ZodString;
|
|
106
|
+
binding: z.ZodObject<{
|
|
107
|
+
use: z.ZodString;
|
|
108
|
+
kind: z.ZodEnum<{
|
|
109
|
+
hook: "hook";
|
|
110
|
+
adapter: "adapter";
|
|
111
|
+
}>;
|
|
112
|
+
operation: z.ZodEnum<{
|
|
113
|
+
preview: "preview";
|
|
114
|
+
hook: "hook";
|
|
115
|
+
read: "read";
|
|
116
|
+
}>;
|
|
117
|
+
identity: z.ZodObject<{
|
|
118
|
+
name: z.ZodString;
|
|
119
|
+
marketplace: z.ZodString;
|
|
120
|
+
resolvedSha: z.ZodString;
|
|
121
|
+
digest: z.ZodString;
|
|
122
|
+
}, z.core.$strict>;
|
|
123
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
124
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
125
|
+
inputDescription: z.ZodOptional<z.ZodString>;
|
|
126
|
+
}, z.core.$strict>;
|
|
127
|
+
workerId: z.ZodOptional<z.ZodString>;
|
|
128
|
+
stepId: z.ZodOptional<z.ZodString>;
|
|
129
|
+
}, z.core.$strict>>;
|
|
130
|
+
context: z.ZodOptional<z.ZodObject<{
|
|
131
|
+
path: z.ZodString;
|
|
132
|
+
digest: z.ZodString;
|
|
133
|
+
}, z.core.$strict>>;
|
|
134
|
+
}, z.core.$strict>>;
|
|
135
|
+
evidences: z.ZodArray<z.ZodObject<{
|
|
136
|
+
verifyRunId: z.ZodString;
|
|
137
|
+
hookId: z.ZodString;
|
|
138
|
+
type: z.ZodEnum<{
|
|
139
|
+
skill: "skill";
|
|
140
|
+
file: "file";
|
|
141
|
+
check: "check";
|
|
142
|
+
}>;
|
|
143
|
+
status: z.ZodEnum<{
|
|
144
|
+
failed: "failed";
|
|
145
|
+
passed: "passed";
|
|
146
|
+
skipped: "skipped";
|
|
147
|
+
}>;
|
|
148
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
149
|
+
digest: z.ZodOptional<z.ZodString>;
|
|
150
|
+
plugin: z.ZodOptional<z.ZodObject<{
|
|
151
|
+
use: z.ZodString;
|
|
152
|
+
kind: z.ZodEnum<{
|
|
153
|
+
hook: "hook";
|
|
154
|
+
adapter: "adapter";
|
|
155
|
+
}>;
|
|
156
|
+
operation: z.ZodEnum<{
|
|
157
|
+
preview: "preview";
|
|
158
|
+
hook: "hook";
|
|
159
|
+
read: "read";
|
|
160
|
+
}>;
|
|
161
|
+
identity: z.ZodObject<{
|
|
162
|
+
name: z.ZodString;
|
|
163
|
+
marketplace: z.ZodString;
|
|
164
|
+
resolvedSha: z.ZodString;
|
|
165
|
+
digest: z.ZodString;
|
|
166
|
+
}, z.core.$strict>;
|
|
167
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
168
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
169
|
+
inputDescription: z.ZodOptional<z.ZodString>;
|
|
170
|
+
}, z.core.$strict>>;
|
|
171
|
+
}, z.core.$strict>>;
|
|
172
|
+
blockingIssues: z.ZodArray<z.ZodString>;
|
|
173
|
+
changedFiles: z.ZodArray<z.ZodString>;
|
|
174
|
+
digest: z.ZodString;
|
|
175
|
+
pendingWork: z.ZodOptional<z.ZodObject<{
|
|
176
|
+
changeName: z.ZodString;
|
|
177
|
+
items: z.ZodArray<z.ZodObject<{
|
|
178
|
+
category: z.ZodString;
|
|
179
|
+
source: z.ZodEnum<{
|
|
180
|
+
code: "code";
|
|
181
|
+
worker: "worker";
|
|
182
|
+
}>;
|
|
183
|
+
file: z.ZodString;
|
|
184
|
+
line: z.ZodNumber;
|
|
185
|
+
text: z.ZodString;
|
|
186
|
+
marker: z.ZodOptional<z.ZodEnum<{
|
|
187
|
+
TODO: "TODO";
|
|
188
|
+
FIXME: "FIXME";
|
|
189
|
+
}>>;
|
|
190
|
+
workerId: z.ZodOptional<z.ZodString>;
|
|
191
|
+
stepId: z.ZodOptional<z.ZodString>;
|
|
192
|
+
}, z.core.$strict>>;
|
|
193
|
+
skippedFiles: z.ZodArray<z.ZodObject<{
|
|
194
|
+
file: z.ZodString;
|
|
195
|
+
reason: z.ZodString;
|
|
196
|
+
}, z.core.$strict>>;
|
|
197
|
+
}, z.core.$strict>>;
|
|
198
|
+
visualAdvisories: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
199
|
+
id: z.ZodString;
|
|
200
|
+
status: z.ZodEnum<{
|
|
201
|
+
open: "open";
|
|
202
|
+
resolved: "resolved";
|
|
203
|
+
}>;
|
|
204
|
+
description: z.ZodString;
|
|
205
|
+
advisory: z.ZodOptional<z.ZodLiteral<true>>;
|
|
206
|
+
evidence: z.ZodOptional<z.ZodString>;
|
|
207
|
+
evidenceDigest: z.ZodOptional<z.ZodString>;
|
|
208
|
+
fingerprint: z.ZodOptional<z.ZodString>;
|
|
209
|
+
stepId: z.ZodString;
|
|
210
|
+
}, z.core.$strict>>>;
|
|
211
|
+
}, z.core.$strict>;
|
|
212
|
+
export type CommandRun = z.infer<typeof CommandRunSchema>;
|
|
213
|
+
export type EvidenceRecord = z.infer<typeof EvidenceRecordSchema>;
|
|
214
|
+
export type QaReport = z.infer<typeof QaReportSchema>;
|
|
215
|
+
export declare function runVerifyCommands(projectRoot: string, changeName: string, config: ProjectConfig, changedFiles: string[]): CommandRun[];
|
|
216
|
+
export declare function recordEvidence(projectRoot: string, changeName: string, input: Omit<EvidenceRecord, 'verifyRunId'>, config: ProjectConfig): EvidenceRecord;
|
|
217
|
+
export declare function writeReport(projectRoot: string, changeName: string, config: ProjectConfig, extra?: {
|
|
218
|
+
commands?: CommandRun[];
|
|
219
|
+
changedFiles?: string[];
|
|
220
|
+
}): {
|
|
221
|
+
status: 'passed' | 'failed';
|
|
222
|
+
report: QaReport;
|
|
223
|
+
};
|
|
224
|
+
export declare function loadVerifiedReport(projectRoot: string, changeName: string, verifyRunId: string): QaReport;
|
|
225
|
+
export declare function renderReportMarkdown(report: QaReport): string;
|
|
226
|
+
export declare function requestFix(projectRoot: string, changeName: string, reason: string): ChangeState;
|
|
227
|
+
export declare function changedFilesFromState(state: ReturnType<typeof requireChange>): string[];
|
|
228
|
+
export {};
|
|
229
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { savePluginEvidence, PluginEvidenceSchema, PluginBindingSchema, pluginBindingCurrent, pluginEvidenceCurrent, runPluginCommand } from '../hooks/plugin-runtime.js';
|
|
4
|
+
import { operationContext } from './source-operations.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { exists, readJson, writeJson } from '../shared/fs.js';
|
|
7
|
+
import { splitCommand } from '../shared/exec.js';
|
|
8
|
+
import { matchPattern } from '../shared/paths.js';
|
|
9
|
+
import { runLoggedCommand } from '../shared/command-log.js';
|
|
10
|
+
import { AppError } from '../shared/errors.js';
|
|
11
|
+
import { sha256 } from '../shared/hash.js';
|
|
12
|
+
import { hookDescriptor, fileHookDigest, fileHookState, hooksFor } from '../hooks/engine.js';
|
|
13
|
+
import { artifactPath, requireChange } from './change.js';
|
|
14
|
+
import { appendEvent, saveState } from './state.js';
|
|
15
|
+
import { NODES } from './types.js';
|
|
16
|
+
import { collectPendingWork, PendingWorkSchema, reviewScopedFiles } from './todo.js';
|
|
17
|
+
import { CommandContextEvidenceSchema, commandContextCurrent, prepareCommandContext } from '../hooks/command-context.js';
|
|
18
|
+
import { visualAdvisories, VisualAdvisorySchema } from './worker-evidence.js';
|
|
19
|
+
const StatusSchema = z.enum(['passed', 'failed', 'skipped']);
|
|
20
|
+
const CommandRunSchema = z.object({
|
|
21
|
+
verifyRunId: z.string(),
|
|
22
|
+
id: z.string(),
|
|
23
|
+
command: z.string(),
|
|
24
|
+
code: z.number().int(),
|
|
25
|
+
ok: z.boolean(),
|
|
26
|
+
skipped: z.boolean(),
|
|
27
|
+
stdout: z.string(),
|
|
28
|
+
stderr: z.string(),
|
|
29
|
+
truncated: z.boolean().optional(),
|
|
30
|
+
log: z.string().optional(),
|
|
31
|
+
plugin: PluginEvidenceSchema.optional(),
|
|
32
|
+
context: CommandContextEvidenceSchema.optional(),
|
|
33
|
+
}).strict();
|
|
34
|
+
const EvidenceRecordSchema = z.object({
|
|
35
|
+
verifyRunId: z.string(),
|
|
36
|
+
hookId: z.string(),
|
|
37
|
+
type: z.enum(['skill', 'file', 'check']),
|
|
38
|
+
status: StatusSchema,
|
|
39
|
+
notes: z.string().optional(),
|
|
40
|
+
digest: z.string().optional(),
|
|
41
|
+
plugin: PluginBindingSchema.optional(),
|
|
42
|
+
}).strict();
|
|
43
|
+
const QaReportSchema = z.object({
|
|
44
|
+
version: z.literal(1),
|
|
45
|
+
verifyRunId: z.string(),
|
|
46
|
+
status: z.enum(['passed', 'failed']),
|
|
47
|
+
commands: z.array(CommandRunSchema),
|
|
48
|
+
evidences: z.array(EvidenceRecordSchema),
|
|
49
|
+
blockingIssues: z.array(z.string()),
|
|
50
|
+
changedFiles: z.array(z.string()),
|
|
51
|
+
digest: z.string(),
|
|
52
|
+
pendingWork: PendingWorkSchema.optional(),
|
|
53
|
+
visualAdvisories: z.array(VisualAdvisorySchema).optional(),
|
|
54
|
+
}).strict();
|
|
55
|
+
export function runVerifyCommands(projectRoot, changeName, config, changedFiles) {
|
|
56
|
+
const verifyRunId = requireActiveVerifyRun(projectRoot, changeName);
|
|
57
|
+
const scoped = reviewScopedFiles(config, changedFiles);
|
|
58
|
+
const runs = [];
|
|
59
|
+
for (const hook of hooksFor(config, 'verify').filter((item) => item.type === 'command')) {
|
|
60
|
+
const descriptor = hookDescriptor(projectRoot, hook, config);
|
|
61
|
+
if (descriptor.type !== 'command')
|
|
62
|
+
throw new AppError('hook_type_mismatch', `需要 command: ${hook.id}`);
|
|
63
|
+
const files = targetFiles(hook, scoped);
|
|
64
|
+
let run;
|
|
65
|
+
if (hook.target === 'inScopeChangedFiles' && files.length === 0) {
|
|
66
|
+
run = {
|
|
67
|
+
verifyRunId,
|
|
68
|
+
id: hook.id,
|
|
69
|
+
command: descriptor.command,
|
|
70
|
+
code: hook.allowEmpty ? 0 : 2,
|
|
71
|
+
ok: Boolean(hook.allowEmpty),
|
|
72
|
+
skipped: Boolean(hook.allowEmpty),
|
|
73
|
+
stdout: '',
|
|
74
|
+
truncated: false,
|
|
75
|
+
stderr: hook.allowEmpty ? '' : 'inScopeChangedFiles 为空且 allowEmpty 未开启',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
else if (descriptor.plugin) {
|
|
79
|
+
const outputDir = artifactPath(projectRoot, changeName, `.opsx/verify/${verifyRunId}/${hook.id}-${randomUUID()}`);
|
|
80
|
+
const prepared = operationContext(projectRoot, changeName, descriptor.plugin, outputDir, undefined, { workerId: verifyRunId, stepId: 'verify' });
|
|
81
|
+
if (hook.target === 'inScopeChangedFiles')
|
|
82
|
+
prepared.context.input.files = files;
|
|
83
|
+
const result = runPluginCommand(projectRoot, descriptor.plugin, prepared.context, [...prepared.files, ...files]);
|
|
84
|
+
run = { verifyRunId, id: hook.id, command: descriptor.command, code: result.code, ok: result.ok, skipped: false, stdout: `完整结果: ${path.relative(projectRoot, result.stdoutPath)}`, stderr: `诊断: ${path.relative(projectRoot, result.stderrPath)}`, log: path.relative(artifactPath(projectRoot, changeName, '.'), result.stdoutPath), plugin: savePluginEvidence(projectRoot, outputDir, result.receipt) };
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const command = descriptor.command;
|
|
88
|
+
const { bin, args } = splitCommand(command);
|
|
89
|
+
const expanded = expandFiles(args, files);
|
|
90
|
+
const log = `.opsx/verify/${verifyRunId}/${hook.id}.log`;
|
|
91
|
+
const context = prepareCommandContext(projectRoot, changeName, path.relative(projectRoot, artifactPath(projectRoot, changeName, `.opsx/verify/${verifyRunId}/${hook.id}.context.json`)), verifyRunId, hook.id);
|
|
92
|
+
const result = runLoggedCommand(bin, expanded, projectRoot, artifactPath(projectRoot, changeName, log), undefined, context.env);
|
|
93
|
+
run = {
|
|
94
|
+
verifyRunId,
|
|
95
|
+
id: hook.id,
|
|
96
|
+
command,
|
|
97
|
+
code: result.code,
|
|
98
|
+
ok: result.ok,
|
|
99
|
+
skipped: false,
|
|
100
|
+
stdout: result.stdout,
|
|
101
|
+
stderr: result.stderr,
|
|
102
|
+
truncated: result.truncated,
|
|
103
|
+
log,
|
|
104
|
+
context: context.evidence,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const parsed = CommandRunSchema.parse(run);
|
|
108
|
+
runs.push(parsed);
|
|
109
|
+
writeJson(commandRunPath(projectRoot, changeName, verifyRunId, hook.id), parsed);
|
|
110
|
+
}
|
|
111
|
+
appendEvent(projectRoot, changeName, 'verify.commands', { verifyRunId, count: runs.length });
|
|
112
|
+
return runs;
|
|
113
|
+
}
|
|
114
|
+
export function recordEvidence(projectRoot, changeName, input, config) {
|
|
115
|
+
const verifyRunId = requireActiveVerifyRun(projectRoot, changeName);
|
|
116
|
+
const hook = hooksFor(config, 'verify').find((item) => item.id === input.hookId);
|
|
117
|
+
if (!hook)
|
|
118
|
+
throw new AppError('unknown_hook', `找不到 hook: ${input.hookId}`);
|
|
119
|
+
if (hook.type === 'command')
|
|
120
|
+
throw new AppError('command_evidence_managed', 'command evidence 由 verify commands 生成');
|
|
121
|
+
if (hook.type !== input.type) {
|
|
122
|
+
throw new AppError('hook_type_mismatch', `hook ${hook.id} 的类型是 ${String(hook.type)},不是 ${input.type}`);
|
|
123
|
+
}
|
|
124
|
+
const record = { ...input, verifyRunId };
|
|
125
|
+
if (hook.use)
|
|
126
|
+
record.plugin = hookDescriptor(projectRoot, hook, config).plugin;
|
|
127
|
+
if (hook.type === 'file') {
|
|
128
|
+
const digest = fileHookDigest(projectRoot, hook);
|
|
129
|
+
if (!digest && input.status === 'passed') {
|
|
130
|
+
throw new AppError('hook_file_missing', `file hook 文件不存在,不能记录 passed: ${hook.path ?? hook.id}`);
|
|
131
|
+
}
|
|
132
|
+
if (input.status === 'passed' && hook.path && fileHookState(projectRoot, hook.path) === 'unfilled') {
|
|
133
|
+
throw new AppError('hook_file_unfilled', `file hook 仍是未填写的模板(含 openspec:fill-me 标记),不能记录 passed: ${hook.path}`);
|
|
134
|
+
}
|
|
135
|
+
if (digest)
|
|
136
|
+
record.digest = digest;
|
|
137
|
+
}
|
|
138
|
+
const parsed = EvidenceRecordSchema.parse(record);
|
|
139
|
+
writeJson(evidencePath(projectRoot, changeName, verifyRunId, hook.id), parsed);
|
|
140
|
+
appendEvent(projectRoot, changeName, 'verify.evidence', { verifyRunId, hookId: hook.id, type: hook.type });
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
export function writeReport(projectRoot, changeName, config, extra) {
|
|
144
|
+
const verifyRunId = requireActiveVerifyRun(projectRoot, changeName);
|
|
145
|
+
const commands = validateCommandRuns(extra?.commands ?? loadCommandRuns(projectRoot, changeName, verifyRunId, config), verifyRunId, config);
|
|
146
|
+
const evidences = loadEvidences(projectRoot, changeName, verifyRunId, config);
|
|
147
|
+
const blocking = [];
|
|
148
|
+
const failOnHook = config.verify?.failurePolicy?.failOnHookFailure ?? true;
|
|
149
|
+
const failOnCmd = config.verify?.failurePolicy?.failOnRequiredCommandFailure ?? true;
|
|
150
|
+
for (const hook of hooksFor(config, 'verify')) {
|
|
151
|
+
// TODO 清单是信息回执,不受严格 failurePolicy 提升为阻塞。
|
|
152
|
+
if (hook.type === 'check' && hook.category === 'todo' && hook.required === false)
|
|
153
|
+
continue;
|
|
154
|
+
if (hook.type === 'command') {
|
|
155
|
+
const descriptor = hookDescriptor(projectRoot, hook, config);
|
|
156
|
+
if (descriptor.type !== 'command')
|
|
157
|
+
throw new AppError('hook_type_mismatch', `需要 command: ${hook.id}`);
|
|
158
|
+
const run = commands.find((item) => item.id === hook.id && item.verifyRunId === verifyRunId);
|
|
159
|
+
if (!run) {
|
|
160
|
+
if (hook.required || failOnCmd)
|
|
161
|
+
blocking.push(`missing:${hook.id}`);
|
|
162
|
+
}
|
|
163
|
+
else if (hook.use && run.skipped && run.ok && hook.allowEmpty && hook.target === 'inScopeChangedFiles'
|
|
164
|
+
&& targetFiles(hook, reviewScopedFiles(config, extra?.changedFiles ?? requireChange(projectRoot, changeName).engineChangedFiles)).length === 0
|
|
165
|
+
&& run.command === descriptor.command) {
|
|
166
|
+
continue; // Explicit empty scope is not an applicable command execution.
|
|
167
|
+
}
|
|
168
|
+
else if (hook.use && (!run.plugin || run.plugin.workerId !== verifyRunId || run.plugin.stepId !== 'verify' || JSON.stringify(run.plugin.binding) !== JSON.stringify(hookDescriptor(projectRoot, hook, config).plugin) || !pluginEvidenceCurrent(projectRoot, run.plugin))) {
|
|
169
|
+
if (hook.required !== false || failOnCmd)
|
|
170
|
+
blocking.push(`stale:${hook.id}`);
|
|
171
|
+
}
|
|
172
|
+
else if (run.context && !commandContextCurrent(projectRoot, run.context)) {
|
|
173
|
+
if (hook.required !== false || failOnCmd)
|
|
174
|
+
blocking.push(`stale:${hook.id}`);
|
|
175
|
+
}
|
|
176
|
+
else if (!run.ok && (hook.required || failOnCmd)) {
|
|
177
|
+
blocking.push(`command:${hook.id}`);
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const evidence = evidences.find((item) => item.hookId === hook.id && item.verifyRunId === verifyRunId);
|
|
182
|
+
if (!evidence) {
|
|
183
|
+
if (hook.required || failOnHook)
|
|
184
|
+
blocking.push(`missing:${hook.id}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const blocks = Boolean(hook.required || failOnHook);
|
|
188
|
+
if (hook.use && (!evidence.plugin || !pluginBindingCurrent(projectRoot, evidence.plugin)
|
|
189
|
+
|| JSON.stringify(evidence.plugin) !== JSON.stringify(hookDescriptor(projectRoot, hook, config).plugin))) {
|
|
190
|
+
if (blocks)
|
|
191
|
+
blocking.push(`stale:${hook.id}`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (evidence.type !== hook.type) {
|
|
195
|
+
if (blocks)
|
|
196
|
+
blocking.push(`invalid:${hook.id}`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (hook.type === 'file') {
|
|
200
|
+
const currentDigest = fileHookDigest(projectRoot, hook);
|
|
201
|
+
if (!currentDigest || evidence.digest !== currentDigest) {
|
|
202
|
+
if (blocks)
|
|
203
|
+
blocking.push(`file:${hook.id}`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (evidence.status !== 'passed' && blocks)
|
|
208
|
+
blocking.push(`hook:${hook.id}`);
|
|
209
|
+
}
|
|
210
|
+
const reportWithoutDigest = QaReportSchema.omit({ digest: true }).parse({
|
|
211
|
+
version: 1,
|
|
212
|
+
verifyRunId,
|
|
213
|
+
status: blocking.length > 0 ? 'failed' : 'passed',
|
|
214
|
+
commands,
|
|
215
|
+
evidences,
|
|
216
|
+
blockingIssues: blocking,
|
|
217
|
+
changedFiles: reviewScopedFiles(config, extra?.changedFiles ?? requireChange(projectRoot, changeName).engineChangedFiles),
|
|
218
|
+
pendingWork: collectPendingWork(projectRoot, changeName, config),
|
|
219
|
+
visualAdvisories: visualAdvisories(projectRoot, changeName),
|
|
220
|
+
});
|
|
221
|
+
const report = QaReportSchema.parse({
|
|
222
|
+
...reportWithoutDigest,
|
|
223
|
+
digest: sha256(JSON.stringify(reportWithoutDigest)),
|
|
224
|
+
});
|
|
225
|
+
writeJson(reportPath(projectRoot, changeName, verifyRunId), report);
|
|
226
|
+
writeJson(artifactPath(projectRoot, changeName, 'reports/qa-report.json'), report);
|
|
227
|
+
appendEvent(projectRoot, changeName, 'verify.report', { verifyRunId, status: report.status, digest: report.digest });
|
|
228
|
+
return { status: report.status, report };
|
|
229
|
+
}
|
|
230
|
+
export function loadVerifiedReport(projectRoot, changeName, verifyRunId) {
|
|
231
|
+
const file = reportPath(projectRoot, changeName, verifyRunId);
|
|
232
|
+
if (!exists(file))
|
|
233
|
+
throw new AppError('qa_report_missing', `缺少当前 verify run 的 QA report: ${verifyRunId}`);
|
|
234
|
+
const parsed = QaReportSchema.safeParse(readJson(file));
|
|
235
|
+
if (!parsed.success)
|
|
236
|
+
throw new AppError('invalid_qa_report', 'QA report 不合法', parsed.error.issues);
|
|
237
|
+
const { digest, ...withoutDigest } = parsed.data;
|
|
238
|
+
if (sha256(JSON.stringify(withoutDigest)) !== digest)
|
|
239
|
+
throw new AppError('qa_report_digest_mismatch', 'QA report digest 不匹配');
|
|
240
|
+
if (parsed.data.verifyRunId !== verifyRunId)
|
|
241
|
+
throw new AppError('qa_report_run_mismatch', 'QA report 不属于当前 verify run');
|
|
242
|
+
for (const run of parsed.data.commands) {
|
|
243
|
+
if (run.context && !commandContextCurrent(projectRoot, run.context))
|
|
244
|
+
throw new AppError('command_evidence_stale', `命令上下文已变化: ${run.id}`);
|
|
245
|
+
if (run.plugin && (run.plugin.workerId !== verifyRunId || !pluginEvidenceCurrent(projectRoot, run.plugin)))
|
|
246
|
+
throw new AppError('command_evidence_stale', `插件验证回执已变化: ${run.id}`);
|
|
247
|
+
}
|
|
248
|
+
for (const evidence of parsed.data.evidences) {
|
|
249
|
+
if (evidence.plugin && !pluginBindingCurrent(projectRoot, evidence.plugin))
|
|
250
|
+
throw new AppError('hook_evidence_stale', `插件技能回执已变化: ${evidence.hookId}`);
|
|
251
|
+
}
|
|
252
|
+
return parsed.data;
|
|
253
|
+
}
|
|
254
|
+
export function renderReportMarkdown(report) {
|
|
255
|
+
const lines = [
|
|
256
|
+
'# QA Report',
|
|
257
|
+
'',
|
|
258
|
+
`Run: ${report.verifyRunId}`,
|
|
259
|
+
`状态: ${report.status}`,
|
|
260
|
+
'',
|
|
261
|
+
'## Commands',
|
|
262
|
+
...report.commands.map((run) => `- ${run.id}: ${run.skipped ? 'skipped' : run.ok ? 'ok' : `fail (${run.code})`}`),
|
|
263
|
+
'',
|
|
264
|
+
'## Evidence',
|
|
265
|
+
...report.evidences.map((item) => `- ${item.hookId} (${item.type}): ${item.status}`),
|
|
266
|
+
];
|
|
267
|
+
if (report.blockingIssues.length > 0) {
|
|
268
|
+
lines.push('', '## Blocking', ...report.blockingIssues.map((item) => `- ${item}`));
|
|
269
|
+
}
|
|
270
|
+
if (report.visualAdvisories?.length) {
|
|
271
|
+
lines.push('', '## 未修复的视觉偏差(已披露,不代表完全还原)', ...report.visualAdvisories.map(item => `- ${item.stepId} / ${item.id}: ${item.description};证据: ${item.evidence}`));
|
|
272
|
+
}
|
|
273
|
+
return `${lines.join('\n')}\n`;
|
|
274
|
+
}
|
|
275
|
+
export function requestFix(projectRoot, changeName, reason) {
|
|
276
|
+
const state = requireChange(projectRoot, changeName);
|
|
277
|
+
if (state.node !== NODES.VERIFY || state.lastIssue?.type !== 'worker_failed' || !state.lastIssue.fixable) {
|
|
278
|
+
throw new AppError('fix_not_allowed', '当前不在可修复的验证失败状态');
|
|
279
|
+
}
|
|
280
|
+
if (!state.verifyRunId)
|
|
281
|
+
throw new AppError('verify_run_missing', '验证失败状态缺少 verify run');
|
|
282
|
+
state.repair = {
|
|
283
|
+
sourceVerifyRunId: state.verifyRunId,
|
|
284
|
+
requestedAt: new Date().toISOString(),
|
|
285
|
+
reason,
|
|
286
|
+
};
|
|
287
|
+
state.node = NODES.CODE;
|
|
288
|
+
state.lastIssue = null;
|
|
289
|
+
state.activeWorker = null;
|
|
290
|
+
saveState(projectRoot, state);
|
|
291
|
+
appendEvent(projectRoot, changeName, 'verify.fix_requested', { reason, verifyRunId: state.verifyRunId });
|
|
292
|
+
return state;
|
|
293
|
+
}
|
|
294
|
+
function targetFiles(hook, changedFiles) {
|
|
295
|
+
if (hook.target !== 'inScopeChangedFiles')
|
|
296
|
+
return [];
|
|
297
|
+
return changedFiles.filter((file) => {
|
|
298
|
+
if (hook.include && hook.include.length > 0 && !hook.include.some((pattern) => matchPattern(file, pattern)))
|
|
299
|
+
return false;
|
|
300
|
+
if (hook.exclude?.some((pattern) => matchPattern(file, pattern)))
|
|
301
|
+
return false;
|
|
302
|
+
return true;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function expandFiles(args, files) {
|
|
306
|
+
const expanded = [];
|
|
307
|
+
for (const arg of args) {
|
|
308
|
+
if (arg === '{files}') {
|
|
309
|
+
expanded.push(...files);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (arg.includes('{files}'))
|
|
313
|
+
throw new AppError('invalid_files_placeholder', '{files} 必须是独立 command 参数');
|
|
314
|
+
expanded.push(arg);
|
|
315
|
+
}
|
|
316
|
+
return expanded;
|
|
317
|
+
}
|
|
318
|
+
function requireActiveVerifyRun(projectRoot, changeName) {
|
|
319
|
+
const state = requireChange(projectRoot, changeName);
|
|
320
|
+
if (state.node !== NODES.VERIFY || state.activeWorker?.stepId !== 'verify' || !state.verifyRunId) {
|
|
321
|
+
throw new AppError('verify_run_inactive', '请先创建当前 verify Worker');
|
|
322
|
+
}
|
|
323
|
+
if (state.activeWorker.workerId !== state.verifyRunId)
|
|
324
|
+
throw new AppError('verify_run_mismatch', 'active Worker 与 verify run 不一致');
|
|
325
|
+
return state.verifyRunId;
|
|
326
|
+
}
|
|
327
|
+
function verifyRoot(projectRoot, changeName, verifyRunId) {
|
|
328
|
+
return artifactPath(projectRoot, changeName, `.opsx/verify/${verifyRunId}`);
|
|
329
|
+
}
|
|
330
|
+
function commandRunPath(projectRoot, changeName, verifyRunId, hookId) {
|
|
331
|
+
return path.join(verifyRoot(projectRoot, changeName, verifyRunId), 'commands', `${hookId}.json`);
|
|
332
|
+
}
|
|
333
|
+
function evidencePath(projectRoot, changeName, verifyRunId, hookId) {
|
|
334
|
+
return path.join(verifyRoot(projectRoot, changeName, verifyRunId), 'evidence', `${hookId}.json`);
|
|
335
|
+
}
|
|
336
|
+
function reportPath(projectRoot, changeName, verifyRunId) {
|
|
337
|
+
return path.join(verifyRoot(projectRoot, changeName, verifyRunId), 'qa-report.json');
|
|
338
|
+
}
|
|
339
|
+
function loadCommandRuns(projectRoot, changeName, verifyRunId, config) {
|
|
340
|
+
const runs = [];
|
|
341
|
+
for (const hook of hooksFor(config, 'verify').filter((item) => item.type === 'command')) {
|
|
342
|
+
const file = commandRunPath(projectRoot, changeName, verifyRunId, hook.id);
|
|
343
|
+
if (!exists(file))
|
|
344
|
+
continue;
|
|
345
|
+
const parsed = CommandRunSchema.safeParse(readJson(file));
|
|
346
|
+
if (!parsed.success)
|
|
347
|
+
throw new AppError('invalid_command_evidence', `command evidence 不合法: ${hook.id}`, parsed.error.issues);
|
|
348
|
+
if (parsed.data.verifyRunId !== verifyRunId || parsed.data.id !== hook.id) {
|
|
349
|
+
throw new AppError('invalid_command_evidence', `command evidence 与当前 run/hook 不匹配: ${hook.id}`);
|
|
350
|
+
}
|
|
351
|
+
runs.push(parsed.data);
|
|
352
|
+
}
|
|
353
|
+
return runs;
|
|
354
|
+
}
|
|
355
|
+
function loadEvidences(projectRoot, changeName, verifyRunId, config) {
|
|
356
|
+
const records = [];
|
|
357
|
+
for (const hook of hooksFor(config, 'verify')) {
|
|
358
|
+
if (hook.type === 'command')
|
|
359
|
+
continue;
|
|
360
|
+
const file = evidencePath(projectRoot, changeName, verifyRunId, hook.id);
|
|
361
|
+
if (!exists(file))
|
|
362
|
+
continue;
|
|
363
|
+
const parsed = EvidenceRecordSchema.safeParse(readJson(file));
|
|
364
|
+
if (!parsed.success)
|
|
365
|
+
throw new AppError('invalid_hook_evidence', `hook evidence 不合法: ${hook.id}`, parsed.error.issues);
|
|
366
|
+
if (parsed.data.verifyRunId !== verifyRunId || parsed.data.hookId !== hook.id || parsed.data.type !== hook.type) {
|
|
367
|
+
throw new AppError('invalid_hook_evidence', `hook evidence 与当前 run/hook 不匹配: ${hook.id}`);
|
|
368
|
+
}
|
|
369
|
+
records.push(parsed.data);
|
|
370
|
+
}
|
|
371
|
+
return records;
|
|
372
|
+
}
|
|
373
|
+
function validateCommandRuns(runs, verifyRunId, config) {
|
|
374
|
+
const known = new Set(hooksFor(config, 'verify').filter((hook) => hook.type === 'command').map((hook) => hook.id));
|
|
375
|
+
const seen = new Set();
|
|
376
|
+
return runs.map((input) => {
|
|
377
|
+
const run = CommandRunSchema.parse(input);
|
|
378
|
+
if (run.verifyRunId !== verifyRunId)
|
|
379
|
+
throw new AppError('command_run_mismatch', `command run 不属于当前 verify: ${run.id}`);
|
|
380
|
+
if (!known.has(run.id))
|
|
381
|
+
throw new AppError('unknown_command_run', `未知 command run: ${run.id}`);
|
|
382
|
+
if (seen.has(run.id))
|
|
383
|
+
throw new AppError('duplicate_command_run', `重复 command run: ${run.id}`);
|
|
384
|
+
seen.add(run.id);
|
|
385
|
+
return run;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
export function changedFilesFromState(state) {
|
|
389
|
+
return [...new Set(state.engineChangedFiles)].sort();
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=verify.js.map
|