wogiflow 2.2.0 → 2.3.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/.claude/commands/wogi-onboard.md +30 -8
- package/.workflow/models/registry.json +1 -1
- package/package.json +1 -1
- package/scripts/flow-api-index.js +128 -63
- package/scripts/flow-function-index.js +65 -63
- package/scripts/flow-pattern-extractor.js +1 -1
- package/scripts/flow-scanner-base.js +200 -7
- package/scripts/flow-skill-generator.js +1 -0
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/registries/component-registry.js +141 -4
- package/.claude/rules/_internal/README.md +0 -64
- package/.claude/rules/_internal/document-structure.md +0 -77
- package/.claude/rules/_internal/dual-repo-management.md +0 -174
- package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
- package/.claude/rules/_internal/github-releases.md +0 -71
- package/.claude/rules/_internal/model-management.md +0 -35
- package/.claude/rules/_internal/self-maintenance.md +0 -87
- package/.claude/rules/architecture/component-reuse.md +0 -38
- package/.claude/rules/code-style/naming-conventions.md +0 -52
- package/.claude/rules/operations/git-workflows.md +0 -92
- package/.claude/rules/operations/scratch-directory.md +0 -54
- package/.claude/rules/security/security-patterns.md +0 -176
- package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
- package/.workflow/specs/architecture.md.template +0 -24
- package/.workflow/specs/stack.md.template +0 -33
- 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(
|
|
57
|
-
.replace(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
|
|
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.
|