wogiflow 1.0.11 → 1.0.13

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 (46) hide show
  1. package/.workflow/specs/architecture.md.template +24 -0
  2. package/.workflow/specs/stack.md.template +33 -0
  3. package/.workflow/specs/testing.md.template +36 -0
  4. package/README.md +90 -1
  5. package/lib/unified-wizard.js +569 -30
  6. package/package.json +1 -1
  7. package/scripts/MEMORY-ARCHITECTURE.md +150 -0
  8. package/scripts/flow +20 -19
  9. package/scripts/flow-auto-context.js +97 -3
  10. package/scripts/flow-conflict-resolver.js +735 -0
  11. package/scripts/flow-context-gatherer.js +520 -0
  12. package/scripts/flow-context-monitor.js +148 -19
  13. package/scripts/flow-damage-control.js +5 -1
  14. package/scripts/flow-export-profile +168 -1
  15. package/scripts/flow-import-profile +257 -6
  16. package/scripts/flow-instruction-richness.js +182 -18
  17. package/scripts/flow-knowledge-router.js +2 -0
  18. package/scripts/flow-knowledge-sync.js +2 -0
  19. package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
  20. package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
  21. package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
  22. package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
  23. package/scripts/flow-memory-db.js +386 -1
  24. package/scripts/flow-memory-sync.js +2 -0
  25. package/scripts/flow-model-adapter.js +53 -29
  26. package/scripts/flow-model-router.js +246 -1
  27. package/scripts/flow-morning.js +94 -0
  28. package/scripts/flow-onboard +223 -10
  29. package/scripts/flow-orchestrate-validation.js +539 -0
  30. package/scripts/flow-orchestrate.js +16 -507
  31. package/scripts/flow-pattern-extractor.js +1265 -0
  32. package/scripts/flow-prompt-composer.js +222 -2
  33. package/scripts/flow-quality-guard.js +594 -0
  34. package/scripts/flow-section-index.js +713 -0
  35. package/scripts/flow-section-resolver.js +484 -0
  36. package/scripts/flow-session-end.js +188 -2
  37. package/scripts/flow-skill-create.js +19 -3
  38. package/scripts/flow-skill-matcher.js +122 -7
  39. package/scripts/flow-statusline-setup.js +218 -0
  40. package/scripts/flow-step-review.js +19 -0
  41. package/scripts/flow-tech-debt.js +734 -0
  42. package/scripts/flow-utils.js +2 -0
  43. package/scripts/hooks/core/long-input-gate.js +293 -0
  44. package/scripts/flow-parallel-detector.js +0 -399
  45. package/scripts/flow-parallel-dispatch.js +0 -987
  46. /package/scripts/{flow-transcript-language.js → flow-long-input-language.js} +0 -0
@@ -0,0 +1,713 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Section Index Generator
5
+ *
6
+ * Creates a section-level index from decisions.md and app-map.md for
7
+ * targeted context loading. Enables "pin" lookups and section references.
8
+ *
9
+ * Features:
10
+ * - Parses decisions.md into indexed sections with semantic pins
11
+ * - Parses app-map.md tables into indexed rows
12
+ * - Auto-regenerates on file change (via watcher)
13
+ * - Supports content hashing for change detection
14
+ *
15
+ * Part of Smart Context System (Phase 1)
16
+ *
17
+ * Usage:
18
+ * node scripts/flow-section-index.js # Generate index
19
+ * node scripts/flow-section-index.js --watch # Watch for changes
20
+ * node scripts/flow-section-index.js --json # Output JSON result
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const crypto = require('crypto');
26
+ const {
27
+ PATHS,
28
+ PROJECT_ROOT,
29
+ readFile,
30
+ writeFile,
31
+ fileExists,
32
+ dirExists,
33
+ success,
34
+ warn,
35
+ info,
36
+ error,
37
+ parseFlags,
38
+ outputJson,
39
+ safeJsonParse
40
+ } = require('./flow-utils');
41
+
42
+ // Re-use existing section parser from flow-rules-sync
43
+ const { parseMarkdownSections, slugify } = require('./flow-rules-sync');
44
+
45
+ // ============================================================
46
+ // Configuration
47
+ // ============================================================
48
+
49
+ const INDEX_PATH = path.join(PATHS.state, 'section-index.json');
50
+ const DEBOUNCE_MS = 500;
51
+
52
+ // Keywords that generate semantic pins for different rule types
53
+ const PIN_KEYWORDS = {
54
+ // Error handling
55
+ 'try-catch': ['try', 'catch', 'error', 'exception', 'throw', 'safe'],
56
+ 'error-handling': ['error', 'handle', 'exception', 'fail', 'catch'],
57
+
58
+ // File operations
59
+ 'fs-read': ['fs', 'read', 'file', 'readFile', 'readFileSync'],
60
+ 'fs-write': ['fs', 'write', 'file', 'writeFile', 'writeFileSync'],
61
+ 'file-safety': ['file', 'path', 'fs', 'exists', 'check'],
62
+
63
+ // JSON operations
64
+ 'json-parse': ['json', 'parse', 'JSON.parse', 'stringify'],
65
+ 'json-safety': ['json', 'safe', 'parse', 'validate'],
66
+
67
+ // Security
68
+ 'prototype-pollution': ['prototype', '__proto__', 'constructor', 'injection'],
69
+ 'path-traversal': ['path', 'traversal', '..', 'join', 'resolve'],
70
+ 'input-validation': ['validate', 'sanitize', 'input', 'user'],
71
+
72
+ // Components
73
+ 'component-creation': ['component', 'create', 'new', 'add'],
74
+ 'component-naming': ['component', 'name', 'naming', 'convention'],
75
+ 'component-reuse': ['component', 'reuse', 'existing', 'variant'],
76
+
77
+ // Naming conventions
78
+ 'naming-convention': ['naming', 'convention', 'case', 'kebab', 'camel'],
79
+ 'file-naming': ['file', 'name', 'naming', 'kebab-case'],
80
+
81
+ // Architecture
82
+ 'model-architecture': ['model', 'architecture', 'system', 'design'],
83
+ 'api-pattern': ['api', 'endpoint', 'route', 'controller'],
84
+
85
+ // UI/UX
86
+ 'variant-naming': ['variant', 'size', 'intent', 'state', 'primary', 'secondary']
87
+ };
88
+
89
+ // ============================================================
90
+ // Pin Generation
91
+ // ============================================================
92
+
93
+ /**
94
+ * Generate semantic pins for a section based on title and content
95
+ * @param {string} title - Section title
96
+ * @param {string} content - Section content
97
+ * @returns {string[]} - Array of pins
98
+ */
99
+ function generatePins(title, content) {
100
+ const pins = new Set();
101
+ const combined = `${title} ${content}`.toLowerCase();
102
+
103
+ // Add pins based on keyword matches
104
+ for (const [pin, keywords] of Object.entries(PIN_KEYWORDS)) {
105
+ const matchCount = keywords.filter(kw => combined.includes(kw.toLowerCase())).length;
106
+ // Require at least 2 keyword matches or strong single match
107
+ if (matchCount >= 2 || (matchCount === 1 && combined.includes(pin.replace(/-/g, ' ')))) {
108
+ pins.add(pin);
109
+ }
110
+ }
111
+
112
+ // Extract significant words from title as pins
113
+ const titleWords = title
114
+ .toLowerCase()
115
+ .replace(/[^a-z0-9\s-]/g, '')
116
+ .split(/\s+/)
117
+ .filter(w => w.length > 2 && !['the', 'and', 'for', 'with', 'from'].includes(w));
118
+
119
+ titleWords.forEach(w => pins.add(w));
120
+
121
+ // Generate compound pins from title
122
+ const titleSlug = slugify(title);
123
+ pins.add(titleSlug);
124
+
125
+ return Array.from(pins);
126
+ }
127
+
128
+ /**
129
+ * Generate content hash for change detection
130
+ * @param {string} content - Content to hash
131
+ * @returns {string} - MD5 hash (first 8 chars)
132
+ */
133
+ function hashContent(content) {
134
+ return crypto.createHash('md5').update(content).digest('hex').substring(0, 8);
135
+ }
136
+
137
+ // ============================================================
138
+ // Decisions.md Parser
139
+ // ============================================================
140
+
141
+ /**
142
+ * Parse decisions.md into indexed sections with hierarchical structure
143
+ * @param {string} content - File content
144
+ * @returns {Object[]} - Array of indexed sections
145
+ */
146
+ function parseDecisionsSections(content) {
147
+ const sections = [];
148
+ const lines = content.split('\n');
149
+
150
+ let currentCategory = null;
151
+ let currentSection = null;
152
+ let currentContent = [];
153
+ let lineStart = 0;
154
+
155
+ for (let i = 0; i < lines.length; i++) {
156
+ const line = lines[i];
157
+
158
+ // Match ## headers (categories)
159
+ const categoryMatch = line.match(/^##\s+(.+)$/);
160
+ if (categoryMatch) {
161
+ // Save previous section
162
+ if (currentSection && currentContent.length > 0) {
163
+ const trimmedContent = currentContent.join('\n').trim();
164
+ if (trimmedContent && !trimmedContent.startsWith('<!--')) {
165
+ sections.push(createDecisionSection(
166
+ currentCategory,
167
+ currentSection,
168
+ trimmedContent,
169
+ lineStart,
170
+ i - 1
171
+ ));
172
+ }
173
+ }
174
+
175
+ currentCategory = categoryMatch[1].trim();
176
+ currentSection = null;
177
+ currentContent = [];
178
+ continue;
179
+ }
180
+
181
+ // Match ### headers (sections within category)
182
+ const sectionMatch = line.match(/^###\s+(.+)$/);
183
+ if (sectionMatch) {
184
+ // Save previous section
185
+ if (currentSection && currentContent.length > 0) {
186
+ const trimmedContent = currentContent.join('\n').trim();
187
+ if (trimmedContent && !trimmedContent.startsWith('<!--')) {
188
+ sections.push(createDecisionSection(
189
+ currentCategory,
190
+ currentSection,
191
+ trimmedContent,
192
+ lineStart,
193
+ i - 1
194
+ ));
195
+ }
196
+ }
197
+
198
+ currentSection = sectionMatch[1].trim();
199
+ currentContent = [];
200
+ lineStart = i + 1;
201
+ continue;
202
+ }
203
+
204
+ // Accumulate content
205
+ if (currentSection && line.trim() !== '---') {
206
+ currentContent.push(line);
207
+ } else if (currentCategory && !currentSection && line.trim() && line.trim() !== '---') {
208
+ // Content directly under category (no subsection)
209
+ currentSection = currentCategory;
210
+ currentContent.push(line);
211
+ lineStart = i;
212
+ }
213
+ }
214
+
215
+ // Save last section
216
+ if (currentSection && currentContent.length > 0) {
217
+ const trimmedContent = currentContent.join('\n').trim();
218
+ if (trimmedContent && !trimmedContent.startsWith('<!--')) {
219
+ sections.push(createDecisionSection(
220
+ currentCategory,
221
+ currentSection,
222
+ trimmedContent,
223
+ lineStart,
224
+ lines.length - 1
225
+ ));
226
+ }
227
+ }
228
+
229
+ return sections;
230
+ }
231
+
232
+ /**
233
+ * Create a decision section object
234
+ */
235
+ function createDecisionSection(category, title, content, lineStart, lineEnd) {
236
+ const categorySlug = category ? slugify(category) : 'general';
237
+ const titleSlug = slugify(title);
238
+ const id = `${categorySlug}:${titleSlug}`;
239
+
240
+ return {
241
+ id,
242
+ title,
243
+ category: category || 'General',
244
+ pins: generatePins(title, content),
245
+ lineStart: lineStart + 1, // 1-indexed
246
+ lineEnd: lineEnd + 1,
247
+ content,
248
+ contentHash: hashContent(content)
249
+ };
250
+ }
251
+
252
+ // ============================================================
253
+ // App-Map.md Parser
254
+ // ============================================================
255
+
256
+ /**
257
+ * Parse app-map.md tables into indexed rows
258
+ * @param {string} content - File content
259
+ * @returns {Object[]} - Array of indexed rows
260
+ */
261
+ function parseAppMapRows(content) {
262
+ const rows = [];
263
+ const lines = content.split('\n');
264
+
265
+ let currentCategory = null;
266
+ let tableHeaders = null;
267
+ let inTable = false;
268
+
269
+ for (let i = 0; i < lines.length; i++) {
270
+ const line = lines[i];
271
+
272
+ // Match ## headers (categories: Screens, Modals, Components)
273
+ const categoryMatch = line.match(/^##\s+(.+)$/);
274
+ if (categoryMatch) {
275
+ currentCategory = categoryMatch[1].trim();
276
+ tableHeaders = null;
277
+ inTable = false;
278
+ continue;
279
+ }
280
+
281
+ // Match table header row
282
+ if (line.startsWith('|') && line.includes('|') && !tableHeaders) {
283
+ tableHeaders = parseTableRow(line);
284
+ inTable = true;
285
+ continue;
286
+ }
287
+
288
+ // Skip separator row
289
+ if (line.match(/^\|[-\s|]+\|$/)) {
290
+ continue;
291
+ }
292
+
293
+ // Parse table data row
294
+ if (inTable && line.startsWith('|') && tableHeaders) {
295
+ const cells = parseTableRow(line);
296
+ if (cells.length > 0 && !cells[0].startsWith('_')) { // Skip example rows
297
+ const row = createAppMapRow(currentCategory, tableHeaders, cells, i + 1);
298
+ if (row) {
299
+ rows.push(row);
300
+ }
301
+ }
302
+ }
303
+
304
+ // End of table
305
+ if (inTable && !line.startsWith('|') && line.trim() !== '') {
306
+ inTable = false;
307
+ tableHeaders = null;
308
+ }
309
+ }
310
+
311
+ return rows;
312
+ }
313
+
314
+ /**
315
+ * Parse a table row into cells
316
+ */
317
+ function parseTableRow(line) {
318
+ return line
319
+ .split('|')
320
+ .map(cell => cell.trim())
321
+ .filter(cell => cell.length > 0);
322
+ }
323
+
324
+ /**
325
+ * Create an app-map row object
326
+ */
327
+ function createAppMapRow(category, headers, cells, lineNumber) {
328
+ if (!category || cells.length < 2) return null;
329
+
330
+ const categorySlug = slugify(category);
331
+ const name = cells[0].replace(/[`*_]/g, ''); // Remove markdown formatting
332
+ const nameSlug = slugify(name);
333
+ const id = `${categorySlug}:${nameSlug}`;
334
+
335
+ // Build data object from headers
336
+ const data = {};
337
+ headers.forEach((header, idx) => {
338
+ if (cells[idx]) {
339
+ data[header.toLowerCase()] = cells[idx].replace(/[`*_]/g, '');
340
+ }
341
+ });
342
+
343
+ // Generate pins
344
+ const pins = new Set([nameSlug, name.toLowerCase()]);
345
+
346
+ // Add category-based pins
347
+ if (category.toLowerCase().includes('screen')) {
348
+ pins.add('screen');
349
+ pins.add('page');
350
+ pins.add('route');
351
+ } else if (category.toLowerCase().includes('modal')) {
352
+ pins.add('modal');
353
+ pins.add('dialog');
354
+ pins.add('popup');
355
+ } else if (category.toLowerCase().includes('component')) {
356
+ pins.add('component');
357
+ pins.add('ui');
358
+ }
359
+
360
+ // Add variant pins if present
361
+ if (data.variants) {
362
+ data.variants.split(',').map(v => v.trim()).forEach(v => pins.add(v.toLowerCase()));
363
+ }
364
+
365
+ return {
366
+ id,
367
+ name,
368
+ category,
369
+ pins: Array.from(pins),
370
+ line: lineNumber,
371
+ path: data.path || null,
372
+ status: data.status || null,
373
+ variants: data.variants ? data.variants.split(',').map(v => v.trim()) : [],
374
+ data
375
+ };
376
+ }
377
+
378
+ // ============================================================
379
+ // Index Generation
380
+ // ============================================================
381
+
382
+ /**
383
+ * Generate the full section index
384
+ * @returns {Object} - Section index object
385
+ */
386
+ function generateIndex() {
387
+ const index = {
388
+ version: '1.0',
389
+ generatedAt: new Date().toISOString(),
390
+ sources: {}
391
+ };
392
+
393
+ // Parse decisions.md
394
+ if (fileExists(PATHS.decisions)) {
395
+ try {
396
+ const decisionsContent = readFile(PATHS.decisions);
397
+ const sections = parseDecisionsSections(decisionsContent);
398
+ index.sources['decisions.md'] = {
399
+ path: PATHS.decisions,
400
+ lastModified: fs.statSync(PATHS.decisions).mtime.toISOString(),
401
+ contentHash: hashContent(decisionsContent),
402
+ sections
403
+ };
404
+ } catch (err) {
405
+ warn(`Error parsing decisions.md: ${err.message}`);
406
+ }
407
+ }
408
+
409
+ // Parse app-map.md
410
+ if (fileExists(PATHS.appMap)) {
411
+ try {
412
+ const appMapContent = readFile(PATHS.appMap);
413
+ const rows = parseAppMapRows(appMapContent);
414
+ index.sources['app-map.md'] = {
415
+ path: PATHS.appMap,
416
+ lastModified: fs.statSync(PATHS.appMap).mtime.toISOString(),
417
+ contentHash: hashContent(appMapContent),
418
+ rows
419
+ };
420
+ } catch (err) {
421
+ warn(`Error parsing app-map.md: ${err.message}`);
422
+ }
423
+ }
424
+
425
+ // Calculate stats
426
+ const decisionsSections = index.sources['decisions.md']?.sections?.length || 0;
427
+ const appMapRows = index.sources['app-map.md']?.rows?.length || 0;
428
+
429
+ index.stats = {
430
+ totalSections: decisionsSections,
431
+ totalRows: appMapRows,
432
+ totalPins: countUniquePins(index)
433
+ };
434
+
435
+ return index;
436
+ }
437
+
438
+ /**
439
+ * Count unique pins across all sources
440
+ */
441
+ function countUniquePins(index) {
442
+ const pins = new Set();
443
+
444
+ for (const source of Object.values(index.sources)) {
445
+ const items = source.sections || source.rows || [];
446
+ for (const item of items) {
447
+ item.pins?.forEach(p => pins.add(p));
448
+ }
449
+ }
450
+
451
+ return pins.size;
452
+ }
453
+
454
+ /**
455
+ * Write index to file
456
+ */
457
+ function writeIndex(index) {
458
+ if (!dirExists(PATHS.state)) {
459
+ fs.mkdirSync(PATHS.state, { recursive: true });
460
+ }
461
+
462
+ writeFile(INDEX_PATH, JSON.stringify(index, null, 2));
463
+ return INDEX_PATH;
464
+ }
465
+
466
+ /**
467
+ * Read existing index
468
+ * Uses safeJsonParse for prototype pollution protection
469
+ */
470
+ function readIndex() {
471
+ if (!fileExists(INDEX_PATH)) {
472
+ return null;
473
+ }
474
+
475
+ // Use safeJsonParse for security (prototype pollution protection)
476
+ return safeJsonParse(INDEX_PATH, null);
477
+ }
478
+
479
+ /**
480
+ * Check if index needs regeneration
481
+ */
482
+ function needsRegeneration() {
483
+ const existingIndex = readIndex();
484
+ if (!existingIndex) return true;
485
+
486
+ // Check decisions.md
487
+ if (fileExists(PATHS.decisions)) {
488
+ const currentHash = hashContent(readFile(PATHS.decisions));
489
+ const indexedHash = existingIndex.sources['decisions.md']?.contentHash;
490
+ if (currentHash !== indexedHash) return true;
491
+ }
492
+
493
+ // Check app-map.md
494
+ if (fileExists(PATHS.appMap)) {
495
+ const currentHash = hashContent(readFile(PATHS.appMap));
496
+ const indexedHash = existingIndex.sources['app-map.md']?.contentHash;
497
+ if (currentHash !== indexedHash) return true;
498
+ }
499
+
500
+ return false;
501
+ }
502
+
503
+ // ============================================================
504
+ // File Watcher
505
+ // ============================================================
506
+
507
+ let debounceTimer = null;
508
+
509
+ /**
510
+ * Start watching source files for changes
511
+ */
512
+ function startWatcher() {
513
+ const filesToWatch = [PATHS.decisions, PATHS.appMap].filter(f => fileExists(f));
514
+
515
+ if (filesToWatch.length === 0) {
516
+ warn('No source files found to watch');
517
+ return;
518
+ }
519
+
520
+ info(`Watching ${filesToWatch.length} files for changes...`);
521
+
522
+ for (const filePath of filesToWatch) {
523
+ fs.watch(filePath, (eventType) => {
524
+ if (eventType === 'change') {
525
+ // Debounce rapid changes
526
+ if (debounceTimer) {
527
+ clearTimeout(debounceTimer);
528
+ }
529
+
530
+ debounceTimer = setTimeout(() => {
531
+ info(`[${new Date().toISOString()}] Change detected, regenerating index...`);
532
+ const index = generateIndex();
533
+ writeIndex(index);
534
+ success(`Section index regenerated (${index.stats.totalSections} sections, ${index.stats.totalRows} rows)`);
535
+ }, DEBOUNCE_MS);
536
+ }
537
+ });
538
+ }
539
+
540
+ info('Press Ctrl+C to stop watching');
541
+ }
542
+
543
+ // ============================================================
544
+ // Public API
545
+ // ============================================================
546
+
547
+ /**
548
+ * Generate and write section index
549
+ * @param {Object} options - { force: boolean }
550
+ * @returns {Object} - { success, indexPath, stats }
551
+ */
552
+ function generateSectionIndex(options = {}) {
553
+ const { force = false } = options;
554
+
555
+ // Check if regeneration is needed
556
+ if (!force && !needsRegeneration()) {
557
+ const existingIndex = readIndex();
558
+ return {
559
+ success: true,
560
+ skipped: true,
561
+ indexPath: INDEX_PATH,
562
+ stats: existingIndex.stats
563
+ };
564
+ }
565
+
566
+ const index = generateIndex();
567
+ const indexPath = writeIndex(index);
568
+
569
+ return {
570
+ success: true,
571
+ skipped: false,
572
+ indexPath,
573
+ stats: index.stats
574
+ };
575
+ }
576
+
577
+ /**
578
+ * Get all sections matching pins
579
+ * @param {string[]} pins - Pins to match
580
+ * @returns {Object[]} - Matching sections
581
+ */
582
+ function getSectionsByPins(pins) {
583
+ const index = readIndex();
584
+ if (!index) return [];
585
+
586
+ const results = [];
587
+ const pinsLower = pins.map(p => p.toLowerCase());
588
+
589
+ for (const source of Object.values(index.sources)) {
590
+ const items = source.sections || source.rows || [];
591
+ for (const item of items) {
592
+ const matchCount = item.pins?.filter(p => pinsLower.includes(p.toLowerCase())).length || 0;
593
+ if (matchCount > 0) {
594
+ results.push({
595
+ ...item,
596
+ source: source.path,
597
+ matchCount,
598
+ matchScore: matchCount / pinsLower.length
599
+ });
600
+ }
601
+ }
602
+ }
603
+
604
+ // Sort by match score
605
+ return results.sort((a, b) => b.matchScore - a.matchScore);
606
+ }
607
+
608
+ /**
609
+ * Get section by ID
610
+ * @param {string} sectionId - Section ID (e.g., "security:file-read-safety")
611
+ * @returns {Object|null} - Section object or null
612
+ */
613
+ function getSectionById(sectionId) {
614
+ const index = readIndex();
615
+ if (!index) return null;
616
+
617
+ for (const source of Object.values(index.sources)) {
618
+ const items = source.sections || source.rows || [];
619
+ const found = items.find(item => item.id === sectionId);
620
+ if (found) {
621
+ return { ...found, source: source.path };
622
+ }
623
+ }
624
+
625
+ return null;
626
+ }
627
+
628
+ // ============================================================
629
+ // Main
630
+ // ============================================================
631
+
632
+ function main() {
633
+ const { flags } = parseFlags(process.argv.slice(2));
634
+
635
+ if (flags.help) {
636
+ console.log(`
637
+ Usage: node scripts/flow-section-index.js [options]
638
+
639
+ Generate section-level index from decisions.md and app-map.md.
640
+
641
+ Options:
642
+ --watch Watch files for changes and auto-regenerate
643
+ --force Force regeneration even if no changes detected
644
+ --json Output result as JSON
645
+ --help Show this help message
646
+
647
+ Examples:
648
+ node scripts/flow-section-index.js # Generate index
649
+ node scripts/flow-section-index.js --watch # Watch for changes
650
+ node scripts/flow-section-index.js --force # Force regeneration
651
+ `);
652
+ process.exit(0);
653
+ }
654
+
655
+ // Watch mode
656
+ if (flags.watch) {
657
+ // Generate initial index
658
+ const result = generateSectionIndex({ force: true });
659
+ if (result.success) {
660
+ success(`Initial index generated: ${result.stats.totalSections} sections, ${result.stats.totalRows} rows`);
661
+ }
662
+ startWatcher();
663
+ return;
664
+ }
665
+
666
+ // Generate index
667
+ const result = generateSectionIndex({ force: flags.force });
668
+
669
+ if (flags.json) {
670
+ outputJson(result);
671
+ return;
672
+ }
673
+
674
+ if (result.skipped) {
675
+ info('Index is up to date (no changes detected)');
676
+ info(` Sections: ${result.stats.totalSections}`);
677
+ info(` Rows: ${result.stats.totalRows}`);
678
+ info(` Unique pins: ${result.stats.totalPins}`);
679
+ return;
680
+ }
681
+
682
+ if (result.success) {
683
+ success('Section index generated');
684
+ info(` Path: ${result.indexPath}`);
685
+ info(` Sections: ${result.stats.totalSections}`);
686
+ info(` Rows: ${result.stats.totalRows}`);
687
+ info(` Unique pins: ${result.stats.totalPins}`);
688
+ } else {
689
+ error('Failed to generate section index');
690
+ process.exit(1);
691
+ }
692
+ }
693
+
694
+ // ============================================================
695
+ // Exports
696
+ // ============================================================
697
+
698
+ module.exports = {
699
+ generateSectionIndex,
700
+ getSectionsByPins,
701
+ getSectionById,
702
+ readIndex,
703
+ needsRegeneration,
704
+ generatePins,
705
+ parseDecisionsSections,
706
+ parseAppMapRows,
707
+ INDEX_PATH
708
+ };
709
+
710
+ // Run if called directly
711
+ if (require.main === module) {
712
+ main();
713
+ }