@guilhermefsousa/open-spec-kit 0.0.1

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 (31) hide show
  1. package/README.md +57 -0
  2. package/bin/open-spec-kit.js +39 -0
  3. package/package.json +51 -0
  4. package/src/commands/doctor.js +324 -0
  5. package/src/commands/init.js +981 -0
  6. package/src/commands/update.js +210 -0
  7. package/src/commands/validate.js +615 -0
  8. package/src/parsers/markdown-sections.js +271 -0
  9. package/src/schemas/projects.schema.js +111 -0
  10. package/src/schemas/spec.schema.js +760 -0
  11. package/templates/agents/agents/spec-hub.agent.md +99 -0
  12. package/templates/agents/rules/hub_structure.instructions.md +49 -0
  13. package/templates/agents/rules/ownership.instructions.md +138 -0
  14. package/templates/agents/scripts/notify-gchat.ps1 +99 -0
  15. package/templates/agents/scripts/notify-gchat.sh +131 -0
  16. package/templates/agents/skills/dev-orchestrator/SKILL.md +573 -0
  17. package/templates/agents/skills/discovery/SKILL.md +406 -0
  18. package/templates/agents/skills/setup-project/SKILL.md +459 -0
  19. package/templates/agents/skills/specifying-features/SKILL.md +379 -0
  20. package/templates/github/agents/spec-hub.agent.md +75 -0
  21. package/templates/github/copilot-instructions.md +102 -0
  22. package/templates/github/instructions/hub_structure.instructions.md +33 -0
  23. package/templates/github/instructions/ownership.instructions.md +45 -0
  24. package/templates/github/prompts/dev.prompt.md +19 -0
  25. package/templates/github/prompts/discovery.prompt.md +20 -0
  26. package/templates/github/prompts/nova-feature.prompt.md +19 -0
  27. package/templates/github/prompts/setup.prompt.md +18 -0
  28. package/templates/github/skills/dev-orchestrator/SKILL.md +9 -0
  29. package/templates/github/skills/discovery/SKILL.md +9 -0
  30. package/templates/github/skills/setup-project/SKILL.md +9 -0
  31. package/templates/github/skills/specifying-features/SKILL.md +9 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Markdown Section Parser
3
+ *
4
+ * Parses markdown into a hierarchical section tree with heading hierarchy,
5
+ * code fence awareness, and table extraction.
6
+ *
7
+ * This is the foundation for all 32 Zod validation rules in osk validate.
8
+ */
9
+
10
+ // --- Code Fence Mask ---
11
+
12
+ /**
13
+ * Build a boolean mask marking lines inside code fences.
14
+ * Lines inside ``` or ~~~ blocks are marked true — headings there are ignored.
15
+ * @param {string[]} lines
16
+ * @returns {boolean[]}
17
+ */
18
+ export function buildCodeFenceMask(lines) {
19
+ const mask = new Array(lines.length).fill(false);
20
+ let inFence = false;
21
+ let fenceMarker = null; // '`' or '~'
22
+ let fenceLen = 0;
23
+
24
+ for (let i = 0; i < lines.length; i++) {
25
+ const trimmed = lines[i].trimStart();
26
+
27
+ if (!inFence) {
28
+ const open = trimmed.match(/^(`{3,}|~{3,})/);
29
+ if (open) {
30
+ inFence = true;
31
+ fenceMarker = open[1][0];
32
+ fenceLen = open[1].length;
33
+ mask[i] = true;
34
+ continue;
35
+ }
36
+ } else {
37
+ mask[i] = true;
38
+ // Check for closing fence: same marker, same or greater length, nothing after
39
+ const close = trimmed.match(/^(`{3,}|~{3,})\s*$/);
40
+ if (close && close[1][0] === fenceMarker && close[1].length >= fenceLen) {
41
+ inFence = false;
42
+ fenceMarker = null;
43
+ fenceLen = 0;
44
+ }
45
+ }
46
+ }
47
+
48
+ return mask;
49
+ }
50
+
51
+ // --- Section Parsing ---
52
+
53
+ /**
54
+ * @typedef {Object} Section
55
+ * @property {number} level - Heading level (1-6)
56
+ * @property {string} title - Heading text (without # prefix)
57
+ * @property {string} content - All content until next heading of same/higher level
58
+ * @property {Section[]} children - Nested subsections
59
+ */
60
+
61
+ const HEADING_RE = /^(#{1,6})\s+(.+)$/;
62
+
63
+ /**
64
+ * Parse markdown into hierarchical section tree.
65
+ * Respects code fences (ignores headings inside them).
66
+ * @param {string} markdown - Raw file content
67
+ * @returns {Section[]} - Tree of sections
68
+ */
69
+ export function parseSections(markdown) {
70
+ // Normalize CRLF → LF so headings aren't broken by trailing \r.
71
+ // Both `.` and `$` in JS regex treat \r as a line terminator,
72
+ // causing HEADING_RE to fail on lines ending with \r.
73
+ const lines = markdown.split(/\r?\n/);
74
+ const mask = buildCodeFenceMask(lines);
75
+ const rootChildren = [];
76
+
77
+ /** @type {{ level: number, section: Section }[]} */
78
+ const stack = [];
79
+
80
+ let currentContentLines = [];
81
+
82
+ function flushContent() {
83
+ if (stack.length > 0) {
84
+ stack[stack.length - 1].section.content = currentContentLines.join('\n').trim();
85
+ }
86
+ currentContentLines = [];
87
+ }
88
+
89
+ for (let i = 0; i < lines.length; i++) {
90
+ if (mask[i]) {
91
+ currentContentLines.push(lines[i]);
92
+ continue;
93
+ }
94
+
95
+ const match = lines[i].match(HEADING_RE);
96
+ if (!match) {
97
+ currentContentLines.push(lines[i]);
98
+ continue;
99
+ }
100
+
101
+ flushContent();
102
+
103
+ const level = match[1].length;
104
+ const title = match[2].trim();
105
+ const section = { level, title, content: '', children: [] };
106
+
107
+ // Pop stack until we find a parent with lower level
108
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) {
109
+ stack.pop();
110
+ }
111
+
112
+ if (stack.length === 0) {
113
+ rootChildren.push(section);
114
+ } else {
115
+ stack[stack.length - 1].section.children.push(section);
116
+ }
117
+
118
+ stack.push({ level, section });
119
+ }
120
+
121
+ flushContent();
122
+
123
+ return rootChildren;
124
+ }
125
+
126
+ // --- Section Search ---
127
+
128
+ /**
129
+ * Find first section matching a title pattern (case-insensitive).
130
+ * Searches recursively through the tree.
131
+ * @param {Section[]} sections
132
+ * @param {RegExp} pattern
133
+ * @returns {Section|null}
134
+ */
135
+ export function findSection(sections, pattern) {
136
+ for (const section of sections) {
137
+ if (pattern.test(section.title)) return section;
138
+ const found = findSection(section.children, pattern);
139
+ if (found) return found;
140
+ }
141
+ return null;
142
+ }
143
+
144
+ /**
145
+ * Collect all sections matching a pattern recursively.
146
+ * @param {Section[]} sections
147
+ * @param {RegExp} pattern
148
+ * @returns {Section[]}
149
+ */
150
+ export function findAllSections(sections, pattern) {
151
+ const results = [];
152
+ for (const section of sections) {
153
+ if (pattern.test(section.title)) results.push(section);
154
+ results.push(...findAllSections(section.children, pattern));
155
+ }
156
+ return results;
157
+ }
158
+
159
+ /**
160
+ * Get all sections at a specific heading level (non-recursive, top-level only).
161
+ * @param {Section[]} sections
162
+ * @param {number} level
163
+ * @returns {Section[]}
164
+ */
165
+ export function getSectionsByLevel(sections, level) {
166
+ const results = [];
167
+ for (const section of sections) {
168
+ if (section.level === level) results.push(section);
169
+ }
170
+ return results;
171
+ }
172
+
173
+ // --- Table Parsing ---
174
+
175
+ const TABLE_SEP_RE = /^\|?\s*[-:]+[-| :]*$/;
176
+
177
+ /**
178
+ * Extract markdown table from content into array of objects.
179
+ * Handles: | Header1 | Header2 | with separator row |---|---|
180
+ * @param {string} content - Section content (may contain multiple tables)
181
+ * @returns {Object[]} - Rows as key-value objects using headers as keys
182
+ */
183
+ export function parseTable(content) {
184
+ const lines = content.split('\n');
185
+ const rows = [];
186
+ let headers = null;
187
+ let inTable = false;
188
+
189
+ for (let i = 0; i < lines.length; i++) {
190
+ const line = lines[i].trim();
191
+
192
+ if (!inTable) {
193
+ // Look for header row: must have | and the next line must be separator
194
+ if (line.includes('|') && i + 1 < lines.length && TABLE_SEP_RE.test(lines[i + 1].trim())) {
195
+ headers = splitTableRow(line);
196
+ inTable = true;
197
+ i++; // skip separator
198
+ continue;
199
+ }
200
+ } else {
201
+ // We're in a table — read data rows until empty line or non-table line
202
+ if (!line || !line.includes('|')) {
203
+ inTable = false;
204
+ headers = null;
205
+ continue;
206
+ }
207
+ if (TABLE_SEP_RE.test(line)) continue; // skip extra separators
208
+
209
+ const cells = splitTableRow(line);
210
+ const row = {};
211
+ for (let j = 0; j < headers.length; j++) {
212
+ const key = headers[j].toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g, '_');
213
+ row[key] = j < cells.length ? cells[j] : '';
214
+ }
215
+ rows.push(row);
216
+ }
217
+ }
218
+
219
+ return rows;
220
+ }
221
+
222
+ /**
223
+ * Split a markdown table row into cell values.
224
+ * @param {string} line
225
+ * @returns {string[]}
226
+ */
227
+ function splitTableRow(line) {
228
+ return line
229
+ .replace(/^\|/, '')
230
+ .replace(/\|$/, '')
231
+ .split('|')
232
+ .map(cell => cell.trim());
233
+ }
234
+
235
+ // --- Utility ---
236
+
237
+ /**
238
+ * Extract all regex matches from content.
239
+ * @param {string} content
240
+ * @param {RegExp} pattern - Must have global flag
241
+ * @returns {string[]} - Unique matches
242
+ */
243
+ export function extractUniqueMatches(content, pattern) {
244
+ return [...new Set((content.match(pattern) || []))];
245
+ }
246
+
247
+ /**
248
+ * Check if content contains any of the unresolved marker keywords.
249
+ * @param {string} content
250
+ * @returns {{ found: boolean, markers: string[] }}
251
+ */
252
+ export function findUnresolvedMarkers(content) {
253
+ const MARKER_RE = /\b(MOCKADO|TODO|A CONFIRMAR|TBD|FIXME|PLACEHOLDER|TKTK)\b/gi;
254
+ const matches = content.match(MARKER_RE) || [];
255
+ const unique = [...new Set(matches.map(m => m.toUpperCase()))];
256
+ return { found: unique.length > 0, markers: unique };
257
+ }
258
+
259
+ /**
260
+ * Get full raw content of a section including its children (reconstructed).
261
+ * Useful for checking content within a section and all its subsections.
262
+ * @param {Section} section
263
+ * @returns {string}
264
+ */
265
+ export function getSectionFullContent(section) {
266
+ let content = section.content;
267
+ for (const child of section.children) {
268
+ content += '\n' + '#'.repeat(child.level) + ' ' + child.title + '\n' + getSectionFullContent(child);
269
+ }
270
+ return content;
271
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Zod schema for projects.yml
3
+ *
4
+ * Central config file read by all components.
5
+ * Validated by `open-spec-kit validate` and `open-spec-kit doctor`.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import { parse as parseYaml } from 'yaml';
10
+
11
+ export const RepoSchema = z.object({
12
+ name: z.string().min(1),
13
+ stack: z.string().optional(),
14
+ type: z.string().optional(),
15
+ purpose: z.string().optional(),
16
+ status: z.enum(['planned', 'active', 'deprecated']).optional(),
17
+ url: z.string().nullable().optional(),
18
+ });
19
+
20
+ export const CodingAgentsSchema = z.record(
21
+ z.string(), // stack keyword (e.g., "dotnet", "nodejs")
22
+ z.string(), // agent name (e.g., "dotnet-engineer")
23
+ ).optional();
24
+
25
+ export const AgentsSchema = z.object({
26
+ coding: CodingAgentsSchema,
27
+ security: z.string().nullable().optional(),
28
+ code_review: z.string().nullable().optional(),
29
+ design_doc: z.string().nullable().optional(),
30
+ principal: z.string().nullable().optional(),
31
+ }).optional();
32
+
33
+ export const ProjectsSchema = z.object({
34
+ project: z.object({
35
+ name: z.string().min(1),
36
+ sigla: z.string().min(2).max(4).optional(),
37
+ domain: z.string().optional(),
38
+ status: z.string().optional(),
39
+ team_size: z.number().optional(),
40
+ preset: z.enum(['lean', 'standard', 'enterprise']).default('standard'),
41
+ stack: z.array(z.string()).optional(),
42
+ agents: z.array(z.string()).optional(), // platforms: claude, copilot
43
+ }).optional(),
44
+ specs: z.object({
45
+ preset: z.enum(['lean', 'standard', 'enterprise']).default('standard'),
46
+ }).optional(),
47
+ repos: z.array(RepoSchema).optional(),
48
+ agents: AgentsSchema,
49
+ confluence: z.object({
50
+ space: z.string().optional(),
51
+ root_page_id: z.union([z.string(), z.number()]).optional(),
52
+ }).optional(),
53
+ });
54
+
55
+ /**
56
+ * Parse projects.yml content and validate with Zod.
57
+ * @param {string} yamlContent - Raw YAML string
58
+ * @returns {{ data: object|null, errors: import('zod').ZodIssue[]|null, preset: string, repoNames: string[], repoStacks: Map<string, string> }}
59
+ */
60
+ export function parseAndValidateProjects(yamlContent) {
61
+ let parsed;
62
+ try {
63
+ parsed = parseYaml(yamlContent);
64
+ } catch (e) {
65
+ return {
66
+ data: null,
67
+ errors: [{ path: ['projects.yml'], message: `YAML parse error: ${e.message}`, code: 'custom' }],
68
+ preset: 'standard',
69
+ repoNames: [],
70
+ repoStacks: new Map(),
71
+ };
72
+ }
73
+
74
+ const result = ProjectsSchema.safeParse(parsed);
75
+
76
+ if (!result.success) {
77
+ return {
78
+ data: parsed,
79
+ errors: result.error.issues,
80
+ preset: extractPreset(parsed),
81
+ repoNames: extractRepoNames(parsed),
82
+ repoStacks: extractRepoStacks(parsed),
83
+ };
84
+ }
85
+
86
+ return {
87
+ data: result.data,
88
+ errors: null,
89
+ preset: extractPreset(result.data),
90
+ repoNames: extractRepoNames(result.data),
91
+ repoStacks: extractRepoStacks(result.data),
92
+ };
93
+ }
94
+
95
+ function extractPreset(data) {
96
+ return data?.specs?.preset || data?.project?.preset || 'standard';
97
+ }
98
+
99
+ function extractRepoNames(data) {
100
+ return (data?.repos || []).map(r => r.name).filter(Boolean);
101
+ }
102
+
103
+ function extractRepoStacks(data) {
104
+ const map = new Map();
105
+ for (const repo of data?.repos || []) {
106
+ if (repo.name && repo.stack) {
107
+ map.set(repo.name, repo.stack);
108
+ }
109
+ }
110
+ return map;
111
+ }