specflow-cc 1.12.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,80 @@ All notable changes to SpecFlow will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.14.0] - 2026-03-05
9
+
10
+ ### Added
11
+
12
+ - **Context Monitor Hook** — agent-facing context awareness via PostToolUse hook
13
+ - Statusline writes bridge file to `/tmp/claude-ctx-{session}.json`
14
+ - New `hooks/context-monitor.js` reads metrics and injects WARNING (35% remaining) / CRITICAL (25%) warnings into agent context
15
+ - Debounce (5 tool uses between warnings), severity escalation bypasses debounce
16
+ - Integrates with `/sf:pause` for graceful session saves
17
+ - Installer auto-registers the hook in settings.json
18
+
19
+ - **`/sf:health`** — diagnose `.specflow/` directory integrity
20
+ - 13 error codes across 3 severity levels (error, warning, info)
21
+ - Checks: STATE.md integrity, orphaned specs, queue consistency, missing directories, stale execution state
22
+ - `--repair` flag for safe auto-fixes (create missing dirs, regenerate STATE.md, clear stale state)
23
+ - Repair verification: re-runs checks after repair to confirm resolution
24
+
25
+ - **`/sf:validate`** — run validation checklist from specification
26
+ - Executes automated checks (test commands), code verifications (grep/glob), and manual prompts
27
+ - Pass/fail report per checklist item with overall validation status
28
+ - Graceful handling when spec has no validation checklist
29
+
30
+ - **Validation Checklist in spec template** — spec-creator generates `## Validation Checklist` section for medium/large specs
31
+ - 3-5 concrete verification steps with expected outcomes
32
+ - Each item: action + expected result (e.g., "Run `npm test` — all pass")
33
+
34
+ - **Enriched completion summaries** in `/sf:done`
35
+ - New sections: Outcome, Key Files, Patterns Established, Deviations
36
+ - Decisions extracted from both spec content and completion section
37
+
38
+ - **Centralized CLI Tooling** (`bin/sf-tools.cjs`) — single Node.js CLI for SpecFlow operations
39
+ - `spec load <id>` — parse spec file, return frontmatter + sections as JSON
40
+ - `spec list [--status <s>]` — list specs with optional status filter
41
+ - `spec next-id` — next available SPEC-XXX number (checks specs/ + archive/)
42
+ - `queue next` — first actionable spec from queue
43
+ - `state get` / `state set-active <id> <status>` — STATE.md CRUD
44
+ - `resolve-model <agent-type>` — model resolution by profile
45
+ - `verify-structure` — `.specflow/` integrity checks
46
+ - `generate-slug <text>` — URL-safe slug generation
47
+ - Modular architecture: `bin/lib/core.cjs`, `state.cjs`, `spec.cjs`, `config.cjs`, `verify.cjs`
48
+ - 42 tests using Node.js `assert` (no external dependencies)
49
+
50
+ ### Fixed
51
+
52
+ - Parent spec now correctly archived after `/sf:split`
53
+ - `.specflow/` directory excluded from git tracking
54
+
55
+ ---
56
+
57
+ ## [1.13.0] - 2026-02-11
58
+
59
+ ### Added
60
+
61
+ - **Autopilot mode** (`/sf:autopilot`) — run the full spec lifecycle autonomously
62
+ - Single spec: `/sf:autopilot` or `/sf:autopilot SPEC-XXX`
63
+ - Batch mode: `/sf:autopilot --all` processes entire queue sequentially
64
+ - Cycle detection: configurable limits for audit (default: 3) and fix (default: 3) cycles
65
+ - Graceful halt on `needs_decomposition` or `paused` specs
66
+ - Summary report with per-spec outcomes and cycle counts
67
+ - Agent failure handling: continues batch on single-spec failure
68
+ - Configurable via `.specflow/config.json` under `"autopilot"` key
69
+
70
+ ### Changed
71
+
72
+ - **Replaced all Bash/awk/sed markdown mutations** with Read+Write tool instructions across 13 agent and command files
73
+ - Eliminates fragile shell-based file editing that could corrupt markdown structure
74
+ - All STATE.md, spec, and archive updates now use explicit Read→Write pattern
75
+ - Affected: spec-creator, spec-auditor, spec-reviser, spec-splitter, spec-executor, spec-executor-orchestrator, impl-reviewer, and 6 command files
76
+
77
+ - `/sf:help` — added Autonomous Execution section with autopilot commands
78
+ - README — added autopilot to workflow diagram, commands table, and typical session
79
+
80
+ ---
81
+
8
82
  ## [1.12.0] - 2026-02-10
9
83
 
10
84
  ### Added
package/README.md CHANGED
@@ -200,15 +200,16 @@ If issues are found, `/sf:fix` addresses them. Loop until approved.
200
200
 
201
201
  ---
202
202
 
203
- ### 5. Verify (Optional)
203
+ ### 5. Validate & Verify (Optional)
204
204
 
205
205
  ```
206
+ /sf:validate
206
207
  /sf:verify
207
208
  ```
208
209
 
209
- Automated checks confirm code exists and tests pass. But does it actually *work*?
210
+ **Validate** runs the spec's validation checklist — automated test commands, code checks, and manual verification prompts. Pass/fail per item.
210
211
 
211
- This step walks you through manual verification:
212
+ **Verify** walks you through interactive acceptance testing:
212
213
 
213
214
  - "Can you log in with OAuth?"
214
215
  - "Does the redirect work?"
@@ -216,7 +217,7 @@ This step walks you through manual verification:
216
217
 
217
218
  You confirm each item. If something's broken, the system helps diagnose and creates fix plans.
218
219
 
219
- **Creates:** Verification record
220
+ **Creates:** Validation/verification record
220
221
 
221
222
  ---
222
223
 
@@ -237,10 +238,12 @@ Your spec becomes documentation: why the code exists, what decisions were made,
237
238
  ```
238
239
  /sf:quick (trivial tasks)
239
240
 
240
- /sf:new /sf:audit /sf:run /sf:review /sf:verify /sf:done
241
-
242
- /sf:revise /sf:fix (optional UAT)
243
- (if needed) (if needed)
241
+ /sf:new /sf:audit /sf:run /sf:review /sf:validate → /sf:verify /sf:done
242
+
243
+ /sf:revise /sf:fix (checklist) (optional UAT)
244
+ (if needed) (if needed)
245
+
246
+ /sf:autopilot — runs the entire flow above automatically
244
247
  ```
245
248
 
246
249
  **Key principle:** Audits and reviews run in fresh context — no bias from creation.
@@ -310,8 +313,10 @@ Six months later, you can read the spec and understand not just *what* was built
310
313
  | `/sf:run` | Implement specification |
311
314
  | `/sf:review` | Review implementation (fresh context) |
312
315
  | `/sf:fix` | Fix based on review feedback |
316
+ | `/sf:validate` | Run validation checklist from spec |
313
317
  | `/sf:verify` | Interactive user acceptance testing |
314
318
  | `/sf:done` | Complete and archive |
319
+ | `/sf:autopilot` | Run full lifecycle autonomously |
315
320
 
316
321
  **Quick mode:**
317
322
 
@@ -393,6 +398,7 @@ before showing interactive options.
393
398
  | `/sf:deps` | Show spec dependencies |
394
399
  | `/sf:pause` | Save session context |
395
400
  | `/sf:resume` | Restore session |
401
+ | `/sf:health` | Diagnose `.specflow/` integrity |
396
402
  | `/sf:help` | Command reference |
397
403
 
398
404
  ---
@@ -455,6 +461,10 @@ Use `max` for maximum quality everywhere, `quality` for critical features, `budg
455
461
  /sf:review # Fresh context review
456
462
  /sf:verify # Manual verification
457
463
  /sf:done # Archive
464
+
465
+ # Or skip manual steps — run everything autonomously
466
+ /sf:autopilot # Process active spec end-to-end
467
+ /sf:autopilot --all # Process entire queue
458
468
  ```
459
469
 
460
470
  ---
@@ -474,6 +484,10 @@ Use `max` for maximum quality everywhere, `quality` for critical features, `budg
474
484
  - Use `/sf:split` to decompose into smaller specs
475
485
  - Or let the system auto-decompose during `/sf:run`
476
486
 
487
+ **STATE.md or queue seems corrupted?**
488
+ - Run `/sf:health` to diagnose issues
489
+ - Use `/sf:health --repair` for safe auto-fixes
490
+
477
491
  ---
478
492
 
479
493
  ## Philosophy
@@ -288,6 +288,14 @@ Append to specification's Review History:
288
288
  - If APPROVED: Status → "done", Next Step → "/sf:done"
289
289
  - If CHANGES_REQUESTED: Status → "review", Next Step → "/sf:fix"
290
290
 
291
+ Update STATE.md by reading the current file content, then writing the updated file with:
292
+ - "**Status:**" line changed to the new status
293
+ - "**Next Step:**" line changed to the new next step
294
+ - No other content modified
295
+
296
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
297
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
298
+
291
299
  </process>
292
300
 
293
301
  <output>
@@ -389,6 +389,14 @@ Update ONLY the Current Position section:
389
389
  - Status → "review"
390
390
  - Next Step → "/sf:review"
391
391
 
392
+ Update STATE.md by reading the current file content, then writing the updated file with:
393
+ - "**Status:**" line changed to the new status
394
+ - "**Next Step:**" line changed to the new next step
395
+ - No other content modified
396
+
397
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
398
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
399
+
392
400
  **CRITICAL — DO NOT go beyond this:**
393
401
  - Do NOT move the spec to Completed Specifications table
394
402
  - Do NOT remove the spec from Queue table
@@ -685,6 +685,14 @@ Update status:
685
685
  - If NEEDS_DECOMPOSITION: Status → "needs_decomposition", Next Step → "/sf:split or /sf:run --parallel"
686
686
  - If NEEDS_REVISION: Status → "revision_requested", Next Step → "/sf:revise"
687
687
 
688
+ Update STATE.md by reading the current file content, then writing the updated file with:
689
+ - "**Status:**" line changed to the new status
690
+ - "**Next Step:**" line changed to the new next step
691
+ - No other content modified
692
+
693
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
694
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
695
+
688
696
  </process>
689
697
 
690
698
  <output>
@@ -146,8 +146,9 @@ Write to `.specflow/specs/SPEC-XXX.md` using the template structure:
146
146
  4. **Task:** What to do
147
147
  5. **Requirements:** Files, interfaces, deletions
148
148
  6. **Acceptance Criteria:** Specific, measurable
149
- 7. **Constraints:** What NOT to do
150
- 8. **Assumptions:** What you assumed (clearly marked)
149
+ 7. **Validation Checklist** (medium/large specs only): 3-5 concrete verification steps with expected outcomes. Each item = action + expected result. Examples: "Run `npm test` — all pass", "POST /api/users with invalid email — returns 422", "Open settings page — new toggle visible"
150
+ 8. **Constraints:** What NOT to do
151
+ 9. **Assumptions:** What you assumed (clearly marked)
151
152
  - **If `<prior_discussion>` provided:** Decisions from discussion are facts, not assumptions
152
153
 
153
154
  ## Step 5.5: Generate Implementation Tasks (for medium and large specs)
@@ -241,6 +242,16 @@ Update `.specflow/STATE.md`:
241
242
  - Set Next Step to "/sf:audit"
242
243
  - Add spec to Queue
243
244
 
245
+ Update STATE.md by reading the current file content, then writing the updated file with:
246
+ - "**Active Specification:**" line changed to the new spec
247
+ - "**Status:**" line changed to "drafting"
248
+ - "**Next Step:**" line changed to "/sf:audit"
249
+ - Queue table updated with new spec entry
250
+ - No other content modified
251
+
252
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
253
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
254
+
244
255
  **If `<prior_discussion>` provided:**
245
256
  Update the discussion file (PRE-XXX.md or DISC-XXX.md):
246
257
  - Set `used_by: SPEC-XXX` in frontmatter
@@ -330,6 +330,10 @@ Write `.specflow/execution/SPEC-XXX-state.json`:
330
330
  | SPEC-XXX | orchestrated | Wave 0/{total} (0%) | {timestamp} |
331
331
  ```
332
332
 
333
+ Update STATE.md by reading the current file content, then writing the updated file with the Execution Status table row added/updated.
334
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
335
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
336
+
333
337
  ## Step 3: Execute Waves
334
338
 
335
339
  For each wave:
@@ -597,6 +601,10 @@ After wave completes (all groups done):
597
601
  | SPEC-XXX | orchestrated | Wave 2/{total} (67%) | {timestamp} |
598
602
  ```
599
603
 
604
+ Update STATE.md by reading the current file content, then writing the updated file with the Execution Status table row updated for this wave.
605
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
606
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
607
+
600
608
  ## Step 4: Aggregate Results
601
609
 
602
610
  Combine all worker results:
@@ -683,6 +691,10 @@ rm .specflow/execution/SPEC-XXX-state.json
683
691
  - Change row to show "Complete" or remove row entirely
684
692
  - Or archive: `mv .specflow/execution/SPEC-XXX-state.json .specflow/execution/archive/`
685
693
 
694
+ Update STATE.md by reading the current file content, then writing the updated file with the Execution Status table row removed or updated to "Complete".
695
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
696
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
697
+
686
698
  **Note:** Only delete on FULL success. If any groups failed or are partial, keep state file for potential retry.
687
699
 
688
700
  ## Step 7: Update STATE.md
@@ -692,6 +704,15 @@ Update ONLY the Current Position section:
692
704
  - Next Step → "/sf:review"
693
705
  - Remove or update Execution Status row
694
706
 
707
+ Update STATE.md by reading the current file content, then writing the updated file with:
708
+ - "**Status:**" line changed to the new status
709
+ - "**Next Step:**" line changed to the new next step
710
+ - Execution Status row removed or updated
711
+ - No other content modified
712
+
713
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
714
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
715
+
695
716
  **CRITICAL — DO NOT go beyond this:**
696
717
  - Do NOT move the spec to Completed Specifications table
697
718
  - Do NOT remove the spec from Queue table
@@ -268,6 +268,14 @@ Update ONLY the Current Position section:
268
268
  - Status → "review"
269
269
  - Next Step → "/sf:review"
270
270
 
271
+ Update STATE.md by reading the current file content, then writing the updated file with:
272
+ - "**Status:**" line changed to the new status
273
+ - "**Next Step:**" line changed to the new next step
274
+ - No other content modified
275
+
276
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
277
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
278
+
271
279
  **CRITICAL — DO NOT go beyond this:**
272
280
  - Do NOT move the spec to Completed Specifications table
273
281
  - Do NOT remove the spec from Queue table
@@ -139,6 +139,14 @@ Set status to "auditing" (ready for re-audit).
139
139
  - Status → "auditing"
140
140
  - Next Step → "/sf:audit"
141
141
 
142
+ Update STATE.md by reading the current file content, then writing the updated file with:
143
+ - "**Status:**" line changed to the new status
144
+ - "**Next Step:**" line changed to the new next step
145
+ - No other content modified
146
+
147
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
148
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
149
+
142
150
  </process>
143
151
 
144
152
  <output>
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: sf-spec-splitter
3
3
  description: Analyzes large specifications and splits them into manageable sub-specifications with dependencies
4
- tools: Read, Write, Glob, Grep
4
+ tools: Read, Write, Glob, Grep, Bash
5
5
  ---
6
6
 
7
7
  <role>
@@ -188,6 +188,16 @@ Update `.specflow/STATE.md`:
188
188
  - Set first child (no dependencies) as Active Specification
189
189
  - Add note to Decisions: "Split SPEC-XXX into N parts"
190
190
 
191
+ Update STATE.md by reading the current file content, then writing the updated file with:
192
+ - Parent spec removed from Queue table
193
+ - All child specs added to Queue table in dependency order
194
+ - First child (no dependencies) set as Active Specification
195
+ - Decisions section updated with split note
196
+ - No other content modified
197
+
198
+ Use the Read tool to read `.specflow/STATE.md`, then use the Write tool to write the updated content.
199
+ Do NOT use Bash (awk, sed, or echo) to modify `.specflow/STATE.md`.
200
+
191
201
  </process>
192
202
 
193
203
  <output>
package/bin/install.js CHANGED
@@ -266,6 +266,26 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
266
266
  console.log(` ${green}✓${reset} Configured statusline`);
267
267
  }
268
268
 
269
+ // Configure context-monitor hook
270
+ const isGlobal = statuslineCommand.includes('$HOME');
271
+ const monitorCommand = isGlobal
272
+ ? 'node "$HOME/.claude/hooks/context-monitor.js"'
273
+ : 'node .claude/hooks/context-monitor.js';
274
+
275
+ if (!settings.hooks) settings.hooks = {};
276
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
277
+
278
+ const hasMonitor = settings.hooks.PostToolUse.some(h =>
279
+ h.command && h.command.includes('context-monitor')
280
+ );
281
+ if (!hasMonitor) {
282
+ settings.hooks.PostToolUse.push({
283
+ type: 'command',
284
+ command: monitorCommand
285
+ });
286
+ console.log(` ${green}✓${reset} Configured context monitor hook`);
287
+ }
288
+
269
289
  writeSettings(settingsPath, settings);
270
290
 
271
291
  console.log(`
@@ -0,0 +1,91 @@
1
+ /**
2
+ * bin/lib/config.cjs — Config reading and model resolution
3
+ *
4
+ * Exports: MODEL_PROFILES, loadConfig(), cmdResolveModel()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const path = require('path');
10
+ const { output, error, safeReadFile } = require('./core.cjs');
11
+
12
+ /**
13
+ * Hardcoded MODEL_PROFILES table.
14
+ * MUST exactly match the profile table in commands/sf/run.md.
15
+ */
16
+ const MODEL_PROFILES = {
17
+ 'spec-creator': { max: 'opus', quality: 'opus', balanced: 'opus', budget: 'sonnet' },
18
+ 'spec-auditor': { max: 'opus', quality: 'opus', balanced: 'opus', budget: 'sonnet' },
19
+ 'spec-splitter': { max: 'opus', quality: 'opus', balanced: 'opus', budget: 'sonnet' },
20
+ 'discusser': { max: 'opus', quality: 'opus', balanced: 'opus', budget: 'sonnet' },
21
+ 'spec-executor': { max: 'opus', quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
22
+ 'spec-executor-orchestrator': { max: 'opus', quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
23
+ 'spec-executor-worker': { max: 'opus', quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
24
+ 'impl-reviewer': { max: 'opus', quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
25
+ 'spec-reviser': { max: 'opus', quality: 'sonnet', balanced: 'sonnet', budget: 'sonnet' },
26
+ 'researcher': { max: 'opus', quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
27
+ 'codebase-scanner': { max: 'opus', quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
28
+ };
29
+
30
+ /**
31
+ * Load .specflow/config.json. Returns defaults if not found.
32
+ * @param {string} cwd - Working directory
33
+ * @returns {Object} Config object
34
+ */
35
+ function loadConfig(cwd) {
36
+ const configPath = path.join(cwd, '.specflow', 'config.json');
37
+ const content = safeReadFile(configPath);
38
+
39
+ if (!content) {
40
+ return { model_profile: 'balanced' };
41
+ }
42
+
43
+ try {
44
+ const config = JSON.parse(content);
45
+ return {
46
+ model_profile: config.model_profile || 'balanced',
47
+ ...config,
48
+ };
49
+ } catch (e) {
50
+ return { model_profile: 'balanced' };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Resolve model for a given agent type based on current profile.
56
+ * @param {string} cwd - Working directory
57
+ * @param {string} agentType - Agent type (e.g., "spec-executor")
58
+ * @param {boolean} raw - Output raw string
59
+ */
60
+ function cmdResolveModel(cwd, agentType, raw) {
61
+ if (!agentType) {
62
+ error('Missing agent type. Usage: resolve-model <agent-type>');
63
+ }
64
+
65
+ const config = loadConfig(cwd);
66
+ const profile = config.model_profile || 'balanced';
67
+
68
+ const agentProfiles = MODEL_PROFILES[agentType];
69
+
70
+ if (!agentProfiles) {
71
+ error('Unknown agent type: ' + agentType + '. Known types: ' + Object.keys(MODEL_PROFILES).join(', '));
72
+ }
73
+
74
+ const model = agentProfiles[profile];
75
+
76
+ if (!model) {
77
+ error('Unknown profile: ' + profile + '. Known profiles: max, quality, balanced, budget');
78
+ }
79
+
80
+ output({
81
+ agent: agentType,
82
+ profile: profile,
83
+ model: model,
84
+ }, raw, model);
85
+ }
86
+
87
+ module.exports = {
88
+ MODEL_PROFILES,
89
+ loadConfig,
90
+ cmdResolveModel,
91
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * bin/lib/core.cjs — Shared utilities for sf-tools CLI
3
+ *
4
+ * Exports: output(), error(), safeReadFile(), parseFrontmatter(), generateSlug()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Output result to stdout as JSON, or raw string if raw flag is set.
14
+ * @param {*} result - The result object to output
15
+ * @param {boolean} raw - If true, output rawValue as plain string
16
+ * @param {string} [rawValue] - Plain string to output when raw is true
17
+ */
18
+ function output(result, raw, rawValue) {
19
+ if (raw && rawValue !== undefined) {
20
+ process.stdout.write(String(rawValue) + '\n');
21
+ } else {
22
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Output error message to stderr and exit with code 1.
28
+ * @param {string} message - Error message
29
+ */
30
+ function error(message) {
31
+ process.stderr.write('Error: ' + message + '\n');
32
+ process.exit(1);
33
+ }
34
+
35
+ /**
36
+ * Safely read a file, returning its contents as a string or null if not found.
37
+ * @param {string} filePath - Absolute or relative path to the file
38
+ * @returns {string|null} File contents or null
39
+ */
40
+ function safeReadFile(filePath) {
41
+ try {
42
+ return fs.readFileSync(filePath, 'utf8');
43
+ } catch (e) {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Parse YAML frontmatter from markdown content.
50
+ * Expects `---` delimited YAML at the start of the file.
51
+ * Returns simple key: value pairs only (no nested objects, no arrays).
52
+ * All values are returned as strings. No type coercion is performed.
53
+ *
54
+ * @param {string} content - Full file content
55
+ * @returns {{ frontmatter: Object, body: string }}
56
+ */
57
+ function parseFrontmatter(content) {
58
+ if (!content || typeof content !== 'string') {
59
+ return { frontmatter: {}, body: content || '' };
60
+ }
61
+
62
+ const trimmed = content.trimStart();
63
+
64
+ if (!trimmed.startsWith('---')) {
65
+ return { frontmatter: {}, body: content };
66
+ }
67
+
68
+ // Find the closing ---
69
+ const secondDash = trimmed.indexOf('---', 3);
70
+ if (secondDash === -1) {
71
+ return { frontmatter: {}, body: content };
72
+ }
73
+
74
+ const yamlBlock = trimmed.substring(3, secondDash).trim();
75
+ const body = trimmed.substring(secondDash + 3).replace(/^\r?\n/, '');
76
+
77
+ const frontmatter = {};
78
+ const lines = yamlBlock.split('\n');
79
+
80
+ for (const line of lines) {
81
+ const trimmedLine = line.trim();
82
+ if (!trimmedLine || trimmedLine.startsWith('#')) continue;
83
+
84
+ const colonIndex = trimmedLine.indexOf(':');
85
+ if (colonIndex === -1) continue;
86
+
87
+ const key = trimmedLine.substring(0, colonIndex).trim();
88
+ const value = trimmedLine.substring(colonIndex + 1).trim();
89
+
90
+ // All values returned as strings — no type coercion
91
+ frontmatter[key] = value;
92
+ }
93
+
94
+ return { frontmatter, body };
95
+ }
96
+
97
+ /**
98
+ * Generate a URL-safe slug from text.
99
+ * Lowercase, replace spaces/special chars with hyphens, collapse multiples, trim edges.
100
+ *
101
+ * @param {string} text - Input text
102
+ * @returns {string} URL-safe slug
103
+ */
104
+ function generateSlug(text) {
105
+ if (!text || typeof text !== 'string') return '';
106
+
107
+ return text
108
+ .toLowerCase()
109
+ .replace(/[^a-z0-9]+/g, '-') // replace non-alphanumeric with hyphens
110
+ .replace(/-+/g, '-') // collapse multiple hyphens
111
+ .replace(/^-|-$/g, ''); // trim leading/trailing hyphens
112
+ }
113
+
114
+ module.exports = {
115
+ output,
116
+ error,
117
+ safeReadFile,
118
+ parseFrontmatter,
119
+ generateSlug,
120
+ };