mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
document runtime query contract
This commit is contained in:
parent
6689b22896
commit
636acab025
2 changed files with 422 additions and 1 deletions
412
docs/development/runtime-api-query-contract.md
Normal file
412
docs/development/runtime-api-query-contract.md
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
# Runtime API Query Contract
|
||||
|
||||
This document defines the ideal end-state for TaskNotes runtime query APIs used by companion plugins such as TaskNotes Workflows.
|
||||
|
||||
## Context
|
||||
|
||||
TaskNotes now exposes an in-process JavaScript runtime API for companion plugins. Recent runtime API work added namespaced task, query, catalog, event, lifecycle, and extension surfaces.
|
||||
|
||||
That direction is correct. The remaining risk is that the current query contract exposes `FilterQuery` directly. `FilterQuery` is powerful, but it was shaped for TaskNotes UI state: every node has an `id`, it carries view grouping fields, and it uses UI-oriented operator names. Companion plugins need a stable task-selection contract, not a leak of current view/filter internals.
|
||||
|
||||
The ideal end-state is:
|
||||
|
||||
- TaskNotes core owns task query semantics.
|
||||
- Companion plugins call TaskNotes for task selection instead of reimplementing filters.
|
||||
- Workflow engines remain orchestration layers, not query engines.
|
||||
- The public query DTO is stable, serializable, validation-friendly, and independent of current UI state.
|
||||
- Bases-compatible syntax can be imported or compiled later, but the runtime API has its own canonical structured model.
|
||||
|
||||
## Goals
|
||||
|
||||
- Provide one public task-selection query format for runtime API, HTTP API, MCP, CLI, and companion plugins.
|
||||
- Keep task query semantics consistent with TaskNotes views where appropriate.
|
||||
- Make query validation and dry-run/explain output deterministic.
|
||||
- Support user-defined fields, computed task fields, file fields, date comparisons, sorting, limits, and grouping without exposing UI-only state.
|
||||
- Preserve a clear boundary between task selection and task mutation.
|
||||
- Keep triggers cheap: workflow/event triggers may filter event payloads, but vault-wide selection belongs in explicit query calls.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not expose arbitrary JavaScript, Dataview, or unrestricted Bases formulas as the runtime query contract.
|
||||
- Do not make the runtime API depend on Obsidian Bases as the query evaluator.
|
||||
- Do not require companion plugins to know TaskNotes frontmatter mapping details.
|
||||
- Do not make every workflow step a hidden query step.
|
||||
- Do not treat the current `FilterQuery` shape as the long-term public contract unless it is intentionally cleaned up first.
|
||||
|
||||
## Current State
|
||||
|
||||
Current runtime APIs include:
|
||||
|
||||
- `api.tasks.list(query?: FilterQuery): Promise<TaskInfo[]>`
|
||||
- `api.query.tasks(query?: FilterQuery): Promise<TaskNotesRuntimeTaskQueryResult>`
|
||||
- `api.query.filterOptions(): Promise<FilterOptions>`
|
||||
- `api.catalog.filterProperties(): TaskNotesRuntimeFilterPropertyDefinition[]`
|
||||
- `api.catalog.filterOperators(): TaskNotesRuntimeFilterOperatorDefinition[]`
|
||||
|
||||
Current strengths:
|
||||
|
||||
- Query execution delegates to `FilterService`.
|
||||
- Filter evaluation already handles nested groups, user fields, project links, computed dependency fields, and completion state.
|
||||
- Catalog metadata gives companion-plugin editors a way to avoid hardcoded status, priority, field, and operator lists.
|
||||
- Mutation APIs route through TaskNotes services and include mutation context for debuggability.
|
||||
|
||||
Current public-contract concerns:
|
||||
|
||||
- `FilterCondition.id` and `FilterGroup.id` are UI state, not query semantics.
|
||||
- `FilterQuery` mixes selection with view grouping state.
|
||||
- `FilterQuery` has no first-class `limit`, `offset`, or paging contract.
|
||||
- `tasks.list(query?)` and `query.tasks(query?)` overlap without a sharply documented difference.
|
||||
- Query result counts do not distinguish total, matched-before-limit, and returned-after-limit.
|
||||
- Operators are UI phrase IDs such as `is-on-or-before`; that may be acceptable, but should be part of an intentional stable vocabulary.
|
||||
- There is no `normalize`, `validate`, or `explain` API for companion plugins to test a query before running an automation.
|
||||
|
||||
## Proposed Public Query DTO
|
||||
|
||||
Introduce a stable public query shape, separate from internal `FilterQuery`.
|
||||
|
||||
```ts
|
||||
export interface TaskNotesRuntimeTaskQuery {
|
||||
where?: TaskNotesRuntimePredicate;
|
||||
sort?: TaskNotesRuntimeSort[];
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
group?: TaskNotesRuntimeGroup[];
|
||||
scope?: TaskNotesRuntimeQueryScope;
|
||||
}
|
||||
|
||||
export type TaskNotesRuntimePredicate =
|
||||
| { all: TaskNotesRuntimePredicate[] }
|
||||
| { any: TaskNotesRuntimePredicate[] }
|
||||
| { not: TaskNotesRuntimePredicate }
|
||||
| TaskNotesRuntimeCondition;
|
||||
|
||||
export interface TaskNotesRuntimeCondition {
|
||||
field: string;
|
||||
op: TaskNotesRuntimeOperator;
|
||||
value?: TaskNotesRuntimeValue;
|
||||
}
|
||||
|
||||
export type TaskNotesRuntimeValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| TaskNotesRuntimeValue[]
|
||||
| { fn: "today" | "now" }
|
||||
| { fn: "date"; value: string }
|
||||
| { fn: "dateAdd"; value: TaskNotesRuntimeValue; amount: number; unit: "day" | "week" | "month" };
|
||||
|
||||
export interface TaskNotesRuntimeSort {
|
||||
field: string;
|
||||
direction?: "asc" | "desc";
|
||||
}
|
||||
|
||||
export interface TaskNotesRuntimeGroup {
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface TaskNotesRuntimeQueryScope {
|
||||
includeArchived?: boolean;
|
||||
folders?: string[];
|
||||
excludeFolders?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
The query engine should normalize this DTO into the internal representation used by `FilterService`.
|
||||
|
||||
## Field Identity
|
||||
|
||||
Runtime query fields should be stable semantic IDs, not raw frontmatter keys.
|
||||
|
||||
Recommended canonical namespaces:
|
||||
|
||||
- `task.title`
|
||||
- `task.status`
|
||||
- `task.priority`
|
||||
- `task.due`
|
||||
- `task.scheduled`
|
||||
- `task.completedDate`
|
||||
- `task.archived`
|
||||
- `task.tags`
|
||||
- `task.contexts`
|
||||
- `task.projects`
|
||||
- `task.recurrence`
|
||||
- `task.timeEstimate`
|
||||
- `task.totalTrackedTime`
|
||||
- `task.hasSubtasks`
|
||||
- `task.isBlocked`
|
||||
- `task.isBlocking`
|
||||
- `task.blockedBy`
|
||||
- `file.path`
|
||||
- `file.name`
|
||||
- `file.folder`
|
||||
- `file.tags`
|
||||
- `file.ctime`
|
||||
- `file.mtime`
|
||||
- `user.<id-or-key>`
|
||||
|
||||
TaskNotes can support aliases for compatibility and authoring ergonomics:
|
||||
|
||||
- Current `FilterProperty` IDs such as `status`, `due`, `dependencies.isBlocked`.
|
||||
- Bases-like aliases such as `note.status` and `note.due`.
|
||||
|
||||
Aliases should compile into canonical field IDs before validation or execution. Run logs and explain output should display the normalized canonical query.
|
||||
|
||||
## Operators
|
||||
|
||||
Use a compact canonical operator vocabulary while accepting legacy aliases.
|
||||
|
||||
Recommended canonical operators:
|
||||
|
||||
- `eq`
|
||||
- `ne`
|
||||
- `contains`
|
||||
- `notContains`
|
||||
- `in`
|
||||
- `notIn`
|
||||
- `exists`
|
||||
- `missing`
|
||||
- `lt`
|
||||
- `lte`
|
||||
- `gt`
|
||||
- `gte`
|
||||
- `isTrue`
|
||||
- `isFalse`
|
||||
|
||||
Compatibility aliases may map from current UI operator IDs:
|
||||
|
||||
- `is` -> `eq`
|
||||
- `is-not` -> `ne`
|
||||
- `does-not-contain` -> `notContains`
|
||||
- `is-before` -> `lt`
|
||||
- `is-on-or-before` -> `lte`
|
||||
- `is-after` -> `gt`
|
||||
- `is-on-or-after` -> `gte`
|
||||
- `is-empty` -> `missing`
|
||||
- `is-not-empty` -> `exists`
|
||||
- `is-checked` -> `isTrue`
|
||||
- `is-not-checked` -> `isFalse`
|
||||
|
||||
The catalog should expose both:
|
||||
|
||||
- canonical operator IDs for API clients
|
||||
- display labels for UI clients
|
||||
- accepted aliases for migration and authoring
|
||||
- operator compatibility by field value type
|
||||
|
||||
## Query Result
|
||||
|
||||
Replace or augment the current result with explicit count semantics.
|
||||
|
||||
```ts
|
||||
export interface TaskNotesRuntimeTaskQueryResult {
|
||||
tasks: TaskInfo[];
|
||||
total: number; // total tasks in scope before where
|
||||
matched: number; // tasks matching where before offset/limit
|
||||
returned: number; // tasks returned after offset/limit
|
||||
groups?: TaskNotesRuntimeQueryGroup[];
|
||||
query: TaskNotesRuntimeNormalizedTaskQuery;
|
||||
warnings?: TaskNotesRuntimeQueryWarning[];
|
||||
}
|
||||
|
||||
export interface TaskNotesRuntimeQueryGroup {
|
||||
key: string;
|
||||
label: string;
|
||||
taskPaths: string[];
|
||||
}
|
||||
```
|
||||
|
||||
If grouping is not requested, omit `groups` or return a single `all` group. Do not make grouping mandatory for simple selection.
|
||||
|
||||
## Validation, Normalization, And Explain
|
||||
|
||||
Add explicit query support helpers:
|
||||
|
||||
```ts
|
||||
export interface TaskNotesRuntimeQueryApi {
|
||||
tasks(query?: TaskNotesRuntimeTaskQuery): Promise<TaskNotesRuntimeTaskQueryResult>;
|
||||
validate(query: unknown): TaskNotesRuntimeQueryValidationResult;
|
||||
normalize(query: unknown): TaskNotesRuntimeNormalizedTaskQuery;
|
||||
explain(query: unknown): Promise<TaskNotesRuntimeQueryExplainResult>;
|
||||
filterOptions(): Promise<FilterOptions>;
|
||||
}
|
||||
```
|
||||
|
||||
`validate` should report unsupported fields, unsupported operators, wrong value types, and invalid helper functions without executing the query.
|
||||
|
||||
`normalize` should:
|
||||
|
||||
- expand aliases
|
||||
- fill defaults
|
||||
- strip UI-only fields
|
||||
- canonicalize operators
|
||||
- resolve field IDs to catalog entries
|
||||
|
||||
`explain` should return enough data for dry runs and workflow debugging:
|
||||
|
||||
- normalized query
|
||||
- candidate count
|
||||
- matched count
|
||||
- returned count
|
||||
- applied sort
|
||||
- applied limit/offset
|
||||
- optimization notes where useful
|
||||
- validation warnings
|
||||
|
||||
## Relationship To Existing `FilterQuery`
|
||||
|
||||
Keep `FilterQuery` as an internal or legacy DTO unless it is deliberately promoted.
|
||||
|
||||
Recommended migration path:
|
||||
|
||||
1. Add `TaskNotesRuntimeTaskQuery` alongside existing `FilterQuery`.
|
||||
2. Add an adapter from runtime query DTO to internal `FilterQuery`.
|
||||
3. Keep `api.tasks.list(query?: FilterQuery)` working for compatibility during the transition.
|
||||
4. Prefer `api.query.tasks(query?: TaskNotesRuntimeTaskQuery)` in docs and companion-plugin examples.
|
||||
5. Optionally allow `api.query.tasks()` to accept legacy `FilterQuery` during a deprecation window, but normalize it immediately.
|
||||
6. Move query contract types toward a standalone runtime API package when the shape stabilizes.
|
||||
|
||||
The adapter should be the only place where public query fields map to internal filter properties, field mapping, user fields, and current `FilterService` operator names.
|
||||
|
||||
## Catalog Requirements
|
||||
|
||||
The runtime catalog should support editor generation and query validation.
|
||||
|
||||
Each queryable field should include:
|
||||
|
||||
- canonical field ID
|
||||
- display label
|
||||
- category
|
||||
- value type
|
||||
- source: `model`, `computed`, `file`, or `user`
|
||||
- writable flag
|
||||
- queryable flag
|
||||
- sortable flag
|
||||
- groupable flag
|
||||
- supported canonical operators
|
||||
- accepted aliases
|
||||
- frontmatter key, where applicable
|
||||
- deprecation metadata, where applicable
|
||||
|
||||
Each operator should include:
|
||||
|
||||
- canonical ID
|
||||
- display label
|
||||
- value required flag
|
||||
- accepted value types
|
||||
- accepted aliases
|
||||
|
||||
## Workflows Integration
|
||||
|
||||
TaskNotes Workflows should use TaskNotes runtime query APIs for task selection.
|
||||
|
||||
Workflow usage should look like this:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- id: due-now
|
||||
type: task.query
|
||||
input:
|
||||
query:
|
||||
where:
|
||||
all:
|
||||
- field: task.status
|
||||
op: notIn
|
||||
value: [done, cancelled]
|
||||
- field: task.due
|
||||
op: lte
|
||||
value:
|
||||
fn: today
|
||||
sort:
|
||||
- field: task.due
|
||||
direction: asc
|
||||
limit: 50
|
||||
|
||||
- id: mark-high
|
||||
type: task.patch
|
||||
forEach: "{{steps.due-now.tasks}}"
|
||||
input:
|
||||
task: "{{item.path}}"
|
||||
patch:
|
||||
priority: high
|
||||
```
|
||||
|
||||
Workflow-level `conditions` and step-level `if` may use a similar predicate model, but they evaluate against workflow context, not the TaskNotes task index. They should not call `api.query.tasks()` implicitly.
|
||||
|
||||
Trigger filters should stay event-local. A trigger can inspect `event`, `before`, `after`, `changes`, `source`, and `path`, but vault-wide queries should run as explicit steps after the workflow starts.
|
||||
|
||||
## Bases Compatibility
|
||||
|
||||
Bases is the right mental-model inspiration, not the runtime dependency.
|
||||
|
||||
TaskNotes should eventually be able to:
|
||||
|
||||
- compile a subset of Bases filter expressions into `TaskNotesRuntimeTaskQuery`
|
||||
- import filters from a `.base` file where the mapping is unambiguous
|
||||
- expose familiar aliases such as `note.status`, `note.due`, and `file.path`
|
||||
|
||||
TaskNotes should not:
|
||||
|
||||
- depend on Obsidian Bases as the query evaluator
|
||||
- claim full Bases formula compatibility unless it actually matches edge cases
|
||||
- expose arbitrary formula strings as the canonical runtime query contract
|
||||
|
||||
If string syntax is added, it should compile into the structured query DTO. The structured form remains canonical.
|
||||
|
||||
## API Naming Decisions
|
||||
|
||||
Recommended naming:
|
||||
|
||||
- `api.query.tasks(query)` is the canonical selection API.
|
||||
- `api.tasks.list()` is a simple unfiltered list or a compatibility alias.
|
||||
- `api.stats.tasks(query)` accepts the same canonical query.
|
||||
- `api.catalog.queryFields()` may be clearer than overloading `catalog.filterProperties()`, but either is acceptable if documented.
|
||||
|
||||
Avoid making companion plugins choose between multiple task-selection APIs with subtly different semantics.
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
Add unit coverage for:
|
||||
|
||||
- normalization from canonical query DTO to internal `FilterQuery`
|
||||
- legacy `FilterQuery` compatibility, if supported
|
||||
- field aliases: `task.status`, `note.status`, `status`
|
||||
- operator aliases: `lte`, `is-on-or-before`, etc.
|
||||
- user fields by ID and key
|
||||
- computed fields: `task.hasSubtasks`, `task.isBlocked`, `task.isBlocking`, `task.status.isCompleted`
|
||||
- date helpers: `today`, `now`, and simple date offsets
|
||||
- limit/offset counts
|
||||
- invalid fields and invalid operator/value combinations
|
||||
- `explain` output
|
||||
- `api.stats.tasks(query)` matching `api.query.tasks(query)` selection semantics
|
||||
|
||||
Add integration/smoke coverage for:
|
||||
|
||||
- a companion-plugin style query through `api.query.tasks`
|
||||
- a workflow-style query selecting overdue incomplete tasks
|
||||
- a query using user fields from settings
|
||||
- a query involving dependency/computed fields
|
||||
|
||||
## Implementation Prompt
|
||||
|
||||
Use this prompt for an implementation pass:
|
||||
|
||||
```text
|
||||
We are hardening the TaskNotes JavaScript runtime API for companion plugins. Inspect the current runtime API in src/api/runtime-api.ts and src/api/TaskNotesAPI.ts, the current FilterQuery types in src/types.ts, and FilterService/filterPredicateEvaluation. Implement a stable public task-selection query DTO that does not leak UI-only FilterQuery state. Keep TaskNotes core as the owner of task query semantics.
|
||||
|
||||
Target outcome:
|
||||
- Add TaskNotesRuntimeTaskQuery, TaskNotesRuntimePredicate, canonical field/operator types, normalization, validation, and explain support.
|
||||
- Keep existing FilterQuery working internally and through a compatibility path where practical.
|
||||
- Make api.query.tasks(query) the canonical public selection API.
|
||||
- Keep api.tasks.list() as simple list/compatibility, but document the distinction.
|
||||
- Expose catalog metadata sufficient for companion-plugin query editors.
|
||||
- Preserve current TaskNotes filtering behavior by translating the public query DTO into the existing FilterService pipeline.
|
||||
- Do not introduce arbitrary JavaScript, Dataview, or a dependency on Obsidian Bases as the evaluator.
|
||||
- Update docs/javascript-api.md and focused unit tests.
|
||||
|
||||
Important boundaries:
|
||||
- Task mutations must continue through TaskService-backed runtime APIs.
|
||||
- Workflow/event triggers should only filter event payloads; vault-wide task selection belongs in explicit query calls.
|
||||
- The normalized query should be visible in query result or explain output so dry runs can be debugged.
|
||||
```
|
||||
|
||||
|
|
@ -34,8 +34,17 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
|
|||
|
||||
## Added
|
||||
|
||||
- **New companion plugin:** TaskNotes Workflows adds Markdown-defined automation for TaskNotes tasks, events, schedules, and manual commands. It keeps automation workflows outside TaskNotes core while using the runtime API for safe task reads, writes, queries, events, and relationship resolution. See [TaskNotes Workflows](https://tasknotes.dev/companion-plugins/tasknotes-workflows/).
|
||||
- (#1530) `Auto-start time tracking` starts a timer when a task moves into active work. Thanks to @Jendrik0 and @benmartinek for the request and follow-up.
|
||||
- (#222, #184) `Stop time tracking on complete` stops active timers when a task is completed. Thanks to @bcohen44 and @cathywu for the requests.
|
||||
- (#265) `Stamp started timestamp` records a custom started timestamp when a task first moves into active work. Thanks to @cathywu, @digitaldoctors, and @Leo310 for the request and discussion.
|
||||
- (#1703) `Clear scheduled when started` clears the scheduled date when a task moves into active work. Thanks to @kmaustral for the request.
|
||||
- (#1293) `Rollover overdue scheduled tasks` moves incomplete tasks with past scheduled dates back to today. Thanks to @grosserfrank for the request.
|
||||
- (#1896) `Escalate upcoming due tasks` raises priority as due dates approach. Thanks to @Neptune-Illusion for the request.
|
||||
- (#214, #518, #1785, #1855) The subtask inheritance workflows can copy parent contexts, tags, priority, planning dates, and dependencies into new subtasks. Thanks to @timfidd, @bepolymathe, @lindsaylearns, @poulpoulsen, and @misolex for the related requests and discussion.
|
||||
- (#1166) `Blocked task review` provides a reminder-style review for blocked tasks without automatically deleting dependency links. Thanks to @jpmoo and @jortscity for the request and follow-up.
|
||||
- Added a versioned TaskNotes JavaScript runtime API for companion plugins, with namespaced model validation, canonical task query validation/normalization/explain helpers, catalogs, query/support helpers, lifecycle events, typed errors/results, task, time-tracking, Pomodoro, recurring-task, settings, NLP, event, and extension-registry surfaces. Runtime API mutations carry source and correlation metadata so companion plugins can debug and coordinate workflow runs. See [JavaScript API](https://tasknotes.dev/javascript-api/).
|
||||
- Added documentation for the companion-plugin model and the TaskNotes Workflows companion plugin. See [Companion Plugins](https://tasknotes.dev/companion-plugins/) and [TaskNotes Workflows](https://tasknotes.dev/companion-plugins/tasknotes-workflows/).
|
||||
- Added documentation for the companion-plugin model. See [Companion Plugins](https://tasknotes.dev/companion-plugins/).
|
||||
- (#288, #345, #361, #523, #573, #703, #925, #929, #1115, #1137, #1260, #1303, #1324, #1394, #1445, #1509, #1735, #1736, #1743, #1780, #1874, #1951, #1974) Added support for TaskNotes spec 0.2.0 materialized occurrences, including recurrence parent/date fields, generated mdbase schema roles, parent reconciliation when occurrence notes are completed, occurrence notes that inherit parent planning metadata without copying history, occurrence note controls in task, calendar, and edit-modal completion menus, and visible occurrence identity on task cards. This gives recurring tasks a concrete occurrence-note path for per-instance state, related notes, subtasks, templates, scheduling changes, completion history, and calendar behavior without forcing every recurring task to create files. See [Recurring Tasks](https://tasknotes.dev/features/recurring-tasks/#materialized-occurrence-notes) and [Property Types Reference](https://tasknotes.dev/settings/property-types-reference/#materialized-occurrence-properties). Thanks to @LuxBetancourt, @luciolebrillante, @jhedlund, @cathywu, @Lorite, @EllenGYY, @JcMinarro, @gsssr, @3zra47, @Leonard-44, @ak-42, @RumiaKitinari, @atos2212-blip, @kmaustral, @eugenedefox, @notDavid, @zitongcharliedeng, and @Jomo94 for the related recurrence, occurrence, completion, and calendar requests.
|
||||
- (#1951) Added Calendar support for recurring tasks stretched between scheduled and due dates when the existing stretch option is enabled. Date-only ranges stay as all-day spans, and timed ranges render once per day in the range. Thanks to @atos2212-blip for the request.
|
||||
- (#1510, #1751, #1792, #1969) Added a Task List and Kanban view option to hide top-level subtasks when their parent task is also in the filtered view, while still allowing inherited expanded relationships to show the subtasks under the parent. Thanks to @Kickdak and @Glint-Eye for the requests, @inigourrestarazu for identifying the project-linked parent edge case, and @stanley-910 and @Spencerduran for the earlier PRs.
|
||||
|
|
|
|||
Loading…
Reference in a new issue