wogiflow 2.2.0 → 2.3.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 (29) hide show
  1. package/.claude/commands/wogi-epics.md +16 -0
  2. package/.claude/commands/wogi-onboard.md +30 -8
  3. package/.claude/commands/wogi-start.md +2 -0
  4. package/.workflow/models/registry.json +1 -1
  5. package/.workflow/templates/claude-md.hbs +25 -0
  6. package/package.json +1 -1
  7. package/scripts/flow-api-index.js +128 -63
  8. package/scripts/flow-function-index.js +65 -63
  9. package/scripts/flow-pattern-extractor.js +1 -1
  10. package/scripts/flow-scanner-base.js +200 -7
  11. package/scripts/flow-skill-generator.js +1 -0
  12. package/scripts/flow-template-extractor.js +1 -1
  13. package/scripts/registries/component-registry.js +141 -4
  14. package/.claude/rules/_internal/README.md +0 -64
  15. package/.claude/rules/_internal/document-structure.md +0 -77
  16. package/.claude/rules/_internal/dual-repo-management.md +0 -174
  17. package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
  18. package/.claude/rules/_internal/github-releases.md +0 -71
  19. package/.claude/rules/_internal/model-management.md +0 -35
  20. package/.claude/rules/_internal/self-maintenance.md +0 -87
  21. package/.claude/rules/architecture/component-reuse.md +0 -38
  22. package/.claude/rules/code-style/naming-conventions.md +0 -52
  23. package/.claude/rules/operations/git-workflows.md +0 -92
  24. package/.claude/rules/operations/scratch-directory.md +0 -54
  25. package/.claude/rules/security/security-patterns.md +0 -176
  26. package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
  27. package/.workflow/specs/architecture.md.template +0 -24
  28. package/.workflow/specs/stack.md.template +0 -33
  29. package/.workflow/specs/testing.md.template +0 -36
@@ -36,6 +36,7 @@ class BaseScanner {
36
36
 
37
37
  this.config = {
38
38
  directories: registryConfig.directories || options.directories || [],
39
+ globPatterns: registryConfig.globPatterns || options.globPatterns || [],
39
40
  filePatterns: options.filePatterns || ['**/*.ts', '**/*.js', '**/*.tsx', '**/*.jsx'],
40
41
  excludePatterns: options.excludePatterns || [
41
42
  '**/*.test.*',
@@ -50,11 +51,13 @@ class BaseScanner {
50
51
  };
51
52
 
52
53
  // Pre-compile exclude patterns to avoid per-file RegExp allocation
54
+ // Use placeholder to prevent ** and * from interfering during replacement
53
55
  this._excludeRegexps = this.config.excludePatterns.map(pattern => {
54
56
  const regexPattern = pattern
55
- .replace(/\*\*/g, '.*')
56
- .replace(/\*/g, '[^/]*')
57
- .replace(/\./g, '\\.');
57
+ .replace(/\*\*/g, '\0GLOBSTAR\0') // Placeholder for **
58
+ .replace(/\./g, '\\.') // Escape dots
59
+ .replace(/\*/g, '[^/]*') // Single * → non-slash wildcard
60
+ .replace(/\0GLOBSTAR\0/g, '.*'); // Restore ** → any path
58
61
  return new RegExp('^' + regexPattern + '$');
59
62
  });
60
63
 
@@ -70,18 +73,96 @@ class BaseScanner {
70
73
  }
71
74
 
72
75
  /**
73
- * Find existing directories from config
76
+ * Find existing directories from config (explicit + glob-discovered)
74
77
  * @returns {string[]} Array of full paths to existing directories
75
78
  */
76
79
  findDirectories() {
77
- const found = [];
80
+ const found = new Set();
81
+
82
+ // 1. Explicit directories from config
78
83
  for (const dir of this.config.directories) {
79
84
  const fullPath = path.join(PROJECT_ROOT, dir);
80
85
  if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
81
- found.push(fullPath);
86
+ found.add(fullPath);
87
+ }
88
+ }
89
+
90
+ // 2. Glob-discovered directories (e.g. "src/**/hooks", "src/**/utils")
91
+ for (const pattern of this.config.globPatterns) {
92
+ for (const dir of this._expandGlobPattern(pattern)) {
93
+ found.add(dir);
82
94
  }
83
95
  }
84
- return found;
96
+
97
+ return [...found];
98
+ }
99
+
100
+ /**
101
+ * Expand a glob pattern like "src/** /hooks" into matching directories.
102
+ * Supports ** (any depth) and * (single segment). No external dependencies.
103
+ * @param {string} pattern - Glob pattern relative to project root
104
+ * @returns {string[]} Array of full paths to matching directories
105
+ */
106
+ _expandGlobPattern(pattern) {
107
+ const results = [];
108
+ const segments = pattern.split('/');
109
+ const MAX_DEPTH = 20;
110
+ const MAX_DIRS = 5000;
111
+ let dirsVisited = 0;
112
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.cache', '.yarn', '.pnpm']);
113
+
114
+ const walk = (currentPath, segIdx, depth) => {
115
+ if (depth > MAX_DEPTH || dirsVisited > MAX_DIRS) return;
116
+ dirsVisited++;
117
+
118
+ if (segIdx >= segments.length) {
119
+ if (fs.existsSync(currentPath) && fs.statSync(currentPath).isDirectory()) {
120
+ results.push(currentPath);
121
+ }
122
+ return;
123
+ }
124
+
125
+ const seg = segments[segIdx];
126
+
127
+ if (seg === '**') {
128
+ // Zero levels: skip this segment
129
+ walk(currentPath, segIdx + 1, depth);
130
+
131
+ // One+ levels: recurse into subdirectories
132
+ if (!fs.existsSync(currentPath) || !fs.statSync(currentPath).isDirectory()) return;
133
+ try {
134
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
135
+ for (const entry of entries) {
136
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
137
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
138
+ walk(path.join(currentPath, entry.name), segIdx, depth + 1);
139
+ }
140
+ } catch (_err) {
141
+ // Permission error — skip
142
+ }
143
+ } else if (seg.includes('*')) {
144
+ if (!fs.existsSync(currentPath) || !fs.statSync(currentPath).isDirectory()) return;
145
+ // Escape regex metacharacters except *, then convert * to [^/]*
146
+ const escaped = seg.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
147
+ const segRegex = new RegExp('^' + escaped + '$');
148
+ try {
149
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
150
+ for (const entry of entries) {
151
+ if (!entry.isDirectory()) continue;
152
+ if (segRegex.test(entry.name)) {
153
+ walk(path.join(currentPath, entry.name), segIdx + 1, depth + 1);
154
+ }
155
+ }
156
+ } catch (_err) {
157
+ // Permission error — skip
158
+ }
159
+ } else {
160
+ walk(path.join(currentPath, seg), segIdx + 1, depth + 1);
161
+ }
162
+ };
163
+
164
+ walk(PROJECT_ROOT, 0, 0);
165
+ return results;
85
166
  }
86
167
 
87
168
  /**
@@ -268,6 +349,118 @@ class BaseScanner {
268
349
  });
269
350
  }
270
351
 
352
+ /**
353
+ * Two-pass AST: collect all top-level declarations and all exported names.
354
+ * Returns { declarations: Map<name, {node, kind}>, exported: Map<name, {isDefault}> }
355
+ * Subclasses call this then intersect with their own registration logic.
356
+ * @param {string} content - File content
357
+ * @returns {Object|null} { declarations, exported } or null if parse fails
358
+ */
359
+ collectExportedDeclarations(content) {
360
+ if (!this.parser) return null;
361
+
362
+ try {
363
+ const ast = this.parser.parse(content, {
364
+ sourceType: 'module',
365
+ plugins: ['typescript', 'jsx', 'decorators-legacy']
366
+ });
367
+
368
+ // Pass 1: Collect all top-level function-like declarations
369
+ const declarations = new Map();
370
+
371
+ this.traverse(ast, {
372
+ FunctionDeclaration: (nodePath) => {
373
+ const parent = nodePath.parent.type;
374
+ if (!nodePath.node.id) return;
375
+ if (parent === 'Program' || parent === 'ExportNamedDeclaration' || parent === 'ExportDefaultDeclaration') {
376
+ declarations.set(nodePath.node.id.name, { node: nodePath.node, kind: 'func' });
377
+ }
378
+ },
379
+ VariableDeclaration: (nodePath) => {
380
+ const parent = nodePath.parent.type;
381
+ if (parent !== 'Program' && parent !== 'ExportNamedDeclaration') return;
382
+ for (const decl of nodePath.node.declarations) {
383
+ if (decl.id?.name && decl.init &&
384
+ (decl.init.type === 'ArrowFunctionExpression' ||
385
+ decl.init.type === 'FunctionExpression')) {
386
+ declarations.set(decl.id.name, { node: decl, kind: 'var' });
387
+ }
388
+ }
389
+ }
390
+ });
391
+
392
+ // Pass 2: Collect all exported names
393
+ const exported = new Map();
394
+
395
+ this.traverse(ast, {
396
+ ExportNamedDeclaration: (nodePath) => {
397
+ const decl = nodePath.node.declaration;
398
+ if (decl) {
399
+ if (decl.type === 'FunctionDeclaration' && decl.id) {
400
+ exported.set(decl.id.name, { isDefault: false });
401
+ } else if (decl.type === 'VariableDeclaration') {
402
+ for (const d of decl.declarations) {
403
+ if (d.id?.name) exported.set(d.id.name, { isDefault: false });
404
+ }
405
+ }
406
+ }
407
+ for (const spec of nodePath.node.specifiers || []) {
408
+ if (spec.local?.name) {
409
+ exported.set(spec.local.name, { isDefault: false });
410
+ }
411
+ }
412
+ },
413
+ ExportDefaultDeclaration: (nodePath) => {
414
+ const decl = nodePath.node.declaration;
415
+ if (decl.type === 'FunctionDeclaration' && decl.id) {
416
+ exported.set(decl.id.name, { isDefault: true });
417
+ } else if (decl.type === 'Identifier') {
418
+ exported.set(decl.name, { isDefault: true });
419
+ }
420
+ }
421
+ });
422
+
423
+ return { declarations, exported };
424
+ } catch (_err) {
425
+ return null;
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Two-pass regex: collect all exported names from file content.
431
+ * Shared across scanners for consistent export detection.
432
+ * @param {string} content - File content
433
+ * @returns {Set<string>} Set of exported names
434
+ */
435
+ collectExportedNamesRegex(content) {
436
+ const exported = new Set();
437
+ let match;
438
+
439
+ // export function / export const / export default function
440
+ const exportedDeclRegex = /export\s+(?:default\s+)?(?:async\s+)?(?:function|const)\s+(\w+)/g;
441
+ while ((match = exportedDeclRegex.exec(content)) !== null) {
442
+ exported.add(match[1]);
443
+ }
444
+
445
+ // export default Name (identifier)
446
+ const exportDefaultIdRegex = /export\s+default\s+([A-Za-z_$]\w*)\s*;/g;
447
+ while ((match = exportDefaultIdRegex.exec(content)) !== null) {
448
+ exported.add(match[1]);
449
+ }
450
+
451
+ // export { Name, Name2 as Alias }
452
+ const exportSpecRegex = /export\s*\{([^}]+)\}/g;
453
+ while ((match = exportSpecRegex.exec(content)) !== null) {
454
+ const specifiers = match[1].split(',');
455
+ for (const spec of specifiers) {
456
+ const name = spec.trim().split(/\s+as\s+/)[0].trim();
457
+ if (name) exported.add(name);
458
+ }
459
+ }
460
+
461
+ return exported;
462
+ }
463
+
271
464
  /**
272
465
  * Parse params from string (regex fallback)
273
466
  * @param {string} paramsStr - Parameter string
@@ -8,6 +8,7 @@
8
8
  const fs = require('node:fs');
9
9
  const path = require('node:path');
10
10
  const { ensureDir, getConfig, invalidateConfigCache, writeJson, PATHS, success } = require('./flow-utils');
11
+ const { getTodayDate } = require('./flow-output');
11
12
 
12
13
  // Import helper functions from tech options
13
14
  let _techOptions = null;
@@ -77,7 +77,7 @@ const IGNORE_PATTERNS = [
77
77
  ];
78
78
 
79
79
  // Colors for CLI
80
- const { colors: c } = require('./flow-output');
80
+ const { colors: c, getTodayDate } = require('./flow-output');
81
81
 
82
82
  // ============================================================================
83
83
  // File Classification
@@ -290,10 +290,147 @@ class ComponentScanner extends BaseScanner {
290
290
  }
291
291
 
292
292
  generateMap() {
293
- // Component registry generates component-index.json only.
294
- // It does NOT overwrite app-map.md — that remains human-curated.
295
- // The index is complementary: auto-generated for machine use.
296
- success(`Component index available at ${path.relative(PROJECT_ROOT, INDEX_PATH)}`);
293
+ const MAP_PATH = path.join(STATE_DIR, 'app-map.md');
294
+
295
+ // Check if app-map.md exists and has content
296
+ let existing = '';
297
+ try {
298
+ existing = fs.readFileSync(MAP_PATH, 'utf-8');
299
+ } catch (_err) {
300
+ // File doesn't exist — will create
301
+ }
302
+
303
+ // Use marker to distinguish auto-generated from human-curated
304
+ const AUTO_MARKER = '<!-- AUTO-GENERATED BY COMPONENT SCANNER -->';
305
+ const isAutoGenerated = existing.includes(AUTO_MARKER);
306
+ const hasContent = existing.split('\n').filter(l => l.startsWith('|')).length > 5;
307
+
308
+ if (hasContent && !isAutoGenerated) {
309
+ // Human-curated content — merge without overwriting
310
+ this._mergeIntoAppMap(MAP_PATH, existing);
311
+ return;
312
+ }
313
+
314
+ // Generate fresh app-map.md from scan results
315
+ const lines = [
316
+ '# App Map',
317
+ '',
318
+ AUTO_MARKER,
319
+ '',
320
+ 'Component and page registry. **Check before creating anything new.**',
321
+ '',
322
+ '> Auto-generated by component scanner. Edit to add context.',
323
+ '',
324
+ '---',
325
+ ''
326
+ ];
327
+
328
+ // Group components by category
329
+ const categories = {};
330
+ for (const comp of this.registry.components) {
331
+ const cat = comp.category || 'uncategorized';
332
+ if (!categories[cat]) categories[cat] = [];
333
+ categories[cat].push(comp);
334
+ }
335
+
336
+ // Components section
337
+ if (this.registry.components.length > 0) {
338
+ lines.push('## Components', '');
339
+
340
+ for (const cat of Object.keys(categories).sort()) {
341
+ const catName = cat.charAt(0).toUpperCase() + cat.slice(1);
342
+ lines.push(`### ${catName}`, '');
343
+ lines.push('| Component | File | Description |');
344
+ lines.push('|-----------|------|-------------|');
345
+
346
+ for (const comp of categories[cat].sort((a, b) => a.name.localeCompare(b.name))) {
347
+ const desc = comp.description || '-';
348
+ lines.push(`| \`${comp.name}\` | \`${comp.file}\` | ${desc} |`);
349
+ }
350
+ lines.push('');
351
+ }
352
+ }
353
+
354
+ // Hooks section
355
+ if (this.registry.hooks.length > 0) {
356
+ lines.push('## Hooks', '');
357
+ lines.push('| Hook | File | Description |');
358
+ lines.push('|------|------|-------------|');
359
+
360
+ for (const hook of this.registry.hooks.sort((a, b) => a.name.localeCompare(b.name))) {
361
+ const desc = hook.description || '-';
362
+ lines.push(`| \`${hook.name}\` | \`${hook.file}\` | ${desc} |`);
363
+ }
364
+ lines.push('');
365
+ }
366
+
367
+ lines.push('---', '');
368
+ lines.push('## Rules', '');
369
+ lines.push('1. **Before creating** → Search this file');
370
+ lines.push('2. **If similar exists** → Add variant, don\'t create new');
371
+ lines.push('3. **After creating** → Run `flow registry-manager scan` to update');
372
+ lines.push('');
373
+
374
+ fs.writeFileSync(MAP_PATH, lines.join('\n'));
375
+ success(`Generated ${path.relative(PROJECT_ROOT, MAP_PATH)} (${this.registry.components.length} components, ${this.registry.hooks.length} hooks)`);
376
+ }
377
+
378
+ /**
379
+ * Merge scanner results into existing app-map.md without overwriting curated content.
380
+ * Adds only components not already present.
381
+ */
382
+ _mergeIntoAppMap(mapPath, existing) {
383
+ // Path containment check (defense-in-depth)
384
+ if (!mapPath.startsWith(STATE_DIR)) {
385
+ warn('Write target outside state directory — skipping merge');
386
+ return;
387
+ }
388
+
389
+ // Extract names already in app-map (with or without backticks)
390
+ const existingNames = new Set();
391
+ const nameRegex = /\|\s*`?(\w+)`?\s*\|/g;
392
+ let m;
393
+ while ((m = nameRegex.exec(existing)) !== null) {
394
+ existingNames.add(m[1]);
395
+ }
396
+
397
+ // Find new components not in app-map
398
+ const newComponents = this.registry.components.filter(c => !existingNames.has(c.name));
399
+ const newHooks = this.registry.hooks.filter(h => !existingNames.has(h.name));
400
+
401
+ if (newComponents.length === 0 && newHooks.length === 0) {
402
+ info(`app-map.md is up to date (${existingNames.size} entries)`);
403
+ return;
404
+ }
405
+
406
+ // Remove existing "## Auto-Discovered" section if present (prevent duplicates)
407
+ const autoDiscoveredIdx = existing.indexOf('\n## Auto-Discovered');
408
+ const base = autoDiscoveredIdx !== -1 ? existing.substring(0, autoDiscoveredIdx) : existing;
409
+
410
+ const additions = ['\n## Auto-Discovered (new entries)', ''];
411
+
412
+ if (newComponents.length > 0) {
413
+ additions.push('### Components', '');
414
+ additions.push('| Component | File | Description |');
415
+ additions.push('|-----------|------|-------------|');
416
+ for (const comp of newComponents.sort((a, b) => a.name.localeCompare(b.name))) {
417
+ additions.push(`| \`${comp.name}\` | \`${comp.file}\` | ${comp.description || '-'} |`);
418
+ }
419
+ additions.push('');
420
+ }
421
+
422
+ if (newHooks.length > 0) {
423
+ additions.push('### Hooks', '');
424
+ additions.push('| Hook | File | Description |');
425
+ additions.push('|------|------|-------------|');
426
+ for (const hook of newHooks.sort((a, b) => a.name.localeCompare(b.name))) {
427
+ additions.push(`| \`${hook.name}\` | \`${hook.file}\` | ${hook.description || '-'} |`);
428
+ }
429
+ additions.push('');
430
+ }
431
+
432
+ fs.writeFileSync(mapPath, base + additions.join('\n'));
433
+ success(`Merged ${newComponents.length} components + ${newHooks.length} hooks into app-map.md`);
297
434
  }
298
435
  }
299
436
 
@@ -1,64 +0,0 @@
1
- ---
2
- alwaysApply: false
3
- description: "Meta-documentation about how project rules are organized"
4
- ---
5
- # Project Rules
6
-
7
- This directory contains coding rules and patterns for this project, organized by category.
8
-
9
- ## Structure
10
-
11
- ```
12
- .claude/rules/
13
- ├── code-style/ # Naming conventions, formatting
14
- │ └── naming-conventions.md
15
- ├── security/ # Security patterns and practices
16
- │ └── security-patterns.md
17
- ├── architecture/ # Design decisions and patterns
18
- │ ├── component-reuse.md
19
- │ └── model-management.md
20
- └── README.md
21
- ```
22
-
23
- ## How Rules Work
24
-
25
- Rules are automatically loaded by Claude Code based on:
26
- - **alwaysApply: true** - Rule is always loaded
27
- - **alwaysApply: false** - Rule is loaded based on `globs` or `description` relevance
28
- - **globs** - File patterns that trigger rule loading
29
-
30
- ## Adding New Rules
31
-
32
- 1. Choose the appropriate category subdirectory
33
- 2. Create a `.md` file with frontmatter:
34
-
35
- ```yaml
36
- ---
37
- alwaysApply: false
38
- description: "Brief description for relevance matching"
39
- globs: src/**/*.ts # Optional: only load for these files
40
- ---
41
- ```
42
-
43
- 3. Write the rule content in markdown
44
-
45
- ## Categories
46
-
47
- | Category | Purpose |
48
- |----------|---------|
49
- | code-style | Naming conventions, formatting, file structure |
50
- | security | Security patterns, input validation, safe practices |
51
- | architecture | Design decisions, component patterns, system organization |
52
-
53
- ## Auto-Generation
54
-
55
- Some rules can be auto-generated from `.workflow/state/decisions.md`:
56
-
57
- ```bash
58
- node scripts/flow-rules-sync.js
59
- ```
60
-
61
- The sync script will route rules to appropriate category subdirectories.
62
-
63
- ---
64
- Last updated: 2026-01-12
@@ -1,77 +0,0 @@
1
- ---
2
- alwaysApply: false
3
- description: "All AI-context documents must use PIN markers for targeted context loading"
4
- globs: ".workflow/**/*.md"
5
- ---
6
-
7
- # Document Structure for AI Context
8
-
9
- All documents in `.workflow/` that are used as AI context MUST follow the PIN standard.
10
-
11
- ## Required Structure
12
-
13
- ### 1. Header with PIN List
14
- Every document starts with a comment listing all pins in the document:
15
- ```markdown
16
- <!-- PINS: pin1, pin2, pin3 -->
17
- ```
18
-
19
- ### 2. Section PIN Markers
20
- Each major section has a PIN marker comment:
21
- ```markdown
22
- ### Section Title
23
- <!-- PIN: section-specific-pin -->
24
- [Content]
25
- ```
26
-
27
- ### 3. PIN Naming Convention
28
- - Use kebab-case: `user-authentication`, not `userAuthentication`
29
- - Use semantic names: `error-handling`, not `eh`
30
- - Use compound names for specificity: `json-parse-safety`
31
-
32
- ## Why PINs Matter
33
-
34
- The PIN system enables:
35
- 1. **Targeted context loading**: Only load sections relevant to current task
36
- 2. **Cheaper model routing**: Haiku can fetch only relevant sections for Opus
37
- 3. **Change detection**: Hash sections independently for smart invalidation
38
- 4. **Cross-reference**: Link sections by PIN across documents
39
-
40
- ## Example Document
41
-
42
- ```markdown
43
- # Config Reference
44
-
45
- <!-- PINS: database, authentication, api-keys, environment -->
46
-
47
- ## Database Settings
48
- <!-- PIN: database -->
49
- | Setting | Default | Description |
50
- |---------|---------|-------------|
51
-
52
- ## Authentication
53
- <!-- PIN: authentication -->
54
- | Setting | Default | Description |
55
- |---------|---------|-------------|
56
- ```
57
-
58
- ## Parsing
59
-
60
- The PIN system automatically parses documents with:
61
- - `flow-section-index.js` - Generates section index with pins
62
- - `flow-section-resolver.js` - Resolves sections by PIN lookup
63
- - `getSectionsByPins(['auth', 'security'])` - Fetch only relevant sections
64
-
65
- ## Files That Must Have PINs
66
-
67
- | File | Required PINs |
68
- |------|---------------|
69
- | `decisions.md` | Per coding rule/pattern |
70
- | `app-map.md` | Per component/screen |
71
- | `product.md` | Per product section |
72
- | `stack.md` | Per technology |
73
-
74
- ## Validation
75
-
76
- Run `node scripts/flow-section-index.js --force` to regenerate the index.
77
- Check `.workflow/state/section-index.json` for indexed sections and their pins.