@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,125 @@
1
+ import path from 'node:path';
2
+ import { parse as parseYaml } from 'yaml';
3
+ import { z } from 'zod';
4
+ import { exists, listFilesRecursive, readText, writeText } from '../shared/fs.js';
5
+ import { AppError } from '../shared/errors.js';
6
+ import { packageRoot } from '../shared/pkg.js';
7
+ import { resolveInside } from '../shared/paths.js';
8
+ import { PluginMinVersionSchema, ProjectConfigPatchSchema, ProjectConfigSchema } from './schema.js';
9
+ import { compareVersions } from '../shared/version.js';
10
+ const ScaffoldEntrySchema = z.object({
11
+ /** 项目内目标路径,通常与某个 required file hook 的 path 一致 */
12
+ path: z.string().min(1),
13
+ /** 相对 presets/ 目录的模板文件 */
14
+ template: z.string().min(1),
15
+ description: z.string().optional(),
16
+ }).strict();
17
+ export const TeamPresetSchema = z.object({
18
+ schemaVersion: z.literal('openspec.team-preset.v1'),
19
+ id: z.string(),
20
+ version: z.string(),
21
+ title: z.string(),
22
+ description: z.string().optional(),
23
+ marketplaces: z.array(z.object({
24
+ name: z.string(),
25
+ repo: z.string(),
26
+ ref: z.string(),
27
+ }).strict()).optional(),
28
+ plugins: z.array(z.object({
29
+ name: z.string(),
30
+ marketplace: z.string(),
31
+ minVersion: PluginMinVersionSchema.optional(),
32
+ }).strict()).optional(),
33
+ configPatch: ProjectConfigPatchSchema.optional(),
34
+ /** 应用 preset 时按需生成的项目文件:只创建、不覆盖 */
35
+ scaffold: z.array(ScaffoldEntrySchema).optional(),
36
+ notes: z.array(z.string()).optional(),
37
+ warnings: z.array(z.string()).optional(),
38
+ }).strict();
39
+ export function presetsDir() {
40
+ return path.join(packageRoot(), 'presets');
41
+ }
42
+ export function listPresets() {
43
+ return listFilesRecursive(presetsDir())
44
+ .filter((file) => file.endsWith('.yaml') || file.endsWith('.yml'))
45
+ .map((file) => loadPresetFile(file));
46
+ }
47
+ export function loadPreset(id) {
48
+ const file = path.join(presetsDir(), `${id}.yaml`);
49
+ if (!exists(file))
50
+ throw new AppError('preset_not_found', `找不到 preset: ${id}`);
51
+ return loadPresetFile(file);
52
+ }
53
+ export function mergePreset(base, preset) {
54
+ const patch = preset.configPatch ?? { schema: 'spec-product' };
55
+ const merged = {
56
+ schema: 'spec-product',
57
+ taskCategories: { ...base.taskCategories, ...patch.taskCategories },
58
+ hooks: upsertBy(base.hooks ?? [], patch.hooks ?? [], (item) => item.id),
59
+ verify: {
60
+ reviewScope: {
61
+ ...base.verify?.reviewScope,
62
+ ...patch.verify?.reviewScope,
63
+ },
64
+ failurePolicy: {
65
+ ...base.verify?.failurePolicy,
66
+ ...patch.verify?.failurePolicy,
67
+ },
68
+ },
69
+ plugins: mergePluginDependencies(base.plugins ?? [], preset.plugins ?? []),
70
+ // 宿主已声明的 marketplace 保留自己的 repo/ref:preset 的 ref 只是基线,不能把独立升级过的宿主降回去。
71
+ marketplaces: [
72
+ ...(base.marketplaces ?? []),
73
+ ...(preset.marketplaces ?? []).filter((item) => !(base.marketplaces ?? []).some((existing) => existing.name === item.name)),
74
+ ],
75
+ tools: base.tools,
76
+ };
77
+ return ProjectConfigSchema.parse(merged);
78
+ }
79
+ /**
80
+ * 把 preset 声明的模板文件放到项目里。已存在的文件一律不动——
81
+ * 团队填写后的内容不能被重复 init 覆盖;模板本身带 openspec:fill-me 标记,未填写时 file hook 不会放行。
82
+ */
83
+ export function scaffoldPreset(projectRoot, preset) {
84
+ return (preset.scaffold ?? []).map((entry) => {
85
+ const source = resolveInside(presetsDir(), entry.template, 'preset_template_outside_presets');
86
+ if (!exists(source))
87
+ throw new AppError('preset_template_missing', `preset ${preset.id} 缺少模板文件: ${entry.template}`);
88
+ const target = resolveInside(projectRoot, entry.path, 'scaffold_path_outside_project');
89
+ if (exists(target))
90
+ return { path: entry.path, status: 'exists', description: entry.description };
91
+ writeText(target, readText(source));
92
+ return { path: entry.path, status: 'created', description: entry.description };
93
+ });
94
+ }
95
+ function upsertBy(base, patch, key) {
96
+ const values = new Map(base.map((item) => [key(item), item]));
97
+ for (const item of patch)
98
+ values.set(key(item), item);
99
+ return [...values.values()];
100
+ }
101
+ /** 合并时保留了宿主 ref、且与 preset 基线不同的 marketplace,供 init / preset apply 提示。 */
102
+ export function keptMarketplaceRefs(base, preset) {
103
+ return (preset.marketplaces ?? []).flatMap((item) => {
104
+ const existing = (base.marketplaces ?? []).find((candidate) => candidate.name === item.name);
105
+ return existing && existing.ref !== item.ref ? [{ name: item.name, ref: existing.ref, presetRef: item.ref }] : [];
106
+ });
107
+ }
108
+ /** 同一插件取较高的 minVersion:宿主提高过的最低版本不被 preset 默认值调低。 */
109
+ function mergePluginDependencies(base, patch) {
110
+ const previous = new Map(base.map((item) => [`${item.name}@${item.marketplace}`, item]));
111
+ return upsertBy(base, patch.map((item) => {
112
+ const existing = previous.get(`${item.name}@${item.marketplace}`)?.minVersion;
113
+ if (!existing || !item.minVersion)
114
+ return existing && !item.minVersion ? { ...item, minVersion: existing } : item;
115
+ return (compareVersions(existing, item.minVersion) ?? 0) > 0 ? { ...item, minVersion: existing } : item;
116
+ }), (item) => `${item.name}@${item.marketplace}`);
117
+ }
118
+ function loadPresetFile(file) {
119
+ const result = TeamPresetSchema.safeParse(parseYaml(readText(file)));
120
+ if (!result.success) {
121
+ throw new AppError('invalid_preset', `preset 不合法: ${file}`, result.error.issues);
122
+ }
123
+ return result.data;
124
+ }
125
+ //# sourceMappingURL=presets.js.map
@@ -0,0 +1,281 @@
1
+ import { z } from 'zod';
2
+ export declare const HOOK_STAGES: readonly ["collect", "parse", "tech", "plan", "execute", "verify"];
3
+ export declare const HOOK_TYPES: readonly ["command", "skill", "notes", "file", "check"];
4
+ export declare const SOURCE_CAPABILITIES: readonly ["requirements", "interfaces", "design"];
5
+ export declare const DEFAULT_TASK_CATEGORIES: Record<string, {
6
+ notes: string;
7
+ }>;
8
+ export declare const HookSchema: z.ZodObject<{
9
+ id: z.ZodString;
10
+ stage: z.ZodEnum<{
11
+ collect: "collect";
12
+ parse: "parse";
13
+ tech: "tech";
14
+ plan: "plan";
15
+ execute: "execute";
16
+ verify: "verify";
17
+ }>;
18
+ type: z.ZodOptional<z.ZodEnum<{
19
+ command: "command";
20
+ skill: "skill";
21
+ notes: "notes";
22
+ file: "file";
23
+ check: "check";
24
+ }>>;
25
+ capability: z.ZodOptional<z.ZodEnum<{
26
+ requirements: "requirements";
27
+ interfaces: "interfaces";
28
+ design: "design";
29
+ }>>;
30
+ use: z.ZodOptional<z.ZodString>;
31
+ operation: z.ZodOptional<z.ZodLiteral<"preview">>;
32
+ input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
33
+ inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
34
+ name: z.ZodOptional<z.ZodString>;
35
+ notes: z.ZodOptional<z.ZodString>;
36
+ taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
37
+ command: z.ZodOptional<z.ZodString>;
38
+ target: z.ZodOptional<z.ZodEnum<{
39
+ project: "project";
40
+ inScopeChangedFiles: "inScopeChangedFiles";
41
+ }>>;
42
+ include: z.ZodOptional<z.ZodArray<z.ZodString>>;
43
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
44
+ allowEmpty: z.ZodOptional<z.ZodBoolean>;
45
+ required: z.ZodOptional<z.ZodBoolean>;
46
+ purpose: z.ZodOptional<z.ZodString>;
47
+ path: z.ZodOptional<z.ZodString>;
48
+ category: z.ZodOptional<z.ZodString>;
49
+ description: z.ZodOptional<z.ZodString>;
50
+ }, z.core.$strict>;
51
+ /** 插件最低版本:与插件 manifest 的 version 比较,只接受 major.minor.patch。 */
52
+ export declare const PluginMinVersionSchema: z.ZodString;
53
+ declare const ProjectConfigObjectSchema: z.ZodObject<{
54
+ schema: z.ZodLiteral<"spec-product">;
55
+ taskCategories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
56
+ notes: z.ZodOptional<z.ZodString>;
57
+ }, z.core.$strict>>>>;
58
+ hooks: z.ZodOptional<z.ZodArray<z.ZodObject<{
59
+ id: z.ZodString;
60
+ stage: z.ZodEnum<{
61
+ collect: "collect";
62
+ parse: "parse";
63
+ tech: "tech";
64
+ plan: "plan";
65
+ execute: "execute";
66
+ verify: "verify";
67
+ }>;
68
+ type: z.ZodOptional<z.ZodEnum<{
69
+ command: "command";
70
+ skill: "skill";
71
+ notes: "notes";
72
+ file: "file";
73
+ check: "check";
74
+ }>>;
75
+ capability: z.ZodOptional<z.ZodEnum<{
76
+ requirements: "requirements";
77
+ interfaces: "interfaces";
78
+ design: "design";
79
+ }>>;
80
+ use: z.ZodOptional<z.ZodString>;
81
+ operation: z.ZodOptional<z.ZodLiteral<"preview">>;
82
+ input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
83
+ inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
84
+ name: z.ZodOptional<z.ZodString>;
85
+ notes: z.ZodOptional<z.ZodString>;
86
+ taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
87
+ command: z.ZodOptional<z.ZodString>;
88
+ target: z.ZodOptional<z.ZodEnum<{
89
+ project: "project";
90
+ inScopeChangedFiles: "inScopeChangedFiles";
91
+ }>>;
92
+ include: z.ZodOptional<z.ZodArray<z.ZodString>>;
93
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
94
+ allowEmpty: z.ZodOptional<z.ZodBoolean>;
95
+ required: z.ZodOptional<z.ZodBoolean>;
96
+ purpose: z.ZodOptional<z.ZodString>;
97
+ path: z.ZodOptional<z.ZodString>;
98
+ category: z.ZodOptional<z.ZodString>;
99
+ description: z.ZodOptional<z.ZodString>;
100
+ }, z.core.$strict>>>;
101
+ verify: z.ZodOptional<z.ZodObject<{
102
+ reviewScope: z.ZodOptional<z.ZodObject<{
103
+ includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
104
+ excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
105
+ }, z.core.$strict>>;
106
+ failurePolicy: z.ZodOptional<z.ZodObject<{
107
+ failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
108
+ failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
109
+ }, z.core.$strict>>;
110
+ }, z.core.$strict>>;
111
+ plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
112
+ name: z.ZodString;
113
+ marketplace: z.ZodString;
114
+ minVersion: z.ZodOptional<z.ZodString>;
115
+ }, z.core.$strict>>>;
116
+ marketplaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
117
+ name: z.ZodString;
118
+ repo: z.ZodString;
119
+ ref: z.ZodString;
120
+ }, z.core.$strict>>>;
121
+ tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
122
+ claude: "claude";
123
+ codex: "codex";
124
+ cursor: "cursor";
125
+ }>>>;
126
+ }, z.core.$strict>;
127
+ export declare const ProjectConfigPatchSchema: z.ZodObject<{
128
+ schema: z.ZodOptional<z.ZodLiteral<"spec-product">>;
129
+ taskCategories: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
130
+ notes: z.ZodOptional<z.ZodString>;
131
+ }, z.core.$strict>>>>>;
132
+ hooks: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
133
+ id: z.ZodString;
134
+ stage: z.ZodEnum<{
135
+ collect: "collect";
136
+ parse: "parse";
137
+ tech: "tech";
138
+ plan: "plan";
139
+ execute: "execute";
140
+ verify: "verify";
141
+ }>;
142
+ type: z.ZodOptional<z.ZodEnum<{
143
+ command: "command";
144
+ skill: "skill";
145
+ notes: "notes";
146
+ file: "file";
147
+ check: "check";
148
+ }>>;
149
+ capability: z.ZodOptional<z.ZodEnum<{
150
+ requirements: "requirements";
151
+ interfaces: "interfaces";
152
+ design: "design";
153
+ }>>;
154
+ use: z.ZodOptional<z.ZodString>;
155
+ operation: z.ZodOptional<z.ZodLiteral<"preview">>;
156
+ input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
157
+ inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
158
+ name: z.ZodOptional<z.ZodString>;
159
+ notes: z.ZodOptional<z.ZodString>;
160
+ taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
161
+ command: z.ZodOptional<z.ZodString>;
162
+ target: z.ZodOptional<z.ZodEnum<{
163
+ project: "project";
164
+ inScopeChangedFiles: "inScopeChangedFiles";
165
+ }>>;
166
+ include: z.ZodOptional<z.ZodArray<z.ZodString>>;
167
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
168
+ allowEmpty: z.ZodOptional<z.ZodBoolean>;
169
+ required: z.ZodOptional<z.ZodBoolean>;
170
+ purpose: z.ZodOptional<z.ZodString>;
171
+ path: z.ZodOptional<z.ZodString>;
172
+ category: z.ZodOptional<z.ZodString>;
173
+ description: z.ZodOptional<z.ZodString>;
174
+ }, z.core.$strict>>>>;
175
+ verify: z.ZodOptional<z.ZodOptional<z.ZodObject<{
176
+ reviewScope: z.ZodOptional<z.ZodObject<{
177
+ includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
178
+ excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
179
+ }, z.core.$strict>>;
180
+ failurePolicy: z.ZodOptional<z.ZodObject<{
181
+ failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
182
+ failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
183
+ }, z.core.$strict>>;
184
+ }, z.core.$strict>>>;
185
+ plugins: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
186
+ name: z.ZodString;
187
+ marketplace: z.ZodString;
188
+ minVersion: z.ZodOptional<z.ZodString>;
189
+ }, z.core.$strict>>>>;
190
+ marketplaces: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
191
+ name: z.ZodString;
192
+ repo: z.ZodString;
193
+ ref: z.ZodString;
194
+ }, z.core.$strict>>>>;
195
+ tools: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodEnum<{
196
+ claude: "claude";
197
+ codex: "codex";
198
+ cursor: "cursor";
199
+ }>>>>;
200
+ }, z.core.$strict>;
201
+ export declare const ProjectConfigSchema: z.ZodObject<{
202
+ schema: z.ZodLiteral<"spec-product">;
203
+ taskCategories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
204
+ notes: z.ZodOptional<z.ZodString>;
205
+ }, z.core.$strict>>>>;
206
+ hooks: z.ZodOptional<z.ZodArray<z.ZodObject<{
207
+ id: z.ZodString;
208
+ stage: z.ZodEnum<{
209
+ collect: "collect";
210
+ parse: "parse";
211
+ tech: "tech";
212
+ plan: "plan";
213
+ execute: "execute";
214
+ verify: "verify";
215
+ }>;
216
+ type: z.ZodOptional<z.ZodEnum<{
217
+ command: "command";
218
+ skill: "skill";
219
+ notes: "notes";
220
+ file: "file";
221
+ check: "check";
222
+ }>>;
223
+ capability: z.ZodOptional<z.ZodEnum<{
224
+ requirements: "requirements";
225
+ interfaces: "interfaces";
226
+ design: "design";
227
+ }>>;
228
+ use: z.ZodOptional<z.ZodString>;
229
+ operation: z.ZodOptional<z.ZodLiteral<"preview">>;
230
+ input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
231
+ inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
232
+ name: z.ZodOptional<z.ZodString>;
233
+ notes: z.ZodOptional<z.ZodString>;
234
+ taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
235
+ command: z.ZodOptional<z.ZodString>;
236
+ target: z.ZodOptional<z.ZodEnum<{
237
+ project: "project";
238
+ inScopeChangedFiles: "inScopeChangedFiles";
239
+ }>>;
240
+ include: z.ZodOptional<z.ZodArray<z.ZodString>>;
241
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
242
+ allowEmpty: z.ZodOptional<z.ZodBoolean>;
243
+ required: z.ZodOptional<z.ZodBoolean>;
244
+ purpose: z.ZodOptional<z.ZodString>;
245
+ path: z.ZodOptional<z.ZodString>;
246
+ category: z.ZodOptional<z.ZodString>;
247
+ description: z.ZodOptional<z.ZodString>;
248
+ }, z.core.$strict>>>;
249
+ verify: z.ZodOptional<z.ZodObject<{
250
+ reviewScope: z.ZodOptional<z.ZodObject<{
251
+ includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
252
+ excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
253
+ }, z.core.$strict>>;
254
+ failurePolicy: z.ZodOptional<z.ZodObject<{
255
+ failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
256
+ failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
257
+ }, z.core.$strict>>;
258
+ }, z.core.$strict>>;
259
+ plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
260
+ name: z.ZodString;
261
+ marketplace: z.ZodString;
262
+ minVersion: z.ZodOptional<z.ZodString>;
263
+ }, z.core.$strict>>>;
264
+ marketplaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
265
+ name: z.ZodString;
266
+ repo: z.ZodString;
267
+ ref: z.ZodString;
268
+ }, z.core.$strict>>>;
269
+ tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
270
+ claude: "claude";
271
+ codex: "codex";
272
+ cursor: "cursor";
273
+ }>>>;
274
+ }, z.core.$strict>;
275
+ export type ProjectConfig = z.infer<typeof ProjectConfigObjectSchema>;
276
+ export type ProjectHook = z.infer<typeof HookSchema>;
277
+ export declare function effectiveTaskCategories(config: Pick<ProjectConfig, 'taskCategories'>): Record<string, {
278
+ notes: string;
279
+ }>;
280
+ export {};
281
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,178 @@
1
+ import { z } from 'zod';
2
+ export const HOOK_STAGES = ['collect', 'parse', 'tech', 'plan', 'execute', 'verify'];
3
+ export const HOOK_TYPES = ['command', 'skill', 'notes', 'file', 'check'];
4
+ export const SOURCE_CAPABILITIES = ['requirements', 'interfaces', 'design'];
5
+ export const DEFAULT_TASK_CATEGORIES = {
6
+ ui: { notes: '遵循项目 UI 约定,优先复用现有组件与设计 token。' },
7
+ logic: { notes: '保持业务逻辑内聚,避免跨边界改动。' },
8
+ 'api-codegen': { notes: '仅通过已声明的接口源生成客户端或模型代码。' },
9
+ test: { notes: '覆盖本次变更的可观察行为与回归路径。' },
10
+ infra: { notes: '基础设施改动需可回滚,并写明影响范围。' },
11
+ };
12
+ const PathTokenSchema = z.string().min(1).refine((value) => value !== '.' && value !== '..' && !/[\\/\0]/.test(value), '标识不能包含路径语义');
13
+ const CategoryIdSchema = PathTokenSchema.refine((value) => !['__proto__', 'prototype', 'constructor'].includes(value), '分类 ID 不能使用保留对象键');
14
+ const RelativePathSchema = z.string().min(1).refine((value) => {
15
+ const normalized = value.replace(/\\/g, '/');
16
+ return !normalized.startsWith('/')
17
+ && !/^[A-Za-z]:\//.test(normalized)
18
+ && !normalized.includes('\0')
19
+ && !normalized.split('/').includes('..');
20
+ }, '必须是未越界的相对路径');
21
+ export const HookSchema = z.object({
22
+ id: PathTokenSchema,
23
+ stage: z.enum(HOOK_STAGES),
24
+ type: z.enum(HOOK_TYPES).optional(),
25
+ capability: z.enum(SOURCE_CAPABILITIES).optional(),
26
+ use: z.string().min(1).optional(),
27
+ operation: z.literal('preview').optional(),
28
+ input: z.record(z.string(), z.unknown()).optional(),
29
+ inputFiles: z.array(RelativePathSchema).optional(),
30
+ name: z.string().min(1).optional(),
31
+ notes: z.string().min(1).optional(),
32
+ taskCategories: z.array(CategoryIdSchema).optional(),
33
+ command: z.string().min(1).optional(),
34
+ target: z.enum(['project', 'inScopeChangedFiles']).optional(),
35
+ include: z.array(z.string()).optional(),
36
+ exclude: z.array(z.string()).optional(),
37
+ allowEmpty: z.boolean().optional(),
38
+ required: z.boolean().optional(),
39
+ purpose: z.string().optional(),
40
+ path: RelativePathSchema.optional(),
41
+ category: z.string().min(1).optional(),
42
+ description: z.string().min(1).optional(),
43
+ }).strict().superRefine((hook, ctx) => {
44
+ const type = hook.type;
45
+ const allowedByStage = {
46
+ collect: [undefined, 'skill'],
47
+ parse: [...HOOK_TYPES],
48
+ tech: [...HOOK_TYPES],
49
+ plan: [...HOOK_TYPES],
50
+ execute: [...HOOK_TYPES],
51
+ verify: ['command', 'skill', 'file', 'check'],
52
+ };
53
+ if (!allowedByStage[hook.stage].includes(type)) {
54
+ ctx.addIssue({ code: 'custom', message: `${hook.stage} stage 不支持 type: ${String(type)}` });
55
+ }
56
+ if (hook.stage === 'collect' && !hook.use && type !== 'skill') {
57
+ ctx.addIssue({ code: 'custom', message: `collect hook ${hook.id} 需要 use 或 type: skill` });
58
+ }
59
+ if (hook.stage !== 'collect' && !type) {
60
+ ctx.addIssue({ code: 'custom', message: `${hook.stage} hook ${hook.id} 需要 type` });
61
+ }
62
+ if (type === 'notes' && !hook.notes)
63
+ ctx.addIssue({ code: 'custom', message: `notes hook ${hook.id} 需要 notes` });
64
+ if (type === 'command' && !hook.command && !hook.use)
65
+ ctx.addIssue({ code: 'custom', message: `command hook ${hook.id} 需要 command` });
66
+ if (type === 'skill' && !hook.name && !hook.use)
67
+ ctx.addIssue({ code: 'custom', message: `skill hook ${hook.id} 需要 name` });
68
+ if (type === 'file' && !hook.path)
69
+ ctx.addIssue({ code: 'custom', message: `file hook ${hook.id} 需要 path` });
70
+ if (type === 'check' && (!hook.category || !hook.description)) {
71
+ ctx.addIssue({ code: 'custom', message: `check hook ${hook.id} 需要 category 与 description` });
72
+ }
73
+ if (hook.use && [hook.name, hook.command, hook.path, hook.notes, hook.category, hook.description].some((value) => value !== undefined)) {
74
+ ctx.addIssue({ code: 'custom', message: `hook ${hook.id} 的 use 与内联实现互斥` });
75
+ }
76
+ if (hook.use && hook.stage !== 'collect' && type !== 'command' && type !== 'skill') {
77
+ ctx.addIssue({ code: 'custom', message: `插件 Hook 仅支持 command/skill: ${hook.id}` });
78
+ }
79
+ if (hook.operation && (!hook.use || hook.type !== 'command' || hook.stage === 'collect')) {
80
+ ctx.addIssue({ code: 'custom', message: `preview 需要非 collect 的 command use: ${hook.id}` });
81
+ }
82
+ if (!hook.use && (hook.input || hook.inputFiles)) {
83
+ ctx.addIssue({ code: 'custom', message: `结构化参数需要插件 use: ${hook.id}` });
84
+ }
85
+ if (hook.stage === 'collect' && (hook.input || hook.inputFiles)) {
86
+ ctx.addIssue({ code: 'custom', message: `collect 沿用旧来源协议,不接受结构化操作参数: ${hook.id}` });
87
+ }
88
+ if (hook.taskCategories && hook.stage !== 'execute') {
89
+ ctx.addIssue({ code: 'custom', message: `taskCategories 只允许用于 execute hook: ${hook.id}` });
90
+ }
91
+ });
92
+ const TaskCategorySchema = z.object({ notes: z.string().optional() }).strict();
93
+ const ImmutableMarketplaceRefSchema = z.string().min(1).refine((value) => /^[0-9a-f]{40}$/i.test(value) || /^refs\/tags\/[^/]+$/.test(value), 'marketplace ref 必须是完整 Git SHA 或 refs/tags/<tag>');
94
+ const MarketplaceSchema = z.object({
95
+ name: PathTokenSchema,
96
+ repo: z.string().min(1),
97
+ ref: ImmutableMarketplaceRefSchema,
98
+ }).strict();
99
+ /** 插件最低版本:与插件 manifest 的 version 比较,只接受 major.minor.patch。 */
100
+ export const PluginMinVersionSchema = z.string().regex(/^\d+\.\d+\.\d+$/, 'minVersion 必须是 major.minor.patch');
101
+ const PluginDependencySchema = z.object({
102
+ name: PathTokenSchema,
103
+ marketplace: PathTokenSchema,
104
+ minVersion: PluginMinVersionSchema.optional(),
105
+ }).strict();
106
+ const VerifySchema = z.object({
107
+ reviewScope: z.object({
108
+ includeDirs: z.array(z.string()).optional(),
109
+ excludeDirs: z.array(z.string()).optional(),
110
+ }).strict().optional(),
111
+ failurePolicy: z.object({
112
+ failOnHookFailure: z.boolean().optional(),
113
+ failOnRequiredCommandFailure: z.boolean().optional(),
114
+ }).strict().optional(),
115
+ }).strict();
116
+ const ProjectConfigObjectSchema = z.object({
117
+ schema: z.literal('spec-product'),
118
+ taskCategories: z.record(CategoryIdSchema, TaskCategorySchema.nullable()).optional(),
119
+ hooks: z.array(HookSchema).optional(),
120
+ verify: VerifySchema.optional(),
121
+ plugins: z.array(PluginDependencySchema).optional(),
122
+ marketplaces: z.array(MarketplaceSchema).optional(),
123
+ tools: z.array(z.enum(['claude', 'codex', 'cursor'])).optional(),
124
+ }).strict();
125
+ export const ProjectConfigPatchSchema = ProjectConfigObjectSchema.partial();
126
+ export const ProjectConfigSchema = ProjectConfigObjectSchema.superRefine((config, ctx) => {
127
+ const ids = new Set();
128
+ const collectCapabilities = new Set();
129
+ for (const [index, hook] of (config.hooks ?? []).entries()) {
130
+ if (ids.has(hook.id))
131
+ ctx.addIssue({ code: 'custom', path: ['hooks', index, 'id'], message: `重复 hook id: ${hook.id}` });
132
+ ids.add(hook.id);
133
+ if (hook.stage === 'collect' && hook.capability) {
134
+ if (collectCapabilities.has(hook.capability)) {
135
+ ctx.addIssue({ code: 'custom', path: ['hooks', index, 'capability'], message: `重复 collect capability: ${hook.capability}` });
136
+ }
137
+ collectCapabilities.add(hook.capability);
138
+ }
139
+ }
140
+ const marketplaceNames = new Set();
141
+ for (const [index, marketplace] of (config.marketplaces ?? []).entries()) {
142
+ if (marketplaceNames.has(marketplace.name)) {
143
+ ctx.addIssue({ code: 'custom', path: ['marketplaces', index, 'name'], message: `重复 marketplace: ${marketplace.name}` });
144
+ }
145
+ marketplaceNames.add(marketplace.name);
146
+ }
147
+ const pluginSelectors = new Set();
148
+ for (const [index, plugin] of (config.plugins ?? []).entries()) {
149
+ const selector = `${plugin.name}@${plugin.marketplace}`;
150
+ if (pluginSelectors.has(selector)) {
151
+ ctx.addIssue({ code: 'custom', path: ['plugins', index], message: `重复 plugin: ${selector}` });
152
+ }
153
+ pluginSelectors.add(selector);
154
+ }
155
+ if (new Set(config.tools ?? []).size !== (config.tools ?? []).length) {
156
+ ctx.addIssue({ code: 'custom', path: ['tools'], message: 'tools 不能重复' });
157
+ }
158
+ const categories = new Set(Object.keys(effectiveTaskCategories(config)));
159
+ for (const [index, hook] of (config.hooks ?? []).entries()) {
160
+ for (const category of hook.taskCategories ?? []) {
161
+ if (!categories.has(category)) {
162
+ ctx.addIssue({ code: 'custom', path: ['hooks', index, 'taskCategories'], message: `未知 task category: ${category}` });
163
+ }
164
+ }
165
+ }
166
+ });
167
+ export function effectiveTaskCategories(config) {
168
+ const merged = Object.assign(Object.create(null), DEFAULT_TASK_CATEGORIES);
169
+ for (const [id, value] of Object.entries(config.taskCategories ?? {})) {
170
+ if (value === null) {
171
+ delete merged[id];
172
+ continue;
173
+ }
174
+ merged[id] = { notes: value.notes ?? merged[id]?.notes ?? '' };
175
+ }
176
+ return merged;
177
+ }
178
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,5 @@
1
+ export declare function archiveChange(projectRoot: string, changeName: string): {
2
+ from: string;
3
+ to: string;
4
+ };
5
+ //# sourceMappingURL=archive.d.ts.map
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+ import { exists, ensureDir, removePath, writeText } from '../shared/fs.js';
3
+ import { AppError } from '../shared/errors.js';
4
+ import { changeRoot, stampArchivePath } from './change.js';
5
+ import { appendEvent } from './state.js';
6
+ import { NODES } from './types.js';
7
+ import { requireChange } from './change.js';
8
+ import { renameSync } from 'node:fs';
9
+ import { loadVerifiedReport } from './verify.js';
10
+ import { collectPendingWork, renderPendingWork } from './todo.js';
11
+ import { loadConfig } from '../config/load.js';
12
+ import { resolveInside } from '../shared/paths.js';
13
+ export function archiveChange(projectRoot, changeName) {
14
+ const state = requireChange(projectRoot, changeName);
15
+ if (state.node !== NODES.COMPLETED) {
16
+ throw new AppError('not_completed', '只有 completed 的 change 才能归档');
17
+ }
18
+ const from = changeRoot(projectRoot, changeName);
19
+ const toRel = stampArchivePath(projectRoot, changeName);
20
+ const to = resolveInside(projectRoot, toRel, 'archive_path_outside_project');
21
+ if (!exists(from))
22
+ throw new AppError('change_not_found', `找不到 change: ${changeName}`);
23
+ const report = state.verifyRunId ? loadVerifiedReport(projectRoot, changeName, state.verifyRunId) : undefined;
24
+ const pending = report?.pendingWork ?? collectPendingWork(projectRoot, changeName, loadConfig(projectRoot));
25
+ writeText(resolveInside(from, 'reports/pending-work.md'), renderPendingWork(pending));
26
+ ensureDir(path.dirname(to));
27
+ appendEvent(projectRoot, changeName, 'change.archived', { to: toRel });
28
+ removePath(path.join(from, '.opsx', 'workers'));
29
+ removePath(path.join(from, '.opsx', 'source-update-backup'));
30
+ renameSync(from, to);
31
+ return { from: path.relative(projectRoot, from), to: toRel };
32
+ }
33
+ //# sourceMappingURL=archive.js.map
@@ -0,0 +1,12 @@
1
+ import type { ChangeState } from './types.js';
2
+ export declare function changeRoot(projectRoot: string, changeName: string): string;
3
+ export declare function createChange(projectRoot: string, name: string, description?: string): ChangeState;
4
+ export declare function listChangeNames(projectRoot: string): string[];
5
+ export declare function requireChange(projectRoot: string, changeName: string): ChangeState;
6
+ export declare function resolveChangeName(projectRoot: string, requested?: string): string;
7
+ export declare function artifactPath(projectRoot: string, changeName: string, relative: string): string;
8
+ export declare function artifactRel(changeName: string, relative: string): string;
9
+ export declare function stampArchivePath(projectRoot: string, changeName: string): string;
10
+ export declare function assertChangeName(name: string): void;
11
+ export declare function readOptional(projectRoot: string, changeName: string, relative: string): string | null;
12
+ //# sourceMappingURL=change.d.ts.map