mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
refactor: reorganize architecture and add dataflow foundation
- Move settings-metadata from src/data to src/common for better organization - Add initial dataflow architecture with core modules: - QueryAPI for unified task querying - Repository for centralized data management - Storage abstraction layer - Event system foundation - Parser interfaces for different entry types - Project resolver and augmentor modules - Improve SettingsIndexer with better performance logging - Update and enhance SettingsSearch tests - Add comprehensive refactor documentation and specification
This commit is contained in:
parent
f5a275cc47
commit
f5d4dca189
15 changed files with 769 additions and 80 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -41,3 +41,4 @@ CLAUDE.md
|
|||
.claude
|
||||
|
||||
dist
|
||||
dist/*
|
||||
232
docs/task-genius-refactor-plan.md
Normal file
232
docs/task-genius-refactor-plan.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
## Task Genius 数据流全面重构计划(Refactor Plan)
|
||||
|
||||
### 目的(Goals)
|
||||
- 去重与统一:消除项目识别、文件级元数据解析、任务解析、增强/继承、索引、缓存、持久化、事件传递在多处重复实现与分散入口的问题。
|
||||
- 稳定与持久:建立单一数据源与可验证的数据流,降低未来演进/扩展成本。
|
||||
- 一致与同步:保证视图数据与底层缓存/索引严格一致,避免“视图数据不同步”。
|
||||
- 快速冷启动:最大程度复用持久化数据,避免用户反复 reindex。
|
||||
- 事件驱动:所有上行/下行数据变更通过 Obsidian 事件系统传播,而不是上级组件直接 setTasks。
|
||||
- 增量更新:跟随 Obsidian 的 vault/metadataCache/设置事件做最小代价的增量更新。
|
||||
|
||||
### 非目标(Non-goals)
|
||||
- 不改变用户可见的功能语义与现有配置的含义(除非标注为改进项)。
|
||||
- 不一次性大爆炸替换;采用阶段性迁移确保回退路径。
|
||||
|
||||
---
|
||||
|
||||
## 设计原则
|
||||
- 单一职责与单一真相源(Single Source of Truth):
|
||||
- 项目识别只在 ProjectResolver(统一接口)进行;解析器与增强器不再分散探测。
|
||||
- 继承/增强只在 TaskAugmentor 进行;解析器仅负责“从文本抽取”。
|
||||
- 事件优于回调:统一使用 Obsidian workspace 事件分发数据变更,视图通过事件 + 查询 API 获取数据。
|
||||
- 可重放、可回退:Worker 与主线程遵循相同协议;主线程兜底。
|
||||
- 可持久与可验证:内容哈希 + 版本/架构版本管理;命名空间化缓存;精细化失效与一致性校验。
|
||||
- 增量优先:文件内容变动仅重解析该文件;项目配置变动仅重跑增强;设置变动按 scope 精准失效。
|
||||
- 向后兼容:提供 Adapter 兼容旧 setTasks 调用路径,逐步下线。
|
||||
|
||||
---
|
||||
|
||||
## 数据拥有权与边界(Ownership)
|
||||
- 源事件(File events):ObsidianSource 拥有,统一发出内部文件事件。
|
||||
- 项目识别(Project):ProjectResolver 拥有,对外只暴露“文件 → {tgProject, enhancedMetadata}”。
|
||||
- 解析器(Parsers):Markdown/Canvas/FileLevel 仅负责“抽取任务/文件级任务”,不写入项目或其他增强信息。
|
||||
- 增强/继承(Augment):TaskAugmentor 拥有“task > file > project”的可配置优先级合并。
|
||||
- 索引(Indexing):TaskIndexer 拥有内存索引与查询,不做解析与增强。
|
||||
- 持久化(Persistence):StorageAdapter(基于 LocalStorageCache)拥有命名空间与 hash/版本校验。
|
||||
- 事件与查询(Events & Query):Events 模块拥有事件契约;QueryAPI 对外只读查询。
|
||||
|
||||
---
|
||||
|
||||
## 目标数据流(高层)
|
||||
1) Obsidian 事件 → Sources 标准化为 FileChanged/FileDeleted/Renamed 等内部事件。
|
||||
2) ParsersEntry(根据文件类型)抽取:
|
||||
- FileLevelTaskParser:从 frontmatter/tags 形成“文件级任务”。
|
||||
- MarkdownTaskParserEntry / CanvasTaskParserEntry:从正文生成“基础任务”。
|
||||
3) ProjectResolver 计算项目 + 项目增强元数据(可并行)。
|
||||
4) TaskAugmentor:将“文件级 + 正文任务”与“文件元数据 + 项目增强元数据”按策略合并到任务对象。
|
||||
5) TaskIndexer.updateIndexWithTasks(filePath, tasksEnhanced)。
|
||||
6) Persistence:写入命名空间缓存(见下);必要时更新 consolidated 索引快照。
|
||||
7) 事件广播 task-genius:task-cache-updated { changedFiles }。
|
||||
8) 视图订阅事件 → QueryAPI 拉取 → 渲染。
|
||||
|
||||
---
|
||||
|
||||
## 组件与目录结构(建议新增 src/dataflow/*)
|
||||
- sources/ObsidianSource.ts
|
||||
- 订阅 vault/metadataCache/设置事件;统一去抖与批量合并;发出内部文件事件。
|
||||
- project/ProjectResolver.ts
|
||||
- 合并现有 ProjectConfigManager + ProjectDataCache/Worker 的“对外接口”。
|
||||
- 输入:filePath;输出:{ tgProject?, enhancedMetadata, timestamp }。
|
||||
- 统一“frontmatter 指定项目、目录 config、tag/link 提示”的优先级;可配置。
|
||||
- parsers/
|
||||
- MarkdownTaskParserEntry.ts(包装 ConfigurableTaskParser.parseLegacy)
|
||||
- CanvasTaskParserEntry.ts(包装 CanvasParser)
|
||||
- FileLevelTaskParser.ts(包装 FileMetadataTaskParser,禁用内部项目探测)
|
||||
- augment/TaskAugmentor.ts
|
||||
- 单一继承/增强实现:
|
||||
- 标量:task 显式 > file > project > 默认
|
||||
- 数组:合并去重(保持稳定次序)
|
||||
- 状态/完成:只取 task 行级
|
||||
- 复发:task 显式优先
|
||||
- 子任务继承:按 FileMetadataInheritance/设置 per-key 控制
|
||||
- workers/WorkerOrchestrator.ts
|
||||
- 统一任务/项目命令:parseFileTasks、batchParse、computeProjectData、batchCompute
|
||||
- 并发控制、重试、回退、指标
|
||||
- indexer/TaskRepository.ts
|
||||
- 组合 TaskIndexer + Persistence;提供查询 API 的后端依赖
|
||||
- persistence/StorageAdapter.ts
|
||||
- 命名空间 + hash(内容)+ 版本/架构版本 + 粒度化失效
|
||||
- Keyspace:
|
||||
- tasks.raw:<filePath>
|
||||
- project.data:<filePath>
|
||||
- tasks.augmented:<filePath>
|
||||
- consolidated:taskIndex
|
||||
- meta:version / meta:schemaVersion
|
||||
- events/Events.ts
|
||||
- 事件常量/载荷类型/帮助方法(emit/subscribe)。与 obsidian-ex.d.ts 契约一致。
|
||||
- api/QueryAPI.ts
|
||||
- 为视图提供只读查询(按项目/标签/状态/时间窗口等),屏蔽内部实现细节。
|
||||
|
||||
---
|
||||
|
||||
## 最大化避免与现有功能重复的策略
|
||||
1) 单一入口与关停重复:
|
||||
- FileMetadataTaskParser:关闭其内部项目探测,统一由 ProjectResolver 提供 project 值后在 Augmentor 注入。
|
||||
- ConfigurableTaskParser/CanvasParser:不处理项目/继承,仅返回“基础任务”。
|
||||
- TaskManager 内的解析/增强/持久化逻辑收敛到新模块,TaskManager 变为编排薄层。
|
||||
2) 功能所有权迁移表(旧 → 新):
|
||||
- 项目识别:FileMetadataTaskParser.detectProjectFromFile / ProjectData* → ProjectResolver
|
||||
- 继承/增强:ConfigurableTaskParser 内部继承 / FileMetadataInheritance 分散 → TaskAugmentor
|
||||
- 数据广播:上层 setTasks → Events + QueryAPI
|
||||
- Worker 管理:TaskWorkerManager/ProjectDataWorkerManager → WorkerOrchestrator(内部保留原 Worker)
|
||||
- 持久化:LocalStorageCache 直接使用 → StorageAdapter 管理命名空间/版本/hash
|
||||
3) Adapter 与 Deprecation:
|
||||
- ViewComponentManager.setTasks 标记 deprecated;提供兼容 Adapter:setTasks 内部转为发事件 + 触发查询,避免一次性改动所有视图。
|
||||
- FileMetadataTaskParser 暴露“允许/禁止项目探测”开关;重构期禁用。
|
||||
|
||||
---
|
||||
|
||||
## 视图同步与一致性保证
|
||||
- 事件驱动 + 拉取查询:视图统一订阅 task-genius:task-cache-updated / filter-changed,随后调用 QueryAPI 拉取。彻底替代上级 setTasks 链接。
|
||||
- 原子性:单文件变更导出单批次“增强后任务”提交到 Indexer,再触发事件,视图永远看到同一版本。
|
||||
- 序号/时间戳:在广播载荷中携带 sequence/timestamp,视图可丢弃过期更新。
|
||||
- 去抖/批量:Sources 对频繁变动进行批处理,减少 UI 抖动。
|
||||
- 兼容策略:在迁移期间,旧视图通过 Adapter 仍可工作。
|
||||
|
||||
---
|
||||
|
||||
## 持久化与缓存策略
|
||||
- 命名空间:
|
||||
- tasks.raw:<filePath> → 基础解析结果
|
||||
- project.data:<filePath> → {tgProject, enhancedMetadata}
|
||||
- tasks.augmented:<filePath> → Augmentor 合成产物(可选,提升冷启动)
|
||||
- consolidated:taskIndex → 全量索引快照(可选)
|
||||
- 版本与架构版本:
|
||||
- meta:version(插件版本) + meta:schemaVersion(缓存结构版本);不兼容时精准清理对应命名空间。
|
||||
- 内容哈希:
|
||||
- tasks.raw 基于文件内容 hash;project.data 基于“有效配置 + frontmatter + tags/links”等来源 hash。
|
||||
- 仅当 hash 改变时重算;mtime 仅用于快速预判。
|
||||
- 失效矩阵:
|
||||
- 文件正文变动:重算 tasks.raw → augment → 更新 index → 事件。
|
||||
- 文件 frontmatter/tags 变动:重做 file→task 继承与 augment(若正文未变,可跳过正文解析)。
|
||||
- 项目配置变动:仅失效 project.data 与 tasks.augmented,重做 augment;跳过正文解析。
|
||||
- 设置变动:按 scope(解析/增强/索引)精准失效。
|
||||
|
||||
---
|
||||
|
||||
## 失败与回退
|
||||
- Worker 异常:自动回退主线程解析;熔断/退避重试;错误事件与日志。
|
||||
- 缓存损坏:校验失败则清理对应命名空间并重建;保留其余命名空间。
|
||||
- 事件风暴:限流与批处理;视图侧忽略过期序号。
|
||||
|
||||
---
|
||||
|
||||
## 性能与 SLO
|
||||
- 冷启动:优先使用 consolidated:taskIndex 或 tasks.augmented,目标 P50 < 300ms(视库大小微调)。
|
||||
- 增量更新:单文件保存到视图更新 P95 < 150ms(含解析 + augment + index + 事件)。
|
||||
- 内存:索引结构保持与现有相当;新增缓存命名空间带来可控增量。
|
||||
|
||||
---
|
||||
|
||||
## 向后兼容与迁移
|
||||
- Adapter:
|
||||
- 视图 setTasks 调用 → 转为发事件并触发 QueryAPI,从而兼容旧代码;逐视图切换到纯事件。
|
||||
- 逐步替换:
|
||||
- ProjectResolver 接管后,禁用 FileMetadataTaskParser 的项目探测。
|
||||
- Augmentor 接管继承后,解析器不再做继承。
|
||||
- 配置兼容:沿用原设置字段并提供合理默认;新增 per-key 策略作为高级选项。
|
||||
|
||||
---
|
||||
|
||||
## 阶段性实施与检查清单
|
||||
Phase A:事件封装 + 查询 API(2 天)
|
||||
- [ ] 新增 events/Events.ts,封装现有触发点(保持事件名兼容)。
|
||||
- [ ] 新增 api/QueryAPI.ts,提供最小查询(全部任务/按项目/按标签)。
|
||||
- [ ] TaskManager 触发统一走 Events.ts。
|
||||
|
||||
Phase B:解析入口收敛 + Augmentor(3 天)
|
||||
- [ ] 新建 parsers/*Entry.ts;主线程路径先打通。
|
||||
- [ ] 新建 augment/TaskAugmentor.ts,落地继承策略(task>file>project)。
|
||||
- [ ] 修改 FileMetadataTaskParser:允许禁用项目探测(默认禁用)。
|
||||
|
||||
Phase C:持久化命名空间 + hash(2 天)
|
||||
- [ ] StorageAdapter.ts:命名空间 + 版本/架构版本 + hash。
|
||||
- [ ] 冷启动优先加载 consolidated 或 tasks.augmented;兼容旧数据。
|
||||
|
||||
Phase D:WorkerOrchestrator(3 天)
|
||||
- [ ] 合并任务/项目 worker 调度;统一命令协议与回退策略。
|
||||
- [ ] 指标与日志:成功率、延迟、退避情况。
|
||||
|
||||
Phase E:视图去 setTasks(试点 2 个视图)(3 天)
|
||||
- [ ] 视图订阅事件 + QueryAPI 拉取;移除直接 setTasks。
|
||||
- [ ] 保留 Adapter 以兼容未迁移视图。
|
||||
|
||||
Phase F:项目配置变更仅增强重算(2 天)
|
||||
- [ ] 变更监听 → 失效 project.data 与 tasks.augmented → augment → 事件。
|
||||
|
||||
Phase G:全面测试与性能调优(2 天)
|
||||
- [ ] 单测/集成测补齐;SLO 校验;回归修复。
|
||||
|
||||
---
|
||||
|
||||
## 测试计划(重点)
|
||||
- 冷启动:无修改 → 不 reindex;版本兼容 → 直接用。
|
||||
- 文件增量:正文变更、frontmatter/tags 变更、重命名/删除;仅影响对应文件。
|
||||
- 项目配置变更:仅增强重算;正文不解析。
|
||||
- 设置变更:按 scope 精准失效。
|
||||
- 事件一致性:视图通过事件 + 查询看到一致数据;序号去重验证。
|
||||
- 兼容回归:
|
||||
- 空/Null 排序规则:空值永远排在非空之后(双向排序)。
|
||||
- 中文标签重复历史问题:复发任务创建链路校验不重复,解析层不二次注入。
|
||||
- 性能:P50/P95 指标达标;压力下稳定。
|
||||
|
||||
---
|
||||
|
||||
## 回退计划
|
||||
- 任意阶段可切回旧路径:
|
||||
- 解析:保留旧 TaskWorkerManager/TaskManager 主路径开关。
|
||||
- 事件:保留 setTasks Adapter。
|
||||
- 缓存:清空新命名空间后回退旧 consolidated 流程。
|
||||
|
||||
---
|
||||
|
||||
## 开放问题(需确认)
|
||||
- 事件载荷是否需要强制携带 diff(新增/删除/修改的 taskIds)以优化大视图刷新?
|
||||
- 默认继承策略中是否存在“项目优先于文件”的字段(例如 SLA 类字段)?
|
||||
- consolidated:taskIndex 是否纳入默认启用,还是作为可选以节省空间?
|
||||
|
||||
---
|
||||
|
||||
## 附:事件契约(建议)
|
||||
- task-genius:cache-ready { initial: boolean, timestamp, seq }
|
||||
- task-genius:task-cache-updated { changedFiles?: string[], stats?, timestamp, seq }
|
||||
- task-genius:file-updated { path, reason, timestamp }
|
||||
- task-genius:project-data-updated { affectedFiles: string[], timestamp }
|
||||
- task-genius:settings-changed { scopes: string[], timestamp }
|
||||
|
||||
说明:保持与现有 obsidian-ex.d.ts 的兼容命名(如 task-cache-updated)。
|
||||
|
||||
---
|
||||
|
||||
## 结束语
|
||||
本方案以“单一真相源 + 事件驱动 + 命名空间缓存 + 可回退”为核心,最大化复用现有解析/索引能力,在不破坏现有功能的前提下,消除重复与耦合点,确保视图数据与缓存/索引一致。分阶段推进、可测可回退,可支撑后续长期演进。
|
||||
164
docs/task-genius-refactor-spec.md
Normal file
164
docs/task-genius-refactor-spec.md
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
## Task Genius 数据流重构实施规范(Implementation Spec)
|
||||
|
||||
本规范补充《task-genius-refactor-plan.md》,面向执行同学,提供可直接落地的事件载荷类型、QueryAPI 函数签名、StorageAdapter 的 Key 规则,以及示例代码与任务清单。
|
||||
|
||||
---
|
||||
|
||||
### 1. 事件载荷类型(建议)
|
||||
|
||||
事件名称与 obsidian-ex.d.ts 兼容,并统一通过 Events.ts 导出常量与帮助方法。
|
||||
|
||||
- task-genius:cache-ready
|
||||
- payload: { initial: boolean; timestamp: number; seq: number }
|
||||
- task-genius:task-cache-updated
|
||||
- payload: { changedFiles?: string[]; stats?: { total: number; changed: number }; timestamp: number; seq: number }
|
||||
- task-genius:file-updated
|
||||
- payload: { path: string; reason: "modify" | "frontmatter" | "rename" | "delete"; timestamp: number }
|
||||
- task-genius:project-data-updated
|
||||
- payload: { affectedFiles: string[]; reason?: string; timestamp: number }
|
||||
- task-genius:settings-changed
|
||||
- payload: { scopes: ("parser" | "augment" | "index" | "view")[]; timestamp: number }
|
||||
- 保留:task-genius:task-completed/task-added/task-updated/task-deleted(按现有类型)
|
||||
|
||||
辅助:
|
||||
- seq 为单调递增序号(TaskRepository 维护)
|
||||
- timestamp 为事件生成时间
|
||||
|
||||
---
|
||||
|
||||
### 2. QueryAPI 函数签名(草案)
|
||||
|
||||
文件:src/dataflow/api/QueryAPI.ts
|
||||
|
||||
- getAllTasks(): Promise<Task[]>
|
||||
- getTasksByProject(projectName: string): Promise<Task[]>
|
||||
- getTasksByTags(tags: string[]): Promise<Task[]>
|
||||
- getTasksByStatus(completed: boolean): Promise<Task[]>
|
||||
- getTasksByDateRange(opts: { from?: number; to?: number; field?: "due" | "start" | "scheduled" }): Promise<Task[]>
|
||||
- getTaskById(id: string): Promise<Task | null>
|
||||
- getIndexSummary(): Promise<{ total: number; byProject: Record<string, number>; byTag: Record<string, number> }>
|
||||
|
||||
实现要点:
|
||||
- 背后依赖 TaskRepository(组合 TaskIndexer + Persistence),严禁直接访问内部索引结构于视图层。
|
||||
- 函数返回的 Task 均为“增强后任务”。
|
||||
|
||||
---
|
||||
|
||||
### 3. StorageAdapter Key 规则与版本
|
||||
|
||||
文件:src/dataflow/persistence/StorageAdapter.ts
|
||||
|
||||
- 命名空间规范(namespace:key)
|
||||
- tasks.raw:<filePath>
|
||||
- project.data:<filePath>
|
||||
- tasks.augmented:<filePath>
|
||||
- consolidated:taskIndex
|
||||
- meta:version(插件版本)
|
||||
- meta:schemaVersion(缓存架构版本)
|
||||
|
||||
- 哈希与版本
|
||||
- tasks.raw 记录:{ hash: string; time: number; version: string; schema: number; data: Task[] }
|
||||
- project.data 记录:{ hash: string; time: number; version: string; schema: number; data: { tgProject?: TgProject; enhancedMetadata: Record<string, any> } }
|
||||
- tasks.augmented 记录:{ hash: string; time: number; version: string; schema: number; data: Task[] }
|
||||
- consolidated:taskIndex 记录:{ time: number; version: string; schema: number; data: TaskCacheSnapshot }
|
||||
|
||||
- 兼容策略
|
||||
- 版本或 schema 不兼容时仅清理受影响命名空间。
|
||||
- hash 优先判定是否需要重算;mtime 仅作为辅助。
|
||||
|
||||
---
|
||||
|
||||
### 4. 代码示例(2 段)
|
||||
|
||||
示例 A:视图订阅事件 + QueryAPI 拉取(替代 setTasks)
|
||||
|
||||
```ts
|
||||
// 在某视图 onOpen 内
|
||||
import { subscribeTaskCacheUpdated } from "src/dataflow/events/Events";
|
||||
import { QueryAPI } from "src/dataflow/api/QueryAPI";
|
||||
|
||||
let unsub: () => void;
|
||||
this.registerEvent(
|
||||
(unsub = subscribeTaskCacheUpdated(async (payload) => {
|
||||
const tasks = await QueryAPI.getTasksByProject(this.currentProject ?? "");
|
||||
this.renderer.render(tasks); // 视图自身的渲染方法
|
||||
}))
|
||||
);
|
||||
|
||||
// onClose 时
|
||||
if (unsub) unsub();
|
||||
```
|
||||
|
||||
示例 B:单文件变更 → 解析 + 增强 + 索引 + 事件
|
||||
|
||||
```ts
|
||||
import { ParsersEntry } from "src/dataflow/parsers";
|
||||
import { ProjectResolver } from "src/dataflow/project/ProjectResolver";
|
||||
import { TaskAugmentor } from "src/dataflow/augment/TaskAugmentor";
|
||||
import { TaskRepository } from "src/dataflow/indexer/TaskRepository";
|
||||
import { emitTaskCacheUpdated } from "src/dataflow/events/Events";
|
||||
import { StorageAdapter } from "src/dataflow/persistence/StorageAdapter";
|
||||
|
||||
async function handleFileChanged(filePath: string) {
|
||||
// 1. 尝试读取 tasks.raw(hash 命中则跳过正文解析)
|
||||
const raw = await StorageAdapter.loadRaw(filePath);
|
||||
const needsParse = !(raw && StorageAdapter.isRawValid(filePath, raw));
|
||||
const rawTasks = needsParse
|
||||
? await ParsersEntry.parseFile(filePath) // Markdown/Canvas/FileLevel 分流
|
||||
: raw.data;
|
||||
|
||||
if (needsParse) await StorageAdapter.storeRaw(filePath, rawTasks);
|
||||
|
||||
// 2. 读取/计算项目数据(可并行)
|
||||
const proj = await ProjectResolver.getProjectData(filePath);
|
||||
await StorageAdapter.storeProjectData(filePath, proj);
|
||||
|
||||
// 3. 增强合并
|
||||
const augmented = await TaskAugmentor.merge({ filePath, rawTasks, project: proj });
|
||||
await StorageAdapter.storeAugmented(filePath, augmented);
|
||||
|
||||
// 4. 更新索引
|
||||
await TaskRepository.updateFile(filePath, augmented);
|
||||
|
||||
// 5. 广播事件
|
||||
emitTaskCacheUpdated({ changedFiles: [filePath], timestamp: Date.now() });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 执行清单(给贡献者)
|
||||
|
||||
1) 新增骨架文件
|
||||
- src/dataflow/events/Events.ts:事件常量与 emit/subscribe;内部维护 seq。
|
||||
- src/dataflow/api/QueryAPI.ts:空壳方法先返回现有 TaskManager 数据(过渡)。
|
||||
- src/dataflow/parsers/MarkdownTaskParserEntry.ts:包装现有 ConfigurableTaskParser.parseLegacy。
|
||||
- src/dataflow/parsers/CanvasTaskParserEntry.ts:包装现有 CanvasParser。
|
||||
- src/dataflow/parsers/FileLevelTaskParser.ts:包装现有 FileMetadataTaskParser,并添加“禁用项目探测”配置(默认禁用)。
|
||||
- src/dataflow/augment/TaskAugmentor.ts:实现继承策略(task>file>project);支持子任务控制。
|
||||
- src/dataflow/project/ProjectResolver.ts:统一项目识别接口;内部复用 ProjectConfigManager/ProjectDataCache。
|
||||
- src/dataflow/persistence/StorageAdapter.ts:命名空间 + 版本/schema + hash 校验;读写 helpers。
|
||||
- src/dataflow/indexer/TaskRepository.ts:组合 TaskIndexer 与 StorageAdapter;对外提供查询。
|
||||
|
||||
2) TaskManager 接入
|
||||
- 触发事件统一改为 Events.ts 的封装。
|
||||
- 冷启动改为优先读取 consolidated 或 tasks.augmented,失败再回落旧流程。
|
||||
- 单文件变更路径替换为“解析入口 + Augmentor + Repository”的新流水线(逐步灰度)。
|
||||
|
||||
3) 视图迁移(首批 1-2 个)
|
||||
- 去除直接 setTasks 调用,改成订阅 task-cache-updated + QueryAPI 拉取。
|
||||
- 保留 Adapter 以兼容未迁移视图。
|
||||
|
||||
4) 持久化与失效策略
|
||||
- 引入命名空间 key 规则与 hash 计算;对项目配置变更只失效 project.data 与 tasks.augmented。
|
||||
|
||||
5) 测试
|
||||
- 单测:ParsersEntry、TaskAugmentor(继承策略)、StorageAdapter(hash/版本/失效)、ProjectResolver(优先级)。
|
||||
- 集成:单文件改动/重命名/删除、项目配置变更、冷启动、事件序号。
|
||||
|
||||
6) 文档
|
||||
- 在 README 或开发文档中标注 setTasks 已废弃(迁移至事件 + QueryAPI)。
|
||||
|
||||
---
|
||||
|
||||
如需我补充具体类型定义(TypeScript 接口)或更详细的函数注释,请告知要优先完善的模块。
|
||||
|
|
@ -10,15 +10,15 @@ const mockPrepareFuzzySearch = jest.fn((query: string) => {
|
|||
const mockT = jest.fn((key: string) => key);
|
||||
|
||||
jest.mock("obsidian", () => ({
|
||||
prepareFuzzySearch: mockPrepareFuzzySearch
|
||||
prepareFuzzySearch: mockPrepareFuzzySearch,
|
||||
}));
|
||||
|
||||
jest.mock("../translations/helper", () => ({
|
||||
t: mockT
|
||||
t: mockT,
|
||||
}));
|
||||
|
||||
import { SettingsIndexer } from "../components/settings/SettingsIndexer";
|
||||
import { SETTINGS_METADATA } from "../data/settings-metadata";
|
||||
import { SETTINGS_METADATA } from "../common/settings-metadata";
|
||||
|
||||
describe("Settings Search Tests", () => {
|
||||
let indexer: SettingsIndexer;
|
||||
|
|
@ -45,7 +45,9 @@ describe("Settings Search Tests", () => {
|
|||
});
|
||||
|
||||
test("should have progress bar settings", () => {
|
||||
const progressBarMain = SETTINGS_METADATA.find(item => item.id === "progress-bar-main");
|
||||
const progressBarMain = SETTINGS_METADATA.find(
|
||||
(item) => item.id === "progress-bar-main"
|
||||
);
|
||||
expect(progressBarMain).toBeDefined();
|
||||
expect(progressBarMain?.name).toBe("Progress bar");
|
||||
expect(progressBarMain?.tabId).toBe("progress-bar");
|
||||
|
|
@ -53,7 +55,9 @@ describe("Settings Search Tests", () => {
|
|||
});
|
||||
|
||||
test("should have enable task filter setting", () => {
|
||||
const taskFilter = SETTINGS_METADATA.find(item => item.id === "enable-task-filter");
|
||||
const taskFilter = SETTINGS_METADATA.find(
|
||||
(item) => item.id === "enable-task-filter"
|
||||
);
|
||||
expect(taskFilter).toBeDefined();
|
||||
expect(taskFilter?.name).toBe("Enable Task Filter");
|
||||
expect(taskFilter?.tabId).toBe("task-filter");
|
||||
|
|
@ -63,7 +67,7 @@ describe("Settings Search Tests", () => {
|
|||
describe("SettingsIndexer", () => {
|
||||
test("should initialize correctly", () => {
|
||||
expect(indexer).toBeDefined();
|
||||
|
||||
|
||||
// Should not be initialized yet
|
||||
const stats = indexer.getStats();
|
||||
expect(stats.itemCount).toBeGreaterThan(0);
|
||||
|
|
@ -76,30 +80,40 @@ describe("Settings Search Tests", () => {
|
|||
|
||||
test("should search by name", () => {
|
||||
const results = indexer.search("progress");
|
||||
|
||||
|
||||
console.log("Search results for 'progress':", results.length);
|
||||
results.forEach(result => {
|
||||
console.log(`- ${result.item.name} (${result.matchType}, score: ${result.score})`);
|
||||
results.forEach((result) => {
|
||||
console.log(
|
||||
`- ${result.item.name} (${result.matchType}, score: ${result.score})`
|
||||
);
|
||||
});
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
|
||||
|
||||
// Should find progress bar settings
|
||||
const progressResult = results.find(result =>
|
||||
result.item.id === "progress-bar-main" ||
|
||||
result.item.name.toLowerCase().includes("progress")
|
||||
const progressResult = results.find(
|
||||
(result) =>
|
||||
result.item.id === "progress-bar-main" ||
|
||||
result.item.name.toLowerCase().includes("progress")
|
||||
);
|
||||
expect(progressResult).toBeDefined();
|
||||
});
|
||||
|
||||
test("should search by description", () => {
|
||||
const results = indexer.search("toggle");
|
||||
|
||||
|
||||
console.log("Search results for 'toggle':", results.length);
|
||||
results.forEach(result => {
|
||||
console.log(`- ${result.item.name} (${result.matchType}, score: ${result.score})`);
|
||||
results.forEach((result) => {
|
||||
console.log(
|
||||
`- ${result.item.name} (${result.matchType}, score: ${result.score})`
|
||||
);
|
||||
if (result.item.description) {
|
||||
console.log(` Description: ${result.item.description.substring(0, 100)}...`);
|
||||
console.log(
|
||||
` Description: ${result.item.description.substring(
|
||||
0,
|
||||
100
|
||||
)}...`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -108,10 +122,12 @@ describe("Settings Search Tests", () => {
|
|||
|
||||
test("should search by keywords", () => {
|
||||
const results = indexer.search("checkbox");
|
||||
|
||||
|
||||
console.log("Search results for 'checkbox':", results.length);
|
||||
results.forEach(result => {
|
||||
console.log(`- ${result.item.name} (${result.matchType}, score: ${result.score})`);
|
||||
results.forEach((result) => {
|
||||
console.log(
|
||||
`- ${result.item.name} (${result.matchType}, score: ${result.score})`
|
||||
);
|
||||
console.log(` Keywords: ${result.item.keywords.join(", ")}`);
|
||||
});
|
||||
|
||||
|
|
@ -120,22 +136,30 @@ describe("Settings Search Tests", () => {
|
|||
|
||||
test("should prioritize name matches over description matches", () => {
|
||||
const results = indexer.search("filter");
|
||||
|
||||
|
||||
console.log("Search results for 'filter' with scores:");
|
||||
results.forEach(result => {
|
||||
console.log(`- ${result.item.name} (${result.matchType}, score: ${result.score})`);
|
||||
results.forEach((result) => {
|
||||
console.log(
|
||||
`- ${result.item.name} (${result.matchType}, score: ${result.score})`
|
||||
);
|
||||
});
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
|
||||
|
||||
// Find name matches and description matches
|
||||
const nameMatches = results.filter(r => r.matchType === "name");
|
||||
const descriptionMatches = results.filter(r => r.matchType === "description");
|
||||
|
||||
const nameMatches = results.filter((r) => r.matchType === "name");
|
||||
const descriptionMatches = results.filter(
|
||||
(r) => r.matchType === "description"
|
||||
);
|
||||
|
||||
if (nameMatches.length > 0 && descriptionMatches.length > 0) {
|
||||
// Name matches should have higher scores
|
||||
const highestNameScore = Math.max(...nameMatches.map(r => r.score));
|
||||
const highestDescScore = Math.max(...descriptionMatches.map(r => r.score));
|
||||
const highestNameScore = Math.max(
|
||||
...nameMatches.map((r) => r.score)
|
||||
);
|
||||
const highestDescScore = Math.max(
|
||||
...descriptionMatches.map((r) => r.score)
|
||||
);
|
||||
expect(highestNameScore).toBeGreaterThan(highestDescScore);
|
||||
}
|
||||
});
|
||||
|
|
@ -165,7 +189,7 @@ describe("Settings Search Tests", () => {
|
|||
const lowerResults = indexer.search("progress");
|
||||
const upperResults = indexer.search("PROGRESS");
|
||||
const mixedResults = indexer.search("Progress");
|
||||
|
||||
|
||||
expect(lowerResults.length).toBe(upperResults.length);
|
||||
expect(lowerResults.length).toBe(mixedResults.length);
|
||||
});
|
||||
|
|
@ -174,14 +198,14 @@ describe("Settings Search Tests", () => {
|
|||
describe("Translation Integration", () => {
|
||||
test("should call translation function for setting names", () => {
|
||||
indexer.search("progress");
|
||||
|
||||
|
||||
// Should have called t() for translating setting names
|
||||
expect(mockT).toHaveBeenCalled();
|
||||
|
||||
|
||||
// Check if it was called with expected translation keys
|
||||
const calls = mockT.mock.calls.map((call: any) => call[0]);
|
||||
console.log("Translation calls:", calls.slice(0, 10)); // Show first 10 calls
|
||||
|
||||
|
||||
expect(calls).toContain("Progress bar");
|
||||
});
|
||||
});
|
||||
|
|
@ -208,7 +232,7 @@ describe("Settings Search Tests", () => {
|
|||
const start = performance.now();
|
||||
indexer.initialize();
|
||||
const end = performance.now();
|
||||
|
||||
|
||||
const buildTime = end - start;
|
||||
console.log(`Index build time: ${buildTime.toFixed(2)}ms`);
|
||||
expect(buildTime).toBeLessThan(50); // Should be under 50ms
|
||||
|
|
@ -216,13 +240,17 @@ describe("Settings Search Tests", () => {
|
|||
|
||||
test("should search quickly", () => {
|
||||
indexer.initialize(); // Pre-initialize
|
||||
|
||||
|
||||
const start = performance.now();
|
||||
const results = indexer.search("progress");
|
||||
const end = performance.now();
|
||||
|
||||
|
||||
const searchTime = end - start;
|
||||
console.log(`Search time: ${searchTime.toFixed(2)}ms for ${results.length} results`);
|
||||
console.log(
|
||||
`Search time: ${searchTime.toFixed(2)}ms for ${
|
||||
results.length
|
||||
} results`
|
||||
);
|
||||
expect(searchTime).toBeLessThan(10); // Should be under 10ms
|
||||
});
|
||||
});
|
||||
|
|
@ -230,45 +258,50 @@ describe("Settings Search Tests", () => {
|
|||
describe("Specific Setting Tests", () => {
|
||||
test("should find 'Enable Task Filter' setting", () => {
|
||||
const results = indexer.search("enable task filter");
|
||||
|
||||
|
||||
console.log("Searching for 'enable task filter':");
|
||||
results.forEach(result => {
|
||||
results.forEach((result) => {
|
||||
console.log(`- ${result.item.name} (ID: ${result.item.id})`);
|
||||
});
|
||||
|
||||
const exactMatch = results.find(r => r.item.id === "enable-task-filter");
|
||||
|
||||
const exactMatch = results.find(
|
||||
(r) => r.item.id === "enable-task-filter"
|
||||
);
|
||||
expect(exactMatch).toBeDefined();
|
||||
});
|
||||
|
||||
test("should find progress bar settings", () => {
|
||||
const results = indexer.search("progress bar");
|
||||
|
||||
|
||||
console.log("Searching for 'progress bar':");
|
||||
results.forEach(result => {
|
||||
results.forEach((result) => {
|
||||
console.log(`- ${result.item.name} (ID: ${result.item.id})`);
|
||||
});
|
||||
|
||||
const progressBarMain = results.find(r => r.item.id === "progress-bar-main");
|
||||
|
||||
const progressBarMain = results.find(
|
||||
(r) => r.item.id === "progress-bar-main"
|
||||
);
|
||||
expect(progressBarMain).toBeDefined();
|
||||
expect(progressBarMain?.matchType).toBe("name");
|
||||
});
|
||||
|
||||
test("should find settings by partial name", () => {
|
||||
const results = indexer.search("workflow");
|
||||
|
||||
|
||||
console.log("Searching for 'workflow':");
|
||||
results.forEach(result => {
|
||||
results.forEach((result) => {
|
||||
console.log(`- ${result.item.name} (ID: ${result.item.id})`);
|
||||
});
|
||||
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
|
||||
|
||||
// Should find workflow-related settings
|
||||
const workflowSettings = results.filter(r =>
|
||||
r.item.name.toLowerCase().includes("workflow") ||
|
||||
r.item.tabId === "workflow"
|
||||
const workflowSettings = results.filter(
|
||||
(r) =>
|
||||
r.item.name.toLowerCase().includes("workflow") ||
|
||||
r.item.tabId === "workflow"
|
||||
);
|
||||
expect(workflowSettings.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { prepareFuzzySearch } from "obsidian";
|
||||
import { SETTINGS_METADATA } from "../../data/settings-metadata";
|
||||
import { SettingsSearchIndex, SettingSearchItem, SearchResult } from "../../types/SettingsSearch";
|
||||
import { SETTINGS_METADATA } from "../../common/settings-metadata";
|
||||
import {
|
||||
SettingsSearchIndex,
|
||||
SettingSearchItem,
|
||||
SearchResult,
|
||||
} from "../../types/SettingsSearch";
|
||||
import { t } from "../../translations/helper";
|
||||
|
||||
/**
|
||||
|
|
@ -12,7 +16,6 @@ export class SettingsIndexer {
|
|||
private isInitialized: boolean = false;
|
||||
private rootEl: HTMLElement | null = null;
|
||||
|
||||
|
||||
constructor(rootEl?: HTMLElement | null) {
|
||||
this.index = {
|
||||
items: [],
|
||||
|
|
@ -39,13 +42,14 @@ export class SettingsIndexer {
|
|||
|
||||
const endTime = performance.now();
|
||||
console.log(
|
||||
`[SettingsIndexer] Index built in ${(endTime - startTime).toFixed(2)}ms with ${this.index.items.length} items`
|
||||
`[SettingsIndexer] Index built in ${(endTime - startTime).toFixed(
|
||||
2
|
||||
)}ms with ${this.index.items.length} items`
|
||||
);
|
||||
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从静态 SETTINGS_METADATA 构建索引(回退)
|
||||
*/
|
||||
|
|
@ -54,7 +58,9 @@ export class SettingsIndexer {
|
|||
const translatedItem: SettingSearchItem = {
|
||||
...item,
|
||||
name: t(item.translationKey),
|
||||
description: item.descriptionKey ? t(item.descriptionKey) : item.description,
|
||||
description: item.descriptionKey
|
||||
? t(item.descriptionKey)
|
||||
: item.description,
|
||||
};
|
||||
this.addItemToIndex(translatedItem);
|
||||
}
|
||||
|
|
@ -65,17 +71,24 @@ export class SettingsIndexer {
|
|||
*/
|
||||
private buildIndexFromDOM(root: HTMLElement): void {
|
||||
// 查找所有设置 section
|
||||
const sectionEls = Array.from(root.querySelectorAll<HTMLElement>(".settings-tab-section"));
|
||||
const sectionEls = Array.from(
|
||||
root.querySelectorAll<HTMLElement>(".settings-tab-section")
|
||||
);
|
||||
const seenIds = new Set<string>();
|
||||
|
||||
sectionEls.forEach((section) => {
|
||||
const tabId = section.getAttribute("data-tab-id") || "general";
|
||||
const category = section.getAttribute("data-category") || "core";
|
||||
|
||||
const settingItems = Array.from(section.querySelectorAll<HTMLElement>(".setting-item"));
|
||||
const settingItems = Array.from(
|
||||
section.querySelectorAll<HTMLElement>(".setting-item")
|
||||
);
|
||||
settingItems.forEach((el, idx) => {
|
||||
const nameEl = el.querySelector<HTMLElement>(".setting-item-name");
|
||||
const descEl = el.querySelector<HTMLElement>(".setting-item-description");
|
||||
const nameEl =
|
||||
el.querySelector<HTMLElement>(".setting-item-name");
|
||||
const descEl = el.querySelector<HTMLElement>(
|
||||
".setting-item-description"
|
||||
);
|
||||
const name = (nameEl?.textContent || "").trim();
|
||||
const description = (descEl?.textContent || "").trim();
|
||||
|
||||
|
|
@ -142,9 +155,7 @@ export class SettingsIndexer {
|
|||
.filter(Boolean);
|
||||
// 去重并限制数量,优先较长的词
|
||||
const uniq = Array.from(new Set(tokens));
|
||||
return uniq
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.slice(0, 12);
|
||||
return uniq.sort((a, b) => b.length - a.length).slice(0, 12);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -192,8 +203,12 @@ export class SettingsIndexer {
|
|||
if (nameMatch) {
|
||||
results.push({
|
||||
item,
|
||||
score: this.calculateScore(normalizedQuery, item.name, 'name'),
|
||||
matchType: 'name'
|
||||
score: this.calculateScore(
|
||||
normalizedQuery,
|
||||
item.name,
|
||||
"name"
|
||||
),
|
||||
matchType: "name",
|
||||
});
|
||||
seenIds.add(item.id);
|
||||
}
|
||||
|
|
@ -207,8 +222,12 @@ export class SettingsIndexer {
|
|||
if (descMatch) {
|
||||
results.push({
|
||||
item,
|
||||
score: this.calculateScore(normalizedQuery, item.description, 'description'),
|
||||
matchType: 'description'
|
||||
score: this.calculateScore(
|
||||
normalizedQuery,
|
||||
item.description,
|
||||
"description"
|
||||
),
|
||||
matchType: "description",
|
||||
});
|
||||
seenIds.add(item.id);
|
||||
}
|
||||
|
|
@ -223,8 +242,12 @@ export class SettingsIndexer {
|
|||
if (keywordMatch) {
|
||||
results.push({
|
||||
item,
|
||||
score: this.calculateScore(normalizedQuery, keyword, 'keyword'),
|
||||
matchType: 'keyword'
|
||||
score: this.calculateScore(
|
||||
normalizedQuery,
|
||||
keyword,
|
||||
"keyword"
|
||||
),
|
||||
matchType: "keyword",
|
||||
});
|
||||
seenIds.add(item.id);
|
||||
break; // 只需要一个关键词匹配即可
|
||||
|
|
@ -233,9 +256,7 @@ export class SettingsIndexer {
|
|||
}
|
||||
|
||||
// 按分数排序并限制结果数量
|
||||
return results
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, maxResults);
|
||||
return results.sort((a, b) => b.score - a.score).slice(0, maxResults);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -245,7 +266,11 @@ export class SettingsIndexer {
|
|||
* @param matchType 匹配类型
|
||||
* @returns 匹配分数
|
||||
*/
|
||||
private calculateScore(query: string, target: string, matchType: 'name' | 'description' | 'keyword'): number {
|
||||
private calculateScore(
|
||||
query: string,
|
||||
target: string,
|
||||
matchType: "name" | "description" | "keyword"
|
||||
): number {
|
||||
const lowerTarget = target.toLowerCase();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
|
|
@ -255,7 +280,7 @@ export class SettingsIndexer {
|
|||
const baseScores = {
|
||||
name: 100,
|
||||
description: 60,
|
||||
keyword: 80
|
||||
keyword: 80,
|
||||
};
|
||||
score += baseScores[matchType];
|
||||
|
||||
|
|
@ -299,7 +324,7 @@ export class SettingsIndexer {
|
|||
this.initialize();
|
||||
}
|
||||
|
||||
return this.index.items.find(item => item.id === itemId);
|
||||
return this.index.items.find((item) => item.id === itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -318,7 +343,11 @@ export class SettingsIndexer {
|
|||
* 获取索引统计信息
|
||||
* @returns 索引统计
|
||||
*/
|
||||
public getStats(): { itemCount: number; tabCount: number; keywordCount: number } {
|
||||
public getStats(): {
|
||||
itemCount: number;
|
||||
tabCount: number;
|
||||
keywordCount: number;
|
||||
} {
|
||||
if (!this.isInitialized) {
|
||||
this.initialize();
|
||||
}
|
||||
|
|
@ -326,7 +355,7 @@ export class SettingsIndexer {
|
|||
return {
|
||||
itemCount: this.index.items.length,
|
||||
tabCount: this.index.tabMap.size,
|
||||
keywordCount: this.index.keywordMap.size
|
||||
keywordCount: this.index.keywordMap.size,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
src/dataflow/api/QueryAPI.ts
Normal file
35
src/dataflow/api/QueryAPI.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type { Task } from "../../types/task";
|
||||
import type { App } from "obsidian";
|
||||
|
||||
// Simple names; a thin facade over internal repository/index
|
||||
export class QueryAPI {
|
||||
constructor(private app: App) {}
|
||||
|
||||
// TODO: wire to TaskRepository after it is introduced
|
||||
async all(): Promise<Task[]> {
|
||||
// Placeholder: later call repository.getAll()
|
||||
// Keep behavior consistent with current TaskManager.getAllTasks()
|
||||
return [];
|
||||
}
|
||||
|
||||
async byProject(project: string): Promise<Task[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async byTags(tags: string[]): Promise<Task[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async byStatus(completed: boolean): Promise<Task[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async byDateRange(opts: { from?: number; to?: number; field?: "due" | "start" | "scheduled" }): Promise<Task[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async byId(id: string): Promise<Task | null> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
34
src/dataflow/augment/Augmentor.ts
Normal file
34
src/dataflow/augment/Augmentor.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Task } from "../../types/task";
|
||||
|
||||
export interface FileContext {
|
||||
filePath: string;
|
||||
fileMeta?: Record<string, any>;
|
||||
project?: { name?: string; data?: Record<string, any> } | null;
|
||||
}
|
||||
|
||||
// Simple naming: merge file/project context into tasks
|
||||
export class Augmentor {
|
||||
constructor(private options?: { inherit?: Record<string, "task" | "file" | "project" | "merge-array"> }) {}
|
||||
|
||||
merge(ctx: FileContext, tasks: Task[]): Task[] {
|
||||
const inherit = this.options?.inherit || {};
|
||||
return tasks.map((t) => this.mergeOne(ctx, t, inherit));
|
||||
}
|
||||
|
||||
private mergeOne(ctx: FileContext, t: Task, inherit: Record<string, string>): Task {
|
||||
const meta = { ...(t.metadata || {}) } as Record<string, any>;
|
||||
|
||||
// Example basic rules (keep minimal; extend later):
|
||||
// project
|
||||
if (ctx.project?.name && (meta.project == null)) {
|
||||
meta.project = ctx.project.name;
|
||||
}
|
||||
// tags (merge-array)
|
||||
const fileTags = Array.isArray(ctx.fileMeta?.tags) ? ctx.fileMeta!.tags : [];
|
||||
const taskTags = Array.isArray(meta.tags) ? meta.tags : [];
|
||||
meta.tags = Array.from(new Set([...(taskTags as any), ...fileTags]));
|
||||
|
||||
return { ...t, metadata: meta } as Task;
|
||||
}
|
||||
}
|
||||
|
||||
42
src/dataflow/events/Events.ts
Normal file
42
src/dataflow/events/Events.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import type { App, EventRef } from "obsidian";
|
||||
|
||||
// Keep names simple and consistent
|
||||
export const Events = {
|
||||
CACHE_READY: "task-genius:cache-ready",
|
||||
TASK_CACHE_UPDATED: "task-genius:task-cache-updated",
|
||||
FILE_UPDATED: "task-genius:file-updated",
|
||||
PROJECT_DATA_UPDATED: "task-genius:project-data-updated",
|
||||
SETTINGS_CHANGED: "task-genius:settings-changed",
|
||||
TASK_COMPLETED: "task-genius:task-completed",
|
||||
TASK_ADDED: "task-genius:task-added",
|
||||
TASK_UPDATED: "task-genius:task-updated",
|
||||
TASK_DELETED: "task-genius:task-deleted",
|
||||
} as const;
|
||||
|
||||
export type SeqClock = { next(): number };
|
||||
|
||||
let _seq = 0;
|
||||
export const Seq: SeqClock = {
|
||||
next() {
|
||||
_seq = (_seq + 1) >>> 0;
|
||||
return _seq;
|
||||
},
|
||||
};
|
||||
|
||||
// Emit helpers (payload kept as any to maintain compatibility with existing trigger signatures)
|
||||
export function emit(app: App, name: string, payload?: any): void {
|
||||
// @ts-expect-error keep compatibility with existing Obsidian typing overloads
|
||||
app.workspace.trigger(name, payload);
|
||||
}
|
||||
|
||||
export function on(app: App, name: string, handler: (...args: any[]) => void): EventRef {
|
||||
// Consumers should use component.registerEvent(on(...)) to auto-unsub
|
||||
return app.workspace.on(name as any, handler) as EventRef;
|
||||
}
|
||||
|
||||
// Convenience
|
||||
export const onTaskCacheUpdated = (app: App, handler: (payload: any) => void) =>
|
||||
on(app, Events.TASK_CACHE_UPDATED, handler);
|
||||
export const emitTaskCacheUpdated = (app: App, payload: any) =>
|
||||
emit(app, Events.TASK_CACHE_UPDATED, payload);
|
||||
|
||||
17
src/dataflow/indexer/Repository.ts
Normal file
17
src/dataflow/indexer/Repository.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { Task } from "../../types/task";
|
||||
|
||||
// Thin repository façade; will delegate to existing TaskIndexer
|
||||
export class Repository {
|
||||
async updateFile(path: string, tasks: Task[]): Promise<void> {
|
||||
// TODO: wire to TaskIndexer.updateIndexWithTasks
|
||||
}
|
||||
|
||||
async all(): Promise<Task[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async byProject(project: string): Promise<Task[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
12
src/dataflow/parsers/CanvasEntry.ts
Normal file
12
src/dataflow/parsers/CanvasEntry.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { CanvasParser } from "../../utils/parsing/CanvasParser";
|
||||
import type { Task } from "../../types/task";
|
||||
import { getConfig } from "../../common/task-parser-config";
|
||||
import TaskProgressBarPlugin from "../../index";
|
||||
|
||||
// This entry requires plugin to provide config like original code did
|
||||
export async function parseCanvas(content: string, filePath: string, plugin: TaskProgressBarPlugin): Promise<Task[]> {
|
||||
const config = getConfig(plugin.settings.preferMetadataFormat, plugin);
|
||||
const parser = new CanvasParser(config);
|
||||
return parser.parseCanvasFile(content, filePath);
|
||||
}
|
||||
|
||||
19
src/dataflow/parsers/FileMetaEntry.ts
Normal file
19
src/dataflow/parsers/FileMetaEntry.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { FileMetadataTaskParser } from "../../utils/workers/FileMetadataTaskParser";
|
||||
import type { Task } from "../../types/task";
|
||||
import type { FileParsingConfiguration } from "../../common/setting-definition";
|
||||
|
||||
// Project detection is disabled here; project will be added in augment stage
|
||||
export async function parseFileMeta(
|
||||
content: string,
|
||||
filePath: string,
|
||||
fileCache: any,
|
||||
config: FileParsingConfiguration
|
||||
): Promise<Task[]> {
|
||||
const parser = new FileMetadataTaskParser({
|
||||
...config,
|
||||
// ensure project detection defaults are respected; detection methods are passed as undefined here
|
||||
} as any, undefined);
|
||||
const { tasks } = parser.parseFileForTasks(filePath, content, fileCache);
|
||||
return tasks;
|
||||
}
|
||||
|
||||
30
src/dataflow/parsers/MarkdownEntry.ts
Normal file
30
src/dataflow/parsers/MarkdownEntry.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { MarkdownTaskParser } from "../../utils/workers/ConfigurableTaskParser";
|
||||
import type { Task } from "../../types/task";
|
||||
import { TaskParserConfig, MetadataParseMode } from "../../types/TaskParserConfig";
|
||||
|
||||
// Default config for compatibility
|
||||
const defaultConfig: TaskParserConfig = {
|
||||
parseMetadata: true,
|
||||
parseTags: true,
|
||||
parseComments: true,
|
||||
parseHeadings: true,
|
||||
maxIndentSize: 10,
|
||||
maxParseIterations: 1000,
|
||||
maxMetadataIterations: 10,
|
||||
maxTagLength: 50,
|
||||
maxEmojiValueLength: 100,
|
||||
maxStackOperations: 100,
|
||||
maxStackSize: 50,
|
||||
statusMapping: {},
|
||||
emojiMapping: {},
|
||||
metadataParseMode: MetadataParseMode.Both,
|
||||
specialTagPrefixes: {}
|
||||
};
|
||||
|
||||
const parser = new MarkdownTaskParser(defaultConfig);
|
||||
|
||||
export async function parseMarkdown(content: string, filePath: string, fileMetadata?: Record<string, any>): Promise<Task[]> {
|
||||
// Use existing legacy output for compatibility
|
||||
return parser.parseLegacy(content, filePath, fileMetadata);
|
||||
}
|
||||
|
||||
26
src/dataflow/persistence/Storage.ts
Normal file
26
src/dataflow/persistence/Storage.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { Task } from "../../types/task";
|
||||
|
||||
// Simple storage facade; will wrap LocalStorageCache later
|
||||
export type RawRecord = { hash: string; time: number; data: Task[] };
|
||||
export type ProjectRecord = { hash: string; time: number; data: { project?: string; meta: Record<string, any> } };
|
||||
export type AugmentedRecord = { hash: string; time: number; data: Task[] };
|
||||
|
||||
export const Keys = {
|
||||
raw: (path: string) => `tasks.raw:${path}`,
|
||||
project: (path: string) => `project.data:${path}`,
|
||||
augmented: (path: string) => `tasks.augmented:${path}`,
|
||||
consolidated: () => `consolidated:taskIndex`,
|
||||
};
|
||||
|
||||
export class Storage {
|
||||
// Placeholder API; integrate with LocalStorageCache in next step
|
||||
async loadRaw(path: string): Promise<RawRecord | null> { return null }
|
||||
async storeRaw(path: string, rec: RawRecord): Promise<void> {}
|
||||
|
||||
async loadProject(path: string): Promise<ProjectRecord | null> { return null }
|
||||
async storeProject(path: string, rec: ProjectRecord): Promise<void> {}
|
||||
|
||||
async loadAugmented(path: string): Promise<AugmentedRecord | null> { return null }
|
||||
async storeAugmented(path: string, rec: AugmentedRecord): Promise<void> {}
|
||||
}
|
||||
|
||||
15
src/dataflow/project/Resolver.ts
Normal file
15
src/dataflow/project/Resolver.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Simple project resolver skeleton
|
||||
export interface ProjectData {
|
||||
name?: string;
|
||||
meta: Record<string, any>;
|
||||
}
|
||||
|
||||
export class Resolver {
|
||||
constructor() {}
|
||||
|
||||
// TODO: integrate with ProjectConfigManager/ProjectDataCache
|
||||
async get(path: string): Promise<ProjectData | null> {
|
||||
return { name: undefined, meta: {} };
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue