moflo 4.13.4 → 4.13.5
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/bin/generate-code-map.mjs +13 -123
- package/bin/index-guidance.mjs +37 -4
- package/bin/index-patterns.mjs +15 -25
- package/bin/lib/index-fingerprint.mjs +15 -2
- package/bin/lib/source-scope.mjs +219 -0
- package/dist/src/cli/memory/bridge-embedder.js +5 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* flo-codemap # Via PATH
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
29
29
|
import { resolve, dirname, relative, basename, extname } from 'path';
|
|
30
30
|
import { fileURLToPath } from 'url';
|
|
31
31
|
import { execSync, execFileSync, spawn } from 'child_process';
|
|
@@ -33,6 +33,7 @@ import { memoryDbPath, MOFLO_DIR, findProjectRoot } from './lib/moflo-paths.mjs'
|
|
|
33
33
|
import { openBackend } from './lib/get-backend.mjs';
|
|
34
34
|
import { applyIncrementalChunks, schemeTaggedContentHash } from './lib/incremental-write.mjs';
|
|
35
35
|
import { resolveMofloBin } from './lib/resolve-bin.mjs';
|
|
36
|
+
import { listScopedSourceFiles } from './lib/source-scope.mjs';
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -50,13 +51,10 @@ const HASH_CACHE_PATH = resolve(projectRoot, MOFLO_DIR, 'code-map-hash.txt');
|
|
|
50
51
|
// delegates here rather than shipping a second, divergent generator.
|
|
51
52
|
// v2: unified scheme — invalidates every pre-#1260 cache (bare hash or the
|
|
52
53
|
// old directory-level `flo memory code-map` output) exactly once on pickup.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
const
|
|
57
|
-
'node_modules', 'dist', 'build', '.next', 'coverage',
|
|
58
|
-
'.claude', 'template', 'back-office-template',
|
|
59
|
-
];
|
|
54
|
+
// v3: #1497 — enumeration now honours code_map.directories/exclude on the git
|
|
55
|
+
// path and drops test files, so every consumer's code-map rebuilds once and
|
|
56
|
+
// the orphan sweep removes the out-of-scope rows.
|
|
57
|
+
const SCHEME_VERSION = 3;
|
|
60
58
|
|
|
61
59
|
// Heuristic descriptions for well-known directory names
|
|
62
60
|
const DIR_DESCRIPTIONS = {
|
|
@@ -163,53 +161,6 @@ function countMissingEmbeddings(db) {
|
|
|
163
161
|
// Source file enumeration — git ls-files with filesystem fallback
|
|
164
162
|
// ---------------------------------------------------------------------------
|
|
165
163
|
|
|
166
|
-
/** Read code_map config from moflo.yaml (directories, extensions, exclude). */
|
|
167
|
-
function readCodeMapConfig() {
|
|
168
|
-
const defaults = {
|
|
169
|
-
directories: ['src'],
|
|
170
|
-
extensions: [
|
|
171
|
-
'.ts', '.tsx', '.js', '.mjs', '.cjs', '.jsx', // JS/TS
|
|
172
|
-
'.py', '.pyi', // Python
|
|
173
|
-
'.go', // Go
|
|
174
|
-
'.java', '.kt', '.kts', // JVM
|
|
175
|
-
'.cs', // C#
|
|
176
|
-
'.rs', // Rust
|
|
177
|
-
'.rb', // Ruby
|
|
178
|
-
'.swift', // Swift
|
|
179
|
-
'.php', // PHP
|
|
180
|
-
'.c', '.h', '.cpp', '.hpp', '.cc', // C/C++
|
|
181
|
-
],
|
|
182
|
-
exclude: [...EXCLUDE_DIRS],
|
|
183
|
-
};
|
|
184
|
-
try {
|
|
185
|
-
const yamlPath = resolve(projectRoot, 'moflo.yaml');
|
|
186
|
-
if (!existsSync(yamlPath)) return defaults;
|
|
187
|
-
const content = readFileSync(yamlPath, 'utf-8');
|
|
188
|
-
// Simple YAML parsing for code_map block
|
|
189
|
-
const block = content.match(/code_map:\s*\n((?:\s+\w+:.*\n?|\s+- .*\n?)+)/);
|
|
190
|
-
if (!block) return defaults;
|
|
191
|
-
const lines = block[1].split('\n');
|
|
192
|
-
let currentKey = null;
|
|
193
|
-
const result = { ...defaults };
|
|
194
|
-
for (const line of lines) {
|
|
195
|
-
const keyMatch = line.match(/^\s+(\w+):/);
|
|
196
|
-
const itemMatch = line.match(/^\s+- (.+)/);
|
|
197
|
-
if (keyMatch) {
|
|
198
|
-
currentKey = keyMatch[1];
|
|
199
|
-
// Inline array: extensions: [".ts", ".tsx"]
|
|
200
|
-
const inlineArray = line.match(/\[([^\]]+)\]/);
|
|
201
|
-
if (inlineArray && (currentKey === 'extensions' || currentKey === 'exclude' || currentKey === 'directories')) {
|
|
202
|
-
result[currentKey] = inlineArray[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
203
|
-
}
|
|
204
|
-
} else if (itemMatch && currentKey) {
|
|
205
|
-
if (!Array.isArray(result[currentKey])) result[currentKey] = [];
|
|
206
|
-
result[currentKey].push(itemMatch[1].trim().replace(/^["']|["']$/g, ''));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return result;
|
|
210
|
-
} catch { return defaults; }
|
|
211
|
-
}
|
|
212
|
-
|
|
213
164
|
/**
|
|
214
165
|
* Extension of `p`, lowercased.
|
|
215
166
|
*
|
|
@@ -222,75 +173,14 @@ function normExt(p) {
|
|
|
222
173
|
return extname(p).toLowerCase();
|
|
223
174
|
}
|
|
224
175
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
let entries;
|
|
230
|
-
try {
|
|
231
|
-
entries = readdirSync(resolve(projectRoot, dir), { withFileTypes: true });
|
|
232
|
-
} catch { return []; }
|
|
233
|
-
for (const entry of entries) {
|
|
234
|
-
if (excludeSet.has(entry.name)) continue;
|
|
235
|
-
// Use forward slashes for consistent cross-platform paths
|
|
236
|
-
const rel = dir ? `${dir}/${entry.name}` : entry.name;
|
|
237
|
-
if (entry.isDirectory()) {
|
|
238
|
-
results.push(...walkDir(rel, extensions, excludeSet, maxDepth, depth + 1));
|
|
239
|
-
} else if (entry.isFile()) {
|
|
240
|
-
if (extensions.has(normExt(entry.name))) results.push(rel);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return results;
|
|
244
|
-
}
|
|
245
|
-
|
|
176
|
+
/**
|
|
177
|
+
* In-scope source files (#1497): `code_map.directories` minus `code_map.exclude`,
|
|
178
|
+
* never gitignored, never a test file — on the git path and the fallback alike.
|
|
179
|
+
*/
|
|
246
180
|
function getSourceFiles() {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const extSet = new Set(config.extensions.map(e => e.toLowerCase()));
|
|
251
|
-
const excludeSet = new Set(config.exclude);
|
|
252
|
-
|
|
253
|
-
// Build git glob patterns from configured extensions.
|
|
254
|
-
// `:(icase)` is required, not cosmetic: git pathspec globs are case-sensitive
|
|
255
|
-
// regardless of `core.ignorecase` (verified on git 2.43 with the setting both
|
|
256
|
-
// on and off), so a bare `*.mjs` misses a committed `Foo.MJS` on every
|
|
257
|
-
// platform. This is the primary enumeration path — normalising `extname()` in
|
|
258
|
-
// the filesystem fallback alone would leave it broken. (#1337)
|
|
259
|
-
const gitGlobArgs = config.extensions.map(ext => `:(icase)*${ext}`);
|
|
260
|
-
|
|
261
|
-
// Try git ls-files first (fast, respects .gitignore)
|
|
262
|
-
try {
|
|
263
|
-
const raw = execFileSync(
|
|
264
|
-
'git', ['ls-files', '--', ...gitGlobArgs],
|
|
265
|
-
{ cwd: projectRoot, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, windowsHide: true }
|
|
266
|
-
).trim();
|
|
267
|
-
|
|
268
|
-
if (raw) {
|
|
269
|
-
const files = raw.split('\n')
|
|
270
|
-
.map(f => f.replace(/\\/g, '/')) // normalize separators
|
|
271
|
-
.filter(f => {
|
|
272
|
-
for (const ex of EXCLUDE_DIRS) {
|
|
273
|
-
if (f.startsWith(ex + '/')) return false;
|
|
274
|
-
}
|
|
275
|
-
return true;
|
|
276
|
-
});
|
|
277
|
-
if (files.length > 0) return files;
|
|
278
|
-
}
|
|
279
|
-
} catch {
|
|
280
|
-
// git not available or not a git repo — fall through
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Fallback: walk configured directories from moflo.yaml
|
|
284
|
-
log('git ls-files returned no files — falling back to filesystem walk');
|
|
285
|
-
const files = [];
|
|
286
|
-
|
|
287
|
-
for (const dir of config.directories) {
|
|
288
|
-
if (existsSync(resolve(projectRoot, dir))) {
|
|
289
|
-
files.push(...walkDir(dir, extSet, excludeSet));
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return files;
|
|
181
|
+
return listScopedSourceFiles(projectRoot, {
|
|
182
|
+
onFallback: () => log('git ls-files returned no files — falling back to filesystem walk'),
|
|
183
|
+
});
|
|
294
184
|
}
|
|
295
185
|
|
|
296
186
|
function isUnchanged(currentHash) {
|
package/bin/index-guidance.mjs
CHANGED
|
@@ -119,7 +119,18 @@ function loadGuidanceDirs() {
|
|
|
119
119
|
resolve(bundledGuidanceDir) !== resolve(projectGuidanceDir) &&
|
|
120
120
|
resolve(bundledGuidanceDir) !== resolve(projectGuidanceDir, 'shipped')
|
|
121
121
|
) {
|
|
122
|
-
|
|
122
|
+
// #1497 — session-start syncs every shipped doc into `.claude/guidance/`,
|
|
123
|
+
// so once that copy exists the bundled one is a byte-identical twin that
|
|
124
|
+
// halves the distinct docs per search. Shadow it — but only when the
|
|
125
|
+
// project's `.claude/guidance` is itself a configured directory; otherwise
|
|
126
|
+
// the synced copy is never indexed and the bundled one is the only copy.
|
|
127
|
+
const projectGuidanceIndexed = userDirs.some(d => resolve(projectRoot, d) === projectGuidanceDir);
|
|
128
|
+
dirs.push({
|
|
129
|
+
path: bundledGuidanceDir,
|
|
130
|
+
prefix: 'moflo-bundled',
|
|
131
|
+
absolute: true,
|
|
132
|
+
shadowDir: projectGuidanceIndexed ? projectGuidanceDir : null,
|
|
133
|
+
});
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
// 3. CLAUDE.md files are NOT indexed — Claude loads them into context automatically.
|
|
@@ -134,7 +145,17 @@ function loadGuidanceDirs() {
|
|
|
134
145
|
// 5. Bundled moflo skills — gated by isSelfRef to prevent double-indexing
|
|
135
146
|
const bundledSkillsDir = resolve(mofloRoot, '.claude/skills');
|
|
136
147
|
if (!isSelfRef && existsSync(bundledSkillsDir) && resolve(bundledSkillsDir) !== resolve(projectSkillsDir)) {
|
|
137
|
-
|
|
148
|
+
// #1497 — skills synced into `.claude/skills/<name>/` are indexed by step 4;
|
|
149
|
+
// index a bundled skill only when the project holds no copy of it (e.g. a
|
|
150
|
+
// skill excluded by a `skills: categories` selection).
|
|
151
|
+
dirs.push({
|
|
152
|
+
path: bundledSkillsDir,
|
|
153
|
+
prefix: 'skill-bundled',
|
|
154
|
+
fileFilter: ['SKILL.md'],
|
|
155
|
+
kind: 'skill',
|
|
156
|
+
absolute: true,
|
|
157
|
+
shadowDir: projectSkillsDir,
|
|
158
|
+
});
|
|
138
159
|
}
|
|
139
160
|
|
|
140
161
|
// 6. SDD spec/plan artifacts are NOT indexed.
|
|
@@ -683,6 +704,17 @@ function walkMdFiles(dir, excludeRoots = []) {
|
|
|
683
704
|
return files;
|
|
684
705
|
}
|
|
685
706
|
|
|
707
|
+
/**
|
|
708
|
+
* True when a bundled file's project-side copy (same relative path under
|
|
709
|
+
* `dirConfig.shadowDir`) exists and is therefore indexed by another entry.
|
|
710
|
+
* Skipping it here also drops it from the run's live prefix set, so the stale
|
|
711
|
+
* sweep removes rows a previous run wrote for the bundled twin.
|
|
712
|
+
*/
|
|
713
|
+
function isShadowed(dirConfig, dirPath, filePath) {
|
|
714
|
+
if (!dirConfig.shadowDir) return false;
|
|
715
|
+
return existsSync(resolve(dirConfig.shadowDir, relative(dirPath, filePath)));
|
|
716
|
+
}
|
|
717
|
+
|
|
686
718
|
function indexDirectory(db, dirConfig) {
|
|
687
719
|
const dirPath = dirConfig.absolute ? dirConfig.path : resolve(projectRoot, dirConfig.path);
|
|
688
720
|
const results = [];
|
|
@@ -693,9 +725,10 @@ function indexDirectory(db, dirConfig) {
|
|
|
693
725
|
}
|
|
694
726
|
|
|
695
727
|
const allMdFiles = walkMdFiles(dirPath, EXCLUDE_ROOTS);
|
|
696
|
-
const filtered = dirConfig.fileFilter
|
|
728
|
+
const filtered = (dirConfig.fileFilter
|
|
697
729
|
? allMdFiles.filter(f => dirConfig.fileFilter.includes(basename(f)))
|
|
698
|
-
: allMdFiles
|
|
730
|
+
: allMdFiles
|
|
731
|
+
).filter(f => !isShadowed(dirConfig, dirPath, f));
|
|
699
732
|
|
|
700
733
|
for (const filePath of filtered) {
|
|
701
734
|
let options = {};
|
package/bin/index-patterns.mjs
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* - API route definitions and middleware usage
|
|
8
8
|
* - Error handling strategies per file
|
|
9
9
|
* - Export conventions per module
|
|
10
|
-
* - Test patterns (describe/it structure)
|
|
11
10
|
* - Configuration patterns
|
|
12
11
|
*
|
|
13
12
|
* Chunk types:
|
|
@@ -24,14 +23,15 @@
|
|
|
24
23
|
* flo-patterns # Via PATH
|
|
25
24
|
*/
|
|
26
25
|
|
|
27
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync
|
|
28
|
-
import { resolve, dirname, relative
|
|
26
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
27
|
+
import { resolve, dirname, relative } from 'path';
|
|
29
28
|
import { fileURLToPath } from 'url';
|
|
30
29
|
import { resolveMofloBin } from './lib/resolve-bin.mjs';
|
|
31
30
|
import { memoryDbPath, MOFLO_DIR, findProjectRoot } from './lib/moflo-paths.mjs';
|
|
32
31
|
import { openBackend } from './lib/get-backend.mjs';
|
|
33
32
|
import { applyIncrementalChunks, computeContentListHash } from './lib/incremental-write.mjs';
|
|
34
33
|
import { createProcessManager } from './lib/process-manager.mjs';
|
|
34
|
+
import { listScopedSourceFiles } from './lib/source-scope.mjs';
|
|
35
35
|
|
|
36
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37
37
|
|
|
@@ -48,11 +48,7 @@ const statsOnly = args.includes('--stats');
|
|
|
48
48
|
function log(msg) { console.log(`[index-patterns] ${msg}`); }
|
|
49
49
|
function debug(msg) { if (verbose) console.log(`[index-patterns] ${msg}`); }
|
|
50
50
|
|
|
51
|
-
const SOURCE_EXTENSIONS =
|
|
52
|
-
const EXCLUDE_DIRS = new Set([
|
|
53
|
-
'node_modules', 'dist', 'build', '.next', 'coverage',
|
|
54
|
-
'.claude', '.swarm', '.moflo', '.git', 'template',
|
|
55
|
-
]);
|
|
51
|
+
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs'];
|
|
56
52
|
|
|
57
53
|
// ---------------------------------------------------------------------------
|
|
58
54
|
// Database helpers
|
|
@@ -113,21 +109,15 @@ function countNamespace(db) {
|
|
|
113
109
|
// File collection
|
|
114
110
|
// ---------------------------------------------------------------------------
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
files.push(...collectSourceFiles(fullPath, maxDepth, depth + 1));
|
|
126
|
-
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
127
|
-
files.push(fullPath);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return files;
|
|
112
|
+
/**
|
|
113
|
+
* Absolute paths of in-scope source files (#1497). Shares code-map's scope —
|
|
114
|
+
* `code_map.directories` minus `code_map.exclude`, never gitignored, never a
|
|
115
|
+
* test file — so a raw repo walk can no longer pull in build output such as
|
|
116
|
+
* `cdk.out/asset.<hash>/index.mjs` or re-index what the tests namespace owns.
|
|
117
|
+
*/
|
|
118
|
+
function collectSourceFiles() {
|
|
119
|
+
return listScopedSourceFiles(projectRoot, { extensions: SOURCE_EXTENSIONS })
|
|
120
|
+
.map(rel => resolve(projectRoot, rel));
|
|
131
121
|
}
|
|
132
122
|
|
|
133
123
|
// ---------------------------------------------------------------------------
|
|
@@ -259,14 +249,14 @@ async function main() {
|
|
|
259
249
|
if (statsOnly) {
|
|
260
250
|
const db = await getDb();
|
|
261
251
|
const count = countNamespace(db);
|
|
262
|
-
const files = collectSourceFiles(
|
|
252
|
+
const files = collectSourceFiles();
|
|
263
253
|
log(`${files.length} source files, ${count} chunks in patterns namespace`);
|
|
264
254
|
db.close();
|
|
265
255
|
return;
|
|
266
256
|
}
|
|
267
257
|
|
|
268
258
|
// Collect files
|
|
269
|
-
const files = collectSourceFiles(
|
|
259
|
+
const files = collectSourceFiles();
|
|
270
260
|
log(`Found ${files.length} source files`);
|
|
271
261
|
|
|
272
262
|
if (files.length === 0) {
|
|
@@ -189,6 +189,14 @@ function newestGuidanceMtime(projectRoot) {
|
|
|
189
189
|
return newest;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
function sourceScopeFingerprint(projectRoot) {
|
|
193
|
+
return {
|
|
194
|
+
sourceList: gitFileListHash(projectRoot, SOURCE_GLOBS),
|
|
195
|
+
config: safeMtime(resolve(projectRoot, 'moflo.yaml')),
|
|
196
|
+
mofloPkg: safeMtime(resolve(projectRoot, 'node_modules/moflo/package.json')),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
192
200
|
/**
|
|
193
201
|
* Per-step fingerprint computers. Each takes `projectRoot` and returns a flat
|
|
194
202
|
* `{ input → value }` object. All MUST be sync, cheap (no sql.js loads), and
|
|
@@ -212,9 +220,14 @@ const STEP_FINGERPRINT_COMPUTERS = {
|
|
|
212
220
|
mofloPkg: safeMtime(resolve(projectRoot, 'node_modules/moflo/package.json')),
|
|
213
221
|
}),
|
|
214
222
|
|
|
215
|
-
|
|
223
|
+
// Source edits, plus the two inputs that change WHICH files are in scope
|
|
224
|
+
// (#1497): `moflo.yaml` (code_map.directories / exclude) and a moflo upgrade
|
|
225
|
+
// (the scope rules ship with the package). Without them a scope fix or a
|
|
226
|
+
// config edit leaves out-of-scope rows in place until some unrelated source
|
|
227
|
+
// edit happens to invalidate the gate.
|
|
228
|
+
'code-map': (projectRoot) => sourceScopeFingerprint(projectRoot),
|
|
216
229
|
'test-index': (projectRoot) => ({ testList: gitFileListHash(projectRoot, TEST_GLOBS) }),
|
|
217
|
-
'patterns-index': (projectRoot) => (
|
|
230
|
+
'patterns-index': (projectRoot) => sourceScopeFingerprint(projectRoot),
|
|
218
231
|
|
|
219
232
|
// Library-docs grounding (#1184). Re-index when installed dependency versions
|
|
220
233
|
// change — which rewrites the lockfile (npm/yarn/pnpm/bun) — or when the dep
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-file scope shared by the code-map and patterns indexers (#1497).
|
|
3
|
+
*
|
|
4
|
+
* Both indexers used to enumerate the whole repository: code-map ran
|
|
5
|
+
* `git ls-files` over every tracked file and applied `code_map.directories` /
|
|
6
|
+
* `code_map.exclude` only in its filesystem fallback, and patterns walked the
|
|
7
|
+
* raw filesystem with no `.gitignore` handling at all — so gitignored build
|
|
8
|
+
* output (e.g. `cdk.out/asset.<hash>/index.mjs`) and every test file landed in
|
|
9
|
+
* semantic search. One scope, applied on every enumeration path, keeps them in
|
|
10
|
+
* step with each other and with what `moflo.yaml` says.
|
|
11
|
+
*
|
|
12
|
+
* Scope rules:
|
|
13
|
+
* - Only files under a configured `code_map.directories` entry (`.` = repo).
|
|
14
|
+
* - Never a gitignored file: enumeration is `git ls-files --cached --others
|
|
15
|
+
* --exclude-standard`, i.e. tracked files plus untracked-but-not-ignored.
|
|
16
|
+
* - Never a path matched by `code_map.exclude` — a bare name matches any
|
|
17
|
+
* path segment, a name containing `/` matches a repo-relative path prefix.
|
|
18
|
+
* - Never a test file — the `tests` namespace owns those.
|
|
19
|
+
*
|
|
20
|
+
* All returned paths are repo-relative with forward slashes on every platform.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
24
|
+
import { resolve, extname } from 'node:path';
|
|
25
|
+
import { execFileSync } from 'node:child_process';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Top-level directories never indexed, whatever the config says. Kept as
|
|
29
|
+
* top-level prefixes (not segment matches) so a legitimate nested
|
|
30
|
+
* `src/template/` is not swallowed by the `template` entry.
|
|
31
|
+
*/
|
|
32
|
+
export const BASELINE_EXCLUDE_DIRS = [
|
|
33
|
+
'node_modules', 'dist', 'build', '.next', 'coverage',
|
|
34
|
+
'.claude', '.swarm', '.moflo', '.git', 'template', 'back-office-template',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** Same shapes `bin/index-tests.mjs` and `moflo.yaml tests.patterns` treat as tests. */
|
|
38
|
+
const TEST_FILE_PATTERNS = [/\.test\.\w+$/i, /\.spec\.\w+$/i, /\.test-\w+\.\w+$/i];
|
|
39
|
+
|
|
40
|
+
const DEFAULT_EXTENSIONS = [
|
|
41
|
+
'.ts', '.tsx', '.js', '.mjs', '.cjs', '.jsx', // JS/TS
|
|
42
|
+
'.py', '.pyi', // Python
|
|
43
|
+
'.go', // Go
|
|
44
|
+
'.java', '.kt', '.kts', // JVM
|
|
45
|
+
'.cs', // C#
|
|
46
|
+
'.rs', // Rust
|
|
47
|
+
'.rb', // Ruby
|
|
48
|
+
'.swift', // Swift
|
|
49
|
+
'.php', // PHP
|
|
50
|
+
'.c', '.h', '.cpp', '.hpp', '.cc', // C/C++
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** Read the `code_map` block from moflo.yaml (directories, extensions, exclude). */
|
|
54
|
+
export function readCodeMapConfig(projectRoot) {
|
|
55
|
+
// Unconfigured `directories` means the whole repo — the scope code-map's git
|
|
56
|
+
// path and the patterns walk both had before #1497. A consumer whose
|
|
57
|
+
// moflo.yaml predates `code_map` (or omits `directories`) must not silently
|
|
58
|
+
// drop to `src/` only, or to nothing at all when there is no `src/` (Rule #2).
|
|
59
|
+
const defaults = {
|
|
60
|
+
directories: ['.'],
|
|
61
|
+
extensions: [...DEFAULT_EXTENSIONS],
|
|
62
|
+
exclude: [...BASELINE_EXCLUDE_DIRS],
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
const yamlPath = resolve(projectRoot, 'moflo.yaml');
|
|
66
|
+
if (!existsSync(yamlPath)) return defaults;
|
|
67
|
+
const content = readFileSync(yamlPath, 'utf-8');
|
|
68
|
+
// Simple YAML parsing for code_map block
|
|
69
|
+
// Indented `#` lines are allowed inside the block so a commented-out list
|
|
70
|
+
// entry does not end it early and drop every entry after it.
|
|
71
|
+
const block = content.match(/code_map:\s*\n((?:\s+\w+:.*\n?|\s+- .*\n?|[ \t]+#.*\n?)+)/);
|
|
72
|
+
if (!block) return defaults;
|
|
73
|
+
const lines = block[1].split(/\r?\n/);
|
|
74
|
+
let currentKey = null;
|
|
75
|
+
const result = { ...defaults };
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
const keyMatch = line.match(/^\s+(\w+):/);
|
|
78
|
+
const itemMatch = line.match(/^\s+- (.+)/);
|
|
79
|
+
if (keyMatch) {
|
|
80
|
+
currentKey = keyMatch[1];
|
|
81
|
+
// Inline array: extensions: [".ts", ".tsx"]
|
|
82
|
+
const inlineArray = line.match(/\[([^\]]+)\]/);
|
|
83
|
+
if (inlineArray && (currentKey === 'extensions' || currentKey === 'exclude' || currentKey === 'directories')) {
|
|
84
|
+
result[currentKey] = inlineArray[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
85
|
+
} else if (currentKey === 'directories' || currentKey === 'exclude' || currentKey === 'extensions') {
|
|
86
|
+
// Block list follows — start from empty rather than appending to the defaults.
|
|
87
|
+
result[currentKey] = [];
|
|
88
|
+
}
|
|
89
|
+
} else if (itemMatch && currentKey) {
|
|
90
|
+
if (!Array.isArray(result[currentKey])) result[currentKey] = [];
|
|
91
|
+
result[currentKey].push(itemMatch[1].trim().replace(/^["']|["']$/g, ''));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
} catch { return defaults; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Normalise a config path to repo-relative forward-slash form ('' = repo root). */
|
|
99
|
+
function normRel(p) {
|
|
100
|
+
return String(p).replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '').replace(/^\.$/, '');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Rule #1: NTFS and APFS are case-insensitive by default, so `Src/` in config
|
|
104
|
+
// must still match git's `src/...` there. Linux compares exactly.
|
|
105
|
+
const foldCase = process.platform === 'win32' || process.platform === 'darwin'
|
|
106
|
+
? (s) => s.toLowerCase()
|
|
107
|
+
: (s) => s;
|
|
108
|
+
|
|
109
|
+
const underPrefix = (path, prefix) => prefix === '' || path === prefix || path.startsWith(prefix + '/');
|
|
110
|
+
|
|
111
|
+
export function isTestFile(relPath) {
|
|
112
|
+
const name = relPath.slice(relPath.lastIndexOf('/') + 1);
|
|
113
|
+
return TEST_FILE_PATTERNS.some(p => p.test(name));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the in-scope predicate for a repo-relative forward-slash path.
|
|
118
|
+
* Exported for tests; indexers should call {@link listScopedSourceFiles}.
|
|
119
|
+
*/
|
|
120
|
+
export function createScopeFilter(config) {
|
|
121
|
+
const dirs = (config.directories || []).map(d => foldCase(normRel(d)));
|
|
122
|
+
const exts = new Set((config.extensions || []).map(e => e.toLowerCase()));
|
|
123
|
+
const excludeNames = new Set();
|
|
124
|
+
const excludePrefixes = [];
|
|
125
|
+
for (const raw of config.exclude || []) {
|
|
126
|
+
const e = foldCase(normRel(raw));
|
|
127
|
+
if (!e) continue;
|
|
128
|
+
if (e.includes('/')) excludePrefixes.push(e);
|
|
129
|
+
else excludeNames.add(e);
|
|
130
|
+
}
|
|
131
|
+
const baseline = BASELINE_EXCLUDE_DIRS.map(foldCase);
|
|
132
|
+
|
|
133
|
+
return (relPath) => {
|
|
134
|
+
const p = foldCase(relPath);
|
|
135
|
+
if (!exts.has(extname(p).toLowerCase())) return false;
|
|
136
|
+
if (!dirs.some(d => underPrefix(p, d))) return false;
|
|
137
|
+
if (baseline.some(b => p.startsWith(b + '/'))) return false;
|
|
138
|
+
if (excludePrefixes.some(e => underPrefix(p, e))) return false;
|
|
139
|
+
const segments = p.split('/');
|
|
140
|
+
if (segments.some(s => excludeNames.has(s))) return false;
|
|
141
|
+
return !isTestFile(p);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Git enumeration: tracked files plus untracked-but-not-ignored files, so a
|
|
147
|
+
* gitignored build artefact is never returned and a brand-new source file is.
|
|
148
|
+
* Returns null when git is unavailable or this is not a work tree.
|
|
149
|
+
*/
|
|
150
|
+
function gitListFiles(projectRoot, extensions) {
|
|
151
|
+
// `:(icase)` — git pathspec globs are case-sensitive regardless of
|
|
152
|
+
// `core.ignorecase`, so a bare `*.mjs` would miss a committed `Foo.MJS` (#1337).
|
|
153
|
+
const pathspecGlobs = extensions.map(ext => `:(icase)*${ext}`);
|
|
154
|
+
try {
|
|
155
|
+
const raw = execFileSync(
|
|
156
|
+
'git', ['ls-files', '--cached', '--others', '--exclude-standard', '--', ...pathspecGlobs],
|
|
157
|
+
// stderr ignored: "not a git repository" is the expected fallback signal,
|
|
158
|
+
// not something to print into a consumer's session-start output.
|
|
159
|
+
{
|
|
160
|
+
cwd: projectRoot, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024, windowsHide: true,
|
|
161
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
162
|
+
},
|
|
163
|
+
).trim();
|
|
164
|
+
if (!raw) return null;
|
|
165
|
+
// --cached + --others can list a path twice (e.g. mid-merge); the caller dedupes.
|
|
166
|
+
return raw.split(/\r?\n/).map(f => f.replace(/\\/g, '/'));
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Filesystem fallback: walk each configured directory. Only the two trees that
|
|
174
|
+
* are never source are pruned here (for speed); the scope filter decides the rest.
|
|
175
|
+
*/
|
|
176
|
+
const WALK_PRUNE = new Set(['node_modules', '.git']);
|
|
177
|
+
|
|
178
|
+
function walkFiles(projectRoot, rel, maxDepth = 8, depth = 0, out = []) {
|
|
179
|
+
if (depth > maxDepth) return out;
|
|
180
|
+
let entries;
|
|
181
|
+
try {
|
|
182
|
+
entries = readdirSync(resolve(projectRoot, rel || '.'), { withFileTypes: true });
|
|
183
|
+
} catch { return out; }
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
186
|
+
if (entry.isDirectory()) {
|
|
187
|
+
if (WALK_PRUNE.has(entry.name)) continue;
|
|
188
|
+
walkFiles(projectRoot, child, maxDepth, depth + 1, out);
|
|
189
|
+
} else if (entry.isFile()) {
|
|
190
|
+
out.push(child);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Every in-scope source file, repo-relative with forward slashes, sorted.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} projectRoot
|
|
200
|
+
* @param {{ extensions?: string[], config?: object, onFallback?: () => void }} [opts]
|
|
201
|
+
* `extensions` overrides `code_map.extensions` (patterns has its own list);
|
|
202
|
+
* `config` bypasses the moflo.yaml read (tests).
|
|
203
|
+
*/
|
|
204
|
+
export function listScopedSourceFiles(projectRoot, opts = {}) {
|
|
205
|
+
const config = { ...(opts.config || readCodeMapConfig(projectRoot)) };
|
|
206
|
+
if (opts.extensions) config.extensions = opts.extensions;
|
|
207
|
+
const inScope = createScopeFilter(config);
|
|
208
|
+
|
|
209
|
+
let candidates = gitListFiles(projectRoot, config.extensions);
|
|
210
|
+
if (!candidates) {
|
|
211
|
+
opts.onFallback?.();
|
|
212
|
+
candidates = [];
|
|
213
|
+
for (const dir of config.directories) {
|
|
214
|
+
const rel = normRel(dir);
|
|
215
|
+
if (existsSync(resolve(projectRoot, rel || '.'))) walkFiles(projectRoot, rel, 8, 0, candidates);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return [...new Set(candidates.filter(inScope))].sort();
|
|
219
|
+
}
|
|
@@ -76,6 +76,11 @@ export const EMBEDDING_MODEL_LEGACY_DEFAULT = 'local';
|
|
|
76
76
|
* `tasklist`, because the dashboard's Flo Runs tab
|
|
77
77
|
* (`daemon-dashboard.ts handleSpells`) reads tasklist; purging it on every
|
|
78
78
|
* session would empty the tab between sessions (#968).
|
|
79
|
+
*
|
|
80
|
+
* `verify` is deliberately NOT here (decided in #1497): `/verify` Step 0 runs a
|
|
81
|
+
* semantic `memory_search` over that namespace to surface prior verdicts for
|
|
82
|
+
* the area being changed, which needs embeddings. Its size is bounded by
|
|
83
|
+
* {@link VERIFY_RETENTION_CAP} instead.
|
|
79
84
|
*/
|
|
80
85
|
export const EPHEMERAL_NAMESPACES = new Set([
|
|
81
86
|
'hive-mind',
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.13.
|
|
3
|
+
"version": "4.13.5",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"@typescript-eslint/parser": "^8.65.0",
|
|
100
100
|
"eslint": "^10.8.0",
|
|
101
101
|
"glob": "^11.1.0",
|
|
102
|
-
"moflo": "^4.13.
|
|
102
|
+
"moflo": "^4.13.4",
|
|
103
103
|
"tsx": "^4.21.0",
|
|
104
104
|
"typescript": "^5.9.3",
|
|
105
105
|
"vitest": "^4.0.0"
|