fix(priority): resolve priority parsing and caching issues

Multiple issues were causing priority values to be lost or reset to 3:

1. Missing metadataParseMode in WorkerOrchestrator causing emoji parsing to fail
2. Augmentor applying default priority value (3) to all tasks
3. Orchestrator re-augmenting cached tasks unnecessarily
4. Priority inheritance overriding explicit task priorities

Changes:
- Add MetadataParseMode.Both to parser configurations in WorkerOrchestrator and Orchestrator
- Include complete emoji mappings for priority symbols (🔺🔼🔽)
- Implement special priority handling in Augmentor to prevent default value application
- Add priority value conversion for text values (highest/high/medium/low/lowest)
- Optimize caching to use augmented tasks directly when available
- Add debug logging for priority extraction and conversion

This ensures priority values are correctly parsed from emojis and metadata,
preserved through cache cycles, and never overridden by defaults.
This commit is contained in:
Quorafind 2025-08-21 10:10:28 +08:00
parent 172e5fc794
commit b8f4586b39
5 changed files with 297 additions and 53 deletions

View file

@ -20,6 +20,7 @@ import { parseMarkdown } from "./parsers/MarkdownEntry";
import { parseCanvas } from "./parsers/CanvasEntry";
import { parseFileMeta } from "./parsers/FileMetaEntry";
import { ConfigurableTaskParser } from "./core/ConfigurableTaskParser";
import { MetadataParseMode } from "../types/TaskParserConfig";
/**
* DataflowOrchestrator - Coordinates all dataflow components
@ -275,52 +276,64 @@ export class DataflowOrchestrator {
// Step 2: Check cache and parse if needed
const rawCached = await this.storage.loadRaw(filePath);
const augmentedCached = await this.storage.loadAugmented(filePath);
const fileContent = await this.vault.cachedRead(file);
let rawTasks: Task[];
let projectData: Awaited<ReturnType<typeof this.projectResolver.get>>;
let needsParsing = false;
let augmentedTasks: Task[];
let needsProcessing = false;
if (rawCached && this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
// Use cached raw tasks - file hasn't changed
console.log(`[DataflowOrchestrator] Using cached tasks for ${filePath} (mtime match)`);
rawTasks = rawCached.data;
// Still need to get project data for augmentation
projectData = await this.projectResolver.get(filePath);
// Check if we can use fully cached augmented tasks
if (rawCached && augmentedCached &&
this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
// Use cached augmented tasks - file hasn't changed and we have augmented data
console.log(`[DataflowOrchestrator] Using cached augmented tasks for ${filePath} (mtime match)`);
augmentedTasks = augmentedCached.data;
} else {
// Parse the file
console.log(`[DataflowOrchestrator] Parsing ${filePath} (cache miss or mtime mismatch)`);
needsParsing = true;
// Need to parse and/or augment
needsProcessing = true;
// Get project data first for parsing
projectData = await this.projectResolver.get(filePath);
rawTasks = await this.parseFile(file, projectData.tgProject);
let rawTasks: Task[];
let projectData: any; // Type will be inferred from projectResolver.get
// Store raw tasks with file content and mtime
await this.storage.storeRaw(filePath, rawTasks, fileContent, mtime);
if (rawCached && this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
// Use cached raw tasks but re-augment (project data might have changed)
console.log(`[DataflowOrchestrator] Re-augmenting cached raw tasks for ${filePath}`);
rawTasks = rawCached.data;
projectData = await this.projectResolver.get(filePath);
} else {
// Parse the file from scratch
console.log(`[DataflowOrchestrator] Parsing ${filePath} (cache miss or mtime mismatch)`);
// Get project data first for parsing
projectData = await this.projectResolver.get(filePath);
rawTasks = await this.parseFile(file, projectData.tgProject);
// Store raw tasks with file content and mtime
await this.storage.storeRaw(filePath, rawTasks, fileContent, mtime);
}
// Store project data
await this.storage.storeProject(filePath, {
tgProject: projectData.tgProject,
enhancedMetadata: projectData.enhancedMetadata
});
// Augment tasks with project and file metadata
const fileMetadata = this.metadataCache.getFileCache(file);
const augmentContext: AugmentContext = {
filePath,
fileMeta: fileMetadata?.frontmatter || {},
projectName: projectData.tgProject?.name,
projectMeta: {
...projectData.enhancedMetadata,
tgProject: projectData.tgProject // Include tgProject in projectMeta
},
tasks: rawTasks
};
augmentedTasks = await this.augmentor.merge(augmentContext);
}
// Store project data
await this.storage.storeProject(filePath, {
tgProject: projectData.tgProject,
enhancedMetadata: projectData.enhancedMetadata
});
// Step 3: Augment tasks with project and file metadata
const fileMetadata = this.metadataCache.getFileCache(file);
const augmentContext: AugmentContext = {
filePath,
fileMeta: fileMetadata?.frontmatter || {},
projectName: projectData.tgProject?.name,
projectMeta: {
...projectData.enhancedMetadata,
tgProject: projectData.tgProject // Include tgProject in projectMeta
},
tasks: rawTasks
};
const augmentedTasks = await this.augmentor.merge(augmentContext);
// Step 4: Update repository (index + storage + events)
// Step 3: Update repository (index + storage + events)
// Generate a unique sequence for this operation
this.lastProcessedSeq = Seq.next();
@ -361,9 +374,33 @@ export class DataflowOrchestrator {
parseTags: true,
parseComments: true,
parseHeadings: true,
metadataParseMode: MetadataParseMode.Both, // Parse both emoji and dataview metadata
maxIndentSize: 8,
maxParseIterations: 4000,
maxMetadataIterations: 400,
maxTagLength: 100,
maxEmojiValueLength: 200,
maxStackOperations: 4000,
maxStackSize: 1000,
customDateFormats: this.plugin.settings.customDateFormats,
statusMapping: this.plugin.settings.statusMapping || {},
emojiMapping: this.plugin.settings.emojiMapping || {},
emojiMapping: this.plugin.settings.emojiMapping || {
"📅": "dueDate",
"🛫": "startDate",
"⏳": "scheduledDate",
"✅": "completedDate",
"❌": "cancelledDate",
"": "createdDate",
"🔁": "recurrence",
"🏁": "onCompletion",
"⛔": "dependsOn",
"🆔": "id",
"🔺": "priority",
"⏫": "priority",
"🔼": "priority",
"🔽": "priority",
"⏬": "priority"
},
specialTagPrefixes: this.plugin.settings.specialTagPrefixes || {},
fileMetadataInheritance: this.plugin.settings.fileMetadataInheritance,
projectConfig: this.plugin.settings.projectConfig
@ -511,8 +548,18 @@ export class DataflowOrchestrator {
const rawCached = await this.storage.loadRaw(filePath);
const fileContent = await this.vault.cachedRead(file);
if (rawCached && this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
// Use cached raw tasks for augmentation
// Check both raw and augmented cache
const augmentedCached = await this.storage.loadAugmented(filePath);
if (rawCached && augmentedCached && this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
// Use cached augmented tasks directly - no need to re-augment
const augmentedTasks = augmentedCached.data;
// Always add to updates - Repository will handle change detection
updates.set(filePath, augmentedTasks);
localSkippedCount++; // Count as skipped since we used cache
} else if (rawCached && this.storage.isRawValid(filePath, rawCached, fileContent, mtime)) {
// Have raw cache but not augmented, need to re-augment
const rawTasks = rawCached.data;
// Get project data
@ -534,7 +581,7 @@ export class DataflowOrchestrator {
// Always add to updates - Repository will handle change detection
updates.set(filePath, augmentedTasks);
skippedCount++; // Count as skipped since we used cache
localSkippedCount++; // Count as skipped since we used cache
} else {
// Parse file as it has changed or is new
// Get project data first for parsing

View file

@ -95,6 +95,43 @@ export class Augmentor {
const originalMetadata = task.metadata || {};
const enhancedMetadata = { ...originalMetadata };
// Special handling for priority: check both task.priority and metadata.priority
// Priority might be at task root level (from parser) or in metadata
// IMPORTANT: Once priority is set, it should NOT be overridden by inheritance
// Debug logging for priority processing
const debug = process.env.NODE_ENV === 'development';
if (debug && (enhancedMetadata.priority !== undefined || (task as any).priority !== undefined)) {
console.log('[Augmentor] Priority processing:', {
metadataPriority: enhancedMetadata.priority,
taskPriority: (task as any).priority,
filePath: ctx.filePath
});
}
// First, ensure we have the priority from task-level if it exists
if ((enhancedMetadata.priority === undefined || enhancedMetadata.priority === null) &&
(task as any).priority !== undefined && (task as any).priority !== null) {
enhancedMetadata.priority = (task as any).priority;
}
// Ensure priority is properly converted to numeric format if it exists
// Clean up null values to undefined for consistency
if (enhancedMetadata.priority === null) {
enhancedMetadata.priority = undefined;
} else if (enhancedMetadata.priority !== undefined) {
const originalPriority = enhancedMetadata.priority;
enhancedMetadata.priority = this.convertPriorityValue(enhancedMetadata.priority);
if (debug) {
console.log('[Augmentor] Priority conversion:', {
original: originalPriority,
converted: enhancedMetadata.priority,
filePath: ctx.filePath
});
}
}
// Apply inheritance for each metadata field
this.applyScalarInheritance(enhancedMetadata, ctx);
this.applyArrayInheritance(enhancedMetadata, ctx);
@ -127,7 +164,30 @@ export class Augmentor {
continue;
}
// Apply inheritance priority: file > project > default
// Special handling for priority - NEVER apply default value
// Priority should only come from task itself, file, or project
if (field === 'priority') {
// Only check file and project sources, skip default
for (const source of ['file', 'project']) {
let value: any;
if (source === 'file') {
value = ctx.fileMeta?.[field];
} else if (source === 'project') {
value = ctx.projectMeta?.[field];
}
if (value !== undefined && value !== null) {
// Convert priority value to numeric format
metadata[field] = this.convertPriorityValue(value);
break;
}
}
// If no priority found, leave it undefined (don't set default)
continue;
}
// Apply inheritance priority for other fields: file > project > default
for (const source of this.strategy.scalarPriority.slice(1)) { // Skip 'task' since we checked above
let value: any;
@ -256,12 +316,69 @@ export class Augmentor {
parentMetadata._subtaskInheritanceRules = this.strategy.subtaskInheritance;
}
/**
* Convert priority value to consistent numeric format
*/
private convertPriorityValue(value: any): number | undefined {
if (value === undefined || value === null || value === "") {
return undefined;
}
// If it's already a number, return it
if (typeof value === "number") {
return value;
}
// If it's a string, try to convert
const strValue = String(value);
// Priority mapping for text and emoji values
const priorityMap: Record<string, number> = {
// Text priorities
highest: 5,
high: 4,
medium: 3,
low: 2,
lowest: 1,
urgent: 5,
critical: 5,
important: 4,
normal: 3,
moderate: 3,
minor: 2,
trivial: 1,
// Emoji priorities (Tasks plugin compatible)
"🔺": 5,
"⏫": 4,
"🔼": 3,
"🔽": 2,
"⏬️": 1,
"⏬": 1,
};
// Try numeric conversion first
const numericValue = parseInt(strValue, 10);
if (!isNaN(numericValue) && numericValue >= 1 && numericValue <= 5) {
return numericValue;
}
// Try priority mapping (including emojis)
const mappedPriority = priorityMap[strValue.toLowerCase()] || priorityMap[strValue];
if (mappedPriority !== undefined) {
return mappedPriority;
}
// If we can't convert, return undefined to avoid setting invalid values
return undefined;
}
/**
* Get default value for a field
*/
private getDefaultValue(field: string): any {
const defaults: Record<string, any> = {
priority: 3, // Medium priority
// Don't set default priority for now - it should come from parser
// If we need to add it back, we should check if task already has priority elsewhere
tags: [],
dependsOn: [],
estimatedTime: undefined,

View file

@ -169,6 +169,18 @@ export class MarkdownTaskParser {
i += linesToSkip;
// Debug: Log priority extraction for each task
const extractedPriority = this.extractLegacyPriority(inheritedMetadata);
if (process.env.NODE_ENV === 'development' || true) { // Always log for debugging
console.log('[Parser] Task priority extraction:', {
lineNumber: i + 1,
content: cleanedContent.substring(0, 50),
metadataPriority: inheritedMetadata.priority,
extractedPriority,
filePath
});
}
const enhancedTask: EnhancedTask = {
id: taskId,
content: cleanedContent,
@ -193,7 +205,7 @@ export class MarkdownTaskParser {
// Legacy fields for backward compatibility
line: i,
children: [],
priority: this.extractLegacyPriority(inheritedMetadata),
priority: extractedPriority,
startDate: this.extractLegacyDate(
inheritedMetadata,
"startDate"
@ -363,6 +375,16 @@ export class MarkdownTaskParser {
if (bracketMatch) {
const [key, value, newRemaining] = bracketMatch;
metadata[key] = value;
// Debug: Log dataview metadata extraction, especially priority
if ((process.env.NODE_ENV === 'development' || true) && key === 'priority') { // Always log for debugging
console.log('[Parser] Dataview priority found:', {
key,
value,
remaining: remaining.substring(0, 50)
});
}
remaining = newRemaining;
foundMatch = true;
continue;
@ -585,8 +607,22 @@ export class MarkdownTaskParser {
.map((id) => id.trim())
.filter((id) => id.length > 0)
.join(",");
} else if (earliestEmoji.key === "priority") {
// For priority emojis, use the emoji itself or the provided value
// This ensures we can distinguish between different priority levels
metadataValue = value || earliestEmoji.emoji;
// Debug: Log priority emoji extraction
if (process.env.NODE_ENV === 'development' || true) { // Always log for debugging
console.log('[Parser] Priority emoji found:', {
emoji: earliestEmoji.emoji,
value,
metadataValue,
position: earliestEmoji.pos
});
}
} else {
// For priority emojis, use specific values based on the emoji
// For other emojis, use provided value or default
metadataValue =
value || this.getDefaultEmojiValue(earliestEmoji.emoji);
}
@ -1092,6 +1128,13 @@ export class MarkdownTaskParser {
moderate: 3, // Alias for medium
minor: 2, // Alias for low
trivial: 1, // Alias for lowest
// Emoji priority mappings
"🔺": 5,
"⏫": 4,
"🔼": 3,
"🔽": 2,
"⏬️": 1,
"⏬": 1,
};
// First try to parse as number
@ -1100,8 +1143,8 @@ export class MarkdownTaskParser {
return numericPriority;
}
// Then try to map string values
const mappedPriority = priorityMap[metadata.priority.toLowerCase()];
// Then try to map string values (including emojis)
const mappedPriority = priorityMap[metadata.priority.toLowerCase()] || priorityMap[metadata.priority];
return mappedPriority;
}
@ -1406,6 +1449,13 @@ export class MarkdownTaskParser {
moderate: 3,
minor: 2,
trivial: 1,
// Emoji priority mappings
"🔺": 5,
"⏫": 4,
"🔼": 3,
"🔽": 2,
"⏬️": 1,
"⏬": 1,
};
// Try numeric conversion first
@ -1414,8 +1464,8 @@ export class MarkdownTaskParser {
return String(numericValue);
}
// Try priority mapping
const mappedPriority = priorityMap[strValue.toLowerCase()];
// Try priority mapping (including emojis)
const mappedPriority = priorityMap[strValue.toLowerCase()] || priorityMap[strValue];
if (mappedPriority !== undefined) {
return String(mappedPriority);
}

View file

@ -3,6 +3,7 @@ import type { Task } from "../../types/task";
import type { CachedProjectData } from "../../cache/project-data-cache";
import { TaskWorkerManager, DEFAULT_WORKER_OPTIONS } from "./TaskWorkerManager";
import { ProjectDataWorkerManager } from "./ProjectDataWorkerManager";
import { MetadataParseMode } from "../../types/TaskParserConfig";
/**
* WorkerOrchestrator - Unified task and project worker management
@ -293,12 +294,39 @@ export class WorkerOrchestrator {
const fileCache = metadataCache.getFileCache(file);
const fileMetadata = fileCache?.frontmatter || {};
// Create parser with default settings
// Create parser with complete settings including metadataParseMode
const parser = new ConfigurableTaskParser({
parseMetadata: true,
parseTags: true,
parseComments: true,
parseHeadings: true
parseHeadings: true,
metadataParseMode: MetadataParseMode.Both, // Parse both emoji and dataview metadata
maxIndentSize: 8,
maxParseIterations: 4000,
maxMetadataIterations: 400,
maxTagLength: 100,
maxEmojiValueLength: 200,
maxStackOperations: 4000,
maxStackSize: 1000,
statusMapping: {},
emojiMapping: {
"📅": "dueDate",
"🛫": "startDate",
"⏳": "scheduledDate",
"✅": "completedDate",
"❌": "cancelledDate",
"": "createdDate",
"🔁": "recurrence",
"🏁": "onCompletion",
"⛔": "dependsOn",
"🆔": "id",
"🔺": "priority",
"⏫": "priority",
"🔼": "priority",
"🔽": "priority",
"⏬": "priority"
},
specialTagPrefixes: {}
});
// Parse tasks - raw extraction only, no project enhancement

View file

@ -247,7 +247,9 @@ export function extractPriority(
// Try emoji format (primary or fallback)
match = remainingContent.match(EMOJI_PRIORITY_REGEX);
if (match && match[1]) {
task.metadata.priority = PRIORITY_MAP[match[1]] ?? undefined;
// match[2] contains emoji symbols, match[3] contains [#A-E] format
const prioritySymbol = match[2] || match[3] || match[1];
task.metadata.priority = PRIORITY_MAP[prioritySymbol] ?? undefined;
if (task.metadata.priority !== undefined) {
remainingContent = remainingContent.replace(match[0], "");
}