fix(file-filter): correct folder rule matching for nested paths

- Fix path matching logic in PathTrie to check all possible starting positions
- Add proper cache key scoping to prevent cross-scope cache pollution
- Apply inline file filter early in worker processing path
- Improve code formatting consistency across files
This commit is contained in:
Quorafind 2025-09-11 16:17:28 +08:00
parent 8f9966efd6
commit 895444ce32
3 changed files with 145 additions and 107 deletions

View file

@ -89,9 +89,12 @@ export class DataflowOrchestrator {
// Initial sync of settings to Augmentor to ensure correct inheritance behavior on startup
try {
const initFmi = this.plugin?.settings?.fileMetadataInheritance;
this.augmentor.updateSettings({fileMetadataInheritance: initFmi});
this.augmentor.updateSettings({ fileMetadataInheritance: initFmi });
} catch (e) {
console.warn('[DataflowOrchestrator][init] Failed to sync settings to Augmentor', e);
console.warn(
"[DataflowOrchestrator][init] Failed to sync settings to Augmentor",
e
);
}
this.storage = this.repository.getStorage();
@ -151,9 +154,9 @@ export class DataflowOrchestrator {
focusHeading: this.plugin.settings.focusHeading || "",
fileParsingConfig: undefined,
fileMetadataInheritance:
this.plugin.settings.fileMetadataInheritance,
this.plugin.settings.fileMetadataInheritance,
enableCustomDateFormats:
this.plugin.settings.enableCustomDateFormats,
this.plugin.settings.enableCustomDateFormats,
customDateFormats: this.plugin.settings.customDateFormats,
// Include tag prefixes for custom dataview field support
projectTagPrefix: this.plugin.settings.projectTagPrefix,
@ -177,7 +180,7 @@ export class DataflowOrchestrator {
this.workerOrchestrator = new WorkerOrchestrator(
taskWorkerManager,
projectWorkerManager,
{enableWorkerProcessing}
{ enableWorkerProcessing }
);
// Initialize Obsidian event source
@ -189,28 +192,28 @@ export class DataflowOrchestrator {
// Initialize TimeParsingService with plugin settings
this.timeParsingService = new TimeParsingService(
this.plugin.settings?.timeParsing ||
({
enabled: true,
supportedLanguages: ["en", "zh"],
dateKeywords: {
start: ["start", "begin", "from"],
due: ["due", "deadline", "by", "until"],
scheduled: ["scheduled", "on", "at"],
},
removeOriginalText: true,
perLineProcessing: true,
realTimeReplacement: true,
timePatterns: {
singleTime: [],
timeRange: [],
rangeSeparators: ["-", "~", ""],
},
timeDefaults: {
preferredFormat: "24h",
defaultPeriod: "AM",
midnightCrossing: "next-day",
},
} as EnhancedTimeParsingConfig)
({
enabled: true,
supportedLanguages: ["en", "zh"],
dateKeywords: {
start: ["start", "begin", "from"],
due: ["due", "deadline", "by", "until"],
scheduled: ["scheduled", "on", "at"],
},
removeOriginalText: true,
perLineProcessing: true,
realTimeReplacement: true,
timePatterns: {
singleTime: [],
timeRange: [],
rangeSeparators: ["-", "~", ""],
},
timeDefaults: {
preferredFormat: "24h",
defaultPeriod: "AM",
midnightCrossing: "next-day",
},
} as EnhancedTimeParsingConfig)
);
// Initialize FileSource (conditionally based on settings)
@ -409,7 +412,7 @@ export class DataflowOrchestrator {
// Listen for ICS events updates
this.eventRefs.push(
on(this.app, Events.ICS_EVENTS_UPDATED, async (payload: any) => {
const {events, seq} = payload;
const { events, seq } = payload;
console.log(
`[DataflowOrchestrator] ICS_EVENTS_UPDATED: ${
events?.length || 0
@ -426,7 +429,7 @@ export class DataflowOrchestrator {
// Listen for file updates from ObsidianSource
this.eventRefs.push(
on(this.app, Events.FILE_UPDATED, async (payload: any) => {
const {path, reason} = payload;
const { path, reason } = payload;
console.log(
`[DataflowOrchestrator] FILE_UPDATED event: ${path} (${reason})`
);
@ -450,7 +453,7 @@ export class DataflowOrchestrator {
// ObsidianSource uses FILE_UPDATED events instead
this.eventRefs.push(
on(this.app, Events.TASK_CACHE_UPDATED, async (payload: any) => {
const {changedFiles, sourceSeq} = payload;
const { changedFiles, sourceSeq } = payload;
// Skip if this is our own event (avoid infinite loop)
// Check sourceSeq to identify origin from our own processing
@ -490,7 +493,7 @@ export class DataflowOrchestrator {
this.app,
Events.WRITE_OPERATION_COMPLETE,
async (payload: any) => {
const {path, taskId} = payload;
const { path, taskId } = payload;
console.log(
`[DataflowOrchestrator] WRITE_OPERATION_COMPLETE: ${path}, taskId: ${taskId}`
);
@ -515,7 +518,7 @@ export class DataflowOrchestrator {
// Listen for direct task updates (from inline editing)
this.eventRefs.push(
on(this.app, Events.TASK_UPDATED, async (payload: any) => {
const {task} = payload;
const { task } = payload;
if (task) {
console.log(
`[DataflowOrchestrator] TASK_UPDATED: ${task.id} in ${task.filePath}`
@ -529,7 +532,7 @@ export class DataflowOrchestrator {
// Listen for task deletion events
this.eventRefs.push(
on(this.app, Events.TASK_DELETED, async (payload: any) => {
const {taskId, filePath, deletedTaskIds, mode} = payload;
const { taskId, filePath, deletedTaskIds, mode } = payload;
console.log(
`[DataflowOrchestrator] TASK_DELETED: ${taskId} in ${filePath}, mode: ${mode}, deleted: ${
deletedTaskIds?.length || 1
@ -557,7 +560,7 @@ export class DataflowOrchestrator {
if (this.fileSource) {
this.eventRefs.push(
on(this.app, Events.FILE_TASK_UPDATED, async (payload: any) => {
const {task} = payload;
const { task } = payload;
console.log(
`[DataflowOrchestrator] FILE_TASK_UPDATED: ${task?.filePath}`
);
@ -570,7 +573,7 @@ export class DataflowOrchestrator {
this.eventRefs.push(
on(this.app, Events.FILE_TASK_REMOVED, async (payload: any) => {
const {filePath} = payload;
const { filePath } = payload;
console.log(
`[DataflowOrchestrator] FILE_TASK_REMOVED: ${filePath}`
);
@ -649,13 +652,13 @@ export class DataflowOrchestrator {
// Apply inline filter even when using cached augmented tasks
const includeInlineCached = this.fileFilterManager
? this.fileFilterManager.shouldIncludePath(
filePath,
"inline"
)
filePath,
"inline"
)
: true;
console.log(
"[DataflowOrchestrator] Inline filter decision (cached augmented)",
{filePath, includeInline: includeInlineCached}
{ filePath, includeInline: includeInlineCached }
);
if (!includeInlineCached) {
augmentedTasks = [];
@ -683,13 +686,13 @@ export class DataflowOrchestrator {
);
const includeInlineReaugment = this.fileFilterManager
? this.fileFilterManager.shouldIncludePath(
filePath,
"inline"
)
filePath,
"inline"
)
: true;
console.log(
"[DataflowOrchestrator] Inline filter decision (re-augment cached raw)",
{filePath, includeInline: includeInlineReaugment}
{ filePath, includeInline: includeInlineReaugment }
);
rawTasks = includeInlineReaugment ? rawCached.data : [];
projectData = await this.projectResolver.get(filePath);
@ -712,27 +715,27 @@ export class DataflowOrchestrator {
try {
const taskWorkerManager = this.workerOrchestrator[
"taskWorkerManager"
] as TaskWorkerManager | undefined;
] as TaskWorkerManager | undefined;
if (taskWorkerManager) {
taskWorkerManager.updateSettings({
preferMetadataFormat:
this.plugin.settings.preferMetadataFormat ||
"tasks",
customDateFormats:
this.plugin.settings.customDateFormats,
this.plugin.settings.customDateFormats,
fileMetadataInheritance:
this.plugin.settings
.fileMetadataInheritance,
this.plugin.settings
.fileMetadataInheritance,
ignoreHeading:
this.plugin.settings.ignoreHeading,
this.plugin.settings.ignoreHeading,
focusHeading: this.plugin.settings.focusHeading,
// Include tag prefixes for custom dataview field support
projectTagPrefix:
this.plugin.settings.projectTagPrefix,
this.plugin.settings.projectTagPrefix,
contextTagPrefix:
this.plugin.settings.contextTagPrefix,
this.plugin.settings.contextTagPrefix,
areaTagPrefix:
this.plugin.settings.areaTagPrefix,
this.plugin.settings.areaTagPrefix,
});
}
} catch (e) {
@ -745,13 +748,13 @@ export class DataflowOrchestrator {
// Apply inline filter for parse path
const includeInlineParse = this.fileFilterManager
? this.fileFilterManager.shouldIncludePath(
filePath,
"inline"
)
filePath,
"inline"
)
: true;
console.log(
"[DataflowOrchestrator] Inline filter decision (parse path)",
{filePath, includeInline: includeInlineParse}
{ filePath, includeInline: includeInlineParse }
);
if (includeInlineParse) {
// Parse the file using workers (single-file path)
@ -869,12 +872,18 @@ export class DataflowOrchestrator {
// Sync inheritance toggle to augmentor so it can respect disabling file frontmatter inheritance
try {
console.debug('[DataflowOrchestrator][updateSettings] fileMetadataInheritance =', settings.fileMetadataInheritance);
console.debug(
"[DataflowOrchestrator][updateSettings] fileMetadataInheritance =",
settings.fileMetadataInheritance
);
this.augmentor.updateSettings({
fileMetadataInheritance: settings.fileMetadataInheritance,
});
} catch (e) {
console.warn("[DataflowOrchestrator] Failed to sync settings to Augmentor", e);
console.warn(
"[DataflowOrchestrator] Failed to sync settings to Augmentor",
e
);
}
// Update FileSource if needed
@ -926,7 +935,7 @@ export class DataflowOrchestrator {
try {
const taskWorkerManager = this.workerOrchestrator?.[
"taskWorkerManager"
] as
] as
| import("./workers/TaskWorkerManager").TaskWorkerManager
| undefined;
if (taskWorkerManager) {
@ -970,7 +979,7 @@ export class DataflowOrchestrator {
const newEnabled: boolean = Boolean(settings?.fileFilter?.enabled);
const rulesCount = Array.isArray(settings?.fileFilter?.rules)
? settings.fileFilter.rules.filter((r: any) => r?.enabled)
.length
.length
: 0;
console.log("[TG Index Filter] settingsChange", {
enabled: newEnabled,
@ -1107,7 +1116,7 @@ export class DataflowOrchestrator {
);
console.log(
"[DataflowOrchestrator] restoreByFilter fallback candidates",
{extra: inlineCandidates.length}
{ extra: inlineCandidates.length }
);
} catch (e) {
console.warn(
@ -1140,7 +1149,7 @@ export class DataflowOrchestrator {
path,
augmented.data,
undefined,
{forceEmit: true}
{ forceEmit: true }
);
restoredFromAugmented++;
this.suppressedInline.delete(path);
@ -1199,7 +1208,7 @@ export class DataflowOrchestrator {
console.warn(
"[DataflowOrchestrator] restore inline failed",
{path, e}
{ path, e }
);
}
}
@ -1235,7 +1244,7 @@ export class DataflowOrchestrator {
} catch (e) {
console.warn(
"[DataflowOrchestrator] restore file-task emit failed",
{path, e}
{ path, e }
);
}
}
@ -1259,7 +1268,7 @@ export class DataflowOrchestrator {
*/
getWorkerStatus(): { enabled: boolean; metrics?: any } {
if (!this.workerOrchestrator) {
return {enabled: false};
return { enabled: false };
}
return {
@ -1358,16 +1367,16 @@ export class DataflowOrchestrator {
// Configure worker manager with plugin settings
const taskWorkerManager = this.workerOrchestrator[
"taskWorkerManager"
] as TaskWorkerManager;
] as TaskWorkerManager;
if (taskWorkerManager) {
taskWorkerManager.updateSettings({
preferMetadataFormat:
this.plugin.settings.preferMetadataFormat ||
"tasks",
customDateFormats:
this.plugin.settings.customDateFormats,
this.plugin.settings.customDateFormats,
fileMetadataInheritance:
this.plugin.settings.fileMetadataInheritance,
this.plugin.settings.fileMetadataInheritance,
projectConfig: this.plugin.settings.projectConfig,
ignoreHeading: this.plugin.settings.ignoreHeading,
focusHeading: this.plugin.settings.focusHeading,
@ -1401,6 +1410,17 @@ export class DataflowOrchestrator {
try {
const file = files.find((f) => f.path === filePath);
if (!file) continue;
// Apply inline file filter early in worker path
const includeInline = this.fileFilterManager
? this.fileFilterManager.shouldIncludePath(
filePath,
"inline"
)
: true;
if (!includeInline) {
updates.set(filePath, []);
continue;
}
// Get file modification time for caching
const fileStat = await this.vault.adapter.stat(
@ -1434,9 +1454,9 @@ export class DataflowOrchestrator {
projectName: projectData?.tgProject?.name,
projectMeta: projectData
? {
...(projectData.enhancedMetadata || {}),
tgProject: projectData.tgProject, // Include tgProject in projectMeta
}
...(projectData.enhancedMetadata || {}),
tgProject: projectData.tgProject, // Include tgProject in projectMeta
}
: {},
tasks: rawTasks,
};
@ -1508,6 +1528,19 @@ export class DataflowOrchestrator {
const rawCached = await this.storage.loadRaw(filePath);
const fileContent = await this.vault.cachedRead(file);
// Apply inline file filter early for all branches
const includeInlineEarly = this.fileFilterManager
? this.fileFilterManager.shouldIncludePath(
filePath,
"inline"
)
: true;
if (!includeInlineEarly) {
updates.set(filePath, []);
localSkippedCount++;
continue;
}
// Check both raw and augmented cache
const augmentedCached = await this.storage.loadAugmented(
filePath
@ -1574,13 +1607,13 @@ export class DataflowOrchestrator {
// Apply file filter scope: skip inline parsing when scope === 'file'
const includeInline = this.fileFilterManager
? this.fileFilterManager.shouldIncludePath(
filePath,
"inline"
)
filePath,
"inline"
)
: true;
console.log(
"[DataflowOrchestrator] Inline filter decision",
{filePath, includeInline}
{ filePath, includeInline }
);
const rawTasks = includeInline
? await this.parseFile(file, projectData.tgProject)

View file

@ -5,7 +5,7 @@
* Provides efficient path matching and caching mechanisms.
*/
import { TFile, TFolder } from "obsidian";
import { normalizePath, TFile, TFolder } from "obsidian";
import { FilterMode } from "../common/setting-definition";
/**
@ -46,7 +46,7 @@ class PathTrie {
* Insert a path into the trie
*/
insert(path: string, isFolder: boolean = true): void {
const parts = this.normalizePath(path)
const parts = normalizePath(path)
.split("/")
.filter((part) => part.length > 0);
let current = this.root;
@ -66,29 +66,30 @@ class PathTrie {
* Check if a path or its parent is in the trie
*/
contains(path: string): boolean {
const parts = this.normalizePath(path)
const parts = normalizePath(path)
.split("/")
.filter((part) => part.length > 0);
let current = this.root;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
console.log(parts, path);
// Check if current path segment matches a folder rule
if (current.children.has(part)) {
// Try to match the rule starting at any segment in the input path
for (let start = 0; start < parts.length; start++) {
let current = this.root;
for (let i = start; i < parts.length; i++) {
const part = parts[i];
if (!current.children.has(part)) {
break; // mismatch at this starting position; try next start
}
current = current.children.get(part)!;
// If this is a folder rule and we're checking a path under it
// If this is a folder rule and we're checking a path under it (or exact folder)
if (current.isEndOfPath && current.isFolder) {
return true;
}
} else {
return false;
}
}
// Check if the exact path matches
return current.isEndOfPath;
// No folder rule matched anywhere in the path
return false;
}
/**
@ -97,13 +98,6 @@ class PathTrie {
clear(): void {
this.root = new PathTrieNode();
}
/**
* Normalize path for consistent matching
*/
private normalizePath(path: string): string {
return path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
}
}
/**
@ -152,14 +146,14 @@ export class FileFilterManager {
}
const filePath = file.path;
const key = this.getCacheKey("file", filePath, scope);
// Check cache first
if (this.cache.has(filePath)) {
return this.cache.get(filePath)!;
if (this.cache.has(key)) {
return this.cache.get(key)!;
}
const result = this.evaluateFile(filePath, scope);
this.cache.set(filePath, result);
this.cache.set(key, result);
return result;
}
@ -175,14 +169,14 @@ export class FileFilterManager {
}
const folderPath = folder.path;
const key = this.getCacheKey("folder", folderPath, scope);
// Check cache first
if (this.cache.has(folderPath)) {
return this.cache.get(folderPath)!;
if (this.cache.has(key)) {
return this.cache.get(key)!;
}
const result = this.evaluateFolder(folderPath, scope);
this.cache.set(folderPath, result);
this.cache.set(key, result);
return result;
}
@ -197,13 +191,14 @@ export class FileFilterManager {
return true;
}
const key = this.getCacheKey("path", path, scope);
// Check cache first
if (this.cache.has(path)) {
return this.cache.get(path)!;
if (this.cache.has(key)) {
return this.cache.get(key)!;
}
const result = this.evaluatePath(path, scope);
this.cache.set(path, result);
this.cache.set(key, result);
return result;
}
@ -224,6 +219,16 @@ export class FileFilterManager {
clearCache(): void {
this.cache.clear();
}
/**
* Build a cache key that is scoped by kind and scope to avoid cross-scope pollution
*/
private getCacheKey(
kind: "file" | "folder" | "path",
path: string,
scope: "both" | "inline" | "file"
): string {
return `${kind}:${scope}:${this.normalizePath(path)}`;
}
/**
* Evaluate if a file should be included

File diff suppressed because one or more lines are too long