@zhuan-ai/zhuanspec 1.3.0

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 (210) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +461 -0
  3. package/README.zh.md +434 -0
  4. package/bin/zhuanspec.js +3 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +356 -0
  7. package/dist/commands/artifact-workflow.d.ts +13 -0
  8. package/dist/commands/artifact-workflow.js +916 -0
  9. package/dist/commands/change.d.ts +35 -0
  10. package/dist/commands/change.js +277 -0
  11. package/dist/commands/completion.d.ts +72 -0
  12. package/dist/commands/completion.js +221 -0
  13. package/dist/commands/config.d.ts +8 -0
  14. package/dist/commands/config.js +198 -0
  15. package/dist/commands/show.d.ts +14 -0
  16. package/dist/commands/show.js +132 -0
  17. package/dist/commands/spec.d.ts +15 -0
  18. package/dist/commands/spec.js +225 -0
  19. package/dist/commands/validate.d.ts +24 -0
  20. package/dist/commands/validate.js +294 -0
  21. package/dist/core/archive.d.ts +30 -0
  22. package/dist/core/archive.js +438 -0
  23. package/dist/core/artifact-graph/graph.d.ts +56 -0
  24. package/dist/core/artifact-graph/graph.js +141 -0
  25. package/dist/core/artifact-graph/index.d.ts +7 -0
  26. package/dist/core/artifact-graph/index.js +13 -0
  27. package/dist/core/artifact-graph/instruction-loader.d.ts +134 -0
  28. package/dist/core/artifact-graph/instruction-loader.js +180 -0
  29. package/dist/core/artifact-graph/resolver.d.ts +61 -0
  30. package/dist/core/artifact-graph/resolver.js +187 -0
  31. package/dist/core/artifact-graph/schema.d.ts +13 -0
  32. package/dist/core/artifact-graph/schema.js +108 -0
  33. package/dist/core/artifact-graph/state.d.ts +12 -0
  34. package/dist/core/artifact-graph/state.js +54 -0
  35. package/dist/core/artifact-graph/types.d.ts +45 -0
  36. package/dist/core/artifact-graph/types.js +43 -0
  37. package/dist/core/completions/command-registry.d.ts +7 -0
  38. package/dist/core/completions/command-registry.js +362 -0
  39. package/dist/core/completions/completion-provider.d.ts +60 -0
  40. package/dist/core/completions/completion-provider.js +102 -0
  41. package/dist/core/completions/factory.d.ts +51 -0
  42. package/dist/core/completions/factory.js +57 -0
  43. package/dist/core/completions/generators/zsh-generator.d.ts +58 -0
  44. package/dist/core/completions/generators/zsh-generator.js +319 -0
  45. package/dist/core/completions/installers/zsh-installer.d.ts +136 -0
  46. package/dist/core/completions/installers/zsh-installer.js +449 -0
  47. package/dist/core/completions/types.d.ts +78 -0
  48. package/dist/core/completions/types.js +2 -0
  49. package/dist/core/config-schema.d.ts +76 -0
  50. package/dist/core/config-schema.js +200 -0
  51. package/dist/core/config.d.ts +16 -0
  52. package/dist/core/config.js +29 -0
  53. package/dist/core/configurators/agents.d.ts +8 -0
  54. package/dist/core/configurators/agents.js +15 -0
  55. package/dist/core/configurators/base.d.ts +7 -0
  56. package/dist/core/configurators/base.js +2 -0
  57. package/dist/core/configurators/claude.d.ts +8 -0
  58. package/dist/core/configurators/claude.js +15 -0
  59. package/dist/core/configurators/cline.d.ts +8 -0
  60. package/dist/core/configurators/cline.js +15 -0
  61. package/dist/core/configurators/codebuddy.d.ts +8 -0
  62. package/dist/core/configurators/codebuddy.js +15 -0
  63. package/dist/core/configurators/costrict.d.ts +8 -0
  64. package/dist/core/configurators/costrict.js +15 -0
  65. package/dist/core/configurators/iflow.d.ts +8 -0
  66. package/dist/core/configurators/iflow.js +15 -0
  67. package/dist/core/configurators/qoder.d.ts +30 -0
  68. package/dist/core/configurators/qoder.js +42 -0
  69. package/dist/core/configurators/qwen.d.ts +24 -0
  70. package/dist/core/configurators/qwen.js +37 -0
  71. package/dist/core/configurators/registry.d.ts +9 -0
  72. package/dist/core/configurators/registry.js +43 -0
  73. package/dist/core/configurators/slash/amazon-q.d.ts +9 -0
  74. package/dist/core/configurators/slash/amazon-q.js +46 -0
  75. package/dist/core/configurators/slash/antigravity.d.ts +9 -0
  76. package/dist/core/configurators/slash/antigravity.js +23 -0
  77. package/dist/core/configurators/slash/auggie.d.ts +9 -0
  78. package/dist/core/configurators/slash/auggie.js +31 -0
  79. package/dist/core/configurators/slash/base.d.ts +19 -0
  80. package/dist/core/configurators/slash/base.js +69 -0
  81. package/dist/core/configurators/slash/claude.d.ts +9 -0
  82. package/dist/core/configurators/slash/claude.js +37 -0
  83. package/dist/core/configurators/slash/cline.d.ts +9 -0
  84. package/dist/core/configurators/slash/cline.js +23 -0
  85. package/dist/core/configurators/slash/codebuddy.d.ts +9 -0
  86. package/dist/core/configurators/slash/codebuddy.js +37 -0
  87. package/dist/core/configurators/slash/codex.d.ts +14 -0
  88. package/dist/core/configurators/slash/codex.js +109 -0
  89. package/dist/core/configurators/slash/costrict.d.ts +9 -0
  90. package/dist/core/configurators/slash/costrict.js +31 -0
  91. package/dist/core/configurators/slash/crush.d.ts +9 -0
  92. package/dist/core/configurators/slash/crush.js +37 -0
  93. package/dist/core/configurators/slash/cursor.d.ts +9 -0
  94. package/dist/core/configurators/slash/cursor.js +37 -0
  95. package/dist/core/configurators/slash/factory.d.ts +10 -0
  96. package/dist/core/configurators/slash/factory.js +35 -0
  97. package/dist/core/configurators/slash/gemini.d.ts +9 -0
  98. package/dist/core/configurators/slash/gemini.js +22 -0
  99. package/dist/core/configurators/slash/github-copilot.d.ts +9 -0
  100. package/dist/core/configurators/slash/github-copilot.js +34 -0
  101. package/dist/core/configurators/slash/iflow.d.ts +9 -0
  102. package/dist/core/configurators/slash/iflow.js +37 -0
  103. package/dist/core/configurators/slash/kilocode.d.ts +9 -0
  104. package/dist/core/configurators/slash/kilocode.js +17 -0
  105. package/dist/core/configurators/slash/opencode.d.ts +12 -0
  106. package/dist/core/configurators/slash/opencode.js +72 -0
  107. package/dist/core/configurators/slash/qoder.d.ts +35 -0
  108. package/dist/core/configurators/slash/qoder.js +76 -0
  109. package/dist/core/configurators/slash/qwen.d.ts +32 -0
  110. package/dist/core/configurators/slash/qwen.js +49 -0
  111. package/dist/core/configurators/slash/registry.d.ts +8 -0
  112. package/dist/core/configurators/slash/registry.js +75 -0
  113. package/dist/core/configurators/slash/roocode.d.ts +9 -0
  114. package/dist/core/configurators/slash/roocode.js +23 -0
  115. package/dist/core/configurators/slash/toml-base.d.ts +10 -0
  116. package/dist/core/configurators/slash/toml-base.js +53 -0
  117. package/dist/core/configurators/slash/windsurf.d.ts +9 -0
  118. package/dist/core/configurators/slash/windsurf.js +23 -0
  119. package/dist/core/converters/json-converter.d.ts +6 -0
  120. package/dist/core/converters/json-converter.js +51 -0
  121. package/dist/core/global-config.d.ts +39 -0
  122. package/dist/core/global-config.js +115 -0
  123. package/dist/core/index.d.ts +2 -0
  124. package/dist/core/index.js +3 -0
  125. package/dist/core/init.d.ts +60 -0
  126. package/dist/core/init.js +861 -0
  127. package/dist/core/list.d.ts +9 -0
  128. package/dist/core/list.js +171 -0
  129. package/dist/core/parsers/change-parser.d.ts +13 -0
  130. package/dist/core/parsers/change-parser.js +193 -0
  131. package/dist/core/parsers/markdown-parser.d.ts +22 -0
  132. package/dist/core/parsers/markdown-parser.js +187 -0
  133. package/dist/core/parsers/requirement-blocks.d.ts +37 -0
  134. package/dist/core/parsers/requirement-blocks.js +201 -0
  135. package/dist/core/project-config.d.ts +34 -0
  136. package/dist/core/project-config.js +79 -0
  137. package/dist/core/schemas/base.schema.d.ts +13 -0
  138. package/dist/core/schemas/base.schema.js +13 -0
  139. package/dist/core/schemas/change.schema.d.ts +73 -0
  140. package/dist/core/schemas/change.schema.js +31 -0
  141. package/dist/core/schemas/index.d.ts +4 -0
  142. package/dist/core/schemas/index.js +4 -0
  143. package/dist/core/schemas/spec.schema.d.ts +18 -0
  144. package/dist/core/schemas/spec.schema.js +15 -0
  145. package/dist/core/skill-discovery.d.ts +24 -0
  146. package/dist/core/skill-discovery.js +153 -0
  147. package/dist/core/specs-apply.d.ts +73 -0
  148. package/dist/core/specs-apply.js +384 -0
  149. package/dist/core/styles/palette.d.ts +7 -0
  150. package/dist/core/styles/palette.js +8 -0
  151. package/dist/core/templates/agents-root-stub.d.ts +2 -0
  152. package/dist/core/templates/agents-root-stub.js +17 -0
  153. package/dist/core/templates/agents-template.d.ts +2 -0
  154. package/dist/core/templates/agents-template.js +706 -0
  155. package/dist/core/templates/claude-template.d.ts +2 -0
  156. package/dist/core/templates/claude-template.js +2 -0
  157. package/dist/core/templates/cline-template.d.ts +2 -0
  158. package/dist/core/templates/cline-template.js +2 -0
  159. package/dist/core/templates/costrict-template.d.ts +2 -0
  160. package/dist/core/templates/costrict-template.js +2 -0
  161. package/dist/core/templates/index.d.ts +17 -0
  162. package/dist/core/templates/index.js +37 -0
  163. package/dist/core/templates/project-template.d.ts +8 -0
  164. package/dist/core/templates/project-template.js +32 -0
  165. package/dist/core/templates/skill-templates.d.ts +103 -0
  166. package/dist/core/templates/skill-templates.js +2131 -0
  167. package/dist/core/templates/slash-command-templates.d.ts +4 -0
  168. package/dist/core/templates/slash-command-templates.js +81 -0
  169. package/dist/core/update.d.ts +4 -0
  170. package/dist/core/update.js +88 -0
  171. package/dist/core/validation/constants.d.ts +34 -0
  172. package/dist/core/validation/constants.js +40 -0
  173. package/dist/core/validation/types.d.ts +18 -0
  174. package/dist/core/validation/types.js +2 -0
  175. package/dist/core/validation/validator.d.ts +33 -0
  176. package/dist/core/validation/validator.js +409 -0
  177. package/dist/core/view.d.ts +8 -0
  178. package/dist/core/view.js +168 -0
  179. package/dist/index.d.ts +3 -0
  180. package/dist/index.js +3 -0
  181. package/dist/utils/change-metadata.d.ts +47 -0
  182. package/dist/utils/change-metadata.js +130 -0
  183. package/dist/utils/change-utils.d.ts +51 -0
  184. package/dist/utils/change-utils.js +100 -0
  185. package/dist/utils/file-system.d.ts +19 -0
  186. package/dist/utils/file-system.js +177 -0
  187. package/dist/utils/index.d.ts +4 -0
  188. package/dist/utils/index.js +5 -0
  189. package/dist/utils/interactive.d.ts +18 -0
  190. package/dist/utils/interactive.js +21 -0
  191. package/dist/utils/item-discovery.d.ts +4 -0
  192. package/dist/utils/item-discovery.js +72 -0
  193. package/dist/utils/match.d.ts +3 -0
  194. package/dist/utils/match.js +22 -0
  195. package/dist/utils/shell-detection.d.ts +20 -0
  196. package/dist/utils/shell-detection.js +41 -0
  197. package/dist/utils/task-progress.d.ts +8 -0
  198. package/dist/utils/task-progress.js +36 -0
  199. package/package.json +81 -0
  200. package/schemas/spec-driven/schema.yaml +205 -0
  201. package/schemas/spec-driven/templates/design.md +19 -0
  202. package/schemas/spec-driven/templates/proposal.md +43 -0
  203. package/schemas/spec-driven/templates/spec.md +8 -0
  204. package/schemas/spec-driven/templates/tasks.md +25 -0
  205. package/schemas/tdd/schema.yaml +213 -0
  206. package/schemas/tdd/templates/docs.md +0 -0
  207. package/schemas/tdd/templates/implementation.md +11 -0
  208. package/schemas/tdd/templates/spec.md +11 -0
  209. package/schemas/tdd/templates/test.md +11 -0
  210. package/scripts/postinstall.js +147 -0
@@ -0,0 +1,409 @@
1
+ import { readFileSync, promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { SpecSchema, ChangeSchema } from '../schemas/index.js';
4
+ import { MarkdownParser } from '../parsers/markdown-parser.js';
5
+ import { ChangeParser } from '../parsers/change-parser.js';
6
+ import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
7
+ import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js';
8
+ import { FileSystemUtils } from '../../utils/file-system.js';
9
+ export class Validator {
10
+ strictMode;
11
+ constructor(strictMode = false) {
12
+ this.strictMode = strictMode;
13
+ }
14
+ async validateSpec(filePath) {
15
+ const issues = [];
16
+ const specName = this.extractNameFromPath(filePath);
17
+ try {
18
+ const content = readFileSync(filePath, 'utf-8');
19
+ const parser = new MarkdownParser(content);
20
+ const spec = parser.parseSpec(specName);
21
+ const result = SpecSchema.safeParse(spec);
22
+ if (!result.success) {
23
+ issues.push(...this.convertZodErrors(result.error));
24
+ }
25
+ issues.push(...this.applySpecRules(spec, content));
26
+ }
27
+ catch (error) {
28
+ const baseMessage = error instanceof Error ? error.message : 'Unknown error';
29
+ const enriched = this.enrichTopLevelError(specName, baseMessage);
30
+ issues.push({
31
+ level: 'ERROR',
32
+ path: 'file',
33
+ message: enriched,
34
+ });
35
+ }
36
+ return this.createReport(issues);
37
+ }
38
+ /**
39
+ * Validate spec content from a string (used for pre-write validation of rebuilt specs)
40
+ */
41
+ async validateSpecContent(specName, content) {
42
+ const issues = [];
43
+ try {
44
+ const parser = new MarkdownParser(content);
45
+ const spec = parser.parseSpec(specName);
46
+ const result = SpecSchema.safeParse(spec);
47
+ if (!result.success) {
48
+ issues.push(...this.convertZodErrors(result.error));
49
+ }
50
+ issues.push(...this.applySpecRules(spec, content));
51
+ }
52
+ catch (error) {
53
+ const baseMessage = error instanceof Error ? error.message : 'Unknown error';
54
+ const enriched = this.enrichTopLevelError(specName, baseMessage);
55
+ issues.push({ level: 'ERROR', path: 'file', message: enriched });
56
+ }
57
+ return this.createReport(issues);
58
+ }
59
+ async validateChange(filePath) {
60
+ const issues = [];
61
+ const changeName = this.extractNameFromPath(filePath);
62
+ try {
63
+ const content = readFileSync(filePath, 'utf-8');
64
+ const changeDir = path.dirname(filePath);
65
+ const parser = new ChangeParser(content, changeDir);
66
+ const change = await parser.parseChangeWithDeltas(changeName);
67
+ const result = ChangeSchema.safeParse(change);
68
+ if (!result.success) {
69
+ issues.push(...this.convertZodErrors(result.error));
70
+ }
71
+ issues.push(...this.applyChangeRules(change, content));
72
+ }
73
+ catch (error) {
74
+ const baseMessage = error instanceof Error ? error.message : 'Unknown error';
75
+ const enriched = this.enrichTopLevelError(changeName, baseMessage);
76
+ issues.push({
77
+ level: 'ERROR',
78
+ path: 'file',
79
+ message: enriched,
80
+ });
81
+ }
82
+ return this.createReport(issues);
83
+ }
84
+ /**
85
+ * Validate delta-formatted spec files under a change directory.
86
+ * Enforces:
87
+ * - At least one delta across all files
88
+ * - ADDED/MODIFIED: each requirement has SHALL/MUST and at least one scenario
89
+ * - REMOVED: names only; no scenario/description required
90
+ * - RENAMED: pairs well-formed
91
+ * - No duplicates within sections; no cross-section conflicts per spec
92
+ */
93
+ async validateChangeDeltaSpecs(changeDir) {
94
+ const issues = [];
95
+ const specsDir = path.join(changeDir, 'specs');
96
+ let totalDeltas = 0;
97
+ const missingHeaderSpecs = [];
98
+ const emptySectionSpecs = [];
99
+ try {
100
+ const entries = await fs.readdir(specsDir, { withFileTypes: true });
101
+ for (const entry of entries) {
102
+ if (!entry.isDirectory())
103
+ continue;
104
+ const specName = entry.name;
105
+ const specFile = path.join(specsDir, specName, 'spec.md');
106
+ let content;
107
+ try {
108
+ content = await fs.readFile(specFile, 'utf-8');
109
+ }
110
+ catch {
111
+ continue;
112
+ }
113
+ const plan = parseDeltaSpec(content);
114
+ const entryPath = `${specName}/spec.md`;
115
+ const sectionNames = [];
116
+ if (plan.sectionPresence.added)
117
+ sectionNames.push('## ADDED Requirements');
118
+ if (plan.sectionPresence.modified)
119
+ sectionNames.push('## MODIFIED Requirements');
120
+ if (plan.sectionPresence.removed)
121
+ sectionNames.push('## REMOVED Requirements');
122
+ if (plan.sectionPresence.renamed)
123
+ sectionNames.push('## RENAMED Requirements');
124
+ const hasSections = sectionNames.length > 0;
125
+ const hasEntries = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0;
126
+ if (!hasEntries) {
127
+ if (hasSections)
128
+ emptySectionSpecs.push({ path: entryPath, sections: sectionNames });
129
+ else
130
+ missingHeaderSpecs.push(entryPath);
131
+ }
132
+ const addedNames = new Set();
133
+ const modifiedNames = new Set();
134
+ const removedNames = new Set();
135
+ const renamedFrom = new Set();
136
+ const renamedTo = new Set();
137
+ // Validate ADDED
138
+ for (const block of plan.added) {
139
+ const key = normalizeRequirementName(block.name);
140
+ totalDeltas++;
141
+ if (addedNames.has(key)) {
142
+ issues.push({ level: 'ERROR', path: entryPath, message: `ADDED 中存在重复的要求: "${block.name}"` });
143
+ }
144
+ else {
145
+ addedNames.add(key);
146
+ }
147
+ const requirementText = this.extractRequirementText(block.raw);
148
+ if (!requirementText) {
149
+ issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" 缺少要求文本` });
150
+ }
151
+ else if (!this.containsShallOrMust(requirementText)) {
152
+ issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" 必须包含 SHALL 或 MUST` });
153
+ }
154
+ const scenarioCount = this.countScenarios(block.raw);
155
+ if (scenarioCount < 1) {
156
+ issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" 必须包含至少一个场景` });
157
+ }
158
+ }
159
+ // Validate MODIFIED
160
+ for (const block of plan.modified) {
161
+ const key = normalizeRequirementName(block.name);
162
+ totalDeltas++;
163
+ if (modifiedNames.has(key)) {
164
+ issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED 中存在重复的要求: "${block.name}"` });
165
+ }
166
+ else {
167
+ modifiedNames.add(key);
168
+ }
169
+ const requirementText = this.extractRequirementText(block.raw);
170
+ if (!requirementText) {
171
+ issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" 缺少要求文本` });
172
+ }
173
+ else if (!this.containsShallOrMust(requirementText)) {
174
+ issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" 必须包含 SHALL 或 MUST` });
175
+ }
176
+ const scenarioCount = this.countScenarios(block.raw);
177
+ if (scenarioCount < 1) {
178
+ issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" 必须包含至少一个场景` });
179
+ }
180
+ }
181
+ // Validate REMOVED (names only)
182
+ for (const name of plan.removed) {
183
+ const key = normalizeRequirementName(name);
184
+ totalDeltas++;
185
+ if (removedNames.has(key)) {
186
+ issues.push({ level: 'ERROR', path: entryPath, message: `REMOVED 中存在重复的要求: "${name}"` });
187
+ }
188
+ else {
189
+ removedNames.add(key);
190
+ }
191
+ }
192
+ // Validate RENAMED pairs
193
+ for (const { from, to } of plan.renamed) {
194
+ const fromKey = normalizeRequirementName(from);
195
+ const toKey = normalizeRequirementName(to);
196
+ totalDeltas++;
197
+ if (renamedFrom.has(fromKey)) {
198
+ issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED 中存在重复的 FROM: "${from}"` });
199
+ }
200
+ else {
201
+ renamedFrom.add(fromKey);
202
+ }
203
+ if (renamedTo.has(toKey)) {
204
+ issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED 中存在重复的 TO: "${to}"` });
205
+ }
206
+ else {
207
+ renamedTo.add(toKey);
208
+ }
209
+ }
210
+ // Cross-section conflicts (within the same spec file)
211
+ for (const n of modifiedNames) {
212
+ if (removedNames.has(n)) {
213
+ issues.push({ level: 'ERROR', path: entryPath, message: `要求同时出现在 MODIFIED 和 REMOVED 中: "${n}"` });
214
+ }
215
+ if (addedNames.has(n)) {
216
+ issues.push({ level: 'ERROR', path: entryPath, message: `要求同时出现在 MODIFIED 和 ADDED 中: "${n}"` });
217
+ }
218
+ }
219
+ for (const n of addedNames) {
220
+ if (removedNames.has(n)) {
221
+ issues.push({ level: 'ERROR', path: entryPath, message: `要求同时出现在 ADDED 和 REMOVED 中: "${n}"` });
222
+ }
223
+ }
224
+ for (const { from, to } of plan.renamed) {
225
+ const fromKey = normalizeRequirementName(from);
226
+ const toKey = normalizeRequirementName(to);
227
+ if (modifiedNames.has(fromKey)) {
228
+ issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED 引用了 RENAMED 中的旧名称。请使用新标题 "${to}"` });
229
+ }
230
+ if (addedNames.has(toKey)) {
231
+ issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED TO 与 ADDED 中的 "${to}" 冲突` });
232
+ }
233
+ }
234
+ }
235
+ }
236
+ catch {
237
+ // If no specs dir, treat as no deltas
238
+ }
239
+ for (const { path: specPath, sections } of emptySectionSpecs) {
240
+ issues.push({
241
+ level: 'ERROR',
242
+ path: specPath,
243
+ message: `Delta sections ${this.formatSectionList(sections)} were found, but no requirement entries parsed. Ensure each section includes at least one "### Requirement:" block (REMOVED may use bullet list syntax).`,
244
+ });
245
+ }
246
+ for (const path of missingHeaderSpecs) {
247
+ issues.push({
248
+ level: 'ERROR',
249
+ path,
250
+ message: 'No delta sections found. Add headers such as "## ADDED Requirements" or move non-delta notes outside specs/.',
251
+ });
252
+ }
253
+ if (totalDeltas === 0) {
254
+ issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
255
+ }
256
+ return this.createReport(issues);
257
+ }
258
+ convertZodErrors(error) {
259
+ return error.issues.map(err => {
260
+ let message = err.message;
261
+ if (message === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
262
+ message = `${message}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
263
+ }
264
+ return {
265
+ level: 'ERROR',
266
+ path: err.path.join('.'),
267
+ message,
268
+ };
269
+ });
270
+ }
271
+ applySpecRules(spec, content) {
272
+ const issues = [];
273
+ if (spec.overview.length < MIN_PURPOSE_LENGTH) {
274
+ issues.push({
275
+ level: 'WARNING',
276
+ path: 'overview',
277
+ message: VALIDATION_MESSAGES.PURPOSE_TOO_BRIEF,
278
+ });
279
+ }
280
+ spec.requirements.forEach((req, index) => {
281
+ if (req.text.length > MAX_REQUIREMENT_TEXT_LENGTH) {
282
+ issues.push({
283
+ level: 'INFO',
284
+ path: `requirements[${index}]`,
285
+ message: VALIDATION_MESSAGES.REQUIREMENT_TOO_LONG,
286
+ });
287
+ }
288
+ if (req.scenarios.length === 0) {
289
+ issues.push({
290
+ level: 'WARNING',
291
+ path: `requirements[${index}].scenarios`,
292
+ message: `${VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS}. ${VALIDATION_MESSAGES.GUIDE_SCENARIO_FORMAT}`,
293
+ });
294
+ }
295
+ });
296
+ return issues;
297
+ }
298
+ applyChangeRules(change, content) {
299
+ const issues = [];
300
+ const MIN_DELTA_DESCRIPTION_LENGTH = 10;
301
+ change.deltas.forEach((delta, index) => {
302
+ if (!delta.description || delta.description.length < MIN_DELTA_DESCRIPTION_LENGTH) {
303
+ issues.push({
304
+ level: 'WARNING',
305
+ path: `deltas[${index}].description`,
306
+ message: VALIDATION_MESSAGES.DELTA_DESCRIPTION_TOO_BRIEF,
307
+ });
308
+ }
309
+ if ((delta.operation === 'ADDED' || delta.operation === 'MODIFIED') &&
310
+ (!delta.requirements || delta.requirements.length === 0)) {
311
+ issues.push({
312
+ level: 'WARNING',
313
+ path: `deltas[${index}].requirements`,
314
+ message: `${delta.operation} ${VALIDATION_MESSAGES.DELTA_MISSING_REQUIREMENTS}`,
315
+ });
316
+ }
317
+ });
318
+ return issues;
319
+ }
320
+ enrichTopLevelError(itemId, baseMessage) {
321
+ const msg = baseMessage.trim();
322
+ if (msg === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
323
+ return `${msg}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
324
+ }
325
+ if (msg.includes('Spec must have a Purpose section') || msg.includes('Spec must have a Requirements section')) {
326
+ return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_SPEC_SECTIONS}`;
327
+ }
328
+ if (msg.includes('Change must have a Why section') || msg.includes('Change must have a What Changes section')) {
329
+ return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_CHANGE_SECTIONS}`;
330
+ }
331
+ return msg;
332
+ }
333
+ extractNameFromPath(filePath) {
334
+ const normalizedPath = FileSystemUtils.toPosixPath(filePath);
335
+ const parts = normalizedPath.split('/');
336
+ // Look for the directory name after 'specs' or 'changes'
337
+ for (let i = parts.length - 1; i >= 0; i--) {
338
+ if (parts[i] === 'specs' || parts[i] === 'changes') {
339
+ if (i < parts.length - 1) {
340
+ return parts[i + 1];
341
+ }
342
+ }
343
+ }
344
+ // Fallback to filename without extension if not in expected structure
345
+ const fileName = parts[parts.length - 1] ?? '';
346
+ const dotIndex = fileName.lastIndexOf('.');
347
+ return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
348
+ }
349
+ createReport(issues) {
350
+ const errors = issues.filter(i => i.level === 'ERROR').length;
351
+ const warnings = issues.filter(i => i.level === 'WARNING').length;
352
+ const info = issues.filter(i => i.level === 'INFO').length;
353
+ const valid = this.strictMode
354
+ ? errors === 0 && warnings === 0
355
+ : errors === 0;
356
+ return {
357
+ valid,
358
+ issues,
359
+ summary: {
360
+ errors,
361
+ warnings,
362
+ info,
363
+ },
364
+ };
365
+ }
366
+ isValid(report) {
367
+ return report.valid;
368
+ }
369
+ extractRequirementText(blockRaw) {
370
+ const lines = blockRaw.split('\n');
371
+ // Skip header line (index 0)
372
+ let i = 1;
373
+ // Find the first substantial text line, skipping metadata and blank lines
374
+ for (; i < lines.length; i++) {
375
+ const line = lines[i];
376
+ // Stop at scenario headers
377
+ if (/^####\s+/.test(line))
378
+ break;
379
+ const trimmed = line.trim();
380
+ // Skip blank lines
381
+ if (trimmed.length === 0)
382
+ continue;
383
+ // Skip metadata lines (lines starting with ** like **ID**, **Priority**, etc.)
384
+ if (/^\*\*[^*]+\*\*:/.test(trimmed))
385
+ continue;
386
+ // Found first non-metadata, non-blank line - this is the requirement text
387
+ return trimmed;
388
+ }
389
+ // No requirement text found
390
+ return undefined;
391
+ }
392
+ containsShallOrMust(text) {
393
+ return /\b(SHALL|MUST)\b/.test(text);
394
+ }
395
+ countScenarios(blockRaw) {
396
+ const matches = blockRaw.match(/^####\s+/gm);
397
+ return matches ? matches.length : 0;
398
+ }
399
+ formatSectionList(sections) {
400
+ if (sections.length === 0)
401
+ return '';
402
+ if (sections.length === 1)
403
+ return sections[0];
404
+ const head = sections.slice(0, -1);
405
+ const last = sections[sections.length - 1];
406
+ return `${head.join(', ')} and ${last}`;
407
+ }
408
+ }
409
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1,8 @@
1
+ export declare class ViewCommand {
2
+ execute(targetPath?: string): Promise<void>;
3
+ private getChangesData;
4
+ private getSpecsData;
5
+ private displaySummary;
6
+ private createProgressBar;
7
+ }
8
+ //# sourceMappingURL=view.d.ts.map
@@ -0,0 +1,168 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import chalk from 'chalk';
4
+ import { getTaskProgressForChange } from '../utils/task-progress.js';
5
+ import { MarkdownParser } from './parsers/markdown-parser.js';
6
+ export class ViewCommand {
7
+ async execute(targetPath = '.') {
8
+ const zhuanspecDir = path.join(targetPath, 'zhuanspec');
9
+ if (!fs.existsSync(zhuanspecDir)) {
10
+ console.error(chalk.red('No zhuanspec directory found'));
11
+ process.exit(1);
12
+ }
13
+ console.log(chalk.bold('\nZhuanSpec Dashboard\n'));
14
+ console.log('═'.repeat(60));
15
+ // Get changes and specs data
16
+ const changesData = await this.getChangesData(zhuanspecDir);
17
+ const specsData = await this.getSpecsData(zhuanspecDir);
18
+ // Display summary metrics
19
+ this.displaySummary(changesData, specsData);
20
+ // Display draft changes
21
+ if (changesData.draft.length > 0) {
22
+ console.log(chalk.bold.gray('\nDraft Changes'));
23
+ console.log('─'.repeat(60));
24
+ changesData.draft.forEach((change) => {
25
+ console.log(` ${chalk.gray('○')} ${change.name}`);
26
+ });
27
+ }
28
+ // Display active changes
29
+ if (changesData.active.length > 0) {
30
+ console.log(chalk.bold.cyan('\nActive Changes'));
31
+ console.log('─'.repeat(60));
32
+ changesData.active.forEach((change) => {
33
+ const progressBar = this.createProgressBar(change.progress.completed, change.progress.total);
34
+ const percentage = change.progress.total > 0
35
+ ? Math.round((change.progress.completed / change.progress.total) * 100)
36
+ : 0;
37
+ console.log(` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}`);
38
+ });
39
+ }
40
+ // Display completed changes
41
+ if (changesData.completed.length > 0) {
42
+ console.log(chalk.bold.green('\nCompleted Changes'));
43
+ console.log('─'.repeat(60));
44
+ changesData.completed.forEach((change) => {
45
+ console.log(` ${chalk.green('✓')} ${change.name}`);
46
+ });
47
+ }
48
+ // Display specifications
49
+ if (specsData.length > 0) {
50
+ console.log(chalk.bold.blue('\nSpecifications'));
51
+ console.log('─'.repeat(60));
52
+ // Sort specs by requirement count (descending)
53
+ specsData.sort((a, b) => b.requirementCount - a.requirementCount);
54
+ specsData.forEach(spec => {
55
+ const reqLabel = spec.requirementCount === 1 ? 'requirement' : 'requirements';
56
+ console.log(` ${chalk.blue('▪')} ${chalk.bold(spec.name.padEnd(30))} ${chalk.dim(`${spec.requirementCount} ${reqLabel}`)}`);
57
+ });
58
+ }
59
+ console.log('\n' + '═'.repeat(60));
60
+ console.log(chalk.dim(`\nUse ${chalk.white('zhuanspec list --changes')} or ${chalk.white('zhuanspec list --specs')} for detailed views`));
61
+ }
62
+ async getChangesData(zhuanspecDir) {
63
+ const changesDir = path.join(zhuanspecDir, 'changes');
64
+ if (!fs.existsSync(changesDir)) {
65
+ return { draft: [], active: [], completed: [] };
66
+ }
67
+ const draft = [];
68
+ const active = [];
69
+ const completed = [];
70
+ const entries = fs.readdirSync(changesDir, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ if (entry.isDirectory() && entry.name !== 'archive') {
73
+ const progress = await getTaskProgressForChange(changesDir, entry.name);
74
+ if (progress.total === 0) {
75
+ // No tasks defined yet - still in planning/draft phase
76
+ draft.push({ name: entry.name });
77
+ }
78
+ else if (progress.completed === progress.total) {
79
+ // All tasks complete
80
+ completed.push({ name: entry.name });
81
+ }
82
+ else {
83
+ // Has tasks but not all complete
84
+ active.push({ name: entry.name, progress });
85
+ }
86
+ }
87
+ }
88
+ // Sort all categories by name for deterministic ordering
89
+ draft.sort((a, b) => a.name.localeCompare(b.name));
90
+ // Sort active changes by completion percentage (ascending) and then by name
91
+ active.sort((a, b) => {
92
+ const percentageA = a.progress.total > 0 ? a.progress.completed / a.progress.total : 0;
93
+ const percentageB = b.progress.total > 0 ? b.progress.completed / b.progress.total : 0;
94
+ if (percentageA < percentageB)
95
+ return -1;
96
+ if (percentageA > percentageB)
97
+ return 1;
98
+ return a.name.localeCompare(b.name);
99
+ });
100
+ completed.sort((a, b) => a.name.localeCompare(b.name));
101
+ return { draft, active, completed };
102
+ }
103
+ async getSpecsData(zhuanspecDir) {
104
+ const specsDir = path.join(zhuanspecDir, 'specs');
105
+ if (!fs.existsSync(specsDir)) {
106
+ return [];
107
+ }
108
+ const specs = [];
109
+ const entries = fs.readdirSync(specsDir, { withFileTypes: true });
110
+ for (const entry of entries) {
111
+ if (entry.isDirectory()) {
112
+ const specFile = path.join(specsDir, entry.name, 'spec.md');
113
+ if (fs.existsSync(specFile)) {
114
+ try {
115
+ const content = fs.readFileSync(specFile, 'utf-8');
116
+ const parser = new MarkdownParser(content);
117
+ const spec = parser.parseSpec(entry.name);
118
+ const requirementCount = spec.requirements.length;
119
+ specs.push({ name: entry.name, requirementCount });
120
+ }
121
+ catch (error) {
122
+ // If spec cannot be parsed, include with 0 count
123
+ specs.push({ name: entry.name, requirementCount: 0 });
124
+ }
125
+ }
126
+ }
127
+ }
128
+ return specs;
129
+ }
130
+ displaySummary(changesData, specsData) {
131
+ const totalChanges = changesData.draft.length + changesData.active.length + changesData.completed.length;
132
+ const totalSpecs = specsData.length;
133
+ const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0);
134
+ // Calculate total task progress
135
+ let totalTasks = 0;
136
+ let completedTasks = 0;
137
+ changesData.active.forEach((change) => {
138
+ totalTasks += change.progress.total;
139
+ completedTasks += change.progress.completed;
140
+ });
141
+ changesData.completed.forEach(() => {
142
+ // Completed changes count as 100% done (we don't know exact task count)
143
+ // This is a simplification
144
+ });
145
+ console.log(chalk.bold('Summary:'));
146
+ console.log(` ${chalk.cyan('●')} Specifications: ${chalk.bold(totalSpecs)} specs, ${chalk.bold(totalRequirements)} requirements`);
147
+ if (changesData.draft.length > 0) {
148
+ console.log(` ${chalk.gray('●')} Draft Changes: ${chalk.bold(changesData.draft.length)}`);
149
+ }
150
+ console.log(` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`);
151
+ console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`);
152
+ if (totalTasks > 0) {
153
+ const overallProgress = Math.round((completedTasks / totalTasks) * 100);
154
+ console.log(` ${chalk.magenta('●')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)`);
155
+ }
156
+ }
157
+ createProgressBar(completed, total, width = 20) {
158
+ if (total === 0)
159
+ return chalk.dim('─'.repeat(width));
160
+ const percentage = completed / total;
161
+ const filled = Math.round(percentage * width);
162
+ const empty = width - filled;
163
+ const filledBar = chalk.green('█'.repeat(filled));
164
+ const emptyBar = chalk.dim('░'.repeat(empty));
165
+ return `[${filledBar}${emptyBar}]`;
166
+ }
167
+ }
168
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1,3 @@
1
+ export * from './cli/index.js';
2
+ export * from './core/index.js';
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './cli/index.js';
2
+ export * from './core/index.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,47 @@
1
+ import { type ChangeMetadata } from '../core/artifact-graph/types.js';
2
+ /**
3
+ * Error thrown when change metadata validation fails.
4
+ */
5
+ export declare class ChangeMetadataError extends Error {
6
+ readonly metadataPath: string;
7
+ readonly cause?: Error | undefined;
8
+ constructor(message: string, metadataPath: string, cause?: Error | undefined);
9
+ }
10
+ /**
11
+ * Validates that a schema name is valid (exists in available schemas).
12
+ *
13
+ * @param schemaName - The schema name to validate
14
+ * @returns The validated schema name
15
+ * @throws Error if schema is not found
16
+ */
17
+ export declare function validateSchemaName(schemaName: string): string;
18
+ /**
19
+ * Writes change metadata to .zhuanspec.yaml in the change directory.
20
+ *
21
+ * @param changeDir - The path to the change directory
22
+ * @param metadata - The metadata to write
23
+ * @throws ChangeMetadataError if validation fails or write fails
24
+ */
25
+ export declare function writeChangeMetadata(changeDir: string, metadata: ChangeMetadata): void;
26
+ /**
27
+ * Reads change metadata from .zhuanspec.yaml in the change directory.
28
+ *
29
+ * @param changeDir - The path to the change directory
30
+ * @returns The validated metadata, or null if no metadata file exists
31
+ * @throws ChangeMetadataError if the file exists but is invalid
32
+ */
33
+ export declare function readChangeMetadata(changeDir: string): ChangeMetadata | null;
34
+ /**
35
+ * Resolves the schema for a change, with explicit override taking precedence.
36
+ *
37
+ * Resolution order:
38
+ * 1. Explicit schema (if provided)
39
+ * 2. Schema from .zhuanspec.yaml metadata (if exists)
40
+ * 3. Default 'spec-driven'
41
+ *
42
+ * @param changeDir - The path to the change directory
43
+ * @param explicitSchema - Optional explicit schema override
44
+ * @returns The resolved schema name
45
+ */
46
+ export declare function resolveSchemaForChange(changeDir: string, explicitSchema?: string): string;
47
+ //# sourceMappingURL=change-metadata.d.ts.map