@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,201 @@
1
+ export function normalizeRequirementName(name) {
2
+ return name.trim();
3
+ }
4
+ const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/;
5
+ /**
6
+ * Extracts the Requirements section from a spec file and parses requirement blocks.
7
+ */
8
+ export function extractRequirementsSection(content) {
9
+ const normalized = normalizeLineEndings(content);
10
+ const lines = normalized.split('\n');
11
+ const reqHeaderIndex = lines.findIndex(l => /^##\s+Requirements\s*$/i.test(l));
12
+ if (reqHeaderIndex === -1) {
13
+ // No requirements section; create an empty one at the end
14
+ const before = content.trimEnd();
15
+ const headerLine = '## Requirements';
16
+ return {
17
+ before: before ? before + '\n\n' : '',
18
+ headerLine,
19
+ preamble: '',
20
+ bodyBlocks: [],
21
+ after: '\n',
22
+ };
23
+ }
24
+ // Find end of this section: next line that starts with '## ' at same or higher level
25
+ let endIndex = lines.length;
26
+ for (let i = reqHeaderIndex + 1; i < lines.length; i++) {
27
+ if (/^##\s+/.test(lines[i])) {
28
+ endIndex = i;
29
+ break;
30
+ }
31
+ }
32
+ const before = lines.slice(0, reqHeaderIndex).join('\n');
33
+ const headerLine = lines[reqHeaderIndex];
34
+ const sectionBodyLines = lines.slice(reqHeaderIndex + 1, endIndex);
35
+ // Parse requirement blocks within section body
36
+ const blocks = [];
37
+ let cursor = 0;
38
+ let preambleLines = [];
39
+ // Collect preamble lines until first requirement header
40
+ while (cursor < sectionBodyLines.length && !/^###\s+Requirement:/.test(sectionBodyLines[cursor])) {
41
+ preambleLines.push(sectionBodyLines[cursor]);
42
+ cursor++;
43
+ }
44
+ while (cursor < sectionBodyLines.length) {
45
+ const headerStart = cursor;
46
+ const headerLineCandidate = sectionBodyLines[cursor];
47
+ const headerMatch = headerLineCandidate.match(REQUIREMENT_HEADER_REGEX);
48
+ if (!headerMatch) {
49
+ // Not a requirement header; skip line defensively
50
+ cursor++;
51
+ continue;
52
+ }
53
+ const name = normalizeRequirementName(headerMatch[1]);
54
+ cursor++;
55
+ // Gather lines until next requirement header or end of section
56
+ const bodyLines = [headerLineCandidate];
57
+ while (cursor < sectionBodyLines.length && !/^###\s+Requirement:/.test(sectionBodyLines[cursor]) && !/^##\s+/.test(sectionBodyLines[cursor])) {
58
+ bodyLines.push(sectionBodyLines[cursor]);
59
+ cursor++;
60
+ }
61
+ const raw = bodyLines.join('\n').trimEnd();
62
+ blocks.push({ headerLine: headerLineCandidate, name, raw });
63
+ }
64
+ const after = lines.slice(endIndex).join('\n');
65
+ const preamble = preambleLines.join('\n').trimEnd();
66
+ return {
67
+ before: before.trimEnd() ? before + '\n' : before,
68
+ headerLine,
69
+ preamble,
70
+ bodyBlocks: blocks,
71
+ after: after.startsWith('\n') ? after : '\n' + after,
72
+ };
73
+ }
74
+ function normalizeLineEndings(content) {
75
+ return content.replace(/\r\n?/g, '\n');
76
+ }
77
+ /**
78
+ * Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
79
+ */
80
+ export function parseDeltaSpec(content) {
81
+ const normalized = normalizeLineEndings(content);
82
+ const sections = splitTopLevelSections(normalized);
83
+ const addedLookup = getSectionCaseInsensitive(sections, 'ADDED Requirements');
84
+ const modifiedLookup = getSectionCaseInsensitive(sections, 'MODIFIED Requirements');
85
+ const removedLookup = getSectionCaseInsensitive(sections, 'REMOVED Requirements');
86
+ const renamedLookup = getSectionCaseInsensitive(sections, 'RENAMED Requirements');
87
+ const added = parseRequirementBlocksFromSection(addedLookup.body);
88
+ const modified = parseRequirementBlocksFromSection(modifiedLookup.body);
89
+ const removedNames = parseRemovedNames(removedLookup.body);
90
+ const renamedPairs = parseRenamedPairs(renamedLookup.body);
91
+ return {
92
+ added,
93
+ modified,
94
+ removed: removedNames,
95
+ renamed: renamedPairs,
96
+ sectionPresence: {
97
+ added: addedLookup.found,
98
+ modified: modifiedLookup.found,
99
+ removed: removedLookup.found,
100
+ renamed: renamedLookup.found,
101
+ },
102
+ };
103
+ }
104
+ function splitTopLevelSections(content) {
105
+ const lines = content.split('\n');
106
+ const result = {};
107
+ const indices = [];
108
+ for (let i = 0; i < lines.length; i++) {
109
+ const m = lines[i].match(/^(##)\s+(.+)$/);
110
+ if (m) {
111
+ const level = m[1].length; // only care for '##'
112
+ indices.push({ title: m[2].trim(), index: i, level });
113
+ }
114
+ }
115
+ for (let i = 0; i < indices.length; i++) {
116
+ const current = indices[i];
117
+ const next = indices[i + 1];
118
+ const body = lines.slice(current.index + 1, next ? next.index : lines.length).join('\n');
119
+ result[current.title] = body;
120
+ }
121
+ return result;
122
+ }
123
+ function getSectionCaseInsensitive(sections, desired) {
124
+ const target = desired.toLowerCase();
125
+ for (const [title, body] of Object.entries(sections)) {
126
+ if (title.toLowerCase() === target)
127
+ return { body, found: true };
128
+ }
129
+ return { body: '', found: false };
130
+ }
131
+ function parseRequirementBlocksFromSection(sectionBody) {
132
+ if (!sectionBody)
133
+ return [];
134
+ const lines = normalizeLineEndings(sectionBody).split('\n');
135
+ const blocks = [];
136
+ let i = 0;
137
+ while (i < lines.length) {
138
+ // Seek next requirement header
139
+ while (i < lines.length && !/^###\s+Requirement:/.test(lines[i]))
140
+ i++;
141
+ if (i >= lines.length)
142
+ break;
143
+ const headerLine = lines[i];
144
+ const m = headerLine.match(REQUIREMENT_HEADER_REGEX);
145
+ if (!m) {
146
+ i++;
147
+ continue;
148
+ }
149
+ const name = normalizeRequirementName(m[1]);
150
+ const buf = [headerLine];
151
+ i++;
152
+ while (i < lines.length && !/^###\s+Requirement:/.test(lines[i]) && !/^##\s+/.test(lines[i])) {
153
+ buf.push(lines[i]);
154
+ i++;
155
+ }
156
+ blocks.push({ headerLine, name, raw: buf.join('\n').trimEnd() });
157
+ }
158
+ return blocks;
159
+ }
160
+ function parseRemovedNames(sectionBody) {
161
+ if (!sectionBody)
162
+ return [];
163
+ const names = [];
164
+ const lines = normalizeLineEndings(sectionBody).split('\n');
165
+ for (const line of lines) {
166
+ const m = line.match(REQUIREMENT_HEADER_REGEX);
167
+ if (m) {
168
+ names.push(normalizeRequirementName(m[1]));
169
+ continue;
170
+ }
171
+ // Also support bullet list of headers
172
+ const bullet = line.match(/^\s*-\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
173
+ if (bullet) {
174
+ names.push(normalizeRequirementName(bullet[1]));
175
+ }
176
+ }
177
+ return names;
178
+ }
179
+ function parseRenamedPairs(sectionBody) {
180
+ if (!sectionBody)
181
+ return [];
182
+ const pairs = [];
183
+ const lines = normalizeLineEndings(sectionBody).split('\n');
184
+ let current = {};
185
+ for (const line of lines) {
186
+ const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
187
+ const toMatch = line.match(/^\s*-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
188
+ if (fromMatch) {
189
+ current.from = normalizeRequirementName(fromMatch[1]);
190
+ }
191
+ else if (toMatch) {
192
+ current.to = normalizeRequirementName(toMatch[1]);
193
+ if (current.from && current.to) {
194
+ pairs.push({ from: current.from, to: current.to });
195
+ current = {};
196
+ }
197
+ }
198
+ }
199
+ return pairs;
200
+ }
201
+ //# sourceMappingURL=requirement-blocks.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Project-level configuration loaded from zhuanspec/config.yaml.
3
+ *
4
+ * Supports three key sections:
5
+ * - context: Project background injected into all artifact instructions
6
+ * - rules: Per-artifact or global rules injected into instructions
7
+ * - defaultSchema: Project-wide default schema name
8
+ */
9
+ export interface ProjectConfig {
10
+ /** Project context injected into all instructions */
11
+ context?: string;
12
+ /** Global rules applied to all artifacts */
13
+ rules?: string;
14
+ /** Per-artifact rules (key = artifact ID, value = rules text) */
15
+ artifactRules?: Record<string, string>;
16
+ /** Default schema for the project */
17
+ defaultSchema?: string;
18
+ }
19
+ /**
20
+ * Loads project-level configuration from zhuanspec/config.yaml.
21
+ *
22
+ * @param projectRoot - Project root directory
23
+ * @returns Parsed project config, or empty config if file doesn't exist
24
+ */
25
+ export declare function loadProjectConfig(projectRoot: string): ProjectConfig;
26
+ /**
27
+ * Gets the effective rules for a specific artifact, combining global and per-artifact rules.
28
+ *
29
+ * @param config - Project configuration
30
+ * @param artifactId - The artifact ID (e.g., "proposal", "specs", "tasks")
31
+ * @returns Combined rules string, or undefined if no rules apply
32
+ */
33
+ export declare function getEffectiveRules(config: ProjectConfig, artifactId: string): string | undefined;
34
+ //# sourceMappingURL=project-config.d.ts.map
@@ -0,0 +1,79 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as yaml from 'yaml';
4
+ const PROJECT_CONFIG_FILENAME = 'config.yaml';
5
+ /**
6
+ * Loads project-level configuration from zhuanspec/config.yaml.
7
+ *
8
+ * @param projectRoot - Project root directory
9
+ * @returns Parsed project config, or empty config if file doesn't exist
10
+ */
11
+ export function loadProjectConfig(projectRoot) {
12
+ const configPath = path.join(projectRoot, 'zhuanspec', PROJECT_CONFIG_FILENAME);
13
+ if (!fs.existsSync(configPath)) {
14
+ return {};
15
+ }
16
+ let content;
17
+ try {
18
+ content = fs.readFileSync(configPath, 'utf-8');
19
+ }
20
+ catch {
21
+ return {};
22
+ }
23
+ if (!content.trim()) {
24
+ return {};
25
+ }
26
+ let parsed;
27
+ try {
28
+ parsed = yaml.parse(content);
29
+ }
30
+ catch (err) {
31
+ const msg = err instanceof Error ? err.message : String(err);
32
+ console.warn(`Warning: Invalid YAML in ${configPath}: ${msg}`);
33
+ return {};
34
+ }
35
+ if (typeof parsed !== 'object' || parsed === null) {
36
+ return {};
37
+ }
38
+ const raw = parsed;
39
+ const config = {};
40
+ if (typeof raw.context === 'string') {
41
+ config.context = raw.context;
42
+ }
43
+ if (typeof raw.rules === 'string') {
44
+ config.rules = raw.rules;
45
+ }
46
+ if (typeof raw.artifactRules === 'object' && raw.artifactRules !== null) {
47
+ const artifactRules = {};
48
+ for (const [key, value] of Object.entries(raw.artifactRules)) {
49
+ if (typeof value === 'string') {
50
+ artifactRules[key] = value;
51
+ }
52
+ }
53
+ if (Object.keys(artifactRules).length > 0) {
54
+ config.artifactRules = artifactRules;
55
+ }
56
+ }
57
+ if (typeof raw.defaultSchema === 'string') {
58
+ config.defaultSchema = raw.defaultSchema;
59
+ }
60
+ return config;
61
+ }
62
+ /**
63
+ * Gets the effective rules for a specific artifact, combining global and per-artifact rules.
64
+ *
65
+ * @param config - Project configuration
66
+ * @param artifactId - The artifact ID (e.g., "proposal", "specs", "tasks")
67
+ * @returns Combined rules string, or undefined if no rules apply
68
+ */
69
+ export function getEffectiveRules(config, artifactId) {
70
+ const parts = [];
71
+ if (config.rules) {
72
+ parts.push(config.rules);
73
+ }
74
+ if (config.artifactRules?.[artifactId]) {
75
+ parts.push(config.artifactRules[artifactId]);
76
+ }
77
+ return parts.length > 0 ? parts.join('\n\n') : undefined;
78
+ }
79
+ //# sourceMappingURL=project-config.js.map
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ export declare const ScenarioSchema: z.ZodObject<{
3
+ rawText: z.ZodString;
4
+ }, z.core.$strip>;
5
+ export declare const RequirementSchema: z.ZodObject<{
6
+ text: z.ZodString;
7
+ scenarios: z.ZodArray<z.ZodObject<{
8
+ rawText: z.ZodString;
9
+ }, z.core.$strip>>;
10
+ }, z.core.$strip>;
11
+ export type Scenario = z.infer<typeof ScenarioSchema>;
12
+ export type Requirement = z.infer<typeof RequirementSchema>;
13
+ //# sourceMappingURL=base.schema.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ import { VALIDATION_MESSAGES } from '../validation/constants.js';
3
+ export const ScenarioSchema = z.object({
4
+ rawText: z.string().min(1, VALIDATION_MESSAGES.SCENARIO_EMPTY),
5
+ });
6
+ export const RequirementSchema = z.object({
7
+ text: z.string()
8
+ .min(1, VALIDATION_MESSAGES.REQUIREMENT_EMPTY)
9
+ .refine((text) => text.includes('SHALL') || text.includes('MUST'), VALIDATION_MESSAGES.REQUIREMENT_NO_SHALL),
10
+ scenarios: z.array(ScenarioSchema)
11
+ .min(1, VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS),
12
+ });
13
+ //# sourceMappingURL=base.schema.js.map
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod';
2
+ export declare const DeltaOperationType: z.ZodEnum<{
3
+ ADDED: "ADDED";
4
+ MODIFIED: "MODIFIED";
5
+ REMOVED: "REMOVED";
6
+ RENAMED: "RENAMED";
7
+ }>;
8
+ export declare const DeltaSchema: z.ZodObject<{
9
+ spec: z.ZodString;
10
+ operation: z.ZodEnum<{
11
+ ADDED: "ADDED";
12
+ MODIFIED: "MODIFIED";
13
+ REMOVED: "REMOVED";
14
+ RENAMED: "RENAMED";
15
+ }>;
16
+ description: z.ZodString;
17
+ requirement: z.ZodOptional<z.ZodObject<{
18
+ text: z.ZodString;
19
+ scenarios: z.ZodArray<z.ZodObject<{
20
+ rawText: z.ZodString;
21
+ }, z.core.$strip>>;
22
+ }, z.core.$strip>>;
23
+ requirements: z.ZodOptional<z.ZodArray<z.ZodObject<{
24
+ text: z.ZodString;
25
+ scenarios: z.ZodArray<z.ZodObject<{
26
+ rawText: z.ZodString;
27
+ }, z.core.$strip>>;
28
+ }, z.core.$strip>>>;
29
+ rename: z.ZodOptional<z.ZodObject<{
30
+ from: z.ZodString;
31
+ to: z.ZodString;
32
+ }, z.core.$strip>>;
33
+ }, z.core.$strip>;
34
+ export declare const ChangeSchema: z.ZodObject<{
35
+ name: z.ZodString;
36
+ why: z.ZodString;
37
+ whatChanges: z.ZodString;
38
+ deltas: z.ZodArray<z.ZodObject<{
39
+ spec: z.ZodString;
40
+ operation: z.ZodEnum<{
41
+ ADDED: "ADDED";
42
+ MODIFIED: "MODIFIED";
43
+ REMOVED: "REMOVED";
44
+ RENAMED: "RENAMED";
45
+ }>;
46
+ description: z.ZodString;
47
+ requirement: z.ZodOptional<z.ZodObject<{
48
+ text: z.ZodString;
49
+ scenarios: z.ZodArray<z.ZodObject<{
50
+ rawText: z.ZodString;
51
+ }, z.core.$strip>>;
52
+ }, z.core.$strip>>;
53
+ requirements: z.ZodOptional<z.ZodArray<z.ZodObject<{
54
+ text: z.ZodString;
55
+ scenarios: z.ZodArray<z.ZodObject<{
56
+ rawText: z.ZodString;
57
+ }, z.core.$strip>>;
58
+ }, z.core.$strip>>>;
59
+ rename: z.ZodOptional<z.ZodObject<{
60
+ from: z.ZodString;
61
+ to: z.ZodString;
62
+ }, z.core.$strip>>;
63
+ }, z.core.$strip>>;
64
+ metadata: z.ZodOptional<z.ZodObject<{
65
+ version: z.ZodDefault<z.ZodString>;
66
+ format: z.ZodLiteral<"zhuanspec-change">;
67
+ sourcePath: z.ZodOptional<z.ZodString>;
68
+ }, z.core.$strip>>;
69
+ }, z.core.$strip>;
70
+ export type DeltaOperation = z.infer<typeof DeltaOperationType>;
71
+ export type Delta = z.infer<typeof DeltaSchema>;
72
+ export type Change = z.infer<typeof ChangeSchema>;
73
+ //# sourceMappingURL=change.schema.d.ts.map
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { RequirementSchema } from './base.schema.js';
3
+ import { MIN_WHY_SECTION_LENGTH, MAX_WHY_SECTION_LENGTH, MAX_DELTAS_PER_CHANGE, VALIDATION_MESSAGES } from '../validation/constants.js';
4
+ export const DeltaOperationType = z.enum(['ADDED', 'MODIFIED', 'REMOVED', 'RENAMED']);
5
+ export const DeltaSchema = z.object({
6
+ spec: z.string().min(1, VALIDATION_MESSAGES.DELTA_SPEC_EMPTY),
7
+ operation: DeltaOperationType,
8
+ description: z.string().min(1, VALIDATION_MESSAGES.DELTA_DESCRIPTION_EMPTY),
9
+ requirement: RequirementSchema.optional(),
10
+ requirements: z.array(RequirementSchema).optional(),
11
+ rename: z.object({
12
+ from: z.string(),
13
+ to: z.string(),
14
+ }).optional(),
15
+ });
16
+ export const ChangeSchema = z.object({
17
+ name: z.string().min(1, VALIDATION_MESSAGES.CHANGE_NAME_EMPTY),
18
+ why: z.string()
19
+ .min(MIN_WHY_SECTION_LENGTH, VALIDATION_MESSAGES.CHANGE_WHY_TOO_SHORT)
20
+ .max(MAX_WHY_SECTION_LENGTH, VALIDATION_MESSAGES.CHANGE_WHY_TOO_LONG),
21
+ whatChanges: z.string().min(1, VALIDATION_MESSAGES.CHANGE_WHAT_EMPTY),
22
+ deltas: z.array(DeltaSchema)
23
+ .min(1, VALIDATION_MESSAGES.CHANGE_NO_DELTAS)
24
+ .max(MAX_DELTAS_PER_CHANGE, VALIDATION_MESSAGES.CHANGE_TOO_MANY_DELTAS),
25
+ metadata: z.object({
26
+ version: z.string().default('1.0.0'),
27
+ format: z.literal('zhuanspec-change'),
28
+ sourcePath: z.string().optional(),
29
+ }).optional(),
30
+ });
31
+ //# sourceMappingURL=change.schema.js.map
@@ -0,0 +1,4 @@
1
+ export { ScenarioSchema, RequirementSchema, type Scenario, type Requirement, } from './base.schema.js';
2
+ export { SpecSchema, type Spec, } from './spec.schema.js';
3
+ export { DeltaOperationType, DeltaSchema, ChangeSchema, type DeltaOperation, type Delta, type Change, } from './change.schema.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export { ScenarioSchema, RequirementSchema, } from './base.schema.js';
2
+ export { SpecSchema, } from './spec.schema.js';
3
+ export { DeltaOperationType, DeltaSchema, ChangeSchema, } from './change.schema.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ export declare const SpecSchema: z.ZodObject<{
3
+ name: z.ZodString;
4
+ overview: z.ZodString;
5
+ requirements: z.ZodArray<z.ZodObject<{
6
+ text: z.ZodString;
7
+ scenarios: z.ZodArray<z.ZodObject<{
8
+ rawText: z.ZodString;
9
+ }, z.core.$strip>>;
10
+ }, z.core.$strip>>;
11
+ metadata: z.ZodOptional<z.ZodObject<{
12
+ version: z.ZodDefault<z.ZodString>;
13
+ format: z.ZodLiteral<"zhuanspec">;
14
+ sourcePath: z.ZodOptional<z.ZodString>;
15
+ }, z.core.$strip>>;
16
+ }, z.core.$strip>;
17
+ export type Spec = z.infer<typeof SpecSchema>;
18
+ //# sourceMappingURL=spec.schema.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ import { RequirementSchema } from './base.schema.js';
3
+ import { VALIDATION_MESSAGES } from '../validation/constants.js';
4
+ export const SpecSchema = z.object({
5
+ name: z.string().min(1, VALIDATION_MESSAGES.SPEC_NAME_EMPTY),
6
+ overview: z.string().min(1, VALIDATION_MESSAGES.SPEC_PURPOSE_EMPTY),
7
+ requirements: z.array(RequirementSchema)
8
+ .min(1, VALIDATION_MESSAGES.SPEC_NO_REQUIREMENTS),
9
+ metadata: z.object({
10
+ version: z.string().default('1.0.0'),
11
+ format: z.literal('zhuanspec'),
12
+ sourcePath: z.string().optional(),
13
+ }).optional(),
14
+ });
15
+ //# sourceMappingURL=spec.schema.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Represents a discovered skill from the filesystem.
3
+ */
4
+ export interface DiscoveredSkill {
5
+ /** Skill name from YAML frontmatter */
6
+ name: string;
7
+ /** Skill description from YAML frontmatter */
8
+ description: string;
9
+ /** Absolute path to the SKILL.md file */
10
+ filePath: string;
11
+ }
12
+ /**
13
+ * Discover available skills by scanning priority paths.
14
+ * Uses short-circuit strategy: once a non-empty directory is found, stops scanning.
15
+ *
16
+ * @param _projectRoot - Project root path (reserved for future project-level skill scanning)
17
+ * @returns Array of discovered skills
18
+ */
19
+ export declare function discoverSkills(_projectRoot?: string): DiscoveredSkill[];
20
+ /**
21
+ * Format discovered skills for display.
22
+ */
23
+ export declare function formatSkillsList(skills: DiscoveredSkill[], json: boolean): string;
24
+ //# sourceMappingURL=skill-discovery.d.ts.map
@@ -0,0 +1,153 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ /**
5
+ * Scan priority paths (highest to lowest).
6
+ * Once a non-empty directory is found, scanning stops (short-circuit strategy).
7
+ */
8
+ const SCAN_PRIORITY = [
9
+ '~/.ai-config-hub/skills',
10
+ // Future fallback paths can be added here
11
+ ];
12
+ /**
13
+ * Resolve a path that may contain ~ to an absolute path.
14
+ */
15
+ function resolvePath(p) {
16
+ if (p.startsWith('~/') || p === '~') {
17
+ return path.join(os.homedir(), p.slice(1));
18
+ }
19
+ return path.resolve(p);
20
+ }
21
+ /**
22
+ * Parse YAML frontmatter from a SKILL.md file content.
23
+ * Frontmatter is delimited by --- at the start of the file.
24
+ */
25
+ function parseFrontmatter(content) {
26
+ const trimmed = content.trimStart();
27
+ if (!trimmed.startsWith('---')) {
28
+ return {};
29
+ }
30
+ const endIndex = trimmed.indexOf('---', 3);
31
+ if (endIndex === -1) {
32
+ return {};
33
+ }
34
+ const frontmatterBlock = trimmed.slice(3, endIndex).trim();
35
+ const result = {};
36
+ for (const line of frontmatterBlock.split('\n')) {
37
+ const colonIndex = line.indexOf(':');
38
+ if (colonIndex === -1)
39
+ continue;
40
+ const key = line.slice(0, colonIndex).trim();
41
+ const value = line.slice(colonIndex + 1).trim();
42
+ // Remove quotes if present
43
+ const unquoted = value.replace(/^["']|["']$/g, '');
44
+ if (key === 'name') {
45
+ result.name = unquoted;
46
+ }
47
+ else if (key === 'description') {
48
+ result.description = unquoted;
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+ /**
54
+ * Recursively find all SKILL.md files under a directory.
55
+ */
56
+ function findSkillFiles(dir) {
57
+ const results = [];
58
+ let entries;
59
+ try {
60
+ entries = fs.readdirSync(dir, { withFileTypes: true });
61
+ }
62
+ catch {
63
+ return results;
64
+ }
65
+ for (const entry of entries) {
66
+ const fullPath = path.join(dir, entry.name);
67
+ if (entry.isDirectory()) {
68
+ results.push(...findSkillFiles(fullPath));
69
+ }
70
+ else if (entry.isFile() && entry.name === 'SKILL.md') {
71
+ results.push(fullPath);
72
+ }
73
+ }
74
+ return results;
75
+ }
76
+ /**
77
+ * Discover available skills by scanning priority paths.
78
+ * Uses short-circuit strategy: once a non-empty directory is found, stops scanning.
79
+ *
80
+ * @param _projectRoot - Project root path (reserved for future project-level skill scanning)
81
+ * @returns Array of discovered skills
82
+ */
83
+ export function discoverSkills(_projectRoot) {
84
+ for (const scanPath of SCAN_PRIORITY) {
85
+ const resolved = resolvePath(scanPath);
86
+ // Check if directory exists
87
+ try {
88
+ const stat = fs.statSync(resolved);
89
+ if (!stat.isDirectory())
90
+ continue;
91
+ }
92
+ catch {
93
+ continue;
94
+ }
95
+ // Scan for SKILL.md files
96
+ const skillFiles = findSkillFiles(resolved);
97
+ if (skillFiles.length === 0)
98
+ continue;
99
+ // Parse skills from found files
100
+ const skills = [];
101
+ const seenNames = new Set();
102
+ for (const filePath of skillFiles) {
103
+ try {
104
+ const content = fs.readFileSync(filePath, 'utf-8');
105
+ const fm = parseFrontmatter(content);
106
+ if (!fm.name)
107
+ continue;
108
+ // Filter out zhuanspec-* workflow skills
109
+ if (fm.name.startsWith('zhuanspec-'))
110
+ continue;
111
+ // Deduplicate by name
112
+ if (seenNames.has(fm.name))
113
+ continue;
114
+ seenNames.add(fm.name);
115
+ skills.push({
116
+ name: fm.name,
117
+ description: fm.description || '',
118
+ filePath,
119
+ });
120
+ }
121
+ catch {
122
+ // Skip unreadable files
123
+ continue;
124
+ }
125
+ }
126
+ // Short-circuit: found a valid directory with skills, stop scanning
127
+ if (skills.length > 0) {
128
+ return skills;
129
+ }
130
+ }
131
+ // No skills found in any priority path
132
+ return [];
133
+ }
134
+ /**
135
+ * Format discovered skills for display.
136
+ */
137
+ export function formatSkillsList(skills, json) {
138
+ if (json) {
139
+ return JSON.stringify(skills.map(s => ({ name: s.name, description: s.description, filePath: s.filePath })), null, 2);
140
+ }
141
+ if (skills.length === 0) {
142
+ return 'No skills discovered.';
143
+ }
144
+ const lines = [`Found ${skills.length} skill(s):\n`];
145
+ for (const skill of skills) {
146
+ lines.push(` ${skill.name}`);
147
+ if (skill.description) {
148
+ lines.push(` ${skill.description}`);
149
+ }
150
+ }
151
+ return lines.join('\n');
152
+ }
153
+ //# sourceMappingURL=skill-discovery.js.map