specflow-cc 1.13.0 → 1.14.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.
@@ -0,0 +1,241 @@
1
+ /**
2
+ * bin/lib/state.cjs — STATE.md CRUD operations
3
+ *
4
+ * Exports: cmdStateGet(), cmdStateSetActive(), cmdQueueNext()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { output, error, safeReadFile } = require('./core.cjs');
12
+
13
+ /**
14
+ * Extract a bold-field value from STATE.md content.
15
+ * Matches patterns like: **Field:** value
16
+ * @param {string} content - STATE.md content
17
+ * @param {string} field - Field name (e.g., "Status")
18
+ * @returns {string|null}
19
+ */
20
+ function extractBoldField(content, field) {
21
+ const regex = new RegExp(`\\*\\*${field}:\\*\\*\\s*(.+)`, 'i');
22
+ const match = content.match(regex);
23
+ return match ? match[1].trim() : null;
24
+ }
25
+
26
+ /**
27
+ * Extract the active spec ID from STATE.md.
28
+ * The spec ID is on the line immediately after "## Active Specification".
29
+ * @param {string} content - STATE.md content
30
+ * @returns {string|null}
31
+ */
32
+ function extractActiveSpec(content) {
33
+ const lines = content.split('\n');
34
+ for (let i = 0; i < lines.length; i++) {
35
+ if (lines[i].trim() === '## Active Specification') {
36
+ // Next non-empty line has the spec ID
37
+ for (let j = i + 1; j < lines.length; j++) {
38
+ const line = lines[j].trim();
39
+ if (line && !line.startsWith('**') && !line.startsWith('#')) {
40
+ return line;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Parse the queue table from STATE.md.
50
+ * Expects pipe-delimited markdown table with columns:
51
+ * Priority | ID | Title | Status | Complexity | Depends On
52
+ * @param {string} content - STATE.md content
53
+ * @returns {Array<Object>}
54
+ */
55
+ function parseQueueTable(content) {
56
+ const lines = content.split('\n');
57
+ const queue = [];
58
+ let inQueue = false;
59
+ let headerFound = false;
60
+ let separatorFound = false;
61
+
62
+ for (const line of lines) {
63
+ const trimmed = line.trim();
64
+
65
+ if (trimmed === '## Queue') {
66
+ inQueue = true;
67
+ continue;
68
+ }
69
+
70
+ if (inQueue && trimmed.startsWith('##') && trimmed !== '## Queue') {
71
+ break; // next section
72
+ }
73
+
74
+ if (!inQueue) continue;
75
+
76
+ if (trimmed.startsWith('|') && !headerFound) {
77
+ // Check if this is the header row
78
+ if (trimmed.toLowerCase().includes('priority') && trimmed.toLowerCase().includes('id')) {
79
+ headerFound = true;
80
+ continue;
81
+ }
82
+ }
83
+
84
+ if (headerFound && !separatorFound && trimmed.startsWith('|') && trimmed.includes('---')) {
85
+ separatorFound = true;
86
+ continue;
87
+ }
88
+
89
+ if (headerFound && separatorFound && trimmed.startsWith('|')) {
90
+ const cells = trimmed.split('|').map(c => c.trim()).filter(c => c !== '');
91
+ if (cells.length >= 4) {
92
+ queue.push({
93
+ priority: cells[0] || '',
94
+ id: cells[1] || '',
95
+ title: cells[2] || '',
96
+ status: cells[3] || '',
97
+ complexity: cells[4] || '',
98
+ depends_on: cells[5] || '',
99
+ });
100
+ }
101
+ }
102
+ }
103
+
104
+ return queue;
105
+ }
106
+
107
+ /**
108
+ * Get current active spec, status, and next step from STATE.md.
109
+ * @param {string} cwd - Working directory
110
+ * @param {boolean} raw - Output raw string
111
+ */
112
+ function cmdStateGet(cwd, raw) {
113
+ const statePath = path.join(cwd, '.specflow', 'STATE.md');
114
+ const content = safeReadFile(statePath);
115
+
116
+ if (!content) {
117
+ error('STATE.md not found at ' + statePath);
118
+ }
119
+
120
+ const activeSpec = extractActiveSpec(content);
121
+ const status = extractBoldField(content, 'Status');
122
+ const nextStep = extractBoldField(content, 'Next Step');
123
+
124
+ const result = {
125
+ active_spec: activeSpec || null,
126
+ status: status || null,
127
+ next_step: nextStep || null,
128
+ };
129
+
130
+ output(result, raw, result.active_spec);
131
+ }
132
+
133
+ /**
134
+ * Update active spec, status, and optionally next step in STATE.md.
135
+ * @param {string} cwd - Working directory
136
+ * @param {string} id - Spec ID (e.g., "SPEC-007")
137
+ * @param {string} status - New status
138
+ * @param {string} [nextStep] - Optional new next step
139
+ * @param {boolean} raw - Output raw string
140
+ */
141
+ function cmdStateSetActive(cwd, id, status, nextStep, raw) {
142
+ const statePath = path.join(cwd, '.specflow', 'STATE.md');
143
+ const content = safeReadFile(statePath);
144
+
145
+ if (!content) {
146
+ error('STATE.md not found at ' + statePath);
147
+ }
148
+
149
+ const lines = content.split('\n');
150
+ const result = [];
151
+ let inActiveSection = false;
152
+ let specIdReplaced = false;
153
+
154
+ for (let i = 0; i < lines.length; i++) {
155
+ const trimmed = lines[i].trim();
156
+
157
+ if (trimmed === '## Active Specification') {
158
+ inActiveSection = true;
159
+ result.push(lines[i]);
160
+ continue;
161
+ }
162
+
163
+ if (inActiveSection && trimmed.startsWith('## ') && trimmed !== '## Active Specification') {
164
+ inActiveSection = false;
165
+ }
166
+
167
+ if (inActiveSection && !specIdReplaced && trimmed && !trimmed.startsWith('**') && !trimmed.startsWith('#')) {
168
+ // This is the spec ID line — replace it
169
+ result.push(id);
170
+ specIdReplaced = true;
171
+ continue;
172
+ }
173
+
174
+ if (inActiveSection && trimmed.startsWith('**Status:**')) {
175
+ result.push('**Status:** ' + status);
176
+ continue;
177
+ }
178
+
179
+ if (inActiveSection && trimmed.startsWith('**Next Step:**')) {
180
+ if (nextStep !== undefined && nextStep !== null) {
181
+ result.push('**Next Step:** ' + nextStep);
182
+ } else {
183
+ result.push(lines[i]); // preserve existing
184
+ }
185
+ continue;
186
+ }
187
+
188
+ result.push(lines[i]);
189
+ }
190
+
191
+ fs.writeFileSync(statePath, result.join('\n'), 'utf8');
192
+
193
+ const resultObj = {
194
+ updated: true,
195
+ active_spec: id,
196
+ status: status,
197
+ next_step: nextStep || extractBoldField(result.join('\n'), 'Next Step'),
198
+ };
199
+
200
+ output(resultObj, raw, 'updated');
201
+ }
202
+
203
+ /**
204
+ * Get the first actionable spec from the queue.
205
+ * "Actionable" = status is not "done" or "complete".
206
+ * @param {string} cwd - Working directory
207
+ * @param {boolean} raw - Output raw string
208
+ */
209
+ function cmdQueueNext(cwd, raw) {
210
+ const statePath = path.join(cwd, '.specflow', 'STATE.md');
211
+ const content = safeReadFile(statePath);
212
+
213
+ if (!content) {
214
+ error('STATE.md not found at ' + statePath);
215
+ }
216
+
217
+ const queue = parseQueueTable(content);
218
+
219
+ const next = queue.find(entry => {
220
+ const s = entry.status.toLowerCase();
221
+ return s !== 'done' && s !== 'complete';
222
+ });
223
+
224
+ if (next) {
225
+ output({
226
+ id: next.id,
227
+ title: next.title,
228
+ status: next.status,
229
+ priority: next.priority,
230
+ }, raw, next.id);
231
+ } else {
232
+ output({ id: null }, raw, '');
233
+ }
234
+ }
235
+
236
+ module.exports = {
237
+ cmdStateGet,
238
+ cmdStateSetActive,
239
+ cmdQueueNext,
240
+ extractActiveSpec,
241
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * bin/lib/verify.cjs — Structure verification checks
3
+ *
4
+ * Exports: cmdVerifyStructure()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { output, safeReadFile, parseFrontmatter } = require('./core.cjs');
12
+ const { extractActiveSpec } = require('./state.cjs');
13
+
14
+ /**
15
+ * Verify .specflow/ directory structure and integrity.
16
+ * Performs 6 read-only checks. Does NOT repair issues.
17
+ *
18
+ * @param {string} cwd - Working directory
19
+ * @param {boolean} raw - Output raw string
20
+ */
21
+ function cmdVerifyStructure(cwd, raw) {
22
+ const checks = [];
23
+ const errors = [];
24
+
25
+ // Check 1: .specflow/ directory exists
26
+ const specflowDir = path.join(cwd, '.specflow');
27
+ const specflowExists = fs.existsSync(specflowDir) && fs.statSync(specflowDir).isDirectory();
28
+ checks.push({ name: '.specflow/ directory exists', passed: specflowExists });
29
+ if (!specflowExists) {
30
+ errors.push('.specflow/ directory not found');
31
+ }
32
+
33
+ // Check 2: STATE.md exists and has "Active Specification" section
34
+ const statePath = path.join(cwd, '.specflow', 'STATE.md');
35
+ const stateContent = safeReadFile(statePath);
36
+ const stateHasSection = stateContent ? stateContent.includes('## Active Specification') : false;
37
+ const stateValid = !!stateContent && stateHasSection;
38
+ checks.push({ name: 'STATE.md exists with Active Specification section', passed: stateValid });
39
+ if (!stateContent) {
40
+ errors.push('STATE.md not found');
41
+ } else if (!stateHasSection) {
42
+ errors.push('STATE.md missing "## Active Specification" section');
43
+ }
44
+
45
+ // Check 3: specs/ directory exists
46
+ const specsDir = path.join(cwd, '.specflow', 'specs');
47
+ const specsDirExists = fs.existsSync(specsDir) && fs.statSync(specsDir).isDirectory();
48
+ checks.push({ name: '.specflow/specs/ directory exists', passed: specsDirExists });
49
+ if (!specsDirExists) {
50
+ errors.push('.specflow/specs/ directory not found');
51
+ }
52
+
53
+ // Check 4: archive/ directory exists
54
+ const archiveDir = path.join(cwd, '.specflow', 'archive');
55
+ const archiveDirExists = fs.existsSync(archiveDir) && fs.statSync(archiveDir).isDirectory();
56
+ checks.push({ name: '.specflow/archive/ directory exists', passed: archiveDirExists });
57
+ if (!archiveDirExists) {
58
+ errors.push('.specflow/archive/ directory not found');
59
+ }
60
+
61
+ // Check 5: Active spec referenced in STATE.md exists in specs/ (if set)
62
+ let activeSpecCheck = true;
63
+ if (stateContent) {
64
+ const activeSpec = extractActiveSpec(stateContent);
65
+
66
+ if (activeSpec && activeSpec !== 'None' && activeSpec !== '(none)') {
67
+ const activeSpecPath = path.join(specsDir, activeSpec + '.md');
68
+ const activeSpecExists = fs.existsSync(activeSpecPath);
69
+ activeSpecCheck = activeSpecExists;
70
+ if (!activeSpecExists) {
71
+ errors.push('Active spec ' + activeSpec + ' not found in specs/');
72
+ }
73
+ }
74
+ }
75
+ checks.push({ name: 'Active spec exists in specs/ (if set)', passed: activeSpecCheck });
76
+
77
+ // Check 6: All specs in specs/ have valid YAML frontmatter with required fields
78
+ let allSpecsValid = true;
79
+ if (specsDirExists) {
80
+ let specFiles;
81
+ try {
82
+ specFiles = fs.readdirSync(specsDir).filter(f => f.endsWith('.md'));
83
+ } catch (e) {
84
+ specFiles = [];
85
+ }
86
+
87
+ for (const file of specFiles) {
88
+ const content = safeReadFile(path.join(specsDir, file));
89
+ if (!content) {
90
+ allSpecsValid = false;
91
+ errors.push('Cannot read spec file: ' + file);
92
+ continue;
93
+ }
94
+
95
+ const parsed = parseFrontmatter(content);
96
+ const fm = parsed.frontmatter;
97
+ const requiredFields = ['id', 'type', 'status'];
98
+ const missingFields = requiredFields.filter(f => !fm[f]);
99
+
100
+ if (missingFields.length > 0) {
101
+ allSpecsValid = false;
102
+ errors.push(file + ' missing required frontmatter fields: ' + missingFields.join(', '));
103
+ }
104
+ }
105
+ } else {
106
+ allSpecsValid = false;
107
+ }
108
+ checks.push({ name: 'All specs have valid frontmatter (id, type, status)', passed: allSpecsValid });
109
+
110
+ const valid = checks.every(c => c.passed);
111
+
112
+ output({ valid, checks, errors }, raw, valid ? 'valid' : 'invalid');
113
+ }
114
+
115
+ module.exports = {
116
+ cmdVerifyStructure,
117
+ };
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * bin/sf-tools.cjs — Centralized CLI for SpecFlow operations
5
+ *
6
+ * Usage: node bin/sf-tools.cjs <command> [args...]
7
+ *
8
+ * Commands:
9
+ * spec load <id> Parse spec file, return frontmatter + body
10
+ * spec list List all specs
11
+ * spec next-id Next available SPEC-XXX number
12
+ * queue next First actionable spec from queue
13
+ * state get Current active spec, status, next step
14
+ * state set-active <id> <status> [next] Update active spec in STATE.md
15
+ * resolve-model <agent-type> Model for agent by current profile
16
+ * verify-structure Check .specflow/ integrity
17
+ * generate-slug <text> Text to URL-safe slug
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const { output, error, generateSlug } = require('./lib/core.cjs');
23
+ const { cmdStateGet, cmdStateSetActive, cmdQueueNext } = require('./lib/state.cjs');
24
+ const { cmdSpecLoad, cmdSpecList, cmdSpecNextId } = require('./lib/spec.cjs');
25
+ const { cmdResolveModel } = require('./lib/config.cjs');
26
+ const { cmdVerifyStructure } = require('./lib/verify.cjs');
27
+
28
+ const cwd = process.cwd();
29
+ const args = process.argv.slice(2);
30
+ const raw = args.includes('--raw');
31
+ const filteredArgs = args.filter(a => a !== '--raw');
32
+
33
+ /**
34
+ * Command dispatch table.
35
+ * Keys are "command subcommand" or just "command".
36
+ */
37
+ const COMMANDS = {
38
+ 'spec load': () => {
39
+ if (!filteredArgs[2]) error('Missing spec ID. Usage: spec load <id>');
40
+ cmdSpecLoad(cwd, filteredArgs[2], raw);
41
+ },
42
+ 'spec list': () => cmdSpecList(cwd, raw),
43
+ 'spec next-id': () => cmdSpecNextId(cwd, raw),
44
+ 'queue next': () => cmdQueueNext(cwd, raw),
45
+ 'state get': () => cmdStateGet(cwd, raw),
46
+ 'state set-active': () => {
47
+ if (!filteredArgs[2] || !filteredArgs[3]) {
48
+ error('Missing arguments. Usage: state set-active <id> <status> [next_step]');
49
+ }
50
+ cmdStateSetActive(cwd, filteredArgs[2], filteredArgs[3], filteredArgs[4], raw);
51
+ },
52
+ 'resolve-model': () => {
53
+ if (!filteredArgs[1]) error('Missing agent type. Usage: resolve-model <agent-type>');
54
+ cmdResolveModel(cwd, filteredArgs[1], raw);
55
+ },
56
+ 'verify-structure': () => cmdVerifyStructure(cwd, raw),
57
+ 'generate-slug': () => {
58
+ if (!filteredArgs[1]) error('Missing text. Usage: generate-slug <text>');
59
+ output({ slug: generateSlug(filteredArgs[1]) }, raw, generateSlug(filteredArgs[1]));
60
+ },
61
+ };
62
+
63
+ function printUsage() {
64
+ const usage = `sf-tools — SpecFlow CLI
65
+
66
+ Usage: node bin/sf-tools.cjs <command> [args...] [--raw]
67
+
68
+ Commands:
69
+ spec load <id> Parse spec file, return frontmatter + body
70
+ spec list List all specs from .specflow/specs/
71
+ spec next-id Next available SPEC-XXX number
72
+ queue next First actionable spec from queue table
73
+ state get Current active spec, status, next step
74
+ state set-active <id> <status> [next] Update active spec, status, next step
75
+ resolve-model <agent-type> Resolve model for agent by current profile
76
+ verify-structure Check .specflow/ directory integrity
77
+ generate-slug <text> Convert text to URL-safe slug
78
+
79
+ Options:
80
+ --raw Output plain string instead of JSON
81
+
82
+ All commands output JSON to stdout. Errors go to stderr with exit code 1.
83
+ `;
84
+ process.stdout.write(usage);
85
+ }
86
+
87
+ // Main dispatch
88
+ if (filteredArgs.length === 0) {
89
+ printUsage();
90
+ process.exit(0);
91
+ }
92
+
93
+ // Try two-word command first, then single-word
94
+ const twoWord = filteredArgs[0] + ' ' + (filteredArgs[1] || '');
95
+ const oneWord = filteredArgs[0];
96
+
97
+ if (COMMANDS[twoWord]) {
98
+ COMMANDS[twoWord]();
99
+ } else if (COMMANDS[oneWord]) {
100
+ COMMANDS[oneWord]();
101
+ } else {
102
+ error('Unknown command: ' + filteredArgs.join(' ') + '. Run without arguments for usage help.');
103
+ }
@@ -175,7 +175,7 @@ mkdir -p .specflow/archive
175
175
  Update frontmatter:
176
176
  - status → "done"
177
177
 
178
- Add completion timestamp:
178
+ Add completion summary section to the spec:
179
179
 
180
180
  ```markdown
181
181
  ---
@@ -185,14 +185,33 @@ Add completion timestamp:
185
185
  **Completed:** {date} {time}
186
186
  **Total Commits:** {count from Execution Summary}
187
187
  **Review Cycles:** {count of Review v[N] entries}
188
+
189
+ ### Outcome
190
+
191
+ {1-2 sentence summary of what was delivered}
192
+
193
+ ### Key Files
194
+
195
+ - `{path}` — {what it does/why it matters}
196
+
197
+ ### Patterns Established
198
+
199
+ {List any new patterns, conventions, or architectural decisions introduced.
200
+ If none: "None — followed existing patterns."}
201
+
202
+ ### Deviations
203
+
204
+ {Any deviations from the original spec during implementation.
205
+ If none: "None — implemented as specified."}
188
206
  ```
189
207
 
190
208
  ## Step 7: Extract Decisions
191
209
 
192
- Scan specification for important decisions:
210
+ Scan specification and Completion section for important decisions:
193
211
  - Technology choices mentioned in Context or Assumptions
194
212
  - Patterns established during implementation
195
213
  - Constraints discovered
214
+ - Deviations that became new conventions
196
215
 
197
216
  If significant decisions found, add to STATE.md Decisions table:
198
217