wogiflow 2.1.3 → 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-audit.md +189 -3
- package/.claude/commands/wogi-onboard.md +30 -8
- package/.claude/commands/wogi-review.md +86 -13
- package/.claude/commands/wogi-start.md +66 -21
- package/.claude/docs/claude-code-compatibility.md +28 -0
- package/.workflow/templates/claude-md.hbs +32 -2
- package/package.json +1 -1
- package/scripts/flow-api-index.js +128 -63
- package/scripts/flow-audit.js +158 -1
- package/scripts/flow-function-index.js +65 -63
- package/scripts/flow-pattern-extractor.js +1 -1
- package/scripts/flow-progress-tracker.js +289 -0
- package/scripts/flow-prompt-capture.js +263 -170
- package/scripts/flow-scanner-base.js +200 -7
- package/scripts/flow-skill-generator.js +1 -0
- package/scripts/flow-standards-learner.js +167 -3
- package/scripts/flow-task-checkpoint.js +2 -0
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-version-check.js +1 -0
- package/scripts/hooks/core/commit-log-gate.js +146 -0
- package/scripts/hooks/core/post-compact.js +81 -8
- package/scripts/hooks/core/task-completed.js +19 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +60 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +27 -0
- package/scripts/registries/component-registry.js +141 -4
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Purpose: Restore critical state that may have been lost during compaction.
|
|
10
10
|
* - Re-inject durable session context (current task, completed steps, remaining work)
|
|
11
|
-
* - Re-inject
|
|
11
|
+
* - Re-inject acceptance criteria with completion status
|
|
12
|
+
* - Re-inject changed files and last request-log entry
|
|
12
13
|
* - Ensure routing-pending flag is set (compaction = new context, needs re-routing)
|
|
13
14
|
*
|
|
14
15
|
* This hook is non-blocking (fail-open). Compaction should never be prevented
|
|
15
16
|
* by a state restoration failure.
|
|
17
|
+
*
|
|
18
|
+
* v2.0: Hoisted shared requires, added criteria/files/log restoration,
|
|
19
|
+
* fixed criteria done status to read from scenarios.completed[]
|
|
16
20
|
*/
|
|
17
21
|
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const { PATHS, safeJsonParse, getReadyData } = require('../../flow-utils');
|
|
25
|
+
|
|
18
26
|
/**
|
|
19
27
|
* Sanitize a string value before injecting into AI context.
|
|
20
28
|
* Strips markdown heading markers and truncates to prevent prompt manipulation.
|
|
@@ -65,7 +73,6 @@ function handlePostCompact() {
|
|
|
65
73
|
|
|
66
74
|
// 2. Check for active task in ready.json (fallback if no durable session)
|
|
67
75
|
try {
|
|
68
|
-
const { getReadyData } = require('../../flow-utils');
|
|
69
76
|
const readyData = getReadyData();
|
|
70
77
|
if (Array.isArray(readyData.inProgress) && readyData.inProgress.length > 0) {
|
|
71
78
|
const task = readyData.inProgress[0];
|
|
@@ -82,6 +89,77 @@ function handlePostCompact() {
|
|
|
82
89
|
}
|
|
83
90
|
}
|
|
84
91
|
|
|
92
|
+
// 2b. Load acceptance criteria and changed files from task checkpoint
|
|
93
|
+
try {
|
|
94
|
+
const checkpointPath = path.join(PATHS.state, 'task-checkpoint.json');
|
|
95
|
+
const checkpoint = safeJsonParse(checkpointPath, null);
|
|
96
|
+
if (checkpoint && checkpoint.taskId) {
|
|
97
|
+
// Inject acceptance criteria with completion status
|
|
98
|
+
// Derive done status from scenarios.completed[] (the authoritative source)
|
|
99
|
+
// rather than criteria[].done (which is never updated after initialization)
|
|
100
|
+
const completedIndices = new Set(
|
|
101
|
+
(checkpoint.scenarios?.completed || []).map(s => s.index)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (Array.isArray(checkpoint.criteria) && checkpoint.criteria.length > 0) {
|
|
105
|
+
const criteriaLines = checkpoint.criteria.slice(0, 15).map((c, i) => {
|
|
106
|
+
const isDone = completedIndices.has(i);
|
|
107
|
+
const status = isDone ? '✓' : '○';
|
|
108
|
+
return ` ${status} ${sanitize(c.text || c.description || c.id, 120)}`;
|
|
109
|
+
});
|
|
110
|
+
const done = checkpoint.criteria.filter((_, i) => completedIndices.has(i)).length;
|
|
111
|
+
contextParts.push(`**Acceptance Criteria** (${done}/${checkpoint.criteria.length} done):\n${criteriaLines.join('\n')}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Inject changed files list
|
|
115
|
+
if (Array.isArray(checkpoint.changedFiles) && checkpoint.changedFiles.length > 0) {
|
|
116
|
+
const files = checkpoint.changedFiles.slice(0, 20).map(f => ` - ${sanitize(f, 100)}`);
|
|
117
|
+
contextParts.push(`**Changed files this session** (${checkpoint.changedFiles.length}):\n${files.join('\n')}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback: get changed files from git if checkpoint doesn't have them
|
|
122
|
+
if (!checkpoint || !checkpoint.changedFiles || checkpoint.changedFiles.length === 0) {
|
|
123
|
+
try {
|
|
124
|
+
const { execFileSync } = require('node:child_process');
|
|
125
|
+
const gitFiles = execFileSync('git', ['diff', '--name-only', 'HEAD'], {
|
|
126
|
+
encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
127
|
+
}).trim();
|
|
128
|
+
if (gitFiles) {
|
|
129
|
+
const files = gitFiles.split('\n').filter(Boolean).slice(0, 20);
|
|
130
|
+
contextParts.push(`**Uncommitted changes** (${files.length} files):\n${files.map(f => ` - ${f}`).join('\n')}`);
|
|
131
|
+
}
|
|
132
|
+
} catch (_err) {
|
|
133
|
+
// git not available or no changes — skip silently
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (process.env.DEBUG) {
|
|
138
|
+
console.error(`[post-compact] Criteria/files restore failed: ${err.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2c. Load last request-log entry number
|
|
143
|
+
try {
|
|
144
|
+
const logPath = path.join(PATHS.state, 'request-log.md');
|
|
145
|
+
if (fs.existsSync(logPath)) {
|
|
146
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
147
|
+
// Find the last R-NNN entry
|
|
148
|
+
const matches = content.match(/^### R-(\d+)/gm);
|
|
149
|
+
if (matches && matches.length > 0) {
|
|
150
|
+
const lastEntry = matches[0]; // First match = most recent (file is reverse-chronological)
|
|
151
|
+
const num = lastEntry.match(/R-(\d+)/)?.[1];
|
|
152
|
+
if (num) {
|
|
153
|
+
contextParts.push(`**Last request-log entry**: R-${num} (next entry should be R-${parseInt(num, 10) + 1})`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (process.env.DEBUG) {
|
|
159
|
+
console.error(`[post-compact] Request-log read failed: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
85
163
|
// 3. Re-set routing-pending flag
|
|
86
164
|
// After compaction, the AI has fresh context and may try to act without routing.
|
|
87
165
|
// Setting routing-pending ensures the next tool use goes through routing checks.
|
|
@@ -99,8 +177,6 @@ function handlePostCompact() {
|
|
|
99
177
|
|
|
100
178
|
// 4. Load current workflow phase
|
|
101
179
|
try {
|
|
102
|
-
const { PATHS, safeJsonParse } = require('../../flow-utils');
|
|
103
|
-
const path = require('node:path');
|
|
104
180
|
const phasePath = path.join(PATHS.state, 'workflow-phase.json');
|
|
105
181
|
const phaseData = safeJsonParse(phasePath, {});
|
|
106
182
|
if (phaseData.phase && phaseData.phase !== 'idle') {
|
|
@@ -116,9 +192,6 @@ function handlePostCompact() {
|
|
|
116
192
|
// Claude Code stops auto-compaction after 3 consecutive failures.
|
|
117
193
|
// If we detect repeated compactions in quick succession, warn about potential issues.
|
|
118
194
|
try {
|
|
119
|
-
const path = require('node:path');
|
|
120
|
-
const fs = require('node:fs');
|
|
121
|
-
const { PATHS, safeJsonParse } = require('../../flow-utils');
|
|
122
195
|
const compactStatePath = path.join(PATHS.state, '.compact-tracker.json');
|
|
123
196
|
const tracker = safeJsonParse(compactStatePath, { count: 0, lastAt: null });
|
|
124
197
|
const now = Date.now();
|
|
@@ -136,7 +209,7 @@ function handlePostCompact() {
|
|
|
136
209
|
fs.writeFileSync(compactStatePath, JSON.stringify(tracker, null, 2));
|
|
137
210
|
|
|
138
211
|
if (tracker.count >= 3) {
|
|
139
|
-
contextParts.push('**WARNING**: Multiple compactions detected in quick succession. Claude Code 2.1.76+ stops auto-compaction after 3 consecutive failures. If context keeps growing, consider starting a new session
|
|
212
|
+
contextParts.push('**WARNING**: Multiple compactions detected in quick succession. Claude Code 2.1.76+ stops auto-compaction after 3 consecutive failures. If context keeps growing, consider starting a new session.');
|
|
140
213
|
}
|
|
141
214
|
} catch (err) {
|
|
142
215
|
if (process.env.DEBUG) {
|
|
@@ -93,6 +93,12 @@ async function handleTaskCompleted(input) {
|
|
|
93
93
|
completedTask.status = 'completed';
|
|
94
94
|
completedTask.completedAt = new Date().toISOString();
|
|
95
95
|
|
|
96
|
+
// Strip progress prefix from title (e.g., "[3/5] Title" → "Title")
|
|
97
|
+
// Done inside the lock to avoid race conditions with progress tracker
|
|
98
|
+
if (completedTask.title) {
|
|
99
|
+
completedTask.title = completedTask.title.replace(/^\[\d+\/\d+\]\s*/, '');
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
// Remove from inProgress
|
|
97
103
|
ready.inProgress = ready.inProgress.filter(t =>
|
|
98
104
|
(typeof t === 'string' ? t : t.id) !== completedTask.id
|
|
@@ -140,6 +146,19 @@ async function handleTaskCompleted(input) {
|
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
148
|
|
|
149
|
+
// Clear progress tracker state on task completion
|
|
150
|
+
if (result.completed) {
|
|
151
|
+
try {
|
|
152
|
+
const { clearProgress } = require('../../flow-progress-tracker');
|
|
153
|
+
clearProgress();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
// Non-fatal — progress tracker may not exist in older installs
|
|
156
|
+
if (process.env.DEBUG) {
|
|
157
|
+
console.error(`[Task Completed] Progress clear failed: ${err.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
143
162
|
// Update durable history if it exists (under lock to prevent concurrent corruption)
|
|
144
163
|
if (result.completed) {
|
|
145
164
|
try {
|
|
@@ -113,6 +113,26 @@ async function main() {
|
|
|
113
113
|
if (process.env.DEBUG) console.error(`[post-tool-use] setCurrentTask: ${err.message}`);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// v7.0: Initialize task checkpoint with criteria for PostCompact recovery
|
|
117
|
+
try {
|
|
118
|
+
const { saveCheckpoint } = require('../../../flow-task-checkpoint');
|
|
119
|
+
const criteriaList = (task.acceptanceCriteria || task.scenarios || [])
|
|
120
|
+
.map((c, i) => ({
|
|
121
|
+
id: `ac-${i + 1}`,
|
|
122
|
+
text: typeof c === 'string' ? c : (c.description || c.title || `Criterion ${i + 1}`),
|
|
123
|
+
done: false
|
|
124
|
+
}));
|
|
125
|
+
await saveCheckpoint({
|
|
126
|
+
taskId,
|
|
127
|
+
taskTitle,
|
|
128
|
+
currentPhase: 'coding',
|
|
129
|
+
criteria: criteriaList,
|
|
130
|
+
changedFiles: []
|
|
131
|
+
});
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (process.env.DEBUG) console.error(`[post-tool-use] Checkpoint init: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
if (process.env.DEBUG) {
|
|
117
137
|
console.error(`[post-tool-use] Initialized durable session for ${taskId} (prompt-path bridge)`);
|
|
118
138
|
}
|
|
@@ -126,6 +146,31 @@ async function main() {
|
|
|
126
146
|
}
|
|
127
147
|
}
|
|
128
148
|
|
|
149
|
+
// Auto registry scan after successful git commit (fire-and-forget)
|
|
150
|
+
// v7.0: Mechanical enforcement — AI no longer needs to remember to run registry scan.
|
|
151
|
+
// Runs after every commit, regardless of task level (L3 included).
|
|
152
|
+
if (toolName === 'Bash' && toolInput.command && !toolFailed) {
|
|
153
|
+
const { isGitCommit } = require('../../core/commit-log-gate');
|
|
154
|
+
if (isGitCommit(toolInput.command)) {
|
|
155
|
+
try {
|
|
156
|
+
const { RegistryManager } = require('../../../flow-registry-manager');
|
|
157
|
+
const manager = new RegistryManager();
|
|
158
|
+
manager.loadPlugins();
|
|
159
|
+
manager.activatePlugins();
|
|
160
|
+
manager.scanAll().catch((err) => {
|
|
161
|
+
if (process.env.DEBUG) {
|
|
162
|
+
console.error(`[post-tool-use] Auto registry scan failed: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
// Non-blocking — registry manager may not be available
|
|
167
|
+
if (process.env.DEBUG) {
|
|
168
|
+
console.error(`[post-tool-use] Registry manager load error: ${err.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
129
174
|
// Only run validation for Edit/Write
|
|
130
175
|
if (toolName !== 'Edit' && toolName !== 'Write') {
|
|
131
176
|
console.log(JSON.stringify({ continue: true }));
|
|
@@ -140,6 +185,21 @@ async function main() {
|
|
|
140
185
|
return;
|
|
141
186
|
}
|
|
142
187
|
|
|
188
|
+
// v7.0: Track changed files in task checkpoint (continuous state persistence)
|
|
189
|
+
// This ensures the PostCompact hook can restore the changed files list
|
|
190
|
+
// after auto-compaction, making /wogi-pre-compact redundant for file tracking.
|
|
191
|
+
if (filePath && !filePath.includes('.workflow/') && !filePath.includes('.claude/')) {
|
|
192
|
+
try {
|
|
193
|
+
const { trackChangedFile } = require('../../../flow-task-checkpoint');
|
|
194
|
+
trackChangedFile(filePath);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// Non-blocking — checkpoint may not exist yet (no active task)
|
|
197
|
+
if (process.env.DEBUG) {
|
|
198
|
+
console.error(`[post-tool-use] File tracking: ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
143
203
|
// Run validation
|
|
144
204
|
const coreResult = await runValidation({
|
|
145
205
|
filePath,
|
|
@@ -17,6 +17,7 @@ const { checkComponentReuse } = require('../../core/component-check');
|
|
|
17
17
|
const { checkTodoWriteGate } = require('../../core/todowrite-gate');
|
|
18
18
|
const { checkRoutingGate, clearRoutingPending, hasActiveTask } = require('../../core/routing-gate');
|
|
19
19
|
const { checkPhaseGate } = require('../../core/phase-gate');
|
|
20
|
+
const { checkCommitLogGate } = require('../../core/commit-log-gate');
|
|
20
21
|
const { claudeCodeAdapter } = require('../../adapters/claude-code');
|
|
21
22
|
const { markSkillPending } = require('../../../flow-durable-session');
|
|
22
23
|
const { getConfig } = require('../../../flow-utils');
|
|
@@ -242,6 +243,32 @@ async function main() {
|
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
// Commit log gate check (for Bash git commit commands)
|
|
247
|
+
// v9.0: Block git commit when active task has no request-log entry staged.
|
|
248
|
+
// Same mechanical enforcement pattern as routing gate.
|
|
249
|
+
if (toolName === 'Bash' && toolInput.command) {
|
|
250
|
+
try {
|
|
251
|
+
const commitLogResult = checkCommitLogGate(toolInput.command, config);
|
|
252
|
+
if (commitLogResult.blocked) {
|
|
253
|
+
coreResult = {
|
|
254
|
+
allowed: false,
|
|
255
|
+
blocked: true,
|
|
256
|
+
reason: `Commit log gate: ${commitLogResult.reason}`,
|
|
257
|
+
message: commitLogResult.message
|
|
258
|
+
};
|
|
259
|
+
const output = claudeCodeAdapter.transformResult('PreToolUse', coreResult);
|
|
260
|
+
console.log(JSON.stringify(output));
|
|
261
|
+
process.exit(0);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// Fail-open for commit log gate — don't block work if gate has issues
|
|
266
|
+
if (process.env.DEBUG) {
|
|
267
|
+
console.error(`[Hook] Commit log gate error (fail-open): ${err.message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
245
272
|
// Strict adherence check (for Bash commands)
|
|
246
273
|
// v5.0: Block AI from using wrong package manager or port
|
|
247
274
|
if (toolName === 'Bash') {
|
|
@@ -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
|
|