@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.
Files changed (186) hide show
  1. package/.agents/plugins/marketplace.json +13 -0
  2. package/.claude-plugin/marketplace.json +12 -0
  3. package/.claude-plugin/plugin.json +9 -0
  4. package/.codex-plugin/plugin.json +9 -0
  5. package/LICENSE +21 -0
  6. package/README.md +111 -0
  7. package/bin/openspec.js +3 -0
  8. package/dist/cli/commands/change.d.ts +3 -0
  9. package/dist/cli/commands/change.js +37 -0
  10. package/dist/cli/commands/ext.d.ts +3 -0
  11. package/dist/cli/commands/ext.js +196 -0
  12. package/dist/cli/commands/figma.d.ts +3 -0
  13. package/dist/cli/commands/figma.js +73 -0
  14. package/dist/cli/commands/init.d.ts +3 -0
  15. package/dist/cli/commands/init.js +71 -0
  16. package/dist/cli/commands/opsx.d.ts +3 -0
  17. package/dist/cli/commands/opsx.js +324 -0
  18. package/dist/cli/commands/usage.d.ts +3 -0
  19. package/dist/cli/commands/usage.js +21 -0
  20. package/dist/cli/output.d.ts +8 -0
  21. package/dist/cli/output.js +17 -0
  22. package/dist/cli/program.d.ts +4 -0
  23. package/dist/cli/program.js +42 -0
  24. package/dist/cli/register.d.ts +3 -0
  25. package/dist/cli/register.js +13 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +3 -0
  28. package/dist/config/load.d.ts +7 -0
  29. package/dist/config/load.js +55 -0
  30. package/dist/config/presets.d.ts +129 -0
  31. package/dist/config/presets.js +125 -0
  32. package/dist/config/schema.d.ts +281 -0
  33. package/dist/config/schema.js +178 -0
  34. package/dist/core/archive.d.ts +5 -0
  35. package/dist/core/archive.js +33 -0
  36. package/dist/core/change.d.ts +12 -0
  37. package/dist/core/change.js +69 -0
  38. package/dist/core/code-changes.d.ts +9 -0
  39. package/dist/core/code-changes.js +31 -0
  40. package/dist/core/flow.d.ts +6 -0
  41. package/dist/core/flow.js +99 -0
  42. package/dist/core/gates.d.ts +5 -0
  43. package/dist/core/gates.js +71 -0
  44. package/dist/core/instructions.d.ts +21 -0
  45. package/dist/core/instructions.js +102 -0
  46. package/dist/core/source-operations.d.ts +25 -0
  47. package/dist/core/source-operations.js +94 -0
  48. package/dist/core/sources.d.ts +33 -0
  49. package/dist/core/sources.js +234 -0
  50. package/dist/core/state.d.ts +15 -0
  51. package/dist/core/state.js +75 -0
  52. package/dist/core/status.d.ts +31 -0
  53. package/dist/core/status.js +22 -0
  54. package/dist/core/tasks.d.ts +15 -0
  55. package/dist/core/tasks.js +143 -0
  56. package/dist/core/todo.d.ts +31 -0
  57. package/dist/core/todo.js +114 -0
  58. package/dist/core/types.d.ts +297 -0
  59. package/dist/core/types.js +227 -0
  60. package/dist/core/verify.d.ts +229 -0
  61. package/dist/core/verify.js +391 -0
  62. package/dist/core/worker-evidence.d.ts +71 -0
  63. package/dist/core/worker-evidence.js +253 -0
  64. package/dist/core/worker.d.ts +22 -0
  65. package/dist/core/worker.js +478 -0
  66. package/dist/delivery/claude-install.d.ts +4 -0
  67. package/dist/delivery/claude-install.js +44 -0
  68. package/dist/delivery/distribution.d.ts +3 -0
  69. package/dist/delivery/distribution.js +53 -0
  70. package/dist/delivery/skills.d.ts +36 -0
  71. package/dist/delivery/skills.js +178 -0
  72. package/dist/figma/cache.d.ts +19 -0
  73. package/dist/figma/cache.js +115 -0
  74. package/dist/figma/claude.d.ts +14 -0
  75. package/dist/figma/claude.js +121 -0
  76. package/dist/figma/collect.d.ts +3 -0
  77. package/dist/figma/collect.js +57 -0
  78. package/dist/figma/index.d.ts +35 -0
  79. package/dist/figma/index.js +178 -0
  80. package/dist/figma/media.d.ts +5 -0
  81. package/dist/figma/media.js +77 -0
  82. package/dist/figma/response.d.ts +9 -0
  83. package/dist/figma/response.js +27 -0
  84. package/dist/hooks/command-context.d.ts +19 -0
  85. package/dist/hooks/command-context.js +33 -0
  86. package/dist/hooks/engine.d.ts +61 -0
  87. package/dist/hooks/engine.js +314 -0
  88. package/dist/hooks/plugin-runtime.d.ts +188 -0
  89. package/dist/hooks/plugin-runtime.js +198 -0
  90. package/dist/index.d.ts +99 -0
  91. package/dist/index.js +185 -0
  92. package/dist/marketplace/adapters.d.ts +202 -0
  93. package/dist/marketplace/adapters.js +203 -0
  94. package/dist/marketplace/builtin.d.ts +9 -0
  95. package/dist/marketplace/builtin.js +9 -0
  96. package/dist/marketplace/install.d.ts +38 -0
  97. package/dist/marketplace/install.js +108 -0
  98. package/dist/marketplace/registry.d.ts +71 -0
  99. package/dist/marketplace/registry.js +342 -0
  100. package/dist/marketplace/upgrade.d.ts +39 -0
  101. package/dist/marketplace/upgrade.js +104 -0
  102. package/dist/shared/command-log.d.ts +9 -0
  103. package/dist/shared/command-log.js +55 -0
  104. package/dist/shared/errors.d.ts +7 -0
  105. package/dist/shared/errors.js +14 -0
  106. package/dist/shared/exec.d.ts +16 -0
  107. package/dist/shared/exec.js +86 -0
  108. package/dist/shared/fs.d.ts +18 -0
  109. package/dist/shared/fs.js +93 -0
  110. package/dist/shared/git.d.ts +22 -0
  111. package/dist/shared/git.js +132 -0
  112. package/dist/shared/hash.d.ts +5 -0
  113. package/dist/shared/hash.js +29 -0
  114. package/dist/shared/paths.d.ts +16 -0
  115. package/dist/shared/paths.js +97 -0
  116. package/dist/shared/pkg.d.ts +3 -0
  117. package/dist/shared/pkg.js +23 -0
  118. package/dist/shared/version.d.ts +9 -0
  119. package/dist/shared/version.js +34 -0
  120. package/dist/usage/analyze.d.ts +5 -0
  121. package/dist/usage/analyze.js +212 -0
  122. package/dist/usage/claude.d.ts +9 -0
  123. package/dist/usage/claude.js +59 -0
  124. package/dist/usage/collect.d.ts +11 -0
  125. package/dist/usage/collect.js +71 -0
  126. package/dist/usage/doctor.d.ts +72 -0
  127. package/dist/usage/doctor.js +121 -0
  128. package/dist/usage/parse.d.ts +28 -0
  129. package/dist/usage/parse.js +345 -0
  130. package/dist/usage/types.d.ts +103 -0
  131. package/dist/usage/types.js +2 -0
  132. package/docs/getting-started.md +20 -0
  133. package/docs/guides/configuration.md +33 -0
  134. package/docs/guides/cost-benchmark.md +88 -0
  135. package/docs/guides/plugin-migration.md +65 -0
  136. package/docs/guides/smart-figma.md +51 -0
  137. package/docs/guides/source-reading.md +35 -0
  138. package/docs/guides/ui-conventions-template.md +39 -0
  139. package/docs/guides/worker-evidence.md +73 -0
  140. package/docs/guides/workflow.md +74 -0
  141. package/docs/maintainers/architecture.md +27 -0
  142. package/docs/maintainers/plugin-runtime-contract.md +55 -0
  143. package/package.json +78 -0
  144. package/plugin.json +9 -0
  145. package/presets/backend-service.yaml +19 -0
  146. package/presets/flutter-mobile/plugin-hooks.json +68 -0
  147. package/presets/flutter-mobile/ui-conventions.md +122 -0
  148. package/presets/flutter-mobile.yaml +205 -0
  149. package/presets/web-product.yaml +23 -0
  150. package/skills/opsx-apply/SKILL.md +35 -0
  151. package/skills/opsx-apply/nodes/blocked.md +7 -0
  152. package/skills/opsx-apply/nodes/code-task.md +29 -0
  153. package/skills/opsx-apply/nodes/gate-a.md +9 -0
  154. package/skills/opsx-apply/nodes/stop.md +5 -0
  155. package/skills/opsx-apply/nodes/task-build.md +50 -0
  156. package/skills/opsx-apply/nodes/task-repair.md +20 -0
  157. package/skills/opsx-apply/nodes/task-scout.md +28 -0
  158. package/skills/opsx-apply/nodes/task-verify.md +38 -0
  159. package/skills/opsx-archive/SKILL.md +24 -0
  160. package/skills/opsx-archive/nodes/archive.md +15 -0
  161. package/skills/opsx-explore/SKILL.md +20 -0
  162. package/skills/opsx-explore/nodes/explore.md +3 -0
  163. package/skills/opsx-fix-verify/SKILL.md +30 -0
  164. package/skills/opsx-fix-verify/nodes/repair.md +11 -0
  165. package/skills/opsx-fix-verify/nodes/reverify.md +3 -0
  166. package/skills/opsx-fix-verify/nodes/stop.md +3 -0
  167. package/skills/opsx-gate-decision/SKILL.md +20 -0
  168. package/skills/opsx-gate-decision/nodes/decide.md +15 -0
  169. package/skills/opsx-propose/SKILL.md +36 -0
  170. package/skills/opsx-propose/nodes/blocked.md +13 -0
  171. package/skills/opsx-propose/nodes/collect.md +35 -0
  172. package/skills/opsx-propose/nodes/parse.md +72 -0
  173. package/skills/opsx-propose/nodes/plan.md +65 -0
  174. package/skills/opsx-propose/nodes/stop.md +17 -0
  175. package/skills/opsx-propose/nodes/tech.md +77 -0
  176. package/skills/opsx-propose/scripts/read-json-source.mjs +22 -0
  177. package/skills/opsx-resume/SKILL.md +21 -0
  178. package/skills/opsx-resume/nodes/route.md +12 -0
  179. package/skills/opsx-source-update/SKILL.md +23 -0
  180. package/skills/opsx-source-update/nodes/update.md +14 -0
  181. package/skills/opsx-verify/SKILL.md +32 -0
  182. package/skills/opsx-verify/nodes/blocked.md +3 -0
  183. package/skills/opsx-verify/nodes/failed.md +3 -0
  184. package/skills/opsx-verify/nodes/gate-b.md +7 -0
  185. package/skills/opsx-verify/nodes/stop.md +5 -0
  186. 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