diff --git a/docs/migration-v4-to-v5.md b/docs/migration-v4-to-v5.md index 2e4715727..398f19cc7 100644 --- a/docs/migration-v4-to-v5.md +++ b/docs/migration-v4-to-v5.md @@ -53,7 +53,17 @@ Existing tasks without an `id` remain valid and are not changed. Path-based inte ## mdbase collections -If the [mdbase integration](settings/integrations.md#mdbase) is enabled, TaskNotes v5 publishes the portable `tasknotes.task` contract (version 0.3.0-rc.5) used by TaskNotes App and other compatible tools. It is the same contract, byte for byte, that TaskNotes App installs, so the plugin and the app can share a collection. Tasks can list assignees as links to person notes; the property is optional. +If the [mdbase integration](settings/integrations.md#mdbase) is enabled, TaskNotes v5 publishes the portable `tasknotes.task` contract (version 0.3.0-rc.6) used by TaskNotes App and other compatible tools. It is the same contract, byte for byte, that TaskNotes App installs, so the plugin and the app can share a collection. Tasks can list assignees as links to person notes; the property is optional. + +### TaskNotes settings record + +Collection-wide TaskNotes behaviour is stored in `_settings/dev.tasknotes.md`, a settings record that TaskNotes App reads and writes too. It holds natural-language capture triggers, body templates for new tasks and occurrences, whether archiving moves the file and where to, whether completing a task stops its timer, whether recurring tasks uncheck body checkboxes, the link format written to frontmatter, and per-status next status, cycle exclusion and auto-archive settings. Everything else, such as property mappings, statuses, priorities and filename rules, stays in the task type. + +TaskNotes creates the record when it upgrades a collection: from your saved settings for a v4 collection, or from the earlier task type for a 5.0 beta or TaskNotes App collection. This happens in the same backed-up step as the task type upgrade. If the record already exists, TaskNotes keeps every setting it holds and adds only the ones it lacks. A setting missing from the record is read from an earlier task type that still holds it, otherwise from your local settings, so a missing or late-syncing record never loses a value. + +Editing the record changes these settings in TaskNotes; changing them in TaskNotes writes only the changed fields to the record. Other fields and the note body are kept. If both changed, TaskNotes asks which to keep. A record written by a newer settings version is used as written and not changed. + +If another file in `_settings/` also claims to be the TaskNotes collection settings record (`app: dev.tasknotes` and `scope: collection`), such as a sync conflict copy named `dev.tasknotes (conflicted copy).md`, TaskNotes shows a notice naming those files. It keeps using `_settings/dev.tasknotes.md` and does not change, merge or delete the others. Copy any settings you want to keep into `_settings/dev.tasknotes.md`, then delete the other files. When the collection's only active type is an unmodified TaskNotes-generated v0.2 type, TaskNotes upgrades the metadata to mdbase v0.3 automatically. Generated formats from 4.3.2 through 4.13.7 are recognized using their historical writers and your saved settings, including BOM/CRLF formatting and reordered YAML mapping keys. Exact historical output containing v4's invalid unquoted status values can also be regenerated safely from those settings. The original bytes of `mdbase.yaml`, the task type and replaced support resources are kept under `.tasknotes/migrations/`; task notes are never rewritten. @@ -63,11 +73,11 @@ Incomplete synced metadata is not treated as a new collection. If type or suppor Additional v0.2 types or genuinely edited task/support definitions are preserved for review. The notice names the affected file. Back up the vault, finish syncing and restart first. If still blocked, restore the unmodified generated type **and matching saved settings** from a known-good backup, or use mdbase to migrate a separate copy and review all type definitions before replacing the active metadata. Do not delete a user-maintained type merely to make ownership recognition succeed. Known generated support resources tolerate line-ending changes; custom commentary or schema edits are not silently overwritten. -If you used a 5.0 beta, TaskNotes updates its task type to the current contract when the vault opens and tells you once. The previous type is backed up under `.tasknotes/migrations/`. The upgrade keeps existing property-role mappings (including custom stable IDs), binding policies, archive tags, occurrence horizons, custom schema constraints, extensions and Markdown body. It lifts the contract version and adds optional assignee support without rebuilding these policies from plugin defaults. Ordinary settings changes update only plugin-owned options. Text, number, date, boolean and list custom properties retain their declarations through migration and later settings saves, including legacy coercion-compatible schemas. An owned property whose type cannot be resolved stops settings import with a named diagnostic instead of disappearing. A leading UTF-8 BOM and CRLF line endings are accepted for canonical metadata as well as historical metadata; recovery copies retain the original bytes. Task files are not changed. Collections that TaskNotes App has already updated retain their task definitions. If a beta added a second task type named `tasknotes-task` after TaskNotes App updated the collection, TaskNotes moves that duplicate into a unique folder under `.tasknotes/migrations/`, preserving its relative path and actual bytes. Explicit record membership keeps the provider in place with a notice naming the type and record. A concurrent edit is preserved and blocks cleanup; task references are never rewritten. +If you used a 5.0 beta, TaskNotes updates its task type to the current contract when the vault opens and tells you once. The previous type is backed up under `.tasknotes/migrations/`. The upgrade keeps existing property-role mappings (including custom stable IDs), binding policies, archive tags, occurrence horizons, custom schema constraints, extensions and Markdown body. It lifts the contract version and adds optional assignee support without rebuilding these policies from plugin defaults. App behaviour settings move from the type to the [settings record](#tasknotes-settings-record) unchanged. Ordinary settings changes update only plugin-owned options. Text, number, date, boolean and list custom properties retain their declarations through migration and later settings saves, including legacy coercion-compatible schemas. An owned property whose type cannot be resolved stops settings import with a named diagnostic instead of disappearing. A leading UTF-8 BOM and CRLF line endings are accepted for canonical metadata as well as historical metadata; recovery copies retain the original bytes. Task files are not changed. Collections that TaskNotes App has already updated retain their task definitions. If a beta added a second task type named `tasknotes-task` after TaskNotes App updated the collection, TaskNotes moves that duplicate into a unique folder under `.tasknotes/migrations/`, preserving its relative path and actual bytes. Explicit record membership keeps the provider in place with a notice naming the type and record. A concurrent edit is preserved and blocks cleanup; task references are never rewritten. ### Interrupted updates and blocked collections -Both v0.2 migrations and beta metadata upgrades are backed up and journaled. Metadata is staged in sibling temporary files and read back. On desktop and mobile, each replacement first moves the actual active revision into a unique recovery path under `.tasknotes/migrations/metadata-swaps/` and compares those moved bytes with the expected original. The verified stage is published only after the active path is empty. When the adapter exposes a vault filesystem path and Node filesystem access, publication uses an atomic no-replace operation even when Obsidian emulates mobile. A late arrival remains active and blocks the update. On non-native storage, automatic migration remains enabled using the adapter's empty-destination move. A verified publication copy is moved while the intended stage, displaced original and journal are retained until active-byte read-back passes. If read-back differs from the intended stage, TaskNotes reports a conflict and keeps that evidence instead of reporting success. A changed revision captured at displacement is retained, restored to the active path if still empty, and reported as a conflict. Native files and containing directories are flushed where supported. The type is updated before activating its new contract, and configuration is committed last. A write failure restores unchanged original metadata. Restarting TaskNotes first recovers interrupted swaps, including an empty active path with its moved original present, then recovers the enclosing transaction before attempting another upgrade. +Both v0.2 migrations and beta metadata upgrades are backed up and journaled. Metadata is staged in sibling temporary files and read back. On desktop and mobile, each replacement first moves the actual active revision into a unique recovery path under `.tasknotes/migrations/metadata-swaps/` and compares those moved bytes with the expected original. The verified stage is published only after the active path is empty. When the adapter exposes a vault filesystem path and Node filesystem access, publication uses an atomic no-replace operation even when Obsidian emulates mobile. A late arrival remains active and blocks the update. On non-native storage, automatic migration remains enabled using the adapter's empty-destination move. A verified publication copy is moved while the intended stage, displaced original and journal are retained until active-byte read-back passes. If read-back differs from the intended stage, TaskNotes reports a conflict and keeps that evidence instead of reporting success. A changed revision captured at displacement is retained, restored to the active path if still empty, and reported as a conflict. Native files and containing directories are flushed where supported. The type is updated before activating its new contract, and configuration is committed last. A write failure restores unchanged original metadata and removes the files the update created, such as new contracts, schemas, the settings type and the settings record, along with its temporary staging files. A created file is removed only while it still holds exactly what TaskNotes wrote; a file edited since is kept. Restarting TaskNotes first recovers interrupted swaps, including an empty active path with its moved original present, then recovers the enclosing transaction, including the same cleanup, before attempting another upgrade. On iOS/Android adapters that internally check whether the destination exists and then rename, there is still a residual window between those steps. A revision arriving in that window can be overwritten; read-back cannot detect it if the final active bytes equal the intended stage. This adapter path is not an atomic no-replace guarantee. Finish syncing before upgrading, avoid simultaneous metadata updates on other devices, and retain a separate full-vault backup. See [Backup and recovery](guides/backup-recovery.md#interrupted-mdbase-metadata-updates). diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 121110a03..086db2122 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -31,3 +31,14 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l ``` --> + +## Changed + +- With the mdbase integration enabled, collection-wide app behaviour (natural-language triggers, task and occurrence templates, archive moves, stopping timers on completion, resetting checkboxes on recurrence, the frontmatter link format, and per-status next status, cycle exclusion and auto-archive) is now stored in the TaskNotes settings record, `_settings/dev.tasknotes.md`, which TaskNotes App also uses. The task type moves to the `tasknotes.task` 0.3.0-rc.6 contract, which no longer holds these settings. Upgrading a collection creates the record from your saved settings or the earlier task type in the same backed-up step; an existing record keeps its values and gains only missing ones. See [TaskNotes settings record](https://tasknotes.dev/migration-v4-to-v5/#tasknotes-settings-record). + +## Fixed + +- With the mdbase integration enabled, the first change to a setting stored in the TaskNotes settings record after upgrading a 4.x collection (for example turning off "Move archived tasks to folder" or changing the archive folder) is saved to `_settings/dev.tasknotes.md`. Previously it was kept only until Obsidian restarted. +- With the mdbase integration enabled, TaskNotes shows a notice naming any other `_settings/` file that also claims to be its settings record, such as a sync conflict copy of `_settings/dev.tasknotes.md`. It keeps using `_settings/dev.tasknotes.md` and leaves the other files unchanged. See [TaskNotes settings record](https://tasknotes.dev/migration-v4-to-v5/#tasknotes-settings-record). +- With "Use Markdown links in frontmatter" on, TaskNotes now writes Markdown links even when Obsidian's own "Use [[Wikilinks]]" preference is on. This covers the project picker, dependencies, projects typed in natural-language input (task modal, inline conversion, API, MCP and CLI), the parent-note project default and the Bases current-file default. +- A failed or interrupted mdbase metadata update now removes the files it created (contracts, schemas, the settings type and settings record) and its temporary `.tasknotes-stage-` files, as long as they still hold exactly what TaskNotes wrote. Edited files are kept. diff --git a/docs/settings/integrations.md b/docs/settings/integrations.md index 3dd4dc669..2c5523f28 100644 --- a/docs/settings/integrations.md +++ b/docs/settings/integrations.md @@ -5,7 +5,7 @@ These settings control the integration with other plugins and services, such as ## mdbase -Enable the mdbase integration to publish TaskNotes' portable task type, data contract, and schemas for compatible applications. The generated type reflects TaskNotes field mappings, statuses, priorities, filename rules, recurrence behavior, reminders, and user-defined fields. +Enable the mdbase integration to publish TaskNotes' portable task type, data contract, and schemas for compatible applications. The generated type reflects TaskNotes field mappings, statuses, priorities, filename rules, recurrence behavior, reminders, and user-defined fields. App behaviour, such as templates, archive moves, status cycling and natural-language triggers, is kept in the TaskNotes settings record, `_settings/dev.tasknotes.md`, which TaskNotes App shares. See [TaskNotes settings record](../migration-v4-to-v5.md#tasknotes-settings-record). When TaskNotes v5 opens a collection whose only active mdbase type is an unmodified file generated by TaskNotes v4, it automatically upgrades that metadata from mdbase v0.2 to v0.3. Task notes are not rewritten, and custom collection settings are preserved. TaskNotes keeps the previous `mdbase.yaml` and task type under `.tasknotes/migrations/`, generates a legacy-compatible v0.3 type from the retained plugin settings, and restores the v0.2 files if the upgrade cannot be completed. A durable migration record allows interrupted upgrades to recover on the next startup without overwriting concurrent edits. Collections with additional, modified, or hand-maintained types are left unchanged. diff --git a/i18n.manifest.json b/i18n.manifest.json index 6e658be27..30de15041 100644 --- a/i18n.manifest.json +++ b/i18n.manifest.json @@ -21,6 +21,7 @@ "mdbaseSafety.settingsSaved": "59932dd7c85b336da1e70963497097a0f25cc3eb", "mdbaseSafety.blocked": "3610d6309b9c6bf60a6eaf52fbfab285b29e9a88", "mdbaseSafety.multiple": "8e9edf0677fbb6a0c5c63cbfdb84229bfcde27c6", + "mdbaseSafety.competingSettings": "a4a3430b0d8cd973be5a34b2592a68b9c3a5a537", "mdbaseSafety.referenced": "8845207c5dd3bd6ee4a7cb39f6614a58355ba625", "onboarding.title": "f20c32070dc7d3d6880ac085bd73bf066a0eb5bf", "onboarding.intro": "39651d1dbcc95a937c9b0c9b702c2d4aa987959b", @@ -1535,7 +1536,7 @@ "settings.integrations.mdbaseSpec.header": "3122fe68b2d29d2b5955d17b071ff98a8869ec65", "settings.integrations.mdbaseSpec.learnMore": "698859ca41e7c55409c85ecc3e624db48e036ee3", "settings.integrations.mdbaseSpec.enable.name": "dbfee159de77e775f50108e2f21550cd8ee024e8", - "settings.integrations.mdbaseSpec.enable.description": "a660038515638e51bbf1025c8802f704353f86e2", + "settings.integrations.mdbaseSpec.enable.description": "1af5405c6f2ae1aa866e8f299f5eb5b6214b4223", "settings.integrations.timeFormats.justNow": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", "settings.integrations.timeFormats.minutesAgo": "05a31e5617ba90e7dcf1aa3e1124a0d1d40983c7", "settings.integrations.timeFormats.hoursAgo": "e41f72e387f7a43ddab945f3a6dd0d1ae0b14fa5", diff --git a/i18n.state.json b/i18n.state.json index 3b5e300b0..87760c657 100644 --- a/i18n.state.json +++ b/i18n.state.json @@ -88,6 +88,10 @@ "source": "8e9edf0677fbb6a0c5c63cbfdb84229bfcde27c6", "translation": "eb6239ca1cc643edc09cc1e4546cec3881d1dde9" }, + "mdbaseSafety.competingSettings": { + "source": "a4a3430b0d8cd973be5a34b2592a68b9c3a5a537", + "translation": "66488ca614f66eafb258ae1d99288caf7d2b0dbb" + }, "mdbaseSafety.referenced": { "source": "8845207c5dd3bd6ee4a7cb39f6614a58355ba625", "translation": "9b0e5ce455a2b847585645409b85ff48faac5610" @@ -6145,8 +6149,8 @@ "translation": "d7a9bd1fd3a0cb1577977d9475bac8449ebfdfc3" }, "settings.integrations.mdbaseSpec.enable.description": { - "source": "a660038515638e51bbf1025c8802f704353f86e2", - "translation": "2ba021e20f62fa3df56387bf1481781d3d06704e" + "source": "1af5405c6f2ae1aa866e8f299f5eb5b6214b4223", + "translation": "cc20329f0435643ab7d0e592f885022486d89474" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -10010,6 +10014,10 @@ "source": "8e9edf0677fbb6a0c5c63cbfdb84229bfcde27c6", "translation": "7ce3bcac9baf2a72aa09b2e9bf9c9e58d2e45139" }, + "mdbaseSafety.competingSettings": { + "source": "a4a3430b0d8cd973be5a34b2592a68b9c3a5a537", + "translation": "f9b06951bdd9cdabc87c6e752190cd6b8bccf155" + }, "mdbaseSafety.referenced": { "source": "8845207c5dd3bd6ee4a7cb39f6614a58355ba625", "translation": "56133ce1a84f987a1508064546b9997ab984dc91" @@ -16067,8 +16075,8 @@ "translation": "fe19c12c29ebf468eaf24572b10644f8971d6cf6" }, "settings.integrations.mdbaseSpec.enable.description": { - "source": "a660038515638e51bbf1025c8802f704353f86e2", - "translation": "e1c49cda2640b3defe802b2fbdf4acc390a19416" + "source": "1af5405c6f2ae1aa866e8f299f5eb5b6214b4223", + "translation": "2de4ec7bf162126aa6a6c31d4efb82656511aef0" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -19932,6 +19940,10 @@ "source": "8e9edf0677fbb6a0c5c63cbfdb84229bfcde27c6", "translation": "347df4c3274ff5f491b2a80c3a0e4f161e728454" }, + "mdbaseSafety.competingSettings": { + "source": "a4a3430b0d8cd973be5a34b2592a68b9c3a5a537", + "translation": "bbceb7796336ebf930d812935b5ecb0c91d2bf0c" + }, "mdbaseSafety.referenced": { "source": "8845207c5dd3bd6ee4a7cb39f6614a58355ba625", "translation": "74ca7c486ba7c678626a8990c399627d24bf4ca7" @@ -25989,8 +26001,8 @@ "translation": "5b64eaf0ca3cb62ecb7dc6629d5ee07d61b768b6" }, "settings.integrations.mdbaseSpec.enable.description": { - "source": "a660038515638e51bbf1025c8802f704353f86e2", - "translation": "81eeb267eee4966985531c76f609ca14725e041d" + "source": "1af5405c6f2ae1aa866e8f299f5eb5b6214b4223", + "translation": "461d2ecf504e20ef97363d0e4e5f09fe014d9713" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -29854,6 +29866,10 @@ "source": "8e9edf0677fbb6a0c5c63cbfdb84229bfcde27c6", "translation": "322144a9f03ba03c661c19ecabdee7ed5d4ee361" }, + "mdbaseSafety.competingSettings": { + "source": "a4a3430b0d8cd973be5a34b2592a68b9c3a5a537", + "translation": "15174a49fb4c9548d5a3e88461d6c2aba66bc585" + }, "mdbaseSafety.referenced": { "source": "8845207c5dd3bd6ee4a7cb39f6614a58355ba625", "translation": "b3b97089bbaed3499900a9f2f214ab2f9b96b9be" @@ -35911,8 +35927,8 @@ "translation": "5128d782b03833e9790337765d2873b51fb19903" }, "settings.integrations.mdbaseSpec.enable.description": { - "source": "a660038515638e51bbf1025c8802f704353f86e2", - "translation": "bcb41fc1c0caca9834920139bfc74b3b004d588e" + "source": "1af5405c6f2ae1aa866e8f299f5eb5b6214b4223", + "translation": "f6269d7d2373e8b44f22e5ec3f97e0f0830a4644" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -39776,6 +39792,10 @@ "source": "8e9edf0677fbb6a0c5c63cbfdb84229bfcde27c6", "translation": "05e52e84051a32c7c81de684bac7133d83eceba6" }, + "mdbaseSafety.competingSettings": { + "source": "a4a3430b0d8cd973be5a34b2592a68b9c3a5a537", + "translation": "8c95febede911e41637ed23157290ac8becb0179" + }, "mdbaseSafety.referenced": { "source": "8845207c5dd3bd6ee4a7cb39f6614a58355ba625", "translation": "1351b1937e47aa8a6ce94e65a3ccb72d9731fac6" @@ -45833,8 +45853,8 @@ "translation": "3455042469a99be681088e3f88194c164dc9d09c" }, "settings.integrations.mdbaseSpec.enable.description": { - "source": "a660038515638e51bbf1025c8802f704353f86e2", - "translation": "7532fdc85dac407f3381734166f9b9186280eab9" + "source": "1af5405c6f2ae1aa866e8f299f5eb5b6214b4223", + "translation": "b89ebdfdb7ac9c08e77e07e82c12d846fb783d3a" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -49698,6 +49718,10 @@ "source": "8e9edf0677fbb6a0c5c63cbfdb84229bfcde27c6", "translation": "9512633d2afdff571fa5102f91c8dea84ea1424b" }, + "mdbaseSafety.competingSettings": { + "source": "a4a3430b0d8cd973be5a34b2592a68b9c3a5a537", + "translation": "1c3864114f5f55ea5a24a4406ddc1f2bd112a465" + }, "mdbaseSafety.referenced": { "source": "8845207c5dd3bd6ee4a7cb39f6614a58355ba625", "translation": "a34a898534569eb1fcffc8731ee9a2b3ee952d24" @@ -55755,8 +55779,8 @@ "translation": "12c9d21a142d3cab94b48c5d5f97275b4113078a" }, "settings.integrations.mdbaseSpec.enable.description": { - "source": "a660038515638e51bbf1025c8802f704353f86e2", - "translation": "e4d64f809785ce12738df53ba3bc4dcab9abafa0" + "source": "1af5405c6f2ae1aa866e8f299f5eb5b6214b4223", + "translation": "0c4bc95dd34e94c8b2bc48b4be8f868540fe5d8e" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -59620,6 +59644,10 @@ "source": "8e9edf0677fbb6a0c5c63cbfdb84229bfcde27c6", "translation": "940d5edab0f3b77c95b91e4140c2d9fb0416b6ae" }, + "mdbaseSafety.competingSettings": { + "source": "a4a3430b0d8cd973be5a34b2592a68b9c3a5a537", + "translation": "64e9d0b4e190e4eee7630083efd2ac0394512fc0" + }, "mdbaseSafety.referenced": { "source": "8845207c5dd3bd6ee4a7cb39f6614a58355ba625", "translation": "ac3aa3dff10c3f7b236af2e91607f8051f46b375" @@ -65677,8 +65705,8 @@ "translation": "1d0ba8c6a60a6939c5ada08d30fd767a0a69e4c8" }, "settings.integrations.mdbaseSpec.enable.description": { - "source": "a660038515638e51bbf1025c8802f704353f86e2", - "translation": "e5df71864b06bc5e9817cd3660d3421b3aad07cf" + "source": "1af5405c6f2ae1aa866e8f299f5eb5b6214b4223", + "translation": "dacee1cf9e8c1ca1404db11fb64928452d87052b" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", @@ -69542,6 +69570,10 @@ "source": "8e9edf0677fbb6a0c5c63cbfdb84229bfcde27c6", "translation": "ca89ecfe5b20cb2b20fef2a14fb77278d6d4028f" }, + "mdbaseSafety.competingSettings": { + "source": "a4a3430b0d8cd973be5a34b2592a68b9c3a5a537", + "translation": "aabfa44683bcca2a24d1e221b58dc8e285a363a7" + }, "mdbaseSafety.referenced": { "source": "8845207c5dd3bd6ee4a7cb39f6614a58355ba625", "translation": "d9b9e09b92466f0372e3dd5fa044fc3f79b9e709" @@ -75599,8 +75631,8 @@ "translation": "6641fa1b97f35683e8ca3844ca102e4d252e22bf" }, "settings.integrations.mdbaseSpec.enable.description": { - "source": "a660038515638e51bbf1025c8802f704353f86e2", - "translation": "a23f9049f9ede8c6c31dd5e196045566db04c916" + "source": "1af5405c6f2ae1aa866e8f299f5eb5b6214b4223", + "translation": "550f6c5080483081d802ab40eed51ebbc43274f5" }, "settings.integrations.timeFormats.justNow": { "source": "a69f482dfa32327d1f3e8bd7d5ebf8aaf5c7e0aa", diff --git a/package-lock.json b/package-lock.json index 89c661016..33ffacbed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@fullcalendar/multimonth": "^6.1.17", "@fullcalendar/timegrid": "^6.1.17", "@modelcontextprotocol/sdk": "1.32.1", - "@tasknotes/model": "0.3.0-rc.16", + "@tasknotes/model": "0.3.0-rc.20", "ajv": "8.20.0", "ajv-formats": "3.0.1", "chrono-node": "^2.7.5", @@ -2791,9 +2791,9 @@ } }, "node_modules/@tasknotes/model": { - "version": "0.3.0-rc.16", - "resolved": "https://registry.npmjs.org/@tasknotes/model/-/model-0.3.0-rc.16.tgz", - "integrity": "sha512-VHwbTcvfyDaUK0uh3HmP4UddikROm58S8ls9C73q5vicY/Fdy8Pt8Hxj8XLNmG6tvOWDEABSCBT0HGz0E2Ntjg==", + "version": "0.3.0-rc.20", + "resolved": "https://registry.npmjs.org/@tasknotes/model/-/model-0.3.0-rc.20.tgz", + "integrity": "sha512-Id8bJjU0HhBJAqxJLkJOhtkWjq7MV1JWBm/q0yeTeQLiDw05jctceheh9xiM1hJ86PiH6Qu6E+MVwuxNfUnPJQ==", "license": "MIT", "dependencies": { "rrule": "^2.8.1", diff --git a/package.json b/package.json index 60cc57878..be34903e1 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "@fullcalendar/multimonth": "^6.1.17", "@fullcalendar/timegrid": "^6.1.17", "@modelcontextprotocol/sdk": "1.32.1", - "@tasknotes/model": "0.3.0-rc.16", + "@tasknotes/model": "0.3.0-rc.20", "ajv": "8.20.0", "ajv-formats": "3.0.1", "chrono-node": "^2.7.5", diff --git a/scripts/verify-tasknotes-pack.mjs b/scripts/verify-tasknotes-pack.mjs index 5670ed182..8cb8a97b3 100644 --- a/scripts/verify-tasknotes-pack.mjs +++ b/scripts/verify-tasknotes-pack.mjs @@ -2,11 +2,11 @@ import { createHash } from 'node:crypto'; import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const root = new URL('../', import.meta.url); -const text = readFileSync(new URL('vendor/mdbase-contracts/tasknotes.task-0.3.0-rc.17.json', root), 'utf8'); +const text = readFileSync(new URL('vendor/mdbase-contracts/tasknotes.task-0.3.0-rc.19.json', root), 'utf8'); const digest = createHash('sha256').update(text).digest('hex'); -if (digest !== 'aa03bd096f62868b4b414e753cd01024d33d8cd193c902f3cb0f3526c3149e8e') throw new Error('TaskNotes canonical pack integrity mismatch'); +if (digest !== 'ca9ac8ad5c90c7d9a19be226153a32b619b9156f83f95c38ad7ad2346e094b63') throw new Error('TaskNotes canonical pack integrity mismatch'); const pack = JSON.parse(text); -if (pack.manifest.id !== 'tasknotes.task' || pack.manifest.version !== '0.3.0-rc.17') throw new Error('Wrong TaskNotes pack'); +if (pack.manifest.id !== 'tasknotes.task' || pack.manifest.version !== '0.3.0-rc.19') throw new Error('Wrong TaskNotes pack'); const output = '// Generated by scripts/verify-tasknotes-pack.mjs; do not edit.\nexport const canonicalTaskNotesResources = ' + JSON.stringify(pack.resources, null, '\t') + ';\n'; const destination = fileURLToPath(new URL('src/services/canonicalTaskNotesPack.ts', root)); if (process.argv.includes('--check')) { diff --git a/src/bases/BasesViewBase.ts b/src/bases/BasesViewBase.ts index 88d4b0406..12d01480a 100644 --- a/src/bases/BasesViewBase.ts +++ b/src/bases/BasesViewBase.ts @@ -512,7 +512,8 @@ export abstract class BasesViewBase extends Component { fieldMapper: this.plugin.fieldMapper, taskTag: this.plugin.settings.taskTag, userFields: this.plugin.settings.userFields || [], - currentFileLink: () => getBasesCurrentFileLinkDefault(app), + currentFileLink: () => + getBasesCurrentFileLinkDefault(app, this.plugin.settings.useFrontmatterMarkdownLinks), frontmatterProcessor, }); diff --git a/src/bases/basesCreateFileForView.ts b/src/bases/basesCreateFileForView.ts index b13756e42..bcdcc66d5 100644 --- a/src/bases/basesCreateFileForView.ts +++ b/src/bases/basesCreateFileForView.ts @@ -1,4 +1,5 @@ import type { App } from "obsidian"; +import { generateLink } from "../utils/linkUtils"; import type { UserMappedField } from "../types/settings"; import { buildTaskCreationDataFromFrontmatter, @@ -18,11 +19,6 @@ export type BuildBasesTaskCreationDataForViewOptions = { frontmatterProcessor?: BasesFrontmatterProcessor; }; -type ActiveFileLike = { - extension?: string; - path: string; -}; - export function buildBasesTaskCreationDataForView({ config, fieldMapper, @@ -44,11 +40,11 @@ export function buildBasesTaskCreationDataForView({ return buildTaskCreationDataFromFrontmatter(frontmatter, fieldMapper, userFields); } -export function getBasesCurrentFileLinkDefault(app: App): string | null { - const activeFile = app.workspace.getActiveFile() as ActiveFileLike | null; +export function getBasesCurrentFileLinkDefault(app: App, useMarkdownLinks?: boolean): string | null { + const activeFile = app.workspace.getActiveFile(); if (!activeFile || activeFile.extension === "base") { return null; } - return app.fileManager.generateMarkdownLink(activeFile as never, activeFile.path); + return generateLink(app, activeFile, activeFile.path, "", "", useMarkdownLinks); } diff --git a/src/i18n/resources/de.ts b/src/i18n/resources/de.ts index c7f5f3702..8d18a869b 100644 --- a/src/i18n/resources/de.ts +++ b/src/i18n/resources/de.ts @@ -39,6 +39,8 @@ export const de: TranslationTree = { "TaskNotes hat die Aktualisierung der mdbase-Metadaten gestoppt. {details} Prüfen Sie die angegebene Datei und die Sicherungskopien, beheben Sie den Konflikt oder die Zugriffsrechte und laden Sie TaskNotes neu. Aufgabendateien wurden nicht geändert.", multiple: "Mehrere TaskNotes-Typen: {paths}. Behalten Sie einen kanonischen Anbieter oder verschieben Sie die anderen aus dem Typenordner und laden Sie TaskNotes neu. TaskNotes hat die letzte gültige Konfiguration beibehalten.", + competingSettings: + "Weitere TaskNotes-Einstellungsdatensätze gefunden: {paths}. TaskNotes verwendet {record} und ignoriert die anderen. Übernehmen Sie die Einstellungen, die Sie behalten möchten, in {record} und löschen Sie dann die anderen Dateien.", referenced: "TaskNotes hat {typePath} beibehalten: {recordPath} verweist ausdrücklich auf diesen Typ. Prüfen Sie die Typzugehörigkeit, bevor Sie den Anbieter verschieben; Aufgabendateien wurden nicht geändert.", }, @@ -2690,7 +2692,7 @@ export const de: TranslationTree = { enable: { name: "Kanonische mdbase-Konfiguration verwenden", description: - "Veröffentlicht den TaskNotes-Datenvertrag und macht den implementierenden mdbase-Typ zur portablen maßgeblichen Quelle. Dateiänderungen aktualisieren TaskNotes, und Änderungen an portablen Einstellungen aktualisieren die Typbindung.", + "Veröffentlicht den TaskNotes-Datenvertrag und macht den implementierenden mdbase-Typ zusammen mit dem TaskNotes-Einstellungsdatensatz zur portablen maßgeblichen Quelle. Dateiänderungen aktualisieren TaskNotes. Einstellungsänderungen aktualisieren die Typbindung oder, für App-Verhalten wie Vorlagen, Archivverschiebungen und Statuswechsel, den Einstellungsdatensatz.", }, }, timeFormats: { diff --git a/src/i18n/resources/en.ts b/src/i18n/resources/en.ts index 15c693539..9969de30e 100644 --- a/src/i18n/resources/en.ts +++ b/src/i18n/resources/en.ts @@ -28,6 +28,8 @@ export const en: TranslationTree = { settingsSaved: "TaskNotes saved your settings, but could not synchronize the canonical mdbase type.", blocked: "TaskNotes stopped the mdbase metadata update. {details} Review the listed file and recovery copies, resolve the conflict or permissions, then reload TaskNotes. Task files were not changed.", multiple: "Multiple TaskNotes types: {paths}. Keep one canonical provider or move the others out of the types folder, then reload TaskNotes. TaskNotes kept its last-known-good configuration.", + competingSettings: + "Other TaskNotes settings records found: {paths}. TaskNotes is using {record} and ignoring the others. Copy any settings you want to keep into {record}, then delete the other files.", referenced: "TaskNotes kept {typePath}: {recordPath} explicitly references this type. Review its membership before moving the provider; task files were not changed.", }, onboarding: { @@ -2593,7 +2595,7 @@ export const en: TranslationTree = { enable: { name: "Use canonical mdbase configuration", description: - "Publish the TaskNotes data contract and make its implementing mdbase type the portable source of truth. File edits update TaskNotes, and portable settings changes update the type binding.", + "Publish the TaskNotes data contract and make its implementing mdbase type, with the TaskNotes settings record, the portable source of truth. File edits update TaskNotes. Settings changes update the type binding, or the settings record for app behaviour such as templates, archive moves and status cycling.", }, }, timeFormats: { diff --git a/src/i18n/resources/es.ts b/src/i18n/resources/es.ts index 1bd49bbbd..3c0aa3d04 100644 --- a/src/i18n/resources/es.ts +++ b/src/i18n/resources/es.ts @@ -39,6 +39,8 @@ export const es: TranslationTree = { "TaskNotes detuvo la actualización de los metadatos de mdbase. {details} Revise el archivo indicado y las copias de recuperación, resuelva el conflicto o los permisos y vuelva a cargar TaskNotes. Los archivos de tareas no se modificaron.", multiple: "Varios tipos de TaskNotes: {paths}. Conserve un proveedor canónico o mueva los demás fuera de la carpeta de tipos y vuelva a cargar TaskNotes. TaskNotes conservó la última configuración válida.", + competingSettings: + "Se encontraron otros registros de ajustes de TaskNotes: {paths}. TaskNotes usa {record} e ignora los demás. Copie en {record} los ajustes que quiera conservar y después elimine los otros archivos.", referenced: "TaskNotes conservó {typePath}: {recordPath} hace referencia explícita a este tipo. Revise su pertenencia antes de mover el proveedor; los archivos de tareas no se modificaron.", }, @@ -2697,7 +2699,7 @@ export const es: TranslationTree = { enable: { name: "Usar la configuración canónica de mdbase", description: - "Publica el contrato de datos de TaskNotes y convierte el tipo de mdbase que lo implementa en la fuente de referencia portátil. Las ediciones de archivos actualizan TaskNotes y los cambios en la configuración portátil actualizan la vinculación del tipo.", + "Publica el contrato de datos de TaskNotes y convierte su tipo mdbase implementador, junto con el registro de ajustes de TaskNotes, en la fuente de verdad portátil. Las ediciones de archivos actualizan TaskNotes. Los cambios de ajustes actualizan la vinculación del tipo o, para el comportamiento de la app como plantillas, traslados al archivo y ciclo de estados, el registro de ajustes.", }, }, timeFormats: { diff --git a/src/i18n/resources/fr.ts b/src/i18n/resources/fr.ts index e44277153..b8d157f9f 100644 --- a/src/i18n/resources/fr.ts +++ b/src/i18n/resources/fr.ts @@ -39,6 +39,8 @@ export const fr: TranslationTree = { "TaskNotes a arrêté la mise à jour des métadonnées mdbase. {details} Vérifiez le fichier indiqué et les copies de récupération, résolvez le conflit ou les permissions, puis rechargez TaskNotes. Les fichiers de tâches n’ont pas été modifiés.", multiple: "Plusieurs types TaskNotes : {paths}. Gardez un fournisseur canonique ou déplacez les autres hors du dossier des types, puis rechargez TaskNotes. TaskNotes a conservé la dernière configuration valide.", + competingSettings: + "Autres enregistrements de paramètres TaskNotes trouvés : {paths}. TaskNotes utilise {record} et ignore les autres. Copiez dans {record} les paramètres que vous souhaitez conserver, puis supprimez les autres fichiers.", referenced: "TaskNotes a conservé {typePath} : {recordPath} fait explicitement référence à ce type. Vérifiez son appartenance avant de déplacer le fournisseur ; les fichiers de tâches n’ont pas été modifiés.", }, @@ -2704,7 +2706,7 @@ export const fr: TranslationTree = { enable: { name: "Utiliser la configuration mdbase canonique", description: - "Publie le contrat de données TaskNotes et fait du type mdbase qui l’implémente la source de référence portable. Les modifications de fichiers mettent TaskNotes à jour, et les changements des paramètres portables mettent à jour la liaison du type.", + "Publie le contrat de données TaskNotes et fait de son type mdbase d’implémentation, avec l’enregistrement de paramètres TaskNotes, la source de vérité portable. Les modifications de fichiers mettent à jour TaskNotes. Les changements de paramètres mettent à jour la liaison du type ou, pour le comportement de l’application comme les modèles, les déplacements vers l’archive et le cycle des statuts, l’enregistrement de paramètres.", }, }, timeFormats: { diff --git a/src/i18n/resources/ja.ts b/src/i18n/resources/ja.ts index 3fd05a712..87d4363bb 100644 --- a/src/i18n/resources/ja.ts +++ b/src/i18n/resources/ja.ts @@ -39,6 +39,8 @@ export const ja: TranslationTree = { "TaskNotes は mdbase メタデータの更新を停止しました。{details} 表示されたファイルと復旧用コピーを確認し、競合またはアクセス権を解決してから TaskNotes を再読み込みしてください。タスクファイルは変更されていません。", multiple: "TaskNotes の型が複数あります: {paths}。正規のプロバイダーを1つ残すか、他の型を型フォルダーの外へ移動してから TaskNotes を再読み込みしてください。TaskNotes は最後に確認された有効な設定を保持しました。", + competingSettings: + "TaskNotes の設定レコードが他にも見つかりました: {paths}。TaskNotes は {record} を使用し、他のレコードは無視します。残したい設定を {record} にコピーしてから、他のファイルを削除してください。", referenced: "TaskNotes は {typePath} を保持しました。{recordPath} がこの型を明示的に参照しています。プロバイダーを移動する前に型への所属を確認してください。タスクファイルは変更されていません。", }, @@ -2627,7 +2629,7 @@ export const ja: TranslationTree = { enable: { name: "正規のmdbase設定を使用", description: - "TaskNotesのデータ契約を公開し、それを実装するmdbase型を移植可能な信頼できる情報源として使用します。ファイルの編集はTaskNotesを更新し、移植可能な設定の変更は型のバインディングを更新します。", + "TaskNotes データコントラクトを公開し、それを実装する mdbase タイプと TaskNotes 設定レコードをポータブルな信頼できる情報源にします。ファイルの編集は TaskNotes に反映されます。設定の変更はタイプのバインディングに、テンプレート、アーカイブ移動、ステータスの循環などのアプリの動作は設定レコードに書き込まれます。", }, }, timeFormats: { diff --git a/src/i18n/resources/ko.ts b/src/i18n/resources/ko.ts index 8fcf44a03..18f76f7ae 100644 --- a/src/i18n/resources/ko.ts +++ b/src/i18n/resources/ko.ts @@ -37,6 +37,8 @@ export const ko: TranslationTree = { "TaskNotes가 mdbase 메타데이터 업데이트를 중단했습니다. {details} 표시된 파일과 복구 사본을 확인하고 충돌 또는 권한 문제를 해결한 뒤 TaskNotes를 다시 불러오세요. 작업 파일은 변경되지 않았습니다.", multiple: "TaskNotes 유형이 여러 개 있습니다: {paths}. 정식 제공자를 하나만 유지하거나 나머지를 유형 폴더 밖으로 옮긴 뒤 TaskNotes를 다시 불러오세요. TaskNotes는 마지막으로 확인된 유효한 설정을 유지했습니다.", + competingSettings: + "다른 TaskNotes 설정 레코드가 있습니다: {paths}. TaskNotes는 {record}을(를) 사용하고 나머지는 무시합니다. 유지할 설정을 {record}에 복사한 뒤 다른 파일을 삭제하세요.", referenced: "TaskNotes가 {typePath}을 유지했습니다. {recordPath}에서 이 유형을 명시적으로 참조합니다. 제공자를 옮기기 전에 유형 소속을 확인하세요. 작업 파일은 변경되지 않았습니다.", }, @@ -2578,7 +2580,7 @@ export const ko: TranslationTree = { enable: { name: "표준 mdbase 구성 사용", description: - "TaskNotes 데이터 계약을 게시하고 이를 구현하는 mdbase 타입을 이식 가능한 단일 기준 정보로 사용합니다. 파일을 수정하면 TaskNotes가 업데이트되고, 이식 가능한 설정을 변경하면 타입 바인딩이 업데이트됩니다.", + "TaskNotes 데이터 계약을 게시하고, 이를 구현하는 mdbase 유형과 TaskNotes 설정 레코드를 이식 가능한 기준 정보로 만듭니다. 파일을 편집하면 TaskNotes가 업데이트됩니다. 설정 변경은 유형 바인딩을 업데이트하며, 템플릿, 보관 이동, 상태 순환 같은 앱 동작은 설정 레코드를 업데이트합니다.", }, }, timeFormats: { diff --git a/src/i18n/resources/pt.ts b/src/i18n/resources/pt.ts index 9174f6e27..83d4a50ea 100644 --- a/src/i18n/resources/pt.ts +++ b/src/i18n/resources/pt.ts @@ -39,6 +39,8 @@ export const pt: TranslationTree = { "O TaskNotes interrompeu a atualização dos metadados do mdbase. {details} Reveja o ficheiro indicado e as cópias de recuperação, resolva o conflito ou as permissões e recarregue o TaskNotes. Os ficheiros de tarefas não foram alterados.", multiple: "Vários tipos do TaskNotes: {paths}. Mantenha um fornecedor canónico ou mova os restantes para fora da pasta de tipos e recarregue o TaskNotes. O TaskNotes manteve a última configuração válida.", + competingSettings: + "Foram encontrados outros registos de definições do TaskNotes: {paths}. O TaskNotes está a usar {record} e a ignorar os restantes. Copie para {record} as definições que pretende manter e, em seguida, elimine os outros ficheiros.", referenced: "O TaskNotes manteve {typePath}: {recordPath} faz referência explícita a este tipo. Reveja a sua pertença antes de mover o fornecedor; os ficheiros de tarefas não foram alterados.", }, @@ -2684,7 +2686,7 @@ export const pt: TranslationTree = { enable: { name: "Usar a configuração canônica do mdbase", description: - "Publica o contrato de dados do TaskNotes e torna o tipo mdbase que o implementa a fonte de referência portátil. Edições de arquivos atualizam o TaskNotes, e alterações nas configurações portáteis atualizam a vinculação do tipo.", + "Publica o contrato de dados do TaskNotes e torna o seu tipo mdbase implementador, com o registo de definições do TaskNotes, a fonte de verdade portátil. As edições de ficheiros atualizam o TaskNotes. As alterações de definições atualizam a associação do tipo ou, para o comportamento da app como modelos, movimentos para o arquivo e ciclo de estados, o registo de definições.", }, }, timeFormats: { diff --git a/src/i18n/resources/ru.ts b/src/i18n/resources/ru.ts index 5fe31bfda..b7c655306 100644 --- a/src/i18n/resources/ru.ts +++ b/src/i18n/resources/ru.ts @@ -39,6 +39,8 @@ export const ru: TranslationTree = { "TaskNotes остановил обновление метаданных mdbase. {details} Проверьте указанный файл и резервные копии, устраните конфликт или проблему с правами доступа, затем перезагрузите TaskNotes. Файлы задач не изменялись.", multiple: "Найдено несколько типов TaskNotes: {paths}. Оставьте одного канонического поставщика или переместите остальные за пределы папки типов, затем перезагрузите TaskNotes. TaskNotes сохранил последнюю корректную конфигурацию.", + competingSettings: + "Найдены другие записи настроек TaskNotes: {paths}. TaskNotes использует {record} и игнорирует остальные. Перенесите нужные настройки в {record}, затем удалите остальные файлы.", referenced: "TaskNotes сохранил {typePath}: {recordPath} явно ссылается на этот тип. Проверьте принадлежность к типу перед перемещением поставщика; файлы задач не изменялись.", }, @@ -2663,7 +2665,7 @@ export const ru: TranslationTree = { enable: { name: "Использовать каноническую конфигурацию mdbase", description: - "Публикует контракт данных TaskNotes и делает реализующий его тип mdbase переносимым источником истины. Изменения файлов обновляют TaskNotes, а изменения переносимых настроек обновляют привязку типа.", + "Публикует контракт данных TaskNotes и делает реализующий его тип mdbase вместе с записью настроек TaskNotes переносимым источником истины. Изменения файлов обновляют TaskNotes. Изменения настроек обновляют привязку типа, а для поведения приложения, такого как шаблоны, перемещение в архив и переключение статусов, — запись настроек.", }, }, timeFormats: { diff --git a/src/i18n/resources/zh.ts b/src/i18n/resources/zh.ts index 9859986e0..4dbdc4969 100644 --- a/src/i18n/resources/zh.ts +++ b/src/i18n/resources/zh.ts @@ -37,6 +37,8 @@ export const zh: TranslationTree = { "TaskNotes 已停止更新 mdbase 元数据。{details} 请检查列出的文件和恢复副本,解决冲突或权限问题,然后重新加载 TaskNotes。任务文件未被更改。", multiple: "发现多个 TaskNotes 类型:{paths}。请保留一个规范提供者,或将其他类型移出类型文件夹,然后重新加载 TaskNotes。TaskNotes 已保留上次有效的配置。", + competingSettings: + "发现其他 TaskNotes 设置记录:{paths}。TaskNotes 正在使用 {record},并忽略其他记录。请将需要保留的设置复制到 {record},然后删除其他文件。", referenced: "TaskNotes 已保留 {typePath}:{recordPath} 显式引用了此类型。移动提供者之前,请检查其类型归属;任务文件未被更改。", }, @@ -2509,7 +2511,7 @@ export const zh: TranslationTree = { enable: { name: "使用规范的 mdbase 配置", description: - "发布 TaskNotes 数据契约,并将实现该契约的 mdbase 类型作为可移植的权威数据源。文件编辑会更新 TaskNotes,可移植设置的更改会更新类型绑定。", + "发布 TaskNotes 数据契约,并让实现它的 mdbase 类型与 TaskNotes 设置记录成为可移植的唯一可信来源。编辑文件会更新 TaskNotes。更改设置会更新类型绑定;模板、归档移动和状态循环等应用行为则写入设置记录。", }, }, timeFormats: { diff --git a/src/main.ts b/src/main.ts index cb25d9043..e3e87a19b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -109,6 +109,7 @@ import { import { startDateChangeDetection } from "./bootstrap/dateChangeDetection"; import { createTaskNotesLogger } from "./utils/tasknotesLogger"; import { sanitizeLinkAliasText } from "./utils/linkAliasUtils"; +import { generateLink } from "./utils/linkUtils"; import { TASKNOTES_RUNTIME_LIFECYCLE_RAW_EVENTS } from "./api/runtime-api"; import { createTaskNotesPerformanceProfiler, @@ -1325,7 +1326,7 @@ export default class TaskNotesPlugin extends Plugin { const currentFile = this.app.workspace.getActiveFile(); const parentNote = currentFile - ? this.app.fileManager.generateMarkdownLink(currentFile, currentFile.path) + ? generateLink(this.app, currentFile, currentFile.path, "", "", this.settings.useFrontmatterMarkdownLinks) : undefined; return applyParentNoteProjectDefault(prePopulatedValues, parentNote); diff --git a/src/modals/TaskCreationModal.ts b/src/modals/TaskCreationModal.ts index 06ebef936..d9c3c052f 100644 --- a/src/modals/TaskCreationModal.ts +++ b/src/modals/TaskCreationModal.ts @@ -615,7 +615,7 @@ export class TaskCreationModal extends TaskModal { // Handle projects differently - they use file selection, not text input if (parsed.projects && parsed.projects.length > 0) { - this.addProjectsFromStrings(parsed.projects); + this.addProjectsFromStrings(parsed.projects, true); this.renderProjectsList(); } @@ -673,7 +673,7 @@ export class TaskCreationModal extends TaskModal { this.userFields = formState.userFields; if (formState.projectStrings.length > 0) { - this.initializeProjectsFromStrings(formState.projectStrings); + this.initializeProjectsFromStrings(formState.projectStrings, true); } this.details = this.normalizeDetails(this.details); diff --git a/src/modals/TaskModal.ts b/src/modals/TaskModal.ts index b2d1c5d08..988624e41 100644 --- a/src/modals/TaskModal.ts +++ b/src/modals/TaskModal.ts @@ -1140,17 +1140,24 @@ export abstract class TaskModal extends Modal { ); } - protected initializeProjectsFromStrings(projects: string[]): void { + protected initializeProjectsFromStrings(projects: string[], formatLinks = false): void { this.selectedProjectItems = []; - this.addProjectsFromStrings(projects); + this.addProjectsFromStrings(projects, formatLinks); // Don't render immediately - let the caller decide when to render } - protected addProjectsFromStrings(projects: string[]): void { + /** + * Add projects given as text. `formatLinks` writes projects that resolve to a + * note in the TaskNotes link format (new tasks); otherwise links are kept as written. + */ + protected addProjectsFromStrings(projects: string[], formatLinks = false): void { + const context = this.getProjectStringContext(); this.selectedProjectItems = addTaskModalProjectItemsFromStrings( this.selectedProjectItems, projects, - this.getProjectStringContext() + formatLinks + ? { ...context, formatLink: (file) => this.buildProjectReference(file, context.sourcePath) } + : context ); this.updateProjectsFromFiles(); // Don't render immediately - let the caller decide when to render diff --git a/src/modals/taskModalProjects.ts b/src/modals/taskModalProjects.ts index f5d607a57..a623639fd 100644 --- a/src/modals/taskModalProjects.ts +++ b/src/modals/taskModalProjects.ts @@ -13,6 +13,8 @@ export interface TaskModalProjectStringContext { sourcePath: string; getMarkdownFiles: () => readonly TFile[]; resolveLink: (linkPath: string, sourcePath: string) => unknown; + /** When set, a project that resolves to a note is written with this link. */ + formatLink?: (file: TFile) => string; } export interface RenderTaskModalProjectsListOptions { @@ -206,7 +208,7 @@ function createTaskModalProjectItemFromWikiLink( ): TaskModalProjectItem { const file = context.resolveLink(linkPath, context.sourcePath); if (file instanceof TFile) { - return createTaskModalProjectItemFromFile(file, projectString); + return createTaskModalProjectItemFromFile(file, context.formatLink?.(file) ?? projectString); } return { @@ -224,7 +226,7 @@ function createTaskModalProjectItemFromMarkdownLink( const linkPath = parseLinkToPath(projectString); const file = context.resolveLink(linkPath, context.sourcePath); if (file instanceof TFile) { - return createTaskModalProjectItemFromFile(file, projectString); + return createTaskModalProjectItemFromFile(file, context.formatLink?.(file) ?? projectString); } return { @@ -246,7 +248,7 @@ function createTaskModalProjectItemFromPlainText( return { file: matchingFile, name: matchingFile.basename, - link: `[[${matchingFile.basename}]]`, + link: context.formatLink?.(matchingFile) ?? `[[${matchingFile.basename}]]`, }; } diff --git a/src/services/InstantTaskConvertService.ts b/src/services/InstantTaskConvertService.ts index 44f8c7a73..ee2118e6a 100644 --- a/src/services/InstantTaskConvertService.ts +++ b/src/services/InstantTaskConvertService.ts @@ -20,6 +20,7 @@ import { shouldShowFilenameShortenedNotice } from "../utils/filenameGenerator"; import type { InterpolationValues, TranslationKey } from "../i18n"; import { createTaskNotesLogger } from "../utils/tasknotesLogger"; import { sanitizeGeneratedLinkAlias, sanitizeLinkAliasText } from "../utils/linkAliasUtils"; +import { formatResolvedProjectReference, generateLink } from "../utils/linkUtils"; import { publishUserNotice } from "../core/userNotices"; import { modifyVaultFile } from "./VaultMutationService"; @@ -786,8 +787,16 @@ export class InstantTaskConvertService { // Add parent note as project if enabled if (defaults.useParentNoteAsProject && currentFile) { - // The parentNote is already a markdown link, so we can add it directly - projectsArray.push(parentNote); + projectsArray.push( + generateLink( + this.plugin.app, + currentFile, + currentFile.path, + "", + "", + this.plugin.settings.useFrontmatterMarkdownLinks + ) + ); } if (defaults.useParentHeaderAsProject && currentFile) { @@ -806,8 +815,20 @@ export class InstantTaskConvertService { projectsArray.push(...parsedProjects); } - // Remove duplicates - const uniqueProjects = [...new Set(projectsArray)]; + // Remove duplicates, writing resolved project links in the TaskNotes link format + const projectSourcePath = currentFile?.path || ""; + const uniqueProjects = [ + ...new Set( + projectsArray.map((project) => + formatResolvedProjectReference( + this.plugin.app, + project, + projectSourcePath, + this.plugin.settings.useFrontmatterMarkdownLinks + ) + ) + ), + ]; // Apply default reminders if enabled let reminders: Reminder[] | undefined = undefined; @@ -1433,13 +1454,15 @@ export class InstantTaskConvertService { sourcePath ); if (file) { - // Generate the proper relative link - const linkText = this.plugin.app.metadataCache.fileToLinktext( + // Generate the proper relative link in the TaskNotes link format + return generateLink( + this.plugin.app, file, sourcePath, - true + "", + "", + this.plugin.settings.useFrontmatterMarkdownLinks ); - return `[[${linkText}]]`; } } catch (error) { // If file resolution fails, return the original project diff --git a/src/services/MdbaseSpecService.ts b/src/services/MdbaseSpecService.ts index 7af3ba739..1b0485fee 100644 --- a/src/services/MdbaseSpecService.ts +++ b/src/services/MdbaseSpecService.ts @@ -16,6 +16,21 @@ import { type TaskNotesMdbaseResources, } from "@tasknotes/model/mdbase"; import { TASKNOTES_SPEC_VERSION as TASKNOTES_CONTRACT_VERSION } from "@tasknotes/model"; +import { + absentTaskNotesSettingsPatch, + buildTaskNotesSettingsPatch, + changedTaskNotesAppSettings, + completeTaskNotesAppSettingsChanges, + newTaskNotesSettingsFrontmatter, + APP_SETTINGS_FOLDER, + readAppSettingsFrontmatter, + resolveTaskNotesAppSettings, + TASKNOTES_SETTINGS_APP, + TASKNOTES_SETTINGS_TYPE, + writeAppSettingsDocument, + type TaskNotesAppSettingsValues, +} from "@tasknotes/model/settings"; +import { APP_SETTINGS_RECORD_PATH, appSettingsSupportResources } from "./mdbase/appSettingsResources"; import TaskNotesPlugin from "../main"; import { FieldMapping } from "../types"; @@ -24,8 +39,13 @@ import { createTaskNotesLogger } from "../utils/tasknotesLogger"; import { publishUserNotice } from "../core/userNotices"; import { createI18nService } from "../i18n"; import { + appSettingsFingerprint, + appSettingsFromPluginSettings, + applyAppSettingsToPluginSettings, applyCanonicalTaskTypeToSettings, buildTaskNotesModelConfig, + hasLegacyAppSettings, + PREVIOUS_TASKNOTES_CONTRACT_VERSIONS, mergeCanonicalTaskTypeDocument, parseMdbaseTaskTypeDocument, portableSettingsFingerprint, @@ -70,6 +90,10 @@ type CanonicalTypeState = { type: Record; /** The file implements an earlier tasknotes.task version and must be rewritten. */ outdated?: boolean; + /** The earlier binding, which still holds the settings rc.6 moved to the settings record. */ + legacyBinding?: Record; + /** The tasknotes.task version an outdated file implemented. */ + previousVersion?: string; }; type LegacyTaskNotesTypeState = { @@ -98,7 +122,8 @@ class MdbaseConfigurationConflictModal extends Modal { app: TaskNotesPlugin["app"], private readonly typePath: string, private readonly invalidType: boolean, - private readonly resolveChoice: (choice: ConflictChoice) => void + private readonly resolveChoice: (choice: ConflictChoice) => void, + private readonly settingsRecord = false ) { super(app); } @@ -113,12 +138,18 @@ class MdbaseConfigurationConflictModal extends Modal { this.contentEl.createEl("p", { text: this.invalidType ? "Keep the file unchanged so you can repair it manually, or replace its managed TaskNotes fields with the current settings." - : "Choose which version should become canonical. Unknown mdbase extensions will be preserved.", + : this.settingsRecord + ? "Choose which version should become canonical. Using TaskNotes settings writes only the settings you changed; other fields in the file are kept." + : "Choose which version should become canonical. Unknown mdbase extensions will be preserved.", }); const actions = this.contentEl.createDiv({ cls: "modal-button-container" }); const useTypeButton = actions.createEl("button", { - text: this.invalidType ? "Keep file unchanged" : "Use mdbase type", + text: this.invalidType + ? "Keep file unchanged" + : this.settingsRecord + ? "Use settings file" + : "Use mdbase type", }); useTypeButton.addEventListener("click", () => this.finish("type")); @@ -163,6 +194,19 @@ export class MdbaseSpecService { private canonicalResourcePaths = new Set(); private lastKnownTypeContent: string | null = null; private lastAppliedSettingsFingerprint: string | null = null; + /** The TaskNotes settings record as last read or written (null: not yet seen). */ + private lastKnownRecordContent: string | null = null; + /** The record as read at the start of the current synchronization. */ + private currentRecordContent: string | null = null; + /** App settings as last applied from, or written to, the record. */ + private lastAppliedAppFingerprint: string | null = null; + private lastAppliedAppValues: TaskNotesAppSettingsValues | null = null; + /** Set when a synchronization applied app settings read from the record. */ + private appSettingsAppliedFromRecord = false; + private lastRecordNotice: string | null = null; + private lastCompetingRecordsNotice: string | null = null; + /** Whether this session has added the fields an existing record lacked. */ + private recordAbsentFieldsMerged = false; private canonicalReadBlocked = false; private watcherRegistered = false; private canonicalWritePromise: Promise = Promise.resolve(true); @@ -286,20 +330,25 @@ export class MdbaseSpecService { resources, legacyType.path ); + const settingsResources = this.appSettingsResources(resources); const managedPaths = [ resources.paths.contract, resources.paths.taskSchema, resources.paths.bindingSchema, legacyType.path, + ...settingsResources.map(({ path }) => path), + APP_SETTINGS_RECORD_PATH, "mdbase.yaml", ]; const snapshots = await this.snapshotFiles(managedPaths); + const sharedPaths = new Set([...settingsResources.map(({ path }) => path), APP_SETTINGS_RECORD_PATH]); if ( snapshots.find(({ path }) => path === "mdbase.yaml")?.content !== sourceConfig || snapshots.find(({ path }) => path === legacyType.path)?.content !== legacyType.content || snapshots.some( ({ path, content }) => - path !== "mdbase.yaml" && path !== legacyType.path && content !== null && content !== support.get(path) && !isKnownLegacySupport(content) + path !== "mdbase.yaml" && path !== legacyType.path && !sharedPaths.has(path) && + content !== null && content !== support.get(path) && !isKnownLegacySupport(content) ) ) { this.publishNotice( @@ -307,11 +356,21 @@ export class MdbaseSpecService { ); return false; } + // The v4 settings (data.json) become the settings record in the same + // journalled step. A record another device already wrote keeps its fields. + const recordSnapshot = snapshots.find(({ path }) => path === APP_SETTINGS_RECORD_PATH)?.content ?? null; + const recordContent = this.migratedRecordContent(recordSnapshot, YAML.parse(migratedConfig) as MdbaseYamlConfig); const intendedWrites: FileSnapshot[] = [ { path: resources.paths.taskSchema, content: resources.taskSchemaDocument }, { path: resources.paths.bindingSchema, content: resources.bindingSchemaDocument }, { path: legacyType.path, content: resources.typeDocument }, { path: resources.paths.contract, content: resources.contractDocument }, + ...settingsResources + .filter(({ path }) => snapshots.find((snapshot) => snapshot.path === path)?.content === null) + .map(({ path, content }) => ({ path, content })), + ...(recordContent !== null && recordContent !== recordSnapshot + ? [{ path: APP_SETTINGS_RECORD_PATH, content: recordContent }] + : []), { path: "mdbase.yaml", content: migratedConfig }, ]; const backupFolder = await this.writeV02MigrationBackup(sourceConfig, legacyType, snapshots); @@ -342,8 +401,12 @@ export class MdbaseSpecService { await this.assertSnapshotsUnchanged(progress); const state = await this.verifyMigratedV03Collection(legacyType.path, resources); await this.removeMigrationJournal(); + await this.loadAppSettingsRecord(); this.canonicalTypesFolder = typesFolder; this.applyCanonicalState(state); + // The migration wrote the record with every absent field added, so the + // next sync only has to carry local changes. + if (this.lastKnownRecordContent !== null) this.recordAbsentFieldsMerged = true; this.publishNotice( `TaskNotes updated its mdbase metadata to v0.3. Task files were not changed. The previous metadata is backed up in ${backupFolder}.` ); @@ -383,6 +446,22 @@ export class MdbaseSpecService { } } + /** The settings record a v4 migration writes: new, or the existing one with absent fields added. */ + private migratedRecordContent(existing: string | null, config: MdbaseYamlConfig): string | null { + const local = appSettingsFromPluginSettings(this.plugin.settings); + if (existing === null) { + return writeAppSettingsDocument(null, {}, { + frontmatter: newTaskNotesSettingsFrontmatter(completeTaskNotesAppSettingsChanges(local)), + type: TASKNOTES_SETTINGS_TYPE, + typeKey: this.recordTypeKey({ exists: true, config }), + }); + } + const frontmatter = this.parseRecord(existing); + if (!frontmatter || resolveTaskNotesAppSettings(frontmatter).readOnly) return existing; + const patch = absentTaskNotesSettingsPatch(frontmatter, local); + return Object.keys(patch).length > 0 ? writeAppSettingsDocument(existing, patch) : existing; + } + private async readGeneratedV02TaskType( existingCollection: ExistingCollection, exclusive = true @@ -395,7 +474,15 @@ export class MdbaseSpecService { const otherMarkdownTypes = (await this.listMarkdownFilesRecursively(typesFolder)).filter( (path) => path !== typePath ); - if (exclusive && otherMarkdownTypes.length > 0) return null; + // The canonical settings type, left by an earlier attempt or seeded by + // TaskNotes App, does not make the legacy collection ambiguous. + const settingsType = appSettingsSupportResources(DEFAULT_CONTRACTS_FOLDER, typesFolder).find((resource) => resource.seed); + const unrelatedTypes = []; + for (const path of otherMarkdownTypes) { + if (path === settingsType?.path && (await adapter.read(path)) === settingsType.content) continue; + unrelatedTypes.push(path); + } + if (exclusive && unrelatedTypes.length > 0) return null; const content = await adapter.read(typePath); if (!recognizeLegacyTaskType(content, this.plugin.settings)) return null; @@ -431,7 +518,7 @@ export class MdbaseSpecService { const sourceSettings = isRecord(source.settings) ? source.settings : {}; const generatedSettings = generated.settings; const typesFolder = resources.paths.type.split("/").slice(0, -1).join("/"); - const contractsFolder = resources.paths.contract.split("/").slice(0, -1).join("/"); + const contractsFolder = resources.paths.contract.split("/").slice(0, -2).join("/"); const exclude = uniqueStrings([ ...stringValues(generatedSettings.exclude), ...stringValues(sourceSettings.exclude), @@ -506,9 +593,9 @@ export class MdbaseSpecService { const currentContent = current.find(({ path }) => path === snapshot.path)?.content ?? null; if (currentContent === snapshot.content) continue; if (snapshot.content === null) { - // Never delete a newly created support resource during rollback: an - // external process could have changed it after our last comparison. - // Leaving an orphaned generated file is safer than risking user data. + // Created by this migration: remove it only while it still holds + // exactly what the migration wrote (checked again just before removal). + if (currentContent !== null) await this.metadataIO.removeIfUnchanged(snapshot.path, currentContent); continue; } else { await this.writeFileIfUnchanged( @@ -517,6 +604,7 @@ export class MdbaseSpecService { ); } } + await this.metadataIO.removeAbandonedStages(journal.snapshots.map(({ path }) => path)); } private async writeMigrationJournal(journal: MigrationJournal): Promise { @@ -686,6 +774,7 @@ export class MdbaseSpecService { if (!this.plugin.settings.enableMdbaseSpec) return; const collection = await this.readExistingCollection(); if (getSpecFamily(collection.config?.spec_version) !== "v0.3") return; + await this.loadAppSettingsRecord(); const state = await this.readCanonicalType(collection); if (state) { this.applyCanonicalState(state); @@ -762,7 +851,9 @@ export class MdbaseSpecService { if (content === null) throw new Error("mdbase.yaml disappeared while preparing the update."); this.metadataTransaction.stage({ path: "mdbase.yaml", content }, content); } + await this.loadAppSettingsRecord(); await this.syncSettingsToCanonicalTypeSnapshot(existingCollection); + await this.syncAppSettingsRecordSnapshot(existingCollection); }); const notices = this.deferredMetadataNotices; this.deferredMetadataNotices = null; @@ -851,10 +942,12 @@ export class MdbaseSpecService { ); const upgraded = upgradeCanonicalDocument(state.content, resources); await this.writeFileIfUnchanged({ path: state.path, content: state.content }, upgraded); - this.applyCanonicalState({ ...state, content: upgraded, type: parseMdbaseTaskTypeDocument(upgraded).type, outdated: false }); + this.applyCanonicalState({ path: state.path, content: upgraded, type: parseMdbaseTaskTypeDocument(upgraded).type }, false); if (upgraded !== state.content) { this.publishNotice( - `TaskNotes updated ${state.path} to tasknotes.task ${TASKNOTES_CONTRACT_VERSION}. Task files were not changed.` + hasLegacyAppSettings(state.legacyBinding) + ? `TaskNotes updated ${state.path} to tasknotes.task ${TASKNOTES_CONTRACT_VERSION} and moved its app settings to ${APP_SETTINGS_RECORD_PATH}. Task files were not changed.` + : `TaskNotes updated ${state.path} to tasknotes.task ${TASKNOTES_CONTRACT_VERSION}. Task files were not changed.` ); } return; @@ -1001,7 +1094,7 @@ export class MdbaseSpecService { try { const parsed = parseMdbaseTaskTypeDocument(content); - const { type, outdated } = withCurrentTaskNotesContract(parsed.type); + const { type, outdated, legacyBinding, previousVersion } = withCurrentTaskNotesContract(parsed.type); if (!hasTaskNotesImplementation(type)) { return null; } @@ -1014,7 +1107,14 @@ export class MdbaseSpecService { ); return null; } - return { path, content, type, outdated }; + return { + path, + content, + type, + outdated, + ...(legacyBinding ? { legacyBinding } : {}), + ...(previousVersion ? { previousVersion } : {}), + }; } catch (error) { this.canonicalTypePath = path; if (reportErrors) { @@ -1024,12 +1124,199 @@ export class MdbaseSpecService { } } - private applyCanonicalState(state: CanonicalTypeState): void { - try { applyCanonicalTaskTypeToSettings(this.plugin.settings, state.type); } + /** + * Apply the canonical type, and the settings record when it is new to this + * session and the app settings have no unsaved local change, to the plugin + * settings. `includeRecord: false` is for read-back after TaskNotes wrote the + * type from local settings. + */ + private applyCanonicalState(state: CanonicalTypeState, includeRecord = true): void { + const record = includeRecord ? this.recordToApply() : undefined; + try { + applyCanonicalTaskTypeToSettings(this.plugin.settings, state.type, { + ...(record ? { record: record.frontmatter } : {}), + ...(state.legacyBinding ? { legacyBinding: state.legacyBinding } : {}), + }); + } catch (error) { throw new Error(`${state.path}: ${String(error)}`); } this.canonicalTypePath = state.path; this.lastKnownTypeContent = state.content; this.lastAppliedSettingsFingerprint = portableSettingsFingerprint(this.plugin.settings); + if (record) { + this.lastKnownRecordContent = record.content; + this.appSettingsAppliedFromRecord = true; + } + if (record || state.legacyBinding || this.lastAppliedAppFingerprint === null) { + this.rememberAppliedAppSettings(); + } + } + + private rememberAppliedAppSettings(): void { + this.lastAppliedAppValues = appSettingsFromPluginSettings(this.plugin.settings); + this.lastAppliedAppFingerprint = JSON.stringify(this.lastAppliedAppValues); + } + + private localAppSettingsChanged(): boolean { + return this.lastAppliedAppFingerprint !== null && + appSettingsFingerprint(this.plugin.settings) !== this.lastAppliedAppFingerprint; + } + + /** The current record, when it should be applied over the plugin settings. */ + private recordToApply(): { content: string; frontmatter: Record } | undefined { + const content = this.currentRecordContent; + if (content === null || content === this.lastKnownRecordContent) return undefined; + if (this.localAppSettingsChanged()) return undefined; + const frontmatter = this.parseRecord(content); + return frontmatter ? { content, frontmatter } : undefined; + } + + private parseRecord(content: string): Record | undefined { + try { + return readAppSettingsFrontmatter(content); + } catch { + return undefined; + } + } + + private async loadAppSettingsRecord(): Promise { + this.currentRecordContent = await this.metadataIO.read(APP_SETTINGS_RECORD_PATH); + await this.reportCompetingSettingsRecords(); + } + + /** + * Name other files in the settings folder that also claim to be the TaskNotes + * collection settings record, such as sync conflict copies. TaskNotes keeps + * using the canonical record; the others are never changed. + */ + private async reportCompetingSettingsRecords(): Promise { + const adapter = this.plugin.app.vault.adapter; + const competing: string[] = []; + if (typeof adapter.list === "function" && (await adapter.exists(APP_SETTINGS_FOLDER))) { + for (const path of await this.listMarkdownFilesRecursively(APP_SETTINGS_FOLDER)) { + if (path === APP_SETTINGS_RECORD_PATH) continue; + const frontmatter = this.parseRecord(await adapter.read(path)); + if (frontmatter?.app === TASKNOTES_SETTINGS_APP && frontmatter.scope === "collection") competing.push(path); + } + } + if (competing.length === 0) { + this.lastCompetingRecordsNotice = null; + return; + } + const paths = competing.sort().join(", "); + const message = this.plugin.i18n?.translate("mdbaseSafety.competingSettings", { paths, record: APP_SETTINGS_RECORD_PATH }) ?? + `Other TaskNotes settings records found: ${paths}. TaskNotes is using ${APP_SETTINGS_RECORD_PATH} and ignoring the others. Copy any settings you want to keep into ${APP_SETTINGS_RECORD_PATH}, then delete the other files.`; + if (this.lastCompetingRecordsNotice === message) return; + this.lastCompetingRecordsNotice = message; + this.publishNotice(message); + } + + private recordNotice(message: string): void { + if (this.lastRecordNotice === message) return; + this.lastRecordNotice = message; + this.publishNotice(message); + } + + /** + * Keep the TaskNotes settings record (_settings/dev.tasknotes.md) in step with + * the plugin's app settings. Runs inside the type's metadata transaction, so a + * migration writes the record in the same journalled step as the type. + * + * - No record: create it with every app setting (v4 data.json values, or the + * settings an earlier binding held, already applied to the plugin settings). + * - First sight of an existing record: add only the fields it lacks. + * - Changed elsewhere: apply it, or ask when the plugin changed too. + * - Changed locally: write only the fields that changed. + */ + private async syncAppSettingsRecordSnapshot(existingCollection: ExistingCollection): Promise { + if (!this.canonicalTypePath || this.canonicalReadBlocked || this.lastKnownTypeContent === null) return; + const path = APP_SETTINGS_RECORD_PATH; + const content = await this.metadataIO.read(path); + const local = appSettingsFromPluginSettings(this.plugin.settings); + if (content === null) { + const created = writeAppSettingsDocument(null, {}, { + frontmatter: newTaskNotesSettingsFrontmatter(completeTaskNotesAppSettingsChanges(local)), + type: TASKNOTES_SETTINGS_TYPE, + typeKey: this.recordTypeKey(existingCollection), + }); + await this.writeFileIfUnchanged({ path, content: null }, created); + this.lastKnownRecordContent = created; + this.recordAbsentFieldsMerged = true; + this.rememberAppliedAppSettings(); + return; + } + const frontmatter = this.parseRecord(content); + if (!frontmatter) { + this.recordNotice(`${path} could not be read, so TaskNotes left it unchanged and kept its current settings.`); + this.lastKnownRecordContent = content; + return; + } + const readOnly = resolveTaskNotesAppSettings(frontmatter).readOnly; + if (!this.recordAbsentFieldsMerged && (this.lastKnownRecordContent === null || this.lastKnownRecordContent === content)) { + // First sync of this record in the session. applyCanonicalState already + // applied what it holds (over an earlier binding and local settings), so + // the plugin settings now supply the fields the record lacks, plus any + // setting changed locally since then (this sync may be the first save). + this.recordAbsentFieldsMerged = true; + this.lastKnownRecordContent = content; + if (readOnly) { + this.recordNotice(`${path}: ${readOnly}`); + return; + } + const patch = { + ...absentTaskNotesSettingsPatch(frontmatter, local), + ...(this.localAppSettingsChanged() + ? buildTaskNotesSettingsPatch(frontmatter, changedTaskNotesAppSettings(this.lastAppliedAppValues ?? local, local)) + : {}), + }; + if (Object.keys(patch).length > 0) await this.writeRecordPatch(content, patch); + this.rememberAppliedAppSettings(); + return; + } + const externalChanged = content !== this.lastKnownRecordContent; + const localChanged = this.localAppSettingsChanged(); + if (externalChanged && localChanged) { + const choice = await this.askConflict(path, false, true); + if (choice === "type") { + this.applyRecordToSettings(content, frontmatter); + return; + } + } else if (externalChanged) { + this.applyRecordToSettings(content, frontmatter); + return; + } else if (!localChanged) { + return; + } + if (readOnly) { + this.recordNotice(`${path}: ${readOnly}`); + this.lastKnownRecordContent = content; + return; + } + const changes = changedTaskNotesAppSettings(this.lastAppliedAppValues ?? local, local); + const patch = buildTaskNotesSettingsPatch(frontmatter, changes); + if (Object.keys(patch).length > 0) await this.writeRecordPatch(content, patch); + else this.lastKnownRecordContent = content; + this.rememberAppliedAppSettings(); + } + + private async writeRecordPatch(content: string, patch: Record): Promise { + const next = writeAppSettingsDocument(content, patch); + await this.writeFileIfUnchanged({ path: APP_SETTINGS_RECORD_PATH, content }, next); + this.lastKnownRecordContent = next; + } + + private applyRecordToSettings(content: string, frontmatter: Record): void { + const values = resolveTaskNotesAppSettings(frontmatter, { + fallback: appSettingsFromPluginSettings(this.plugin.settings), + }).values; + applyAppSettingsToPluginSettings(this.plugin.settings, values); + this.lastKnownRecordContent = content; + this.appSettingsAppliedFromRecord = true; + this.rememberAppliedAppSettings(); + } + + private recordTypeKey(existingCollection: ExistingCollection): string { + const keys = existingCollection.config?.settings?.explicit_type_keys; + return Array.isArray(keys) && typeof keys[0] === "string" && keys[0].trim() ? keys[0] : "mdbase_type"; } private async writeCanonicalType( @@ -1101,7 +1388,7 @@ export class MdbaseSpecService { } const parsed = parseMdbaseTaskTypeDocument(content); - this.applyCanonicalState({ path, content, type: parsed.type }); + this.applyCanonicalState({ path, content, type: parsed.type }, false); return true; } @@ -1120,10 +1407,13 @@ export class MdbaseSpecService { const state = await this.readCanonicalTypeAtPath(path, false); if (state) states.push(state); } - const current = states.filter((state) => !state.outdated); + // A type on the release before this one (rc.5) is current: this load upgrades it. + const latestPrevious = PREVIOUS_TASKNOTES_CONTRACT_VERSIONS[0]; + const isCurrent = (state: CanonicalTypeState) => !state.outdated || state.previousVersion === latestPrevious; + const current = states.filter(isCurrent); const superseded = states.filter( (state) => - state.outdated && + !isCurrent(state) && state.type.name === "tasknotes-task" && isRecord(state.type["x-tasknotes-generator"]) ); @@ -1157,8 +1447,13 @@ export class MdbaseSpecService { } } this.canonicalTypePath = current[0].path; + // App settings come from the canonical type's collection record only. + const withAppSettings = superseded.filter((state) => hasLegacyAppSettings(state.legacyBinding)); this.publishNotice( - `TaskNotes removed ${superseded.map((state) => state.path).join(", ")}, an earlier duplicate of ${current[0].path}. A copy is in ${folder}.` + `TaskNotes removed ${superseded.map((state) => state.path).join(", ")}, an earlier duplicate of ${current[0].path}. A copy is in ${folder}.` + + (withAppSettings.length > 0 + ? ` App settings it held (such as templates and archive moves) were not used: TaskNotes keeps them in ${APP_SETTINGS_RECORD_PATH}, alongside ${current[0].path}.` + : "") ); return true; } @@ -1193,7 +1488,9 @@ export class MdbaseSpecService { } private isCanonicalPath(path: string): boolean { - if (path === "mdbase.yaml" || path === this.canonicalTypePath) return true; + if (path === "mdbase.yaml" || path === this.canonicalTypePath || path === APP_SETTINGS_RECORD_PATH) return true; + // Other records in the settings folder: a competing copy is reported. + if (path.startsWith(`${APP_SETTINGS_FOLDER}/`) && path.endsWith(".md")) return true; return Boolean( this.canonicalTypesFolder && path.startsWith(`${this.canonicalTypesFolder}/`) && @@ -1248,6 +1545,7 @@ export class MdbaseSpecService { return; } if (getSpecFamily(collection.config?.spec_version) !== "v0.3") return; + await this.loadAppSettingsRecord(); const state = await this.readCanonicalType(collection); if (!state) { if (this.canonicalReadBlocked) return; @@ -1269,6 +1567,15 @@ export class MdbaseSpecService { } if (state.content === this.lastKnownTypeContent) { this.canonicalTypePath = state.path; + if (this.currentRecordContent !== this.lastKnownRecordContent) { + // Only the settings record changed (or disappeared). + this.appSettingsAppliedFromRecord = false; + await this.syncSettingsToCanonicalType(collection); + if (this.appSettingsAppliedFromRecord) { + await this.plugin.saveSettingsDataOnly(); + await this.plugin.settingsLifecycleService?.onCanonicalSettingsChanged(); + } + } return; } @@ -1290,13 +1597,14 @@ export class MdbaseSpecService { await this.plugin.settingsLifecycleService?.onCanonicalSettingsChanged(); } - private askConflict(path: string, invalidType: boolean): Promise { + private askConflict(path: string, invalidType: boolean, settingsRecord = false): Promise { return new Promise((resolve) => { new MdbaseConfigurationConflictModal( this.plugin.app, path, invalidType, - resolve + resolve, + settingsRecord ).open(); }); } @@ -1332,6 +1640,10 @@ export class MdbaseSpecService { this.canonicalResourcePaths.clear(); this.lastKnownTypeContent = null; this.lastAppliedSettingsFingerprint = null; + this.lastKnownRecordContent = null; + this.recordAbsentFieldsMerged = false; + this.lastAppliedAppFingerprint = null; + this.lastAppliedAppValues = null; this.canonicalReadBlocked = false; } @@ -1481,11 +1793,24 @@ export class MdbaseSpecService { } this.canonicalResourcePaths.add(path); } + // The settings record's contracts, schemas and type. These are shared + // with TaskNotes App (and the contracts with mdbase Reader): a file + // already present, identical or not, is left as it is. + for (const resource of this.appSettingsResources(resources)) { + const current = await this.metadataIO.read(resource.path); + if (current === null) await this.writeFileIfUnchanged({ path: resource.path, content: null }, resource.content); + } } finally { this.writeInProgress = previousWriteState; } } + private appSettingsResources(resources: TaskNotesMdbaseResources) { + const contractsFolder = resources.paths.contract.split("/").slice(0, -2).join("/"); + const typesFolder = resources.paths.type.split("/").slice(0, -1).join("/"); + return appSettingsSupportResources(contractsFolder, typesFolder); + } + /** * Build the mdbase.yaml content. */ @@ -2104,16 +2429,32 @@ function parseMigrationJournal(value: unknown): MigrationJournal { DEFAULT_TYPES_FOLDER; const contractsFolder = normalizeJournalFolder(migratedConfig.settings?.contracts_folder) ?? DEFAULT_CONTRACTS_FOLDER; - const expectedPaths = new Set([ + const requiredPaths = [ "mdbase.yaml", `${typesFolder}/task.md`, - `${contractsFolder}/tasknotes.task.md`, "_schemas/tasknotes/tasknotes-task.schema.json", "_schemas/tasknotes/tasknotes-task-binding.schema.json", - ]); + ]; + // Journals from earlier releases hold the unversioned contract; rc.6 writes a + // versioned contract, the settings support resources and the settings record. + const legacyContract = `${contractsFolder}/tasknotes.task.md`; + const settingsPaths = [ + APP_SETTINGS_RECORD_PATH, + ...appSettingsSupportResources(contractsFolder, typesFolder).map(({ path }) => path), + ]; + const versionedContract = /^tasknotes\.task\/[^/]+\.md$/; + const allowed = (path: string) => + requiredPaths.includes(path) || + path === legacyContract || + settingsPaths.includes(path) || + (path.startsWith(`${contractsFolder}/`) && versionedContract.test(path.slice(contractsFolder.length + 1))); + const contracts = snapshotPaths.filter( + (path) => path === legacyContract || (path.startsWith(`${contractsFolder}/`) && versionedContract.test(path.slice(contractsFolder.length + 1))) + ); if ( - snapshotPaths.length !== expectedPaths.size || - snapshotPaths.some((path) => !expectedPaths.has(path)) + requiredPaths.some((path) => !snapshotPaths.includes(path)) || + contracts.length !== 1 || + snapshotPaths.some((path) => !allowed(path)) ) { throw new Error("The pending migration includes files outside TaskNotes metadata."); } diff --git a/src/services/buildTaskCreationDataFromParsed.ts b/src/services/buildTaskCreationDataFromParsed.ts index 5cfb642ed..96491ade3 100644 --- a/src/services/buildTaskCreationDataFromParsed.ts +++ b/src/services/buildTaskCreationDataFromParsed.ts @@ -3,6 +3,7 @@ import type { ParsedTaskData } from "./NaturalLanguageParser"; import type { TaskCreationData } from "../types"; import { combineDateAndTime, getCurrentTimestamp } from "../utils/dateUtils"; import { sanitizeTags } from "../utils/helpers"; +import { formatResolvedProjectReference } from "../utils/linkUtils"; interface BuildTaskCreationDataOptions { creationContext?: TaskCreationData["creationContext"]; @@ -43,7 +44,12 @@ export function buildTaskCreationDataFromParsed( } if (parsed.projects && parsed.projects.length > 0) { - taskData.projects = parsed.projects; + const app = plugin.app; + taskData.projects = app?.metadataCache + ? parsed.projects.map((project) => + formatResolvedProjectReference(app, project, "", plugin.settings.useFrontmatterMarkdownLinks) + ) + : parsed.projects; } if (parsed.tags && parsed.tags.length > 0) { diff --git a/src/services/canonicalTaskNotesPack.ts b/src/services/canonicalTaskNotesPack.ts index 1fe93f3b3..1f9a5e319 100644 --- a/src/services/canonicalTaskNotesPack.ts +++ b/src/services/canonicalTaskNotesPack.ts @@ -5,15 +5,39 @@ export const canonicalTaskNotesResources = [ "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: tasknotes.task\nversion: 0.3.0-rc.5\nname: TaskNotes task\ndescription: Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.5.\nrecord_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task.schema.json\n title: TaskNotes portable task view\n description: The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.5 record contract.\n type: object\n required:\n - status\n - dateCreated\n additionalProperties: false\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n type: string\n minLength: 1\n priority:\n type: string\n minLength: 1\n due:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n pattern: \\S\n description: Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\n attachments:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrenceAnchor:\n enum:\n - scheduled\n - completion\n recurrenceParent:\n type: string\n occurrenceDate:\n type: string\n format: date\n occurrenceMaterialization:\n enum:\n - manual\n - on_completion\n - rolling\n occurrenceNextTrigger:\n enum:\n - completion\n - completion_or_skip\n occurrenceTemplate:\n type: string\n occurrencePastHorizon:\n type: string\n occurrenceFutureHorizon:\n type: string\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n reminders:\n type: array\n items:\n type: object\n blockedBy:\n type: array\n items:\n type: object\n completeInstances:\n type: array\n items:\n type: string\n format: date\n skippedInstances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n sortOrder:\n type: string\nbinding_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\n title: TaskNotes task data-contract binding\n description: Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\n type: object\n required:\n - profiles\n - capabilities\n - title\n - status\n - priority\n - recurrence\n - occurrences\n - links\n - archive\n - time_tracking\n - templating\n properties:\n profiles:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - core-lite\n - recurrence\n - templating\n - materialized-occurrences\n - extended\n capabilities:\n type: array\n uniqueItems: true\n items:\n enum:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - rename\n - archive\n - batch\n - concurrency\n - dry-run\n - migration\n - templating\n title:\n $ref: \"#/$defs/titlePolicy\"\n status:\n $ref: \"#/$defs/statusPolicy\"\n priority:\n $ref: \"#/$defs/priorityPolicy\"\n recurrence:\n $ref: \"#/$defs/recurrencePolicy\"\n occurrences:\n $ref: \"#/$defs/occurrencePolicy\"\n links:\n $ref: \"#/$defs/linkPolicy\"\n archive:\n $ref: \"#/$defs/archivePolicy\"\n time_tracking:\n $ref: \"#/$defs/timeTrackingPolicy\"\n templating:\n $ref: \"#/$defs/templatingPolicy\"\n nlp:\n $ref: \"#/$defs/nlpPolicy\"\n additionalProperties: false\n $defs:\n nonEmptyString:\n type: string\n minLength: 1\n stringSet:\n type: array\n uniqueItems: true\n items:\n $ref: \"#/$defs/nonEmptyString\"\n titlePolicy:\n type: object\n required:\n - storage\n properties:\n storage:\n enum:\n - filename\n - frontmatter\n filename_format:\n enum:\n - title\n - zettel\n - timestamp\n - uuid\n - custom\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n allOf:\n - if:\n properties:\n filename_format:\n const: custom\n required:\n - filename_format\n then:\n properties:\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n required:\n - custom_filename_template\n additionalProperties: true\n statusDefinition:\n type: object\n required:\n - value\n - label\n - color\n - is_completed\n - is_skipped\n - exclude_from_cycle\n - order\n - auto_archive\n - auto_archive_delay_minutes\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n is_completed:\n type: boolean\n is_skipped:\n type: boolean\n exclude_from_cycle:\n type: boolean\n next_status:\n $ref: \"#/$defs/nonEmptyString\"\n order:\n type: number\n auto_archive:\n type: boolean\n auto_archive_delay_minutes:\n type: number\n minimum: 0\n additionalProperties: true\n statusPolicy:\n type: object\n required:\n - values\n - default\n - completed_values\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n completed_values:\n $ref: \"#/$defs/stringSet\"\n skipped_values:\n $ref: \"#/$defs/stringSet\"\n default_skipped:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/statusDefinition\"\n additionalProperties: false\n priorityDefinition:\n type: object\n required:\n - value\n - label\n - color\n - weight\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n weight:\n type: number\n additionalProperties: true\n priorityPolicy:\n type: object\n required:\n - values\n - default\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/priorityDefinition\"\n additionalProperties: false\n recurrencePolicy:\n type: object\n required:\n - syntax\n - maintain_due_date_offset\n - reset_body_checkboxes\n properties:\n syntax:\n const: tasknotes\n maintain_due_date_offset:\n type: boolean\n reset_body_checkboxes:\n type: boolean\n additionalProperties: false\n occurrencePolicy:\n type: object\n required:\n - identity_roles\n - default_materialization\n - default_next_trigger\n properties:\n identity_roles:\n const:\n - recurrenceParent\n - occurrenceDate\n default_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default_next_trigger:\n enum:\n - completion\n - completion_or_skip\n past_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n future_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n linkPolicy:\n type: object\n required:\n - accepted_formats\n - write_format\n properties:\n accepted_formats:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - wikilink\n - markdown\n write_format:\n enum:\n - wikilink\n - markdown\n additionalProperties: false\n archivePolicy:\n type: object\n required:\n - archived_tag\n - move_on_archive\n properties:\n archived_tag:\n $ref: \"#/$defs/nonEmptyString\"\n move_on_archive:\n type: boolean\n folder:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n timeTrackingPolicy:\n type: object\n required:\n - auto_stop_on_complete\n properties:\n auto_stop_on_complete:\n type: boolean\n additionalProperties: false\n templatingPolicy:\n type: object\n required:\n - enabled\n - occurrence_enabled\n properties:\n enabled:\n type: boolean\n template_path:\n $ref: \"#/$defs/nonEmptyString\"\n occurrence_enabled:\n type: boolean\n occurrence_template_path:\n $ref: \"#/$defs/nonEmptyString\"\n failure_mode:\n enum:\n - error\n - warning_fallback\n unknown_variable_policy:\n enum:\n - preserve\n - empty\n additionalProperties: false\n nlpTrigger:\n type: object\n required:\n - property_id\n - trigger\n - enabled\n properties:\n property_id:\n $ref: \"#/$defs/nonEmptyString\"\n trigger:\n $ref: \"#/$defs/nonEmptyString\"\n enabled:\n type: boolean\n additionalProperties: false\n nlpPolicy:\n type: object\n required:\n - triggers\n properties:\n triggers:\n type: array\n items:\n $ref: \"#/$defs/nlpTrigger\"\n additionalProperties: false\n---\n\n# TaskNotes task contract\n\nTypes implement this contract through `implements`; applications consume\nthe normalized contract view rather than assuming frontmatter names.\n" }, { - "source": "types/tasknotes-task/5.md", - "document": "---\nkind: mdbase.type\nname: task\nversion: 4\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a2\n - none\n - open\n - in-progress\n - done\n - cancelled\n default: open\n priority:\n enum: &a4\n - none\n - low\n - normal\n - high\n default: normal\n due: &a1\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled: *a1\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n items:\n type: string\n minLength: 1\n pattern: \\S\n uniqueItems: true\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a3\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n assignees[]:\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.5\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n assignees: assignees\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - materialized-occurrences\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a2\n default: open\n completed_values: *a3\n skipped_values:\n - cancelled\n default_skipped: cancelled\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 0\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: open\n label: Open\n color: \"#64748b\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 1\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: in-progress\n label: In progress\n color: \"#3b82f6\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 2\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: done\n label: Done\n color: \"#22c55e\"\n is_completed: true\n is_skipped: false\n exclude_from_cycle: false\n order: 3\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: cancelled\n label: Cancelled\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: true\n exclude_from_cycle: true\n order: 4\n auto_archive: false\n auto_archive_delay_minutes: 5\n priority:\n values: *a4\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n weight: 0\n - value: low\n label: Low\n color: \"#3b82f6\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#f59e0b\"\n weight: 2\n - value: high\n label: High\n color: \"#ef4444\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n reset_body_checkboxes: false\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n write_format: wikilink\n archive:\n archived_tag: archived\n move_on_archive: false\n time_tracking:\n auto_stop_on_complete: false\n templating:\n enabled: false\n occurrence_enabled: false\nx-tasknotes-generator:\n managed_fields:\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n# Task\n\nTask records live under `tasks/`.\n" + "source": "contracts/tasknotes.task/0.3.0-rc.6.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: tasknotes.task\nversion: 0.3.0-rc.6\nname: TaskNotes task\ndescription: Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.6.\nrecord_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task.schema.json\n title: TaskNotes portable task view\n description: The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.6 record contract.\n type: object\n required:\n - status\n - dateCreated\n additionalProperties: false\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n type: string\n minLength: 1\n priority:\n type: string\n minLength: 1\n due:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n pattern: \\S\n description: Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\n attachments:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrenceAnchor:\n enum:\n - scheduled\n - completion\n recurrenceParent:\n type: string\n occurrenceDate:\n type: string\n format: date\n occurrenceMaterialization:\n enum:\n - manual\n - on_completion\n - rolling\n occurrenceNextTrigger:\n enum:\n - completion\n - completion_or_skip\n occurrenceTemplate:\n type: string\n occurrencePastHorizon:\n type: string\n occurrenceFutureHorizon:\n type: string\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n reminders:\n type: array\n items:\n type: object\n blockedBy:\n type: array\n items:\n type: object\n completeInstances:\n type: array\n items:\n type: string\n format: date\n skippedInstances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n sortOrder:\n type: string\nbinding_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\n title: TaskNotes task data-contract binding\n description: Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.6. App behaviour, such as templates, archive moves, status cycling and natural-language triggers, lives in the app's tasknotes.settings record instead.\n type: object\n required:\n - profiles\n - capabilities\n - title\n - status\n - priority\n - recurrence\n - occurrences\n - links\n - archive\n properties:\n profiles:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - core-lite\n - recurrence\n - templating\n - materialized-occurrences\n - extended\n capabilities:\n type: array\n uniqueItems: true\n items:\n enum:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - rename\n - archive\n - batch\n - concurrency\n - dry-run\n - migration\n - templating\n title:\n $ref: \"#/$defs/titlePolicy\"\n status:\n $ref: \"#/$defs/statusPolicy\"\n priority:\n $ref: \"#/$defs/priorityPolicy\"\n recurrence:\n $ref: \"#/$defs/recurrencePolicy\"\n occurrences:\n $ref: \"#/$defs/occurrencePolicy\"\n links:\n $ref: \"#/$defs/linkPolicy\"\n archive:\n $ref: \"#/$defs/archivePolicy\"\n additionalProperties: false\n $defs:\n nonEmptyString:\n type: string\n minLength: 1\n stringSet:\n type: array\n uniqueItems: true\n items:\n $ref: \"#/$defs/nonEmptyString\"\n titlePolicy:\n type: object\n required:\n - storage\n properties:\n storage:\n enum:\n - filename\n - frontmatter\n filename_format:\n enum:\n - title\n - zettel\n - timestamp\n - uuid\n - custom\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n allOf:\n - if:\n properties:\n filename_format:\n const: custom\n required:\n - filename_format\n then:\n properties:\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n required:\n - custom_filename_template\n additionalProperties: true\n statusDefinition:\n type: object\n required:\n - value\n - label\n - color\n - is_completed\n - is_skipped\n - order\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n is_completed:\n type: boolean\n is_skipped:\n type: boolean\n order:\n type: number\n additionalProperties: true\n statusPolicy:\n type: object\n required:\n - values\n - default\n - completed_values\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n completed_values:\n $ref: \"#/$defs/stringSet\"\n skipped_values:\n $ref: \"#/$defs/stringSet\"\n default_skipped:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/statusDefinition\"\n additionalProperties: false\n priorityDefinition:\n type: object\n required:\n - value\n - label\n - color\n - weight\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n weight:\n type: number\n additionalProperties: true\n priorityPolicy:\n type: object\n required:\n - values\n - default\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/priorityDefinition\"\n additionalProperties: false\n recurrencePolicy:\n type: object\n required:\n - syntax\n - maintain_due_date_offset\n properties:\n syntax:\n const: tasknotes\n maintain_due_date_offset:\n type: boolean\n additionalProperties: false\n occurrencePolicy:\n type: object\n required:\n - identity_roles\n - default_materialization\n - default_next_trigger\n properties:\n identity_roles:\n const:\n - recurrenceParent\n - occurrenceDate\n default_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default_next_trigger:\n enum:\n - completion\n - completion_or_skip\n past_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n future_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n linkPolicy:\n type: object\n required:\n - accepted_formats\n properties:\n accepted_formats:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - wikilink\n - markdown\n additionalProperties: false\n archivePolicy:\n type: object\n required:\n - archived_tag\n properties:\n archived_tag:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n---\n\n# TaskNotes task contract\n\nTypes implement this contract through `implements`; applications consume\nthe normalized contract view rather than assuming frontmatter names.\n" }, { - "source": "schemas/tasknotes.task/0.3.0-rc.5.schema.json", - "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task.schema.json\",\n \"title\": \"TaskNotes portable task view\",\n \"description\": \"The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.5 record contract.\",\n \"type\": \"object\",\n \"required\": [\n \"status\",\n \"dateCreated\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"status\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"priority\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"due\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"scheduled\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"contexts\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"projects\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"assignees\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"pattern\": \"\\\\S\"\n },\n \"description\": \"Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\"\n },\n \"attachments\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"timeEstimate\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"completedDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"dateCreated\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"dateModified\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"recurrence\": {\n \"type\": \"string\"\n },\n \"recurrenceAnchor\": {\n \"enum\": [\n \"scheduled\",\n \"completion\"\n ]\n },\n \"recurrenceParent\": {\n \"type\": \"string\"\n },\n \"occurrenceDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"occurrenceMaterialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"occurrenceNextTrigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"occurrenceTemplate\": {\n \"type\": \"string\"\n },\n \"occurrencePastHorizon\": {\n \"type\": \"string\"\n },\n \"occurrenceFutureHorizon\": {\n \"type\": \"string\"\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"timeEntries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"reminders\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"blockedBy\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"completeInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"skippedInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"icsEventId\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"googleCalendarEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionOriginalScheduled\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"googleCalendarMovedOriginalDates\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"sortOrder\": {\n \"type\": \"string\"\n }\n }\n}\n" + "source": "types/tasknotes-task/6.md", + "document": "---\nkind: mdbase.type\nname: task\nversion: 5\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a2\n - none\n - open\n - in-progress\n - done\n - cancelled\n default: open\n priority:\n enum: &a4\n - none\n - low\n - normal\n - high\n default: normal\n due: &a1\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled: *a1\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n items:\n type: string\n minLength: 1\n pattern: \\S\n uniqueItems: true\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a3\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n assignees[]:\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.6\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n assignees: assignees\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - materialized-occurrences\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a2\n default: open\n completed_values: *a3\n skipped_values:\n - cancelled\n default_skipped: cancelled\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: false\n order: 0\n - value: open\n label: Open\n color: \"#64748b\"\n is_completed: false\n is_skipped: false\n order: 1\n - value: in-progress\n label: In progress\n color: \"#3b82f6\"\n is_completed: false\n is_skipped: false\n order: 2\n - value: done\n label: Done\n color: \"#22c55e\"\n is_completed: true\n is_skipped: false\n order: 3\n - value: cancelled\n label: Cancelled\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: true\n order: 4\n priority:\n values: *a4\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n weight: 0\n - value: low\n label: Low\n color: \"#3b82f6\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#f59e0b\"\n weight: 2\n - value: high\n label: High\n color: \"#ef4444\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n archive:\n archived_tag: archived\nx-tasknotes-generator:\n managed_fields:\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n# Task\n\nTask records live under `tasks/`.\n" }, { - "source": "schemas/tasknotes.task.binding/0.3.0-rc.5.schema.json", - "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\",\n \"title\": \"TaskNotes task data-contract binding\",\n \"description\": \"Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\",\n \"type\": \"object\",\n \"required\": [\n \"profiles\",\n \"capabilities\",\n \"title\",\n \"status\",\n \"priority\",\n \"recurrence\",\n \"occurrences\",\n \"links\",\n \"archive\",\n \"time_tracking\",\n \"templating\"\n ],\n \"properties\": {\n \"profiles\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"core-lite\",\n \"recurrence\",\n \"templating\",\n \"materialized-occurrences\",\n \"extended\"\n ]\n }\n },\n \"capabilities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"dependencies\",\n \"reminders\",\n \"attachments\",\n \"links\",\n \"time-tracking\",\n \"materialized-occurrences\",\n \"rename\",\n \"archive\",\n \"batch\",\n \"concurrency\",\n \"dry-run\",\n \"migration\",\n \"templating\"\n ]\n }\n },\n \"title\": {\n \"$ref\": \"#/$defs/titlePolicy\"\n },\n \"status\": {\n \"$ref\": \"#/$defs/statusPolicy\"\n },\n \"priority\": {\n \"$ref\": \"#/$defs/priorityPolicy\"\n },\n \"recurrence\": {\n \"$ref\": \"#/$defs/recurrencePolicy\"\n },\n \"occurrences\": {\n \"$ref\": \"#/$defs/occurrencePolicy\"\n },\n \"links\": {\n \"$ref\": \"#/$defs/linkPolicy\"\n },\n \"archive\": {\n \"$ref\": \"#/$defs/archivePolicy\"\n },\n \"time_tracking\": {\n \"$ref\": \"#/$defs/timeTrackingPolicy\"\n },\n \"templating\": {\n \"$ref\": \"#/$defs/templatingPolicy\"\n },\n \"nlp\": {\n \"$ref\": \"#/$defs/nlpPolicy\"\n }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"nonEmptyString\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"stringSet\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"titlePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"storage\"\n ],\n \"properties\": {\n \"storage\": {\n \"enum\": [\n \"filename\",\n \"frontmatter\"\n ]\n },\n \"filename_format\": {\n \"enum\": [\n \"title\",\n \"zettel\",\n \"timestamp\",\n \"uuid\",\n \"custom\"\n ]\n },\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"allOf\": [\n {\n \"if\": {\n \"properties\": {\n \"filename_format\": {\n \"const\": \"custom\"\n }\n },\n \"required\": [\n \"filename_format\"\n ]\n },\n \"then\": {\n \"properties\": {\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"required\": [\n \"custom_filename_template\"\n ]\n }\n }\n ],\n \"additionalProperties\": true\n },\n \"statusDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"is_completed\",\n \"is_skipped\",\n \"exclude_from_cycle\",\n \"order\",\n \"auto_archive\",\n \"auto_archive_delay_minutes\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"is_completed\": {\n \"type\": \"boolean\"\n },\n \"is_skipped\": {\n \"type\": \"boolean\"\n },\n \"exclude_from_cycle\": {\n \"type\": \"boolean\"\n },\n \"next_status\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"order\": {\n \"type\": \"number\"\n },\n \"auto_archive\": {\n \"type\": \"boolean\"\n },\n \"auto_archive_delay_minutes\": {\n \"type\": \"number\",\n \"minimum\": 0\n }\n },\n \"additionalProperties\": true\n },\n \"statusPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"completed_values\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"completed_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"skipped_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default_skipped\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/statusDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"priorityDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"weight\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"weight\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"priorityPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/priorityDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"recurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"syntax\",\n \"maintain_due_date_offset\",\n \"reset_body_checkboxes\"\n ],\n \"properties\": {\n \"syntax\": {\n \"const\": \"tasknotes\"\n },\n \"maintain_due_date_offset\": {\n \"type\": \"boolean\"\n },\n \"reset_body_checkboxes\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"occurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"identity_roles\",\n \"default_materialization\",\n \"default_next_trigger\"\n ],\n \"properties\": {\n \"identity_roles\": {\n \"const\": [\n \"recurrenceParent\",\n \"occurrenceDate\"\n ]\n },\n \"default_materialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"default_next_trigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"past_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"future_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"linkPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"accepted_formats\",\n \"write_format\"\n ],\n \"properties\": {\n \"accepted_formats\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"write_format\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"archivePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"archived_tag\",\n \"move_on_archive\"\n ],\n \"properties\": {\n \"archived_tag\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"move_on_archive\": {\n \"type\": \"boolean\"\n },\n \"folder\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"timeTrackingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"auto_stop_on_complete\"\n ],\n \"properties\": {\n \"auto_stop_on_complete\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"templatingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"enabled\",\n \"occurrence_enabled\"\n ],\n \"properties\": {\n \"enabled\": {\n \"type\": \"boolean\"\n },\n \"template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"occurrence_enabled\": {\n \"type\": \"boolean\"\n },\n \"occurrence_template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"failure_mode\": {\n \"enum\": [\n \"error\",\n \"warning_fallback\"\n ]\n },\n \"unknown_variable_policy\": {\n \"enum\": [\n \"preserve\",\n \"empty\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"nlpTrigger\": {\n \"type\": \"object\",\n \"required\": [\n \"property_id\",\n \"trigger\",\n \"enabled\"\n ],\n \"properties\": {\n \"property_id\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"trigger\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"enabled\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nlpPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"triggers\"\n ],\n \"properties\": {\n \"triggers\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/nlpTrigger\"\n }\n }\n },\n \"additionalProperties\": false\n }\n }\n}\n" + "source": "schemas/tasknotes.task/0.3.0-rc.6.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task.schema.json\",\n \"title\": \"TaskNotes portable task view\",\n \"description\": \"The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.6 record contract.\",\n \"type\": \"object\",\n \"required\": [\n \"status\",\n \"dateCreated\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"status\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"priority\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"due\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"scheduled\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"contexts\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"projects\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"assignees\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"pattern\": \"\\\\S\"\n },\n \"description\": \"Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\"\n },\n \"attachments\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"timeEstimate\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"completedDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"dateCreated\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"dateModified\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"recurrence\": {\n \"type\": \"string\"\n },\n \"recurrenceAnchor\": {\n \"enum\": [\n \"scheduled\",\n \"completion\"\n ]\n },\n \"recurrenceParent\": {\n \"type\": \"string\"\n },\n \"occurrenceDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"occurrenceMaterialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"occurrenceNextTrigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"occurrenceTemplate\": {\n \"type\": \"string\"\n },\n \"occurrencePastHorizon\": {\n \"type\": \"string\"\n },\n \"occurrenceFutureHorizon\": {\n \"type\": \"string\"\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"timeEntries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"reminders\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"blockedBy\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"completeInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"skippedInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"icsEventId\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"googleCalendarEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionOriginalScheduled\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"googleCalendarMovedOriginalDates\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"sortOrder\": {\n \"type\": \"string\"\n }\n }\n}\n" + }, + { + "source": "schemas/tasknotes.task.binding/0.3.0-rc.6.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\",\n \"title\": \"TaskNotes task data-contract binding\",\n \"description\": \"Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.6. App behaviour, such as templates, archive moves, status cycling and natural-language triggers, lives in the app's tasknotes.settings record instead.\",\n \"type\": \"object\",\n \"required\": [\n \"profiles\",\n \"capabilities\",\n \"title\",\n \"status\",\n \"priority\",\n \"recurrence\",\n \"occurrences\",\n \"links\",\n \"archive\"\n ],\n \"properties\": {\n \"profiles\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"core-lite\",\n \"recurrence\",\n \"templating\",\n \"materialized-occurrences\",\n \"extended\"\n ]\n }\n },\n \"capabilities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"dependencies\",\n \"reminders\",\n \"attachments\",\n \"links\",\n \"time-tracking\",\n \"materialized-occurrences\",\n \"rename\",\n \"archive\",\n \"batch\",\n \"concurrency\",\n \"dry-run\",\n \"migration\",\n \"templating\"\n ]\n }\n },\n \"title\": {\n \"$ref\": \"#/$defs/titlePolicy\"\n },\n \"status\": {\n \"$ref\": \"#/$defs/statusPolicy\"\n },\n \"priority\": {\n \"$ref\": \"#/$defs/priorityPolicy\"\n },\n \"recurrence\": {\n \"$ref\": \"#/$defs/recurrencePolicy\"\n },\n \"occurrences\": {\n \"$ref\": \"#/$defs/occurrencePolicy\"\n },\n \"links\": {\n \"$ref\": \"#/$defs/linkPolicy\"\n },\n \"archive\": {\n \"$ref\": \"#/$defs/archivePolicy\"\n }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"nonEmptyString\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"stringSet\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"titlePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"storage\"\n ],\n \"properties\": {\n \"storage\": {\n \"enum\": [\n \"filename\",\n \"frontmatter\"\n ]\n },\n \"filename_format\": {\n \"enum\": [\n \"title\",\n \"zettel\",\n \"timestamp\",\n \"uuid\",\n \"custom\"\n ]\n },\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"allOf\": [\n {\n \"if\": {\n \"properties\": {\n \"filename_format\": {\n \"const\": \"custom\"\n }\n },\n \"required\": [\n \"filename_format\"\n ]\n },\n \"then\": {\n \"properties\": {\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"required\": [\n \"custom_filename_template\"\n ]\n }\n }\n ],\n \"additionalProperties\": true\n },\n \"statusDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"is_completed\",\n \"is_skipped\",\n \"order\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"is_completed\": {\n \"type\": \"boolean\"\n },\n \"is_skipped\": {\n \"type\": \"boolean\"\n },\n \"order\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"statusPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"completed_values\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"completed_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"skipped_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default_skipped\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/statusDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"priorityDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"weight\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"weight\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"priorityPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/priorityDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"recurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"syntax\",\n \"maintain_due_date_offset\"\n ],\n \"properties\": {\n \"syntax\": {\n \"const\": \"tasknotes\"\n },\n \"maintain_due_date_offset\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"occurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"identity_roles\",\n \"default_materialization\",\n \"default_next_trigger\"\n ],\n \"properties\": {\n \"identity_roles\": {\n \"const\": [\n \"recurrenceParent\",\n \"occurrenceDate\"\n ]\n },\n \"default_materialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"default_next_trigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"past_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"future_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"linkPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"accepted_formats\"\n ],\n \"properties\": {\n \"accepted_formats\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n }\n },\n \"additionalProperties\": false\n },\n \"archivePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"archived_tag\"\n ],\n \"properties\": {\n \"archived_tag\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n }\n }\n}\n" + }, + { + "source": "schemas/mdbase.app-settings/1.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"mdbase.app-settings 1.0.0 record contract view\",\n \"description\": \"The reserved fields every app settings record shares. Everything else in the record belongs to the app and is described by the app's own settings contract.\",\n \"type\": \"object\",\n \"required\": [\"app\", \"scope\", \"settings_version\"],\n \"properties\": {\n \"app\": {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9]+([.-][a-z0-9]+)*$\",\n \"description\": \"The app whose clients honour these settings, such as tasknotes or dev.mdbase.reader.\"\n },\n \"scope\": {\n \"enum\": [\"collection\", \"user\"],\n \"description\": \"Who the values apply to: everyone using the app in this collection, or one person.\"\n },\n \"user\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"A link to the mdbase.person record the values apply to. Present only when scope is user.\"\n },\n \"settings_version\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"description\": \"The version of the app's settings schema that last wrote the record. A client that does not understand it treats the settings as read-only.\"\n }\n },\n \"allOf\": [\n {\n \"if\": { \"properties\": { \"scope\": { \"const\": \"user\" } }, \"required\": [\"scope\"] },\n \"then\": { \"required\": [\"user\"] },\n \"else\": { \"not\": { \"required\": [\"user\"] } }\n }\n ],\n \"additionalProperties\": true\n}\n" + }, + { + "source": "contracts/mdbase.app-settings/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.app-settings\nversion: 1.0.0\nname: App settings\ndescription: One record per app holding the policy that app's clients honour in this collection.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/mdbase.app-settings/1.0.0.schema.json\n---\n\n# App settings\n\nAn app settings record holds values that every client of one app honours in this\ncollection, such as how new files are named. The values change client behaviour,\nnever what another record means.\n\n## Location and identity\n\nA collection-scope record lives at `_settings/.md`, where `` is the\nrecord's `app` field. Clients read it by that path, without a query. Each\nimplementing type declares `collection.unique` on `app` with `scope: type`, so a\ncollection holds at most one collection-scope record per app.\n\nThe record's `type` field names the app's own settings type, not a shared value.\nGeneric tools find every settings record through this contract.\n\n## Reserved fields\n\n- `app`: the app id.\n- `scope`: `collection`, or `user` for one person's overrides.\n- `user`: a link to an `mdbase.person` record, present only when `scope` is\n `user`.\n- `settings_version`: the version of the app's settings schema that last wrote\n the record.\n\nEvery other frontmatter field belongs to the app and is described by the app's\nown settings contract. The body is free Markdown for people; apps never parse it.\n\n## Client behaviour\n\n- A missing record means every setting has its default. Clients never create the\n record on read.\n- A missing or invalid field takes its default; valid fields are used as\n written. The record is not rewritten until someone saves.\n- A save creates the record at its fixed path the first time, and otherwise\n patches only the fields it changes, guarded by the record's revision.\n- A client that finds a `settings_version` newer than it understands treats the\n settings as read-only and uses them as written.\n\n## Authority\n\nThis artifact is passive. A settings record grants nothing and must never hold\nsecrets: anyone who can read or write records can read or edit it.\n" + }, + { + "source": "schemas/tasknotes.settings/1.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"tasknotes.settings 1.0.0\",\n \"description\": \"Collection-wide behaviour that every TaskNotes client honours. Every field is optional and has a default, so a record with none of them behaves like no record at all.\",\n \"type\": \"object\",\n \"required\": [\n \"app\",\n \"scope\",\n \"settings_version\"\n ],\n \"properties\": {\n \"app\": {\n \"const\": \"dev.tasknotes\",\n \"description\": \"Marks this record as TaskNotes' settings.\"\n },\n \"scope\": {\n \"const\": \"collection\",\n \"description\": \"These settings apply to everyone using TaskNotes in this collection.\"\n },\n \"settings_version\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"description\": \"The version of these settings that last wrote the record. TaskNotes writes 1.\"\n },\n \"nlp_triggers\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"property_id\",\n \"trigger\",\n \"enabled\"\n ],\n \"properties\": {\n \"property_id\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"trigger\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"enabled\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"description\": \"Natural-language capture triggers, such as @ for contexts. Without the field each client uses its own defaults.\"\n },\n \"template_enabled\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Apply template_path to the body of new tasks.\"\n },\n \"template_path\": {\n \"type\": \"string\",\n \"description\": \"The template for new task bodies. Empty or absent means none.\"\n },\n \"occurrence_template_enabled\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Apply occurrence_template_path to new materialized occurrences.\"\n },\n \"occurrence_template_path\": {\n \"type\": \"string\",\n \"description\": \"The template for new occurrence bodies. Empty or absent means none.\"\n },\n \"template_failure_mode\": {\n \"enum\": [\n \"error\",\n \"warning_fallback\"\n ],\n \"description\": \"What a client does when a template cannot be applied.\"\n },\n \"template_unknown_variable_policy\": {\n \"enum\": [\n \"preserve\",\n \"empty\"\n ],\n \"description\": \"What a client writes for a template variable it does not know.\"\n },\n \"move_on_archive\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Move a task's file to archive_folder when it is archived.\"\n },\n \"archive_folder\": {\n \"type\": \"string\",\n \"description\": \"Where archived tasks move to. Empty or absent means the client's default.\"\n },\n \"auto_stop_time_tracking_on_complete\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Stop a running timer when its task is completed.\"\n },\n \"reset_body_checkboxes_on_recurrence\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Uncheck body checkboxes when a recurring task moves to its next occurrence.\"\n },\n \"link_write_format\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ],\n \"default\": \"wikilink\",\n \"description\": \"How clients write links into task frontmatter. Both formats are always read.\"\n },\n \"status_behavior\": {\n \"type\": \"object\",\n \"description\": \"Behaviour per status value. A status without an entry has no next status and no auto-archive, and is out of the status cycle exactly when it is a skipped status.\",\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"next_status\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"The status the cycle moves to next.\"\n },\n \"exclude_from_cycle\": {\n \"type\": \"boolean\",\n \"description\": \"Leave this status out of the status cycle.\"\n },\n \"auto_archive\": {\n \"type\": \"boolean\",\n \"description\": \"Archive a task some time after it takes this status.\"\n },\n \"auto_archive_delay_minutes\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"description\": \"How long after taking this status a task is archived.\"\n }\n },\n \"additionalProperties\": false\n },\n \"default\": {}\n }\n },\n \"additionalProperties\": true\n}\n" + }, + { + "source": "contracts/tasknotes.settings/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: tasknotes.settings\nversion: 1.0.0\nname: TaskNotes settings\ndescription: Collection-wide TaskNotes behaviour, such as templates, archive moves and status cycling.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/tasknotes.settings/1.0.0.schema.json\n---\n\n# TaskNotes settings\n\nThe collection's settings record for TaskNotes, at `_settings/dev.tasknotes.md`.\nIt also implements `mdbase.app-settings`, whose reserved fields (`app`, `scope`,\n`settings_version`) it shares.\n\nThese settings change what TaskNotes clients do, never what a task record\nmeans: that stays in the `tasknotes.task` binding of each task type.\n`tasknotes.task` 0.3.0-rc.6 moved them here from the binding. A client reads\neach setting from this record, then from an earlier binding that still holds\nit, then uses its default, so a collection whose task type is still on rc.5\nkeeps its behaviour until it is upgraded.\n\n## Fields\n\n- `nlp_triggers`: natural-language capture triggers. Without it, each client\n uses its own defaults.\n- `template_enabled`, `template_path`, `occurrence_template_enabled`,\n `occurrence_template_path`, `template_failure_mode`,\n `template_unknown_variable_policy`: body templates for new tasks and\n materialized occurrences.\n- `move_on_archive`, `archive_folder`: whether archiving moves the file, and\n where to.\n- `auto_stop_time_tracking_on_complete`: stop a running timer on completion.\n- `reset_body_checkboxes_on_recurrence`: uncheck body checkboxes when a\n recurring task advances.\n- `link_write_format`: `wikilink` or `markdown` for links written to task\n frontmatter.\n- `status_behavior`: per status value, `next_status`, `exclude_from_cycle`,\n `auto_archive` and `auto_archive_delay_minutes`. A status without an entry\n has no next status and no auto-archive (5 minute delay), and is out of the\n cycle exactly when it is a skipped status.\n\n## Authority\n\nThis artifact is passive and grants nothing.\n" + }, + { + "source": "types/tasknotes-settings/1.md", + "document": "---\nkind: mdbase.type\nname: tasknotes-settings\nversion: 1\ndescription: TaskNotes' collection-wide settings, one record at _settings/dev.tasknotes.md.\nmatch:\n where:\n app:\n eq: dev.tasknotes\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: tasknotes.settings 1.0.0\n description: Collection-wide behaviour that every TaskNotes client honours. Every field is optional and has a default, so a record with none of them behaves like no record at all.\n type: object\n required:\n - app\n - scope\n - settings_version\n properties:\n app:\n const: dev.tasknotes\n description: Marks this record as TaskNotes' settings.\n scope:\n const: collection\n description: These settings apply to everyone using TaskNotes in this collection.\n settings_version:\n type: integer\n minimum: 1\n description: The version of these settings that last wrote the record. TaskNotes writes 1.\n nlp_triggers:\n type: array\n items:\n type: object\n required:\n - property_id\n - trigger\n - enabled\n properties:\n property_id:\n type: string\n minLength: 1\n trigger:\n type: string\n minLength: 1\n enabled:\n type: boolean\n additionalProperties: false\n description: Natural-language capture triggers, such as @ for contexts. Without the field each client uses its own defaults.\n template_enabled:\n type: boolean\n default: false\n description: Apply template_path to the body of new tasks.\n template_path:\n type: string\n description: The template for new task bodies. Empty or absent means none.\n occurrence_template_enabled:\n type: boolean\n default: false\n description: Apply occurrence_template_path to new materialized occurrences.\n occurrence_template_path:\n type: string\n description: The template for new occurrence bodies. Empty or absent means none.\n template_failure_mode:\n enum:\n - error\n - warning_fallback\n description: What a client does when a template cannot be applied.\n template_unknown_variable_policy:\n enum:\n - preserve\n - empty\n description: What a client writes for a template variable it does not know.\n move_on_archive:\n type: boolean\n default: false\n description: Move a task's file to archive_folder when it is archived.\n archive_folder:\n type: string\n description: Where archived tasks move to. Empty or absent means the client's default.\n auto_stop_time_tracking_on_complete:\n type: boolean\n default: false\n description: Stop a running timer when its task is completed.\n reset_body_checkboxes_on_recurrence:\n type: boolean\n default: false\n description: Uncheck body checkboxes when a recurring task moves to its next occurrence.\n link_write_format:\n enum:\n - wikilink\n - markdown\n default: wikilink\n description: How clients write links into task frontmatter. Both formats are always read.\n status_behavior:\n type: object\n description: Behaviour per status value. A status without an entry has no next status and no auto-archive, and is out of the status cycle exactly when it is a skipped status.\n additionalProperties:\n type: object\n properties:\n next_status:\n type: string\n minLength: 1\n description: The status the cycle moves to next.\n exclude_from_cycle:\n type: boolean\n description: Leave this status out of the status cycle.\n auto_archive:\n type: boolean\n description: Archive a task some time after it takes this status.\n auto_archive_delay_minutes:\n type: number\n minimum: 0\n description: How long after taking this status a task is archived.\n additionalProperties: false\n default: {}\n additionalProperties: true\ncollection:\n unique:\n - field: app\n scope: type\nimplements:\n - contract: mdbase.app-settings\n version: 1.0.0\n fields:\n app: app\n scope: scope\n settings_version: settings_version\n - contract: tasknotes.settings\n version: 1.0.0\n fields:\n app: app\n scope: scope\n settings_version: settings_version\n nlp_triggers: nlp_triggers\n template_enabled: template_enabled\n template_path: template_path\n occurrence_template_enabled: occurrence_template_enabled\n occurrence_template_path: occurrence_template_path\n template_failure_mode: template_failure_mode\n template_unknown_variable_policy: template_unknown_variable_policy\n move_on_archive: move_on_archive\n archive_folder: archive_folder\n auto_stop_time_tracking_on_complete: auto_stop_time_tracking_on_complete\n reset_body_checkboxes_on_recurrence: reset_body_checkboxes_on_recurrence\n link_write_format: link_write_format\n status_behavior: status_behavior\n---\n\n# TaskNotes settings\n\nSettings that every TaskNotes client honours in this collection, such as body\ntemplates, archive moves and status cycling. TaskNotes reads them from\n`_settings/dev.tasknotes.md` and writes only the fields a save changes.\n" } ]; diff --git a/src/services/legacyTaskNotesSupport.ts b/src/services/legacyTaskNotesSupport.ts index d0104d99d..52c2b9c7d 100644 --- a/src/services/legacyTaskNotesSupport.ts +++ b/src/services/legacyTaskNotesSupport.ts @@ -2,10 +2,14 @@ // 0.3.0-rc.3 as generated by @tasknotes/model 0.3.0-rc.9 to rc.11 (v5 betas and // earlier TaskNotes App builds) and as published in pack 0.3.0-rc.12. These are // replaced by the current pack without a backup; anything else is preserved. +// Also the tasknotes.task 0.3.0-rc.5 schemas (packs rc.15 to rc.18, TaskNotes +// 5.0 betas), which rc.6 replaces at the same paths. export const legacyTaskNotesSupport: readonly string[] = [ "---\nkind: mdbase.contract\ncontract_type: record\nid: tasknotes.task\nversion: 0.3.0-rc.3\nname: TaskNotes task\ndescription: Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.3.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../_schemas/tasknotes/tasknotes-task.schema.json\nbinding_schema:\n dialect: json-schema-2020-12\n ref: ../_schemas/tasknotes/tasknotes-task-binding.schema.json\n---\n\n# TaskNotes task contract\n\nTypes implement this contract through `implements`; applications consume\nthe normalized contract view rather than assuming frontmatter names.\n", "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task.schema.json\",\n \"title\": \"TaskNotes portable task view\",\n \"description\": \"The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.3 record contract.\",\n \"type\": \"object\",\n \"required\": [\n \"status\",\n \"dateCreated\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"status\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"priority\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"due\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"scheduled\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"contexts\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"projects\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"attachments\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"timeEstimate\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"completedDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"dateCreated\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"dateModified\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"recurrence\": {\n \"type\": \"string\"\n },\n \"recurrenceAnchor\": {\n \"enum\": [\n \"scheduled\",\n \"completion\"\n ]\n },\n \"recurrenceParent\": {\n \"type\": \"string\"\n },\n \"occurrenceDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"occurrenceMaterialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"occurrenceNextTrigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"occurrenceTemplate\": {\n \"type\": \"string\"\n },\n \"occurrencePastHorizon\": {\n \"type\": \"string\"\n },\n \"occurrenceFutureHorizon\": {\n \"type\": \"string\"\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"timeEntries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"reminders\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"blockedBy\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"completeInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"skippedInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"icsEventId\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"googleCalendarEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionOriginalScheduled\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"googleCalendarMovedOriginalDates\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"sortOrder\": {\n \"type\": \"string\"\n }\n }\n}\n", "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\",\n \"title\": \"TaskNotes task data-contract binding\",\n \"description\": \"Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\",\n \"type\": \"object\",\n \"required\": [\n \"profiles\",\n \"capabilities\",\n \"title\",\n \"status\",\n \"priority\",\n \"recurrence\",\n \"occurrences\",\n \"links\",\n \"archive\",\n \"time_tracking\",\n \"templating\"\n ],\n \"properties\": {\n \"profiles\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"core-lite\",\n \"recurrence\",\n \"templating\",\n \"materialized-occurrences\",\n \"extended\"\n ]\n }\n },\n \"capabilities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"dependencies\",\n \"reminders\",\n \"attachments\",\n \"links\",\n \"time-tracking\",\n \"materialized-occurrences\",\n \"rename\",\n \"archive\",\n \"batch\",\n \"concurrency\",\n \"dry-run\",\n \"migration\",\n \"templating\"\n ]\n }\n },\n \"title\": {\n \"$ref\": \"#/$defs/titlePolicy\"\n },\n \"status\": {\n \"$ref\": \"#/$defs/statusPolicy\"\n },\n \"priority\": {\n \"$ref\": \"#/$defs/priorityPolicy\"\n },\n \"runtime_timezone\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"recurrence\": {\n \"$ref\": \"#/$defs/recurrencePolicy\"\n },\n \"occurrences\": {\n \"$ref\": \"#/$defs/occurrencePolicy\"\n },\n \"links\": {\n \"$ref\": \"#/$defs/linkPolicy\"\n },\n \"archive\": {\n \"$ref\": \"#/$defs/archivePolicy\"\n },\n \"time_tracking\": {\n \"$ref\": \"#/$defs/timeTrackingPolicy\"\n },\n \"templating\": {\n \"$ref\": \"#/$defs/templatingPolicy\"\n },\n \"nlp\": {\n \"$ref\": \"#/$defs/nlpPolicy\"\n }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"nonEmptyString\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"stringSet\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"titlePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"storage\"\n ],\n \"properties\": {\n \"storage\": {\n \"enum\": [\n \"filename\",\n \"frontmatter\"\n ]\n },\n \"filename_format\": {\n \"enum\": [\n \"title\",\n \"zettel\",\n \"timestamp\",\n \"uuid\",\n \"custom\"\n ]\n },\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"allOf\": [\n {\n \"if\": {\n \"properties\": {\n \"filename_format\": {\n \"const\": \"custom\"\n }\n },\n \"required\": [\n \"filename_format\"\n ]\n },\n \"then\": {\n \"properties\": {\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"required\": [\n \"custom_filename_template\"\n ]\n }\n }\n ],\n \"additionalProperties\": true\n },\n \"statusDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"is_completed\",\n \"is_skipped\",\n \"exclude_from_cycle\",\n \"order\",\n \"auto_archive\",\n \"auto_archive_delay_minutes\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"is_completed\": {\n \"type\": \"boolean\"\n },\n \"is_skipped\": {\n \"type\": \"boolean\"\n },\n \"exclude_from_cycle\": {\n \"type\": \"boolean\"\n },\n \"next_status\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"order\": {\n \"type\": \"number\"\n },\n \"auto_archive\": {\n \"type\": \"boolean\"\n },\n \"auto_archive_delay_minutes\": {\n \"type\": \"number\",\n \"minimum\": 0\n }\n },\n \"additionalProperties\": true\n },\n \"statusPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"completed_values\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"completed_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"skipped_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default_skipped\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/statusDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"priorityDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"weight\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"weight\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"priorityPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/priorityDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"recurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"syntax\",\n \"maintain_due_date_offset\",\n \"reset_body_checkboxes\"\n ],\n \"properties\": {\n \"syntax\": {\n \"const\": \"tasknotes\"\n },\n \"maintain_due_date_offset\": {\n \"type\": \"boolean\"\n },\n \"reset_body_checkboxes\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"occurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"identity_roles\",\n \"default_materialization\",\n \"default_next_trigger\"\n ],\n \"properties\": {\n \"identity_roles\": {\n \"const\": [\n \"recurrenceParent\",\n \"occurrenceDate\"\n ]\n },\n \"default_materialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"default_next_trigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"past_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"future_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"linkPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"accepted_formats\",\n \"write_format\"\n ],\n \"properties\": {\n \"accepted_formats\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"write_format\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"archivePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"archived_tag\",\n \"move_on_archive\"\n ],\n \"properties\": {\n \"archived_tag\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"move_on_archive\": {\n \"type\": \"boolean\"\n },\n \"folder\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"timeTrackingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"auto_stop_on_complete\"\n ],\n \"properties\": {\n \"auto_stop_on_complete\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"templatingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"enabled\",\n \"occurrence_enabled\"\n ],\n \"properties\": {\n \"enabled\": {\n \"type\": \"boolean\"\n },\n \"template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"occurrence_enabled\": {\n \"type\": \"boolean\"\n },\n \"occurrence_template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"failure_mode\": {\n \"enum\": [\n \"error\",\n \"warning_fallback\"\n ]\n },\n \"unknown_variable_policy\": {\n \"enum\": [\n \"preserve\",\n \"empty\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"nlpTrigger\": {\n \"type\": \"object\",\n \"required\": [\n \"property_id\",\n \"trigger\",\n \"enabled\"\n ],\n \"properties\": {\n \"property_id\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"trigger\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"enabled\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nlpPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"triggers\"\n ],\n \"properties\": {\n \"triggers\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/nlpTrigger\"\n }\n }\n },\n \"additionalProperties\": false\n }\n }\n}\n", "---\nkind: mdbase.contract\ncontract_type: record\nid: tasknotes.task\nversion: 0.3.0-rc.3\nname: TaskNotes task\ndescription: Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.3.\nrecord_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task.schema.json\n title: TaskNotes portable task view\n description: The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.3 record contract.\n type: object\n required:\n - status\n - dateCreated\n additionalProperties: false\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n type: string\n minLength: 1\n priority:\n type: string\n minLength: 1\n due:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n attachments:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrenceAnchor:\n enum:\n - scheduled\n - completion\n recurrenceParent:\n type: string\n occurrenceDate:\n type: string\n format: date\n occurrenceMaterialization:\n enum:\n - manual\n - on_completion\n - rolling\n occurrenceNextTrigger:\n enum:\n - completion\n - completion_or_skip\n occurrenceTemplate:\n type: string\n occurrencePastHorizon:\n type: string\n occurrenceFutureHorizon:\n type: string\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n reminders:\n type: array\n items:\n type: object\n blockedBy:\n type: array\n items:\n type: object\n completeInstances:\n type: array\n items:\n type: string\n format: date\n skippedInstances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n sortOrder:\n type: string\nbinding_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\n title: TaskNotes task data-contract binding\n description: Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\n type: object\n required:\n - profiles\n - capabilities\n - title\n - status\n - priority\n - recurrence\n - occurrences\n - links\n - archive\n - time_tracking\n - templating\n properties:\n profiles:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - core-lite\n - recurrence\n - templating\n - materialized-occurrences\n - extended\n capabilities:\n type: array\n uniqueItems: true\n items:\n enum:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - rename\n - archive\n - batch\n - concurrency\n - dry-run\n - migration\n - templating\n title:\n $ref: \"#/$defs/titlePolicy\"\n status:\n $ref: \"#/$defs/statusPolicy\"\n priority:\n $ref: \"#/$defs/priorityPolicy\"\n recurrence:\n $ref: \"#/$defs/recurrencePolicy\"\n occurrences:\n $ref: \"#/$defs/occurrencePolicy\"\n links:\n $ref: \"#/$defs/linkPolicy\"\n archive:\n $ref: \"#/$defs/archivePolicy\"\n time_tracking:\n $ref: \"#/$defs/timeTrackingPolicy\"\n templating:\n $ref: \"#/$defs/templatingPolicy\"\n nlp:\n $ref: \"#/$defs/nlpPolicy\"\n additionalProperties: false\n $defs:\n nonEmptyString:\n type: string\n minLength: 1\n stringSet:\n type: array\n uniqueItems: true\n items:\n $ref: \"#/$defs/nonEmptyString\"\n titlePolicy:\n type: object\n required:\n - storage\n properties:\n storage:\n enum:\n - filename\n - frontmatter\n filename_format:\n enum:\n - title\n - zettel\n - timestamp\n - uuid\n - custom\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n allOf:\n - if:\n properties:\n filename_format:\n const: custom\n required:\n - filename_format\n then:\n properties:\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n required:\n - custom_filename_template\n additionalProperties: true\n statusDefinition:\n type: object\n required:\n - value\n - label\n - color\n - is_completed\n - is_skipped\n - exclude_from_cycle\n - order\n - auto_archive\n - auto_archive_delay_minutes\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n is_completed:\n type: boolean\n is_skipped:\n type: boolean\n exclude_from_cycle:\n type: boolean\n next_status:\n $ref: \"#/$defs/nonEmptyString\"\n order:\n type: number\n auto_archive:\n type: boolean\n auto_archive_delay_minutes:\n type: number\n minimum: 0\n additionalProperties: true\n statusPolicy:\n type: object\n required:\n - values\n - default\n - completed_values\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n completed_values:\n $ref: \"#/$defs/stringSet\"\n skipped_values:\n $ref: \"#/$defs/stringSet\"\n default_skipped:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/statusDefinition\"\n additionalProperties: false\n priorityDefinition:\n type: object\n required:\n - value\n - label\n - color\n - weight\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n weight:\n type: number\n additionalProperties: true\n priorityPolicy:\n type: object\n required:\n - values\n - default\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/priorityDefinition\"\n additionalProperties: false\n recurrencePolicy:\n type: object\n required:\n - syntax\n - maintain_due_date_offset\n - reset_body_checkboxes\n properties:\n syntax:\n const: tasknotes\n maintain_due_date_offset:\n type: boolean\n reset_body_checkboxes:\n type: boolean\n additionalProperties: false\n occurrencePolicy:\n type: object\n required:\n - identity_roles\n - default_materialization\n - default_next_trigger\n properties:\n identity_roles:\n const:\n - recurrenceParent\n - occurrenceDate\n default_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default_next_trigger:\n enum:\n - completion\n - completion_or_skip\n past_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n future_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n linkPolicy:\n type: object\n required:\n - accepted_formats\n - write_format\n properties:\n accepted_formats:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - wikilink\n - markdown\n write_format:\n enum:\n - wikilink\n - markdown\n additionalProperties: false\n archivePolicy:\n type: object\n required:\n - archived_tag\n - move_on_archive\n properties:\n archived_tag:\n $ref: \"#/$defs/nonEmptyString\"\n move_on_archive:\n type: boolean\n folder:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n timeTrackingPolicy:\n type: object\n required:\n - auto_stop_on_complete\n properties:\n auto_stop_on_complete:\n type: boolean\n additionalProperties: false\n templatingPolicy:\n type: object\n required:\n - enabled\n - occurrence_enabled\n properties:\n enabled:\n type: boolean\n template_path:\n $ref: \"#/$defs/nonEmptyString\"\n occurrence_enabled:\n type: boolean\n occurrence_template_path:\n $ref: \"#/$defs/nonEmptyString\"\n failure_mode:\n enum:\n - error\n - warning_fallback\n unknown_variable_policy:\n enum:\n - preserve\n - empty\n additionalProperties: false\n nlpTrigger:\n type: object\n required:\n - property_id\n - trigger\n - enabled\n properties:\n property_id:\n $ref: \"#/$defs/nonEmptyString\"\n trigger:\n $ref: \"#/$defs/nonEmptyString\"\n enabled:\n type: boolean\n additionalProperties: false\n nlpPolicy:\n type: object\n required:\n - triggers\n properties:\n triggers:\n type: array\n items:\n $ref: \"#/$defs/nlpTrigger\"\n additionalProperties: false\n---\n\n# TaskNotes task contract\n\nTypes implement this contract through `implements`; applications consume\nthe normalized contract view rather than assuming frontmatter names.\n", - "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\",\n \"title\": \"TaskNotes task data-contract binding\",\n \"description\": \"Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\",\n \"type\": \"object\",\n \"required\": [\n \"profiles\",\n \"capabilities\",\n \"title\",\n \"status\",\n \"priority\",\n \"recurrence\",\n \"occurrences\",\n \"links\",\n \"archive\",\n \"time_tracking\",\n \"templating\"\n ],\n \"properties\": {\n \"profiles\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"core-lite\",\n \"recurrence\",\n \"templating\",\n \"materialized-occurrences\",\n \"extended\"\n ]\n }\n },\n \"capabilities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"dependencies\",\n \"reminders\",\n \"attachments\",\n \"links\",\n \"time-tracking\",\n \"materialized-occurrences\",\n \"rename\",\n \"archive\",\n \"batch\",\n \"concurrency\",\n \"dry-run\",\n \"migration\",\n \"templating\"\n ]\n }\n },\n \"title\": {\n \"$ref\": \"#/$defs/titlePolicy\"\n },\n \"status\": {\n \"$ref\": \"#/$defs/statusPolicy\"\n },\n \"priority\": {\n \"$ref\": \"#/$defs/priorityPolicy\"\n },\n \"recurrence\": {\n \"$ref\": \"#/$defs/recurrencePolicy\"\n },\n \"occurrences\": {\n \"$ref\": \"#/$defs/occurrencePolicy\"\n },\n \"links\": {\n \"$ref\": \"#/$defs/linkPolicy\"\n },\n \"archive\": {\n \"$ref\": \"#/$defs/archivePolicy\"\n },\n \"time_tracking\": {\n \"$ref\": \"#/$defs/timeTrackingPolicy\"\n },\n \"templating\": {\n \"$ref\": \"#/$defs/templatingPolicy\"\n },\n \"nlp\": {\n \"$ref\": \"#/$defs/nlpPolicy\"\n }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"nonEmptyString\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"stringSet\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"titlePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"storage\"\n ],\n \"properties\": {\n \"storage\": {\n \"enum\": [\n \"filename\",\n \"frontmatter\"\n ]\n },\n \"filename_format\": {\n \"enum\": [\n \"title\",\n \"zettel\",\n \"timestamp\",\n \"uuid\",\n \"custom\"\n ]\n },\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"allOf\": [\n {\n \"if\": {\n \"properties\": {\n \"filename_format\": {\n \"const\": \"custom\"\n }\n },\n \"required\": [\n \"filename_format\"\n ]\n },\n \"then\": {\n \"properties\": {\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"required\": [\n \"custom_filename_template\"\n ]\n }\n }\n ],\n \"additionalProperties\": true\n },\n \"statusDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"is_completed\",\n \"is_skipped\",\n \"exclude_from_cycle\",\n \"order\",\n \"auto_archive\",\n \"auto_archive_delay_minutes\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"is_completed\": {\n \"type\": \"boolean\"\n },\n \"is_skipped\": {\n \"type\": \"boolean\"\n },\n \"exclude_from_cycle\": {\n \"type\": \"boolean\"\n },\n \"next_status\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"order\": {\n \"type\": \"number\"\n },\n \"auto_archive\": {\n \"type\": \"boolean\"\n },\n \"auto_archive_delay_minutes\": {\n \"type\": \"number\",\n \"minimum\": 0\n }\n },\n \"additionalProperties\": true\n },\n \"statusPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"completed_values\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"completed_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"skipped_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default_skipped\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/statusDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"priorityDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"weight\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"weight\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"priorityPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/priorityDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"recurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"syntax\",\n \"maintain_due_date_offset\",\n \"reset_body_checkboxes\"\n ],\n \"properties\": {\n \"syntax\": {\n \"const\": \"tasknotes\"\n },\n \"maintain_due_date_offset\": {\n \"type\": \"boolean\"\n },\n \"reset_body_checkboxes\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"occurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"identity_roles\",\n \"default_materialization\",\n \"default_next_trigger\"\n ],\n \"properties\": {\n \"identity_roles\": {\n \"const\": [\n \"recurrenceParent\",\n \"occurrenceDate\"\n ]\n },\n \"default_materialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"default_next_trigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"past_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"future_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"linkPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"accepted_formats\",\n \"write_format\"\n ],\n \"properties\": {\n \"accepted_formats\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"write_format\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"archivePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"archived_tag\",\n \"move_on_archive\"\n ],\n \"properties\": {\n \"archived_tag\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"move_on_archive\": {\n \"type\": \"boolean\"\n },\n \"folder\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"timeTrackingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"auto_stop_on_complete\"\n ],\n \"properties\": {\n \"auto_stop_on_complete\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"templatingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"enabled\",\n \"occurrence_enabled\"\n ],\n \"properties\": {\n \"enabled\": {\n \"type\": \"boolean\"\n },\n \"template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"occurrence_enabled\": {\n \"type\": \"boolean\"\n },\n \"occurrence_template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"failure_mode\": {\n \"enum\": [\n \"error\",\n \"warning_fallback\"\n ]\n },\n \"unknown_variable_policy\": {\n \"enum\": [\n \"preserve\",\n \"empty\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"nlpTrigger\": {\n \"type\": \"object\",\n \"required\": [\n \"property_id\",\n \"trigger\",\n \"enabled\"\n ],\n \"properties\": {\n \"property_id\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"trigger\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"enabled\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nlpPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"triggers\"\n ],\n \"properties\": {\n \"triggers\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/nlpTrigger\"\n }\n }\n },\n \"additionalProperties\": false\n }\n }\n}\n" + "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\",\n \"title\": \"TaskNotes task data-contract binding\",\n \"description\": \"Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\",\n \"type\": \"object\",\n \"required\": [\n \"profiles\",\n \"capabilities\",\n \"title\",\n \"status\",\n \"priority\",\n \"recurrence\",\n \"occurrences\",\n \"links\",\n \"archive\",\n \"time_tracking\",\n \"templating\"\n ],\n \"properties\": {\n \"profiles\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"core-lite\",\n \"recurrence\",\n \"templating\",\n \"materialized-occurrences\",\n \"extended\"\n ]\n }\n },\n \"capabilities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"dependencies\",\n \"reminders\",\n \"attachments\",\n \"links\",\n \"time-tracking\",\n \"materialized-occurrences\",\n \"rename\",\n \"archive\",\n \"batch\",\n \"concurrency\",\n \"dry-run\",\n \"migration\",\n \"templating\"\n ]\n }\n },\n \"title\": {\n \"$ref\": \"#/$defs/titlePolicy\"\n },\n \"status\": {\n \"$ref\": \"#/$defs/statusPolicy\"\n },\n \"priority\": {\n \"$ref\": \"#/$defs/priorityPolicy\"\n },\n \"recurrence\": {\n \"$ref\": \"#/$defs/recurrencePolicy\"\n },\n \"occurrences\": {\n \"$ref\": \"#/$defs/occurrencePolicy\"\n },\n \"links\": {\n \"$ref\": \"#/$defs/linkPolicy\"\n },\n \"archive\": {\n \"$ref\": \"#/$defs/archivePolicy\"\n },\n \"time_tracking\": {\n \"$ref\": \"#/$defs/timeTrackingPolicy\"\n },\n \"templating\": {\n \"$ref\": \"#/$defs/templatingPolicy\"\n },\n \"nlp\": {\n \"$ref\": \"#/$defs/nlpPolicy\"\n }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"nonEmptyString\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"stringSet\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"titlePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"storage\"\n ],\n \"properties\": {\n \"storage\": {\n \"enum\": [\n \"filename\",\n \"frontmatter\"\n ]\n },\n \"filename_format\": {\n \"enum\": [\n \"title\",\n \"zettel\",\n \"timestamp\",\n \"uuid\",\n \"custom\"\n ]\n },\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"allOf\": [\n {\n \"if\": {\n \"properties\": {\n \"filename_format\": {\n \"const\": \"custom\"\n }\n },\n \"required\": [\n \"filename_format\"\n ]\n },\n \"then\": {\n \"properties\": {\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"required\": [\n \"custom_filename_template\"\n ]\n }\n }\n ],\n \"additionalProperties\": true\n },\n \"statusDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"is_completed\",\n \"is_skipped\",\n \"exclude_from_cycle\",\n \"order\",\n \"auto_archive\",\n \"auto_archive_delay_minutes\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"is_completed\": {\n \"type\": \"boolean\"\n },\n \"is_skipped\": {\n \"type\": \"boolean\"\n },\n \"exclude_from_cycle\": {\n \"type\": \"boolean\"\n },\n \"next_status\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"order\": {\n \"type\": \"number\"\n },\n \"auto_archive\": {\n \"type\": \"boolean\"\n },\n \"auto_archive_delay_minutes\": {\n \"type\": \"number\",\n \"minimum\": 0\n }\n },\n \"additionalProperties\": true\n },\n \"statusPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"completed_values\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"completed_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"skipped_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default_skipped\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/statusDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"priorityDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"weight\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"weight\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"priorityPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/priorityDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"recurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"syntax\",\n \"maintain_due_date_offset\",\n \"reset_body_checkboxes\"\n ],\n \"properties\": {\n \"syntax\": {\n \"const\": \"tasknotes\"\n },\n \"maintain_due_date_offset\": {\n \"type\": \"boolean\"\n },\n \"reset_body_checkboxes\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"occurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"identity_roles\",\n \"default_materialization\",\n \"default_next_trigger\"\n ],\n \"properties\": {\n \"identity_roles\": {\n \"const\": [\n \"recurrenceParent\",\n \"occurrenceDate\"\n ]\n },\n \"default_materialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"default_next_trigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"past_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"future_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"linkPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"accepted_formats\",\n \"write_format\"\n ],\n \"properties\": {\n \"accepted_formats\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"write_format\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"archivePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"archived_tag\",\n \"move_on_archive\"\n ],\n \"properties\": {\n \"archived_tag\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"move_on_archive\": {\n \"type\": \"boolean\"\n },\n \"folder\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"timeTrackingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"auto_stop_on_complete\"\n ],\n \"properties\": {\n \"auto_stop_on_complete\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"templatingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"enabled\",\n \"occurrence_enabled\"\n ],\n \"properties\": {\n \"enabled\": {\n \"type\": \"boolean\"\n },\n \"template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"occurrence_enabled\": {\n \"type\": \"boolean\"\n },\n \"occurrence_template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"failure_mode\": {\n \"enum\": [\n \"error\",\n \"warning_fallback\"\n ]\n },\n \"unknown_variable_policy\": {\n \"enum\": [\n \"preserve\",\n \"empty\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"nlpTrigger\": {\n \"type\": \"object\",\n \"required\": [\n \"property_id\",\n \"trigger\",\n \"enabled\"\n ],\n \"properties\": {\n \"property_id\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"trigger\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"enabled\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nlpPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"triggers\"\n ],\n \"properties\": {\n \"triggers\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/nlpTrigger\"\n }\n }\n },\n \"additionalProperties\": false\n }\n }\n}\n", + "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task.schema.json\",\n \"title\": \"TaskNotes portable task view\",\n \"description\": \"The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.5 record contract.\",\n \"type\": \"object\",\n \"required\": [\n \"status\",\n \"dateCreated\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"status\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"priority\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"due\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"scheduled\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"contexts\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"projects\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"assignees\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"pattern\": \"\\\\S\"\n },\n \"description\": \"Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\"\n },\n \"attachments\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"timeEstimate\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"completedDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"dateCreated\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"dateModified\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"recurrence\": {\n \"type\": \"string\"\n },\n \"recurrenceAnchor\": {\n \"enum\": [\n \"scheduled\",\n \"completion\"\n ]\n },\n \"recurrenceParent\": {\n \"type\": \"string\"\n },\n \"occurrenceDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"occurrenceMaterialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"occurrenceNextTrigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"occurrenceTemplate\": {\n \"type\": \"string\"\n },\n \"occurrencePastHorizon\": {\n \"type\": \"string\"\n },\n \"occurrenceFutureHorizon\": {\n \"type\": \"string\"\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"timeEntries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"reminders\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"blockedBy\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"completeInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"skippedInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"icsEventId\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"googleCalendarEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionOriginalScheduled\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"googleCalendarMovedOriginalDates\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"sortOrder\": {\n \"type\": \"string\"\n }\n }\n}\n", + "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\",\n \"title\": \"TaskNotes task data-contract binding\",\n \"description\": \"Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\",\n \"type\": \"object\",\n \"required\": [\n \"profiles\",\n \"capabilities\",\n \"title\",\n \"status\",\n \"priority\",\n \"recurrence\",\n \"occurrences\",\n \"links\",\n \"archive\",\n \"time_tracking\",\n \"templating\"\n ],\n \"properties\": {\n \"profiles\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"core-lite\",\n \"recurrence\",\n \"templating\",\n \"materialized-occurrences\",\n \"extended\"\n ]\n }\n },\n \"capabilities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"dependencies\",\n \"reminders\",\n \"attachments\",\n \"links\",\n \"time-tracking\",\n \"materialized-occurrences\",\n \"rename\",\n \"archive\",\n \"batch\",\n \"concurrency\",\n \"dry-run\",\n \"migration\",\n \"templating\"\n ]\n }\n },\n \"title\": {\n \"$ref\": \"#/$defs/titlePolicy\"\n },\n \"status\": {\n \"$ref\": \"#/$defs/statusPolicy\"\n },\n \"priority\": {\n \"$ref\": \"#/$defs/priorityPolicy\"\n },\n \"recurrence\": {\n \"$ref\": \"#/$defs/recurrencePolicy\"\n },\n \"occurrences\": {\n \"$ref\": \"#/$defs/occurrencePolicy\"\n },\n \"links\": {\n \"$ref\": \"#/$defs/linkPolicy\"\n },\n \"archive\": {\n \"$ref\": \"#/$defs/archivePolicy\"\n },\n \"time_tracking\": {\n \"$ref\": \"#/$defs/timeTrackingPolicy\"\n },\n \"templating\": {\n \"$ref\": \"#/$defs/templatingPolicy\"\n },\n \"nlp\": {\n \"$ref\": \"#/$defs/nlpPolicy\"\n }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"nonEmptyString\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"stringSet\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"titlePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"storage\"\n ],\n \"properties\": {\n \"storage\": {\n \"enum\": [\n \"filename\",\n \"frontmatter\"\n ]\n },\n \"filename_format\": {\n \"enum\": [\n \"title\",\n \"zettel\",\n \"timestamp\",\n \"uuid\",\n \"custom\"\n ]\n },\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"allOf\": [\n {\n \"if\": {\n \"properties\": {\n \"filename_format\": {\n \"const\": \"custom\"\n }\n },\n \"required\": [\n \"filename_format\"\n ]\n },\n \"then\": {\n \"properties\": {\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"required\": [\n \"custom_filename_template\"\n ]\n }\n }\n ],\n \"additionalProperties\": true\n },\n \"statusDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"is_completed\",\n \"is_skipped\",\n \"exclude_from_cycle\",\n \"order\",\n \"auto_archive\",\n \"auto_archive_delay_minutes\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"is_completed\": {\n \"type\": \"boolean\"\n },\n \"is_skipped\": {\n \"type\": \"boolean\"\n },\n \"exclude_from_cycle\": {\n \"type\": \"boolean\"\n },\n \"next_status\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"order\": {\n \"type\": \"number\"\n },\n \"auto_archive\": {\n \"type\": \"boolean\"\n },\n \"auto_archive_delay_minutes\": {\n \"type\": \"number\",\n \"minimum\": 0\n }\n },\n \"additionalProperties\": true\n },\n \"statusPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"completed_values\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"completed_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"skipped_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default_skipped\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/statusDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"priorityDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"weight\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"weight\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"priorityPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/priorityDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"recurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"syntax\",\n \"maintain_due_date_offset\",\n \"reset_body_checkboxes\"\n ],\n \"properties\": {\n \"syntax\": {\n \"const\": \"tasknotes\"\n },\n \"maintain_due_date_offset\": {\n \"type\": \"boolean\"\n },\n \"reset_body_checkboxes\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"occurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"identity_roles\",\n \"default_materialization\",\n \"default_next_trigger\"\n ],\n \"properties\": {\n \"identity_roles\": {\n \"const\": [\n \"recurrenceParent\",\n \"occurrenceDate\"\n ]\n },\n \"default_materialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"default_next_trigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"past_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"future_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"linkPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"accepted_formats\",\n \"write_format\"\n ],\n \"properties\": {\n \"accepted_formats\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"write_format\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"archivePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"archived_tag\",\n \"move_on_archive\"\n ],\n \"properties\": {\n \"archived_tag\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"move_on_archive\": {\n \"type\": \"boolean\"\n },\n \"folder\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"timeTrackingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"auto_stop_on_complete\"\n ],\n \"properties\": {\n \"auto_stop_on_complete\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"templatingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"enabled\",\n \"occurrence_enabled\"\n ],\n \"properties\": {\n \"enabled\": {\n \"type\": \"boolean\"\n },\n \"template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"occurrence_enabled\": {\n \"type\": \"boolean\"\n },\n \"occurrence_template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"failure_mode\": {\n \"enum\": [\n \"error\",\n \"warning_fallback\"\n ]\n },\n \"unknown_variable_policy\": {\n \"enum\": [\n \"preserve\",\n \"empty\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"nlpTrigger\": {\n \"type\": \"object\",\n \"required\": [\n \"property_id\",\n \"trigger\",\n \"enabled\"\n ],\n \"properties\": {\n \"property_id\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"trigger\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"enabled\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nlpPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"triggers\"\n ],\n \"properties\": {\n \"triggers\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/nlpTrigger\"\n }\n }\n },\n \"additionalProperties\": false\n }\n }\n}\n", ]; diff --git a/src/services/mdbase/MetadataTransaction.ts b/src/services/mdbase/MetadataTransaction.ts index fdceb250f..2680172f8 100644 --- a/src/services/mdbase/MetadataTransaction.ts +++ b/src/services/mdbase/MetadataTransaction.ts @@ -1,5 +1,6 @@ import YAML from "yaml"; import { SafeMetadata, type MetadataSnapshot } from "./SafeMetadata"; +import { APP_SETTINGS_RECORD_PATH, isAppSettingsMetadataPath } from "./appSettingsResources"; export const METADATA_PENDING = ".tasknotes/migrations/mdbase-v0.3-pending.json"; type Journal = { backupFolder: string; snapshots: MetadataSnapshot[]; intendedWrites: MetadataSnapshot[] }; @@ -92,8 +93,10 @@ export class MetadataTransaction { } private rank(path: string): number { - if (path === "mdbase.yaml") return 3; - if (path.endsWith("/tasknotes.task.md")) return 2; + if (path === "mdbase.yaml") return 4; + // The settings record after its type and contracts. + if (path === APP_SETTINGS_RECORD_PATH) return 3; + if (/(^|\/)(tasknotes\.task|tasknotes\.settings|mdbase\.app-settings)(\/[^/]+)?\.md$/.test(path)) return 2; if (path.endsWith(".json")) return 1; return 0; } @@ -120,6 +123,8 @@ export class MetadataTransaction { if (typeof entry.path !== "string" || (entry.content !== null && typeof entry.content !== "string")) throw new Error("Invalid journal entry"); await this.io.assertPhysical(entry.path); if (entry.path !== "mdbase.yaml" && entry.path !== `${contracts}/tasknotes.task.md` && + !(entry.path.startsWith(`${contracts}/tasknotes.task/`) && entry.path.endsWith(".md")) && + !isAppSettingsMetadataPath(entry.path, contracts, types) && !(/^_schemas\/tasknotes\/tasknotes-task(-binding)?\.schema\.json$/.test(entry.path)) && !(entry.path.startsWith(`${types}/`) && entry.path.endsWith(".md"))) throw new Error(`Unsafe recovery target: ${entry.path}`); } @@ -141,8 +146,15 @@ export class MetadataTransaction { if (content !== snapshot.content && content !== journal.intendedWrites.find((write) => write.path === snapshot.path)?.content) throw new Error(`Concurrent metadata change: ${snapshot.path}`); } for (const snapshot of [...journal.snapshots].reverse()) { - if (snapshot.content === null || current.get(snapshot.path) === snapshot.content) continue; - await this.io.replace({ path: snapshot.path, content: current.get(snapshot.path) ?? null }, snapshot.content); + const content = current.get(snapshot.path) ?? null; + if (content === snapshot.content) continue; + if (snapshot.content === null) { + // Created by this upgrade: remove it while it still holds what was written. + if (content !== null) await this.io.removeIfUnchanged(snapshot.path, content); + continue; + } + await this.io.replace({ path: snapshot.path, content }, snapshot.content); } + await this.io.removeAbandonedStages(journal.snapshots.map(({ path }) => path)); } } diff --git a/src/services/mdbase/SafeMetadata.ts b/src/services/mdbase/SafeMetadata.ts index d67bc93a3..120ce4dec 100644 --- a/src/services/mdbase/SafeMetadata.ts +++ b/src/services/mdbase/SafeMetadata.ts @@ -229,6 +229,44 @@ export class SafeMetadata { } } + /** + * Remove a file a rolled-back write created, only while it still holds exactly + * what was written. A file that changed since is kept. + */ + async removeIfUnchanged(path: string, written: string): Promise { + if (await this.read(path) !== written) return false; + await this.adapter.remove(path); + return true; + } + + /** + * Remove staging files (`.tasknotes-stage-*`) left beside `paths` by an + * interrupted or failed write. Stages a pending swap journal owns are kept. + */ + async removeAbandonedStages(paths: readonly string[]): Promise { + const owned = new Set(); + if (await this.exists(METADATA_SWAPS)) { + for (const journalPath of (await this.adapter.list(METADATA_SWAPS)).files.filter((file) => file.endsWith(".json"))) { + try { + const journal = JSON.parse(await this.adapter.read(journalPath)) as Partial; + if (typeof journal.stage === "string") owned.add(journal.stage); + } catch { /* An unreadable journal is reported by swap recovery. */ } + } + } + for (const path of new Set(paths)) { + await this.assertPhysical(path); + const folder = path.split("/").slice(0, -1).join("/"); + if (folder && !await this.adapter.exists(folder)) continue; + let files: string[]; + try { files = (await this.adapter.list(folder)).files; } catch { continue; } + const prefix = `${path}.tasknotes-stage-`; + for (const file of files) { + const stage = [...owned].some((owner) => file === owner || file.startsWith(`${owner}.`)); + if (file.startsWith(prefix) && !stage) await this.adapter.remove(file); + } + } + } + /** Move, never copy/delete. Even an edit during rename survives in recovery storage. */ async move(snapshot: MetadataSnapshot, destination: string): Promise { if (await this.read(snapshot.path) !== snapshot.content) throw new Error(`Concurrent metadata change: ${snapshot.path}`); diff --git a/src/services/mdbase/appSettingsResources.ts b/src/services/mdbase/appSettingsResources.ts new file mode 100644 index 000000000..da3f12bb0 --- /dev/null +++ b/src/services/mdbase/appSettingsResources.ts @@ -0,0 +1,55 @@ +import { TASKNOTES_SETTINGS_PATH, TASKNOTES_SETTINGS_TYPE } from "@tasknotes/model/settings"; +import { canonicalTaskNotesResources } from "../canonicalTaskNotesPack"; + +/** The TaskNotes settings record, relative to the collection root. */ +export const APP_SETTINGS_RECORD_PATH = TASKNOTES_SETTINGS_PATH; + +export type AppSettingsSupportResource = { + path: string; + content: string; + /** Seeded once: an existing file (for example from TaskNotes App) is kept. */ + seed: boolean; +}; + +const CONTRACTS = ["mdbase.app-settings", "tasknotes.settings"] as const; + +function document(source: string): string { + const resource = canonicalTaskNotesResources.find((entry) => entry.source === source); + if (!resource) throw new Error(`Missing canonical TaskNotes resource: ${source}`); + return resource.document; +} + +/** + * Where contract-relative schemas live: the contracts reference + * `../../schemas//.schema.json`, beside the contracts folder. + */ +export function appSettingsSchemasFolder(contractsFolder: string): string { + const parent = contractsFolder.split("/").slice(0, -1).join("/"); + return parent ? `${parent}/schemas` : "schemas"; +} + +/** + * The settings contracts, their schemas and the tasknotes-settings type, laid + * out as the tasknotes.task 0.3.0-rc.19 pack installs them. + */ +export function appSettingsSupportResources( + contractsFolder: string, + typesFolder: string +): AppSettingsSupportResource[] { + const schemas = appSettingsSchemasFolder(contractsFolder); + return [ + ...CONTRACTS.flatMap((id) => [ + { path: `${schemas}/${id}/1.0.0.schema.json`, content: document(`schemas/${id}/1.0.0.schema.json`), seed: false }, + { path: `${contractsFolder}/${id}/1.0.0.md`, content: document(`contracts/${id}/1.0.0.md`), seed: false }, + ]), + { path: `${typesFolder}/${TASKNOTES_SETTINGS_TYPE}.md`, content: document("types/tasknotes-settings/1.md"), seed: true }, + ]; +} + +/** Whether a path is one the settings support resources or record may occupy. */ +export function isAppSettingsMetadataPath(path: string, contractsFolder: string, typesFolder: string): boolean { + return ( + path === APP_SETTINGS_RECORD_PATH || + appSettingsSupportResources(contractsFolder, typesFolder).some((resource) => resource.path === path) + ); +} diff --git a/src/services/mdbase/preserveCanonical.ts b/src/services/mdbase/preserveCanonical.ts index 69fe3e885..1b58b7616 100644 --- a/src/services/mdbase/preserveCanonical.ts +++ b/src/services/mdbase/preserveCanonical.ts @@ -10,7 +10,7 @@ const normalized = (value: unknown): unknown => Array.isArray(value) ? value.map const implementation = (type: ObjectValue): ObjectValue | undefined => Array.isArray(type.implements) ? type.implements.map(object).find((value) => value.contract === "tasknotes.task") : undefined; -/** Structural contract lift: no settings round-trip and no rewriting existing policies. */ +/** Structural contract lift: no settings round-trip and no rewriting existing policies other than removing those rc.6 moved. */ export function upgradeCanonicalDocument(markdown: string, resources: TaskNotesMdbaseResources): string { const parts = requireFrontmatter(markdown); const document = YAML.parseDocument(parts.frontmatter); @@ -20,6 +20,20 @@ export function upgradeCanonicalDocument(markdown: string, resources: TaskNotesM const desired = implementation(resources.type); if (!desired) throw new Error("Generated type has no TaskNotes implementation"); document.setIn(["implements", index, "version"], desired.version); + // tasknotes.task rc.6 keeps app behaviour in the settings record, which the + // same metadata transaction writes; remove it from the binding in place. + const binding = ["implements", index, "binding"]; + for (const path of [["nlp"], ["templating"], ["time_tracking"], ["archive", "move_on_archive"], ["archive", "folder"], ["recurrence", "reset_body_checkboxes"], ["links", "write_format"]]) { + document.deleteIn([...binding, ...path]); + } + const definitions = object(object(object(implementations[index].binding).status)).definitions; + if (Array.isArray(definitions)) { + definitions.forEach((_definition, position) => { + for (const key of ["next_status", "exclude_from_cycle", "auto_archive", "auto_archive_delay_minutes"]) { + document.deleteIn([...binding, "status", "definitions", position, key]); + } + }); + } if (!object(implementations[index].fields).assignees && !document.hasIn(["schema", "value", "properties", "assignees"])) { document.setIn(["implements", index, "fields", "assignees"], "assignees"); document.setIn(["schema", "value", "properties", "assignees"], copy(object(object(object(resources.type.schema).value).properties).assignees)); diff --git a/src/services/mdbaseCanonicalConfig.ts b/src/services/mdbaseCanonicalConfig.ts index ab56e3975..3889d36e5 100644 --- a/src/services/mdbaseCanonicalConfig.ts +++ b/src/services/mdbaseCanonicalConfig.ts @@ -1,7 +1,15 @@ import { resolveTaskNotesModelConfigFromMdbaseType, + upgradeTaskNotesMdbaseTypeContract, type TaskNotesMdbaseResources, } from "@tasknotes/model/mdbase"; +import { + resolveTaskNotesAppSettings, + taskNotesAppSettingsFromLegacyBinding, + taskNotesAppSettingsFromModelConfig, + taskNotesStatusBehavior, + type TaskNotesAppSettingsValues, +} from "@tasknotes/model/settings"; import { TASKNOTES_SPEC_VERSION as TASKNOTES_CONTRACT_VERSION } from "@tasknotes/model"; import YAML from "yaml"; import { validateContractMapping, validateTaskBinding, validationIssues, compileRecordSchema } from "./mdbase/contractValidation"; @@ -42,38 +50,53 @@ export function parseMdbaseTaskTypeDocument(markdown: string): ParsedMdbaseTaskT } /** - * tasknotes.task versions written by earlier TaskNotes releases. Their binding - * is unchanged in the current version, which only adds optional assignees, so - * such a type is read as the current version and then rewritten. + * tasknotes.task versions written by earlier TaskNotes releases. rc.6 moved + * app behaviour (templates, archive moves, time tracking, checkbox resets, the + * link format, NLP triggers and per-status automation) from the binding to the + * TaskNotes settings record; nothing else changed since rc.3 but the optional + * assignees. Such a type is read as the current version, with those settings + * taken from its binding where the record lacks them, and then rewritten. */ -export const PREVIOUS_TASKNOTES_CONTRACT_VERSIONS: readonly string[] = ["0.3.0-rc.3"]; +export const PREVIOUS_TASKNOTES_CONTRACT_VERSIONS: readonly string[] = ["0.3.0-rc.5", "0.3.0-rc.3"]; /** * Returns the type with an earlier TaskNotes implementation lifted to the - * current contract version, and whether it needed lifting. A type that already - * implements the current version, or no TaskNotes version, is returned as is. + * current contract version, whether it needed lifting, and the earlier binding + * (which still holds the settings rc.6 moved to the settings record). A type + * that already implements the current version, or no TaskNotes version, is + * returned as is. */ export function withCurrentTaskNotesContract(type: UnknownRecord): { type: UnknownRecord; outdated: boolean; + legacyBinding?: UnknownRecord; + /** The tasknotes.task version the file implemented before the upgrade. */ + previousVersion?: string; } { if (!Array.isArray(type.implements) || taskNotesImplementation(type)) { return { type, outdated: false }; } - const index = type.implements.findIndex( - (value) => + const previous = type.implements.find( + (value): value is UnknownRecord => isRecord(value) && value.contract === "tasknotes.task" && typeof value.version === "string" && PREVIOUS_TASKNOTES_CONTRACT_VERSIONS.includes(value.version) ); - if (index < 0) return { type, outdated: false }; - const implementations = [...type.implements]; - implementations[index] = { - ...(implementations[index] as UnknownRecord), - version: TASKNOTES_CONTRACT_VERSION, + if (!previous) return { type, outdated: false }; + const legacyBinding = asRecord(cloneValue(previous.binding)) ?? undefined; + const upgraded = upgradeTaskNotesMdbaseTypeContract(type).type; + return { + type: upgraded, + outdated: true, + previousVersion: previous.version as string, + ...(legacyBinding ? { legacyBinding } : {}), }; - return { type: { ...type, implements: implementations }, outdated: true }; +} + +/** Whether an earlier binding holds any setting rc.6 moved to the settings record. */ +export function hasLegacyAppSettings(binding: unknown): boolean { + return Object.keys(taskNotesAppSettingsFromLegacyBinding(binding)).length > 0; } export function validateCanonicalTaskType(type: UnknownRecord): CanonicalTypeValidationResult { @@ -168,10 +191,25 @@ export function validateCanonicalTaskType(type: UnknownRecord): CanonicalTypeVal return issues.length > 0 ? { valid: false, issues } : { valid: true }; } +export type CanonicalAppSettingsSources = { + /** Frontmatter of the TaskNotes settings record, when it should be applied. */ + record?: UnknownRecord; + /** The binding of an earlier (rc.3 to rc.5) type, before it was lifted. */ + legacyBinding?: UnknownRecord; +}; + +/** + * Apply a canonical type, and the app settings from the settings record, to the + * plugin settings. Each app setting comes from the record, then from an + * earlier binding, then stays as the plugin has it, so a missing or late + * record never loses a value. + */ export function applyCanonicalTaskTypeToSettings( settings: TaskNotesSettings, - type: UnknownRecord + type: UnknownRecord, + sources: CanonicalAppSettingsSources = {} ): void { + const localAppSettings = appSettingsFromPluginSettings(settings); const modelConfig = resolveTaskNotesModelConfigFromMdbaseType( type, buildTaskNotesModelConfig(settings) @@ -181,9 +219,10 @@ export function applyCanonicalTaskTypeToSettings( modelConfig.userFields = resolveRetainedUserFields(type, settings.userFields ?? [], modelConfig.userFields, Object.values(modelConfig.fieldMapping)); const extension = asRecord(taskNotesImplementation(type)?.binding) ?? {}; const title = asRecord(extension.title); - const links = asRecord(extension.links); - const archive = asRecord(extension.archive); - const templating = asRecord(extension.templating); + const appSettings = resolveTaskNotesAppSettings(sources.record, { + legacyBinding: sources.legacyBinding, + fallback: localAppSettings, + }).values; const collectionPath = recordAt(type, ["collection", "path"]); const excludedFolders = asRecord(type["x-tasknotes-generator"])?.excluded_folders; @@ -205,11 +244,6 @@ export function applyCanonicalTaskTypeToSettings( modelConfig.userFields ); settings.maintainDueDateOffsetInRecurring = modelConfig.recurrence.maintainDueDateOffset; - settings.resetCheckboxesOnRecurrence = modelConfig.recurrence.resetCheckboxesOnRecurrence; - settings.autoStopTimeTrackingOnComplete = modelConfig.timeTracking.autoStopOnComplete; - settings.nlpTriggers = { - triggers: (modelConfig.nlp?.triggers ?? []).map((trigger) => ({ ...trigger })), - }; if (collectionPath && typeof collectionPath.folder === "string") { settings.tasksFolder = collectionPath.folder; @@ -225,21 +259,70 @@ export function applyCanonicalTaskTypeToSettings( typeof title?.custom_filename_template === "string" ? title.custom_filename_template : settings.customFilenameTemplate; - settings.useFrontmatterMarkdownLinks = links?.write_format === "markdown"; - settings.moveArchivedTasks = archive?.move_on_archive === true; - settings.archiveFolder = typeof archive?.folder === "string" ? archive.folder : ""; + applyAppSettingsToPluginSettings(settings, appSettings); +} + +/** The settings-record values the plugin settings describe. */ +export function appSettingsFromPluginSettings(settings: TaskNotesSettings): TaskNotesAppSettingsValues { + const creation = settings.taskCreationDefaults; + return taskNotesAppSettingsFromModelConfig(buildTaskNotesModelConfig(settings), { + templating: { + enabled: creation?.useBodyTemplate === true, + templatePath: creation?.bodyTemplate ?? "", + occurrenceEnabled: creation?.useOccurrenceBodyTemplate === true, + occurrenceTemplatePath: creation?.occurrenceBodyTemplate ?? "", + }, + archive: { moveOnArchive: settings.moveArchivedTasks === true, folder: settings.archiveFolder ?? "" }, + links: { writeFormat: settings.useFrontmatterMarkdownLinks ? "markdown" : "wikilink" }, + }); +} + +/** Apply settings-record values to the plugin settings. */ +export function applyAppSettingsToPluginSettings( + settings: TaskNotesSettings, + values: TaskNotesAppSettingsValues +): void { + settings.customStatuses = settings.customStatuses.map((status) => { + const behavior = taskNotesStatusBehavior(values, status); + const next = { + ...status, + excludeFromCycle: behavior.exclude_from_cycle, + autoArchive: behavior.auto_archive, + autoArchiveDelay: behavior.auto_archive_delay_minutes, + }; + if (behavior.next_status) next.nextStatus = behavior.next_status; + else delete next.nextStatus; + return next; + }); + settings.resetCheckboxesOnRecurrence = values.reset_body_checkboxes_on_recurrence; + settings.autoStopTimeTrackingOnComplete = values.auto_stop_time_tracking_on_complete; + if (values.nlp_triggers) { + settings.nlpTriggers = { + ...settings.nlpTriggers, + triggers: values.nlp_triggers.map((trigger) => ({ + propertyId: trigger.property_id, + trigger: trigger.trigger, + enabled: trigger.enabled, + })), + }; + } + settings.useFrontmatterMarkdownLinks = values.link_write_format === "markdown"; + settings.moveArchivedTasks = values.move_on_archive; + settings.archiveFolder = values.archive_folder; settings.taskCreationDefaults = { ...settings.taskCreationDefaults, - useBodyTemplate: templating?.enabled === true, - bodyTemplate: typeof templating?.template_path === "string" ? templating.template_path : "", - useOccurrenceBodyTemplate: templating?.occurrence_enabled === true, - occurrenceBodyTemplate: - typeof templating?.occurrence_template_path === "string" - ? templating.occurrence_template_path - : "", + useBodyTemplate: values.template_enabled, + bodyTemplate: values.template_path, + useOccurrenceBodyTemplate: values.occurrence_template_enabled, + occurrenceBodyTemplate: values.occurrence_template_path, }; } +/** Fingerprint of the settings kept in the TaskNotes settings record. */ +export function appSettingsFingerprint(settings: TaskNotesSettings): string { + return JSON.stringify(appSettingsFromPluginSettings(settings)); +} + export function buildTaskNotesModelConfig(settings: TaskNotesSettings) { return { fieldMapping: { ...settings.fieldMapping }, @@ -280,13 +363,11 @@ export function buildTaskNotesModelConfig(settings: TaskNotesSettings) { }; } +/** Fingerprint of the settings kept in the canonical task type. */ export function portableSettingsFingerprint(settings: TaskNotesSettings): string { - const creationDefaults = settings.taskCreationDefaults; return JSON.stringify({ tasksFolder: settings.tasksFolder, excludedFolders: settings.excludedFolders, - moveArchivedTasks: settings.moveArchivedTasks, - archiveFolder: settings.archiveFolder, taskTag: settings.taskTag, taskIdentificationMethod: settings.taskIdentificationMethod, taskPropertyName: settings.taskPropertyName, @@ -297,24 +378,17 @@ export function portableSettingsFingerprint(settings: TaskNotesSettings): string storeTitleInFilename: settings.storeTitleInFilename, customFilenameTemplate: settings.customFilenameTemplate, fieldMapping: settings.fieldMapping, - customStatuses: settings.customStatuses, + // Per-status automation lives in the settings record (appSettingsFingerprint). + customStatuses: settings.customStatuses.map( + ({ nextStatus: _next, excludeFromCycle: _exclude, autoArchive: _archive, autoArchiveDelay: _delay, ...status }) => status + ), customPriorities: settings.customPriorities, userFields: (settings.userFields ?? []).map((field) => ({ key: field.key, type: field.type, defaultValue: field.defaultValue, })), - autoStopTimeTrackingOnComplete: settings.autoStopTimeTrackingOnComplete, maintainDueDateOffsetInRecurring: settings.maintainDueDateOffsetInRecurring, - resetCheckboxesOnRecurrence: settings.resetCheckboxesOnRecurrence, - nlpTriggers: settings.nlpTriggers?.triggers ?? [], - useFrontmatterMarkdownLinks: settings.useFrontmatterMarkdownLinks, - templating: { - useBodyTemplate: creationDefaults.useBodyTemplate, - bodyTemplate: creationDefaults.bodyTemplate, - useOccurrenceBodyTemplate: creationDefaults.useOccurrenceBodyTemplate, - occurrenceBodyTemplate: creationDefaults.occurrenceBodyTemplate, - }, }); } diff --git a/src/settings/native/properties.ts b/src/settings/native/properties.ts index 9b75992ad..dfd75c490 100644 --- a/src/settings/native/properties.ts +++ b/src/settings/native/properties.ts @@ -689,7 +689,7 @@ export function propertyDefinitions(ctx: SettingsContext): SettingDefinitionItem name: labels[property] ?? property.replace(/([A-Z])/g, " $1").replace(/^./, (char) => char.toUpperCase()), - displayValue: () => mapping[property], + displayValue: () => mapping[property] ?? "", items, }; }); diff --git a/src/types.ts b/src/types.ts index 6768eab78..bf518fdc0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -712,6 +712,7 @@ export interface FieldMapping { dateModified: string; recurrence: string; // RFC 5545 recurrence rule string recurrenceAnchor: string; // User-configurable property name for recurrence_anchor field + recurrenceOriginalDate?: string; // Optional so settings saved before this field existed keep working; defaults to recurrence_original_date recurrenceParent: string; occurrenceDate: string; occurrenceMaterialization: string; diff --git a/src/utils/linkUtils.ts b/src/utils/linkUtils.ts index 440488fd1..5353599fe 100644 --- a/src/utils/linkUtils.ts +++ b/src/utils/linkUtils.ts @@ -191,7 +191,7 @@ function shouldUseResolvedTitle( * @param sourcePath - The path of the file containing the link (for relative paths) * @param subpath - Optional subpath (e.g., heading anchor) * @param alias - Optional display alias - * @param useMarkdownLinks - If true, use Obsidian API which respects user's link format preference (requires third-party plugin for frontmatter) + * @param useMarkdownLinks - If true, write a Markdown link regardless of Obsidian's global link preference (requires third-party plugin for frontmatter) * @returns A link string in wikilink or markdown format */ export function generateLink( @@ -202,14 +202,22 @@ export function generateLink( alias?: string, useMarkdownLinks?: boolean ): string { - // If markdown links are explicitly requested, use Obsidian API if (useMarkdownLinks) { - return app.fileManager.generateMarkdownLink( + // Obsidian's API follows the global "Use [[Wikilinks]]" preference; the + // TaskNotes setting decides the format, so build a Markdown link when the + // API returns a wikilink. + const generated = app.fileManager.generateMarkdownLink( targetFile, sourcePath, subpath || "", alias || "" ); + if (!/^!?\[\[/.test(generated)) return generated; + const linkPath = app.metadataCache.fileToLinktext(targetFile, sourcePath, false); + const destination = encodeURI(`${linkPath}${subpath || ""}`) + .replace(/\(/g, "%28") + .replace(/\)/g, "%29"); + return `[${alias || targetFile.basename}](${destination})`; } // Default: generate wikilink format for frontmatter compatibility (issue #827) @@ -268,3 +276,25 @@ export function generateLinkWithDisplay( ): string { return generateLink(app, targetFile, sourcePath, "", displayName, useMarkdownLinks); } + +/** + * Rewrite a project link that resolves to a note into the TaskNotes link + * format. Plain values, unresolved links and links already in that format are + * returned unchanged. + */ +export function formatResolvedProjectReference( + app: App, + value: string, + sourcePath: string, + useMarkdownLinks?: boolean +): string { + const trimmed = value.trim(); + const wikilink = trimmed.match(/^\[\[([^\]]+)\]\]$/); + const markdown = /^\[[^\]]*\]\([^)]+\)$/.test(trimmed); + if (!wikilink && !markdown) return value; + if (Boolean(useMarkdownLinks) === markdown) return value; + const file = app.metadataCache.getFirstLinkpathDest(parseLinkToPath(trimmed), sourcePath); + if (!file) return value; + const alias = wikilink?.[1].includes("|") ? wikilink[1].slice(wikilink[1].indexOf("|") + 1).trim() : ""; + return generateLink(app, file, sourcePath, "", alias, useMarkdownLinks); +} diff --git a/tests/unit/bases/basesCreateFileForView.test.ts b/tests/unit/bases/basesCreateFileForView.test.ts index fd4cf9935..a185685d9 100644 --- a/tests/unit/bases/basesCreateFileForView.test.ts +++ b/tests/unit/bases/basesCreateFileForView.test.ts @@ -64,21 +64,23 @@ describe("Bases create-file view helpers", () => { expect(app.fileManager.generateMarkdownLink).not.toHaveBeenCalled(); }); - it("uses Obsidian markdown link generation for the active markdown file", () => { - const activeFile = { extension: "md", path: "Projects/Launch.md" }; + it("links the active markdown file in the TaskNotes link format", () => { + const activeFile = { extension: "md", path: "Projects/Launch.md", basename: "Launch" }; const app = { workspace: { getActiveFile: () => activeFile, }, fileManager: { + // Obsidian's global preference is wikilinks. generateMarkdownLink: jest.fn(() => "[[Projects/Launch]]"), }, + metadataCache: { + fileToLinktext: (_file: unknown, _source: string, omitMd = true) => + omitMd ? "Projects/Launch" : "Projects/Launch.md", + }, }; expect(getBasesCurrentFileLinkDefault(app as never)).toBe("[[Projects/Launch]]"); - expect(app.fileManager.generateMarkdownLink).toHaveBeenCalledWith( - activeFile, - activeFile.path - ); + expect(getBasesCurrentFileLinkDefault(app as never, true)).toBe("[Launch](Projects/Launch.md)"); }); }); diff --git a/tests/unit/bootstrap/mdbaseBootstrapRecognition.test.ts b/tests/unit/bootstrap/mdbaseBootstrapRecognition.test.ts index 23e4333f1..7b9a6bc28 100644 --- a/tests/unit/bootstrap/mdbaseBootstrapRecognition.test.ts +++ b/tests/unit/bootstrap/mdbaseBootstrapRecognition.test.ts @@ -50,7 +50,7 @@ it.each(["v4-custom", "beta0-custom"])("runs real bootstrap for %s without a pre expect(YAML.parse(memory.files.get("mdbase.yaml")!).spec_version).toBe("0.3.0"); expect(plugin.fieldMapper.toUserField("due")).toBe("deadline"); expect(plugin.settings.fieldMapping.due).toBe("deadline"); - expect(parseMdbaseTaskTypeDocument(memory.files.get("_types/task.md")!).type.implements).toEqual(expect.arrayContaining([expect.objectContaining({ contract: "tasknotes.task", version: "0.3.0-rc.5" })])); + expect(parseMdbaseTaskTypeDocument(memory.files.get("_types/task.md")!).type.implements).toEqual(expect.arrayContaining([expect.objectContaining({ contract: "tasknotes.task", version: "0.3.0-rc.6" })])); expect(plugin.emitter.trigger).not.toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ message: expect.stringContaining("could not initialize") })); for (const file of ["TaskNotes/Tasks/open.md", "TaskNotes/Tasks/cancelled.md", "paper.md"]) { expect(memory.files.get(file)).toBe(fixture.files[file]); diff --git a/tests/unit/issues/issue-1466-current-file-subtask-base-default.test.ts b/tests/unit/issues/issue-1466-current-file-subtask-base-default.test.ts index 90c8fbab3..5191360f3 100644 --- a/tests/unit/issues/issue-1466-current-file-subtask-base-default.test.ts +++ b/tests/unit/issues/issue-1466-current-file-subtask-base-default.test.ts @@ -46,6 +46,9 @@ function createMockPlugin(activeFile: { path: string; basename: string; extensio fileManager: { generateMarkdownLink: (file: { basename: string }) => `[[${file.basename}]]`, }, + metadataCache: { + fileToLinktext: (file: { basename: string }) => file.basename, + }, }, fieldMapper: { toUserField: (field: string) => field, diff --git a/tests/unit/issues/issue-1705-inline-conversion-parent-project.test.ts b/tests/unit/issues/issue-1705-inline-conversion-parent-project.test.ts index 421c984ce..9b6ad80da 100644 --- a/tests/unit/issues/issue-1705-inline-conversion-parent-project.test.ts +++ b/tests/unit/issues/issue-1705-inline-conversion-parent-project.test.ts @@ -31,6 +31,7 @@ describe("Issue #1705: inline conversion parent note project", () => { plugin.app.fileManager.generateMarkdownLink = jest .fn() .mockReturnValue("[[Projects/Home]]"); + plugin.app.metadataCache.fileToLinktext = jest.fn().mockReturnValue("Projects/Home"); plugin.taskService.createTask = jest.fn().mockImplementation(async (taskData) => { createdTaskData = taskData; return { diff --git a/tests/unit/modals/taskModalProjects.test.ts b/tests/unit/modals/taskModalProjects.test.ts index 53175e131..296ae6749 100644 --- a/tests/unit/modals/taskModalProjects.test.ts +++ b/tests/unit/modals/taskModalProjects.test.ts @@ -223,3 +223,15 @@ describe("taskModalProjects", () => { expect(container.textContent).toBe(""); }); }); + +describe("NLP projects in the creation modal", () => { + it("writes a resolved wikilink project with the configured link format", () => { + const project = file("Projects/Project C.md"); + const context = { + ...createContext([], { "Project C": project }), + formatLink: (target: TFile) => `[${target.basename}](Projects/Project%20C.md)`, + }; + const items = addTaskModalProjectItemsFromStrings([], ["[[Project C]]"], context); + expect(getTaskModalProjectsValue(items)).toBe("[Project C](Projects/Project%20C.md)"); + }); +}); diff --git a/tests/unit/services/MdbaseSpecService.test.ts b/tests/unit/services/MdbaseSpecService.test.ts index c90902b86..6a7d9d066 100644 --- a/tests/unit/services/MdbaseSpecService.test.ts +++ b/tests/unit/services/MdbaseSpecService.test.ts @@ -14,6 +14,7 @@ import YAML from "yaml"; import { FieldMapper } from "../../../src/services/FieldMapper"; import { MdbaseSpecService } from "../../../src/services/MdbaseSpecService"; +import { appSettingsFromPluginSettings } from "../../../src/services/mdbaseCanonicalConfig"; import { DEFAULT_FIELD_MAPPING, DEFAULT_NLP_TRIGGERS, @@ -320,7 +321,7 @@ describe("MdbaseSpecService", () => { }); await service.initialize(); expect(asObject(asObject(YAML.parse(files.get("mdbase.yaml")!)).settings).explicit_type_keys).toEqual([]); - expect(files.get("_contracts/tasknotes.task.md")).toContain("contract_type: record"); + expect(files.get("_contracts/tasknotes.task/0.3.0-rc.6.md")).toContain("contract_type: record"); expect(files.get("_schemas/tasknotes/tasknotes-task.schema.json")).toBeDefined(); const snapshot = new Map(files); await new MdbaseSpecService(plugin).initialize(); @@ -968,7 +969,9 @@ describe("MdbaseSpecService", () => { const canonicalFrontmatter = parseFrontmatter(service.buildTaskTypeDef()); expect(legacyFrontmatter["x-tasknotes"].nlp).toEqual({ triggers }); - expect(tasknotesBinding(canonicalFrontmatter).nlp).toEqual({ triggers }); + // rc.6: capture triggers are app settings, kept in the settings record. + expect(tasknotesBinding(canonicalFrontmatter).nlp).toBeUndefined(); + expect(appSettingsFromPluginSettings(service["plugin"].settings).nlp_triggers).toEqual(triggers); }); it("exports an empty trigger list when capture triggers are unavailable", () => { @@ -979,7 +982,7 @@ describe("MdbaseSpecService", () => { const canonicalFrontmatter = parseFrontmatter(service.buildTaskTypeDef()); expect(legacyFrontmatter["x-tasknotes"].nlp.triggers).toEqual([]); - expect(tasknotesBinding(canonicalFrontmatter).nlp).toEqual({ triggers: [] }); + expect(tasknotesBinding(canonicalFrontmatter).nlp).toBeUndefined(); }); }); @@ -1302,7 +1305,6 @@ describe("MdbaseSpecService", () => { recurrence: { syntax: "tasknotes", maintain_due_date_offset: true, - reset_body_checkboxes: true, }, occurrences: { identity_roles: ["recurrenceParent", "occurrenceDate"], @@ -1311,21 +1313,31 @@ describe("MdbaseSpecService", () => { }, links: { accepted_formats: ["wikilink", "markdown"], - write_format: "markdown", }, archive: { archived_tag: "filed-away", - move_on_archive: true, - folder: "Archive/Tasks", - }, - time_tracking: { auto_stop_on_complete: false }, - templating: { - enabled: true, - template_path: "Templates/Task.md", - occurrence_enabled: true, - occurrence_template_path: "Templates/Occurrence.md", }, }); + // rc.6: app behaviour is not part of the binding; it goes to the settings record. + for (const moved of ["time_tracking", "templating", "nlp"]) expect(extension[moved]).toBeUndefined(); + expect(asObject(extension.recurrence).reset_body_checkboxes).toBeUndefined(); + expect(asObject(extension.links).write_format).toBeUndefined(); + expect(asObject(extension.archive).move_on_archive).toBeUndefined(); + expect(appSettingsFromPluginSettings(service["plugin"].settings)).toMatchObject({ + reset_body_checkboxes_on_recurrence: true, + link_write_format: "markdown", + move_on_archive: true, + archive_folder: "Archive/Tasks", + auto_stop_time_tracking_on_complete: false, + template_enabled: true, + template_path: "Templates/Task.md", + occurrence_template_enabled: true, + occurrence_template_path: "Templates/Occurrence.md", + status_behavior: expect.objectContaining({ + done: expect.objectContaining({ auto_archive: true, auto_archive_delay_minutes: 15 }), + cancelled: expect.objectContaining({ exclude_from_cycle: true }), + }), + }); expect(asObject(taskImplementation.fields)).toMatchObject({ title: "summary", status: "state", @@ -1342,13 +1354,10 @@ describe("MdbaseSpecService", () => { expect.objectContaining({ value: "done", is_completed: true, - auto_archive: true, - auto_archive_delay_minutes: 15, }), expect.objectContaining({ value: "cancelled", is_skipped: true, - exclude_from_cycle: true, }), ]) ); @@ -1477,7 +1486,7 @@ describe("MdbaseSpecService", () => { expect(plugin.app.vault.createFolder).toHaveBeenCalledWith("_types"); expect(files.get("_types/task.md")).toContain("contract: tasknotes.task"); - expect(files.get("_contracts/tasknotes.task.md")).toContain("kind: mdbase.contract"); + expect(files.get("_contracts/tasknotes.task/0.3.0-rc.6.md")).toContain("kind: mdbase.contract"); expect(files.get("_schemas/tasknotes/tasknotes-task.schema.json")).toContain('"$schema"'); expect(files.get("_schemas/tasknotes/tasknotes-task-binding.schema.json")).toContain('"profiles"'); }); @@ -1503,7 +1512,7 @@ describe("MdbaseSpecService", () => { await service.generate(); - expect(files.get("System/contracts/tasknotes.task.md")).toContain("id: tasknotes.task"); + expect(files.get("System/contracts/tasknotes.task/0.3.0-rc.6.md")).toContain("id: tasknotes.task"); }); it("does not overwrite unrecognized v0.2 metadata during generation", async () => { @@ -2044,7 +2053,7 @@ describe("MdbaseSpecService", () => { ).toBe(false); expect([...files.keys()]).toEqual( expect.arrayContaining([ - "_contracts/tasknotes.task.md", + "_contracts/tasknotes.task/0.3.0-rc.6.md", "_schemas/tasknotes/tasknotes-task.schema.json", "_schemas/tasknotes/tasknotes-task-binding.schema.json", ]) @@ -2156,7 +2165,7 @@ describe("MdbaseSpecService", () => { expect(plugin.app.vault.create).not.toHaveBeenCalled(); }); - it("restores the exact v0.2 files without deleting support resources after a failed commit", async () => { + it("restores the exact v0.2 files and removes the files it created after a failed commit", async () => { const plugin = createMockPlugin(); plugin.emitter = { trigger: jest.fn() }; const service = new MdbaseSpecService(plugin); @@ -2188,9 +2197,10 @@ describe("MdbaseSpecService", () => { expect(files.get("mdbase.yaml")).toBe(oldConfig); expect(files.get("_types/task.md")).toBe(oldType); expect(files.get(taskPath)).toBe(taskContent); - expect(files.has("_contracts/tasknotes.task.md")).toBe(true); - expect(files.has("_schemas/tasknotes/tasknotes-task.schema.json")).toBe(true); - expect(files.has("_schemas/tasknotes/tasknotes-task-binding.schema.json")).toBe(true); + // Everything the migration created is removed; only the backups remain. + expect([...files.keys()].filter((path) => !path.startsWith(".tasknotes/")).sort()).toEqual( + ["_types/task.md", "mdbase.yaml", taskPath].sort() + ); expect([...files.keys()].some((path) => path.endsWith("/manifest.json"))).toBe(true); expect(plugin.emitter.trigger).toHaveBeenCalledWith( "user-notice", @@ -2198,7 +2208,32 @@ describe("MdbaseSpecService", () => { ); }); - it("recovers an interrupted rollback without deleting created support resources", async () => { + it("keeps a file the migration created when it was edited before the rollback", async () => { + const plugin = createMockPlugin(); + const service = new MdbaseSpecService(plugin); + const oldType = service.buildTaskTypeDef("0.2.1"); + const edited = "---\napp: dev.tasknotes\nscope: collection\narchive_folder: Mine\n---\n"; + let files: Map; + files = installMemoryVault( + plugin, + { "mdbase.yaml": v02Config(), "_types/task.md": oldType }, + { + failWrite: (path, content) => { + if (path === "mdbase.yaml" && content.includes("spec_version: 0.3.0")) { + files.set("_settings/dev.tasknotes.md", edited); + return true; + } + return false; + }, + } + ); + + await service.initialize(); + + expect(files.get("_settings/dev.tasknotes.md")).toBe(edited); + }); + + it("recovers an interrupted rollback, removing abandoned staging files, and completes the migration", async () => { const plugin = createMockPlugin(); const service = new MdbaseSpecService(plugin); const oldConfig = v02Config(); @@ -2226,12 +2261,16 @@ describe("MdbaseSpecService", () => { expect(files.has(".tasknotes/migrations/mdbase-v0.2-pending.json")).toBe(true); expect(parseFrontmatter(files.get("_types/task.md") ?? "").kind).toBe("mdbase.type"); + // A write killed while staging leaves its temporary file behind. + const stage = "_schemas/tasknotes/tasknotes-task.schema.json.tasknotes-stage-1-1-abc-def"; + files.set(stage, "{}"); failRollback = false; await new MdbaseSpecService(plugin).initialize(); + expect(files.has(stage)).toBe(false); expect(files.has(".tasknotes/migrations/mdbase-v0.2-pending.json")).toBe(false); expect(asObject(YAML.parse(files.get("mdbase.yaml") ?? "")).spec_version).toBe("0.3.0"); - expect(files.has("_contracts/tasknotes.task.md")).toBe(true); + expect(files.has("_contracts/tasknotes.task/0.3.0-rc.6.md")).toBe(true); }); it("does not overwrite a concurrent type edit while rolling back", async () => { @@ -2277,7 +2316,7 @@ describe("MdbaseSpecService", () => { }, { failWrite: (path) => { - if (path === "_contracts/tasknotes.task.md") { + if (path === "_contracts/tasknotes.task/0.3.0-rc.6.md") { files.set("_types/task.md", concurrentType); } return false; diff --git a/tests/unit/services/mdbaseCanonicalConfig.test.ts b/tests/unit/services/mdbaseCanonicalConfig.test.ts index 4b081b677..53e105bdb 100644 --- a/tests/unit/services/mdbaseCanonicalConfig.test.ts +++ b/tests/unit/services/mdbaseCanonicalConfig.test.ts @@ -2,6 +2,7 @@ import { buildTaskNotesMdbaseResources, TASKNOTES_TASK_CONTRACT_VERSION, } from "@tasknotes/model/mdbase"; +import { newTaskNotesSettingsFrontmatter } from "@tasknotes/model/settings"; import YAML from "yaml"; import { DEFAULT_SETTINGS } from "../../../src/settings/defaults"; @@ -82,7 +83,10 @@ describe("canonical mdbase TaskNotes configuration", () => { }, }); - applyCanonicalTaskTypeToSettings(settings, resources.type); + // rc.6: app behaviour comes from the settings record, not the binding. + applyCanonicalTaskTypeToSettings(settings, resources.type, { + record: newTaskNotesSettingsFrontmatter(resources.appSettings), + }); expect(settings.tasksFolder).toBe("Work/Tasks"); expect(settings.fieldMapping.status).toBe("state"); diff --git a/tests/unit/services/mdbaseLegacyRecognition.test.ts b/tests/unit/services/mdbaseLegacyRecognition.test.ts index e15ae2000..81664eb74 100644 --- a/tests/unit/services/mdbaseLegacyRecognition.test.ts +++ b/tests/unit/services/mdbaseLegacyRecognition.test.ts @@ -49,7 +49,7 @@ function assertMigrated(h: ReturnType, typePath = "_types/task.m expect(YAML.parse(h.files.get("mdbase.yaml")!).spec_version).toBe("0.3.0"); const type = parseMdbaseTaskTypeDocument(h.files.get(typePath)!).type; expect(type.implements).toEqual(expect.arrayContaining([expect.objectContaining({ contract: "tasknotes.task" })])); - expect([...h.files.keys()].filter((p) => p.startsWith(typePath.split("/")[0] + "/") && p.endsWith(".md") && p.includes("task"))).toEqual([typePath]); + expect([...h.files.keys()].filter((p) => p.startsWith(typePath.split("/")[0] + "/") && p.endsWith(".md") && p.includes("task") && !p.endsWith("/tasknotes-settings.md"))).toEqual([typePath]); for (const [p, content] of Object.entries(h.entries)) { if (p.startsWith("Tasks/")) expect(h.files.get(p)).toBe(content); } @@ -163,10 +163,11 @@ describe("historical generated type recognition", () => { h.files.set(resource, original.replace(/\n/g, "\r\n") + (edited ? "\nCustom commentary\n" : "")); await h.service.initialize(); if (edited) { - expect(h.files.get(resource)).toContain("Custom commentary"); - expect(h.notices.join(" ")).toMatch(/_contracts\/tasknotes.task.md.*backup/); + // rc.6 adds a versioned contract and leaves the earlier one as it is. + expect(h.files.get(resource)).toBe(original.replace(/\n/g, "\r\n") + "\nCustom commentary\n"); + expect(h.files.has("_contracts/tasknotes.task/0.3.0-rc.6.md")).toBe(true); } else { - expect(parseMdbaseTaskTypeDocument(h.files.get("_types/task.md")!).type.implements).toEqual(expect.arrayContaining([expect.objectContaining({ version: "0.3.0-rc.5" })])); + expect(parseMdbaseTaskTypeDocument(h.files.get("_types/task.md")!).type.implements).toEqual(expect.arrayContaining([expect.objectContaining({ version: "0.3.0-rc.6" })])); expect(h.notices.join(" ")).not.toContain("could not initialize"); } } diff --git a/tests/unit/services/mdbaseMeaning.test.ts b/tests/unit/services/mdbaseMeaning.test.ts index 7d8941e12..26d3f6800 100644 --- a/tests/unit/services/mdbaseMeaning.test.ts +++ b/tests/unit/services/mdbaseMeaning.test.ts @@ -8,6 +8,7 @@ import { parseMdbaseTaskTypeDocument, validateCanonicalTaskType, mergeCanonicalT import { checkCollection, repairCollectionRecord } from "../../../src/services/mdbase/checkCollection"; import { addAppCollectionConfig, applyTaskExclusions, effectiveMembershipKeys, taskExclusionExpression } from "../../../src/services/mdbase/collectionConfig"; import { buildTaskNotesMdbaseResources } from "@tasknotes/model/mdbase"; +import { readAppSettingsFrontmatter, taskNotesAppSettingsFromLegacyBinding, withoutTaskNotesAppSettings } from "@tasknotes/model/settings"; import { createTaskNotesCommandDefinitions } from "../../../src/commands/taskNotesCommands"; const root = path.join(__dirname, "../../fixtures/mdbase-upgrades"); @@ -89,7 +90,10 @@ describe("migration meaning preservation", () => { await h.service.initialize(); const after = typeOf(h.files.get("_types/task.md")!); expect(after.implements[0].fields).toEqual({ ...before.implements[0].fields, assignees: "assignees" }); - expect(after.implements[0].binding).toEqual(before.implements[0].binding); + // rc.6 moves app behaviour out of the binding into the settings record, unchanged. + expect(after.implements[0].binding).toEqual(withoutTaskNotesAppSettings(before.implements[0].binding)); + const record = readAppSettingsFrontmatter(h.files.get("_settings/dev.tasknotes.md")!)!; + expect(record).toMatchObject(taskNotesAppSettingsFromLegacyBinding(before.implements[0].binding)); expect(after.implements[0].fields.id).toBe("uid"); expect(after.implements[0].binding.archive.archived_tag).toBe("cold"); expect(after.implements[0].binding.occurrences.future_horizon).toBe("P90D"); @@ -121,10 +125,12 @@ describe("migration meaning preservation", () => { prior.schema.value.allOf.push({ properties: { title: { maxLength: 200 } } }); prior.schema.value.required.push("due"); const generated = buildTaskNotesMdbaseResources({ archive: { moveOnArchive: true, folder: "New archive" } }); + expect(generated.appSettings).toMatchObject({ move_on_archive: true, archive_folder: "New archive" }); const after = typeOf(mergeCanonicalTaskTypeDocument(doc(prior), generated)); expect(after.implements[0].fields.id).toBe("uid"); expect(after.implements[0].binding.occurrences).toEqual(prior.implements[0].binding.occurrences); - expect(after.implements[0].binding.archive).toEqual({ archived_tag: "cold", move_on_archive: true, folder: "New archive" }); + // Archive moves are app settings in rc.6; the binding keeps only the archived tag. + expect(after.implements[0].binding.archive).toEqual({ archived_tag: "cold" }); expect(after.implements[0]["x-owner"]).toEqual({ note: "keep" }); expect(after.schema.value.properties.uid.maxLength).toBe(99); expect(after.schema.value.properties.title.minLength).toBe(12); diff --git a/tests/unit/services/mdbaseMetadataSwap.test.ts b/tests/unit/services/mdbaseMetadataSwap.test.ts index f3419d19b..ca1b8fae8 100644 --- a/tests/unit/services/mdbaseMetadataSwap.test.ts +++ b/tests/unit/services/mdbaseMetadataSwap.test.ts @@ -44,7 +44,8 @@ function harness(entries: Record) { return { adapter, hooks, files, plugin, notices, io: new SafeMetadata(adapter) }; } const fixture = (name: string): Record => require(`../../fixtures/mdbase-upgrades/${name}.json`).files; -const targets = ["_types/task.md", "mdbase.yaml", "_contracts/tasknotes.task.md"]; +// rc.6 adds its contract at a new versioned path; the task schema is replaced in place. +const targets = ["_types/task.md", "mdbase.yaml", "_schemas/tasknotes/tasknotes-task.schema.json"]; it.each(["v4-custom", "beta3-custom"])("upgrades %s on a no-clobber adapter in cold bootstrap state", async (name) => { const before = fixture(name); @@ -62,7 +63,7 @@ it.each(["v4-custom", "beta3-custom"])("upgrades %s on a no-clobber adapter in c expect(h.plugin.fieldMapper).toBeUndefined(); await new MdbaseSpecService(h.plugin).initialize(); expect(YAML.parse(h.files.get("mdbase.yaml")!).spec_version).toBe("0.3.0"); - expect((parseMdbaseTaskTypeDocument(h.files.get("_types/task.md")!).type as any).implements[0].version).toBe("0.3.0-rc.5"); + expect((parseMdbaseTaskTypeDocument(h.files.get("_types/task.md")!).type as any).implements[0].version).toBe("0.3.0-rc.6"); for (const p of ["paper.md", "TaskNotes/Tasks/open.md", "TaskNotes/Tasks/cancelled.md"]) expect(h.files.get(p)).toBe(before[p]); const after = Object.fromEntries(h.files); await new MdbaseSpecService(h.plugin).initialize(); @@ -160,7 +161,7 @@ it.each(["v4-default", "beta3-custom"].flatMap((name) => ["before displacement", const restarted = harness(interruptedFiles!); restarted.plugin.settings = h.plugin.settings; await new MdbaseSpecService(restarted.plugin).initialize(); - expect((parseMdbaseTaskTypeDocument(restarted.files.get("_types/task.md")!).type as any).implements[0].version).toBe("0.3.0-rc.5"); + expect((parseMdbaseTaskTypeDocument(restarted.files.get("_types/task.md")!).type as any).implements[0].version).toBe("0.3.0-rc.6"); expect([...restarted.files.keys()].filter((p) => path.posix.dirname(p) === METADATA_SWAPS && p.endsWith(".json"))).toEqual([]); expect([...restarted.files.keys()].filter((p) => p.endsWith("-pending.json"))).toEqual([]); }); diff --git a/tests/unit/services/mdbaseMigrationComposition.test.ts b/tests/unit/services/mdbaseMigrationComposition.test.ts index 040fd688a..9385ea085 100644 --- a/tests/unit/services/mdbaseMigrationComposition.test.ts +++ b/tests/unit/services/mdbaseMigrationComposition.test.ts @@ -39,7 +39,7 @@ describe("composed migration writes", () => { expect(h.plugin.fieldMapper).toBeUndefined(); await h.service.initialize(); const type = parseMdbaseTaskTypeDocument(h.files.get("_types/task.md")!).type as any; - expect(type.implements[0].version).toBe("0.3.0-rc.5"); + expect(type.implements[0].version).toBe("0.3.0-rc.6"); expect(type.implements[0].fields.id).toBe("uid"); expect(type.implements[0].binding.occurrences.future_horizon).toBe("P90D"); expect(type.match.expr.$expr).toContain('!file.path.startsWith("Private/")'); diff --git a/tests/unit/services/mdbaseReleaseUpgrades.test.ts b/tests/unit/services/mdbaseReleaseUpgrades.test.ts index 471229221..56834b1c3 100644 --- a/tests/unit/services/mdbaseReleaseUpgrades.test.ts +++ b/tests/unit/services/mdbaseReleaseUpgrades.test.ts @@ -8,6 +8,11 @@ * set up through Connect ("app-", with an mdbase.lock.yaml). This release must * bring every one of them to the current contract without touching records, * and leave a collection that is already current exactly as it is. + * + * tasknotes.task rc.6 moved app behaviour out of the binding: every upgrade + * also writes the TaskNotes settings record (_settings/dev.tasknotes.md) with + * the settings the collection had (data.json for v4, the binding for rc.3 and + * rc.5), in the same journalled step. */ import { TFile } from "obsidian"; import * as fs from "fs"; @@ -98,6 +103,10 @@ function memoryVault(entries: Record) { } async function load(files: Map, vault: unknown, settings: Record) { + return (await start(files, vault, settings)).notices; +} + +async function start(_files: Map, vault: unknown, settings: Record) { const notices: string[] = []; const plugin: any = { settings, @@ -110,7 +119,7 @@ async function load(files: Map, vault: unknown, settings: Record const service = new MdbaseSpecService(plugin); await service.initialize(); service.flushPendingNotices(); - return notices; + return { notices, service }; } function frontmatter(document: string): Record { @@ -136,15 +145,10 @@ describe("upgrading collections written by shipped TaskNotes releases", () => { const notices = await load(files, vault, settings); if (name.startsWith("app-rc17")) { - // Already current: additive App config only; omitted membership keys retain engine defaults. - const { "mdbase.yaml": config, ...rest } = Object.fromEntries(files); - const { "mdbase.yaml": _previous, ...previous } = before; - expect(Object.fromEntries(Object.entries(rest).filter(([path]) => !path.startsWith(".tasknotes/migrations/")))).toEqual(previous); - const configBackups = [...files].filter(([path]) => path.startsWith(".tasknotes/migrations/") && path.endsWith("/mdbase.yaml.bak")); - expect(configBackups.map(([, content]) => content)).toEqual([_previous]); - expect(YAML.parse(config).settings.explicit_type_keys).toBeUndefined(); - expect(YAML.parse(config).settings.record_extensions).toEqual(expect.arrayContaining(["md", "base"])); - expect(notices).toEqual([]); + // Omitted membership keys retain engine defaults. + const config = YAML.parse(files.get("mdbase.yaml")!); + expect(config.settings.explicit_type_keys).toBeUndefined(); + expect(config.settings.record_extensions).toEqual(expect.arrayContaining(["md", "base"])); } const config = YAML.parse(files.get("mdbase.yaml")!); @@ -161,7 +165,24 @@ describe("upgrading collections written by shipped TaskNotes releases", () => { expect(implementation.fields.assignees).toBe("assignees"); expect(type.collection.links["assignees[]"]).toBeDefined(); - expect(files.get("_contracts/tasknotes.task.md")).toBe(canonical(`contracts/tasknotes.task/${TASKNOTES_SPEC_VERSION}.md`)); + expect(files.get(`_contracts/tasknotes.task/${TASKNOTES_SPEC_VERSION}.md`)).toBe(canonical(`contracts/tasknotes.task/${TASKNOTES_SPEC_VERSION}.md`)); + for (const binding of [implementation.binding]) { + expect(binding.templating).toBeUndefined(); + expect(binding.time_tracking).toBeUndefined(); + expect(binding.nlp).toBeUndefined(); + expect(binding.archive.move_on_archive).toBeUndefined(); + expect(binding.status.definitions.some((d: any) => "auto_archive" in d || "next_status" in d)).toBe(false); + } + expect(files.get("_types/tasknotes-settings.md")).toBe(canonical("types/tasknotes-settings/1.md")); + expect(files.get("_contracts/tasknotes.settings/1.0.0.md")).toBe(canonical("contracts/tasknotes.settings/1.0.0.md")); + expect(files.get("_contracts/mdbase.app-settings/1.0.0.md")).toBe(canonical("contracts/mdbase.app-settings/1.0.0.md")); + const record = frontmatter(files.get("_settings/dev.tasknotes.md")!); + expect(record).toMatchObject({ app: "dev.tasknotes", scope: "collection", settings_version: 1 }); + expect(Object.keys(record.status_behavior).sort()).toEqual([...implementation.binding.status.values].sort()); + if (implementation.binding.status.values.includes("cancelled")) { + expect(record.status_behavior.cancelled.exclude_from_cycle).toBe(true); + } + expect(record.link_write_format).toBe("wikilink"); expect(files.get("_schemas/tasknotes/tasknotes-task.schema.json")).toBe( canonical(`schemas/tasknotes.task/${TASKNOTES_SPEC_VERSION}.schema.json`) ); @@ -191,3 +212,101 @@ describe("upgrading collections written by shipped TaskNotes releases", () => { expect(again).toEqual([]); }); }); + +describe("the TaskNotes settings record during the rc.6 upgrade", () => { + const RECORD = "_settings/dev.tasknotes.md"; + + it("keeps an existing record, adds only absent fields, and reads record over binding over local settings", async () => { + const before: Record = { ...JSON.parse(fs.readFileSync(path.join(FIXTURES, "beta3-custom.json"), "utf8")).files }; + // The rc.5 binding holds an archive move that the local settings do not. + before["_types/task.md"] = before["_types/task.md"] + .replace("move_on_archive: false", "move_on_archive: true") + .replace("folder: TaskNotes/Archive", "folder: Legacy/Archive"); + expect(before["_types/task.md"]).toContain("folder: Legacy/Archive"); + // TaskNotes App already wrote a record with some settings. + before[RECORD] = [ + "---", + "type: tasknotes-settings", + "app: dev.tasknotes", + "scope: collection", + "settings_version: 1", + "template_enabled: true", + "template_path: Templates/App.md", + "link_write_format: markdown", + "x-other-client: keep", + "---", + "Notes for people.", + "", + ].join("\n"); + const { files, vault } = memoryVault(before); + const settings = settingsFor("custom"); + + await load(files, vault, settings); + + const content = files.get(RECORD)!; + expect(content).toContain("Notes for people."); + const record = frontmatter(content); + // Present fields are kept as written, and win over the binding. + expect(record).toMatchObject({ template_enabled: true, template_path: "Templates/App.md", link_write_format: "markdown", "x-other-client": "keep" }); + // Absent fields come from the rc.5 binding, not local defaults. + expect(record).toMatchObject({ move_on_archive: true, archive_folder: "Legacy/Archive", auto_stop_time_tracking_on_complete: true }); + expect(record.status_behavior.cancelled.exclude_from_cycle).toBe(true); + expect(settings.taskCreationDefaults.useBodyTemplate).toBe(true); + expect(settings.taskCreationDefaults.bodyTemplate).toBe("Templates/App.md"); + expect(settings.useFrontmatterMarkdownLinks).toBe(true); + expect(settings.moveArchivedTasks).toBe(true); + expect(settings.archiveFolder).toBe("Legacy/Archive"); + + const snapshot = new Map(files); + expect(await load(files, vault, settings)).toEqual([]); + expect(files).toEqual(snapshot); + }); +}); + +describe("saving a moved setting after a v4 migration", () => { + it("writes the first and later saves to the settings record", async () => { + const { files, vault } = memoryVault(JSON.parse(fs.readFileSync(path.join(FIXTURES, "v4-default.json"), "utf8")).files); + const settings = settingsFor("default"); + settings.moveArchivedTasks = true; + settings.archiveFolder = "Old/Archive"; + const { service } = await start(files, vault, settings); + const record = () => frontmatter(files.get("_settings/dev.tasknotes.md")!); + expect(record()).toMatchObject({ move_on_archive: true, archive_folder: "Old/Archive" }); + + settings.moveArchivedTasks = false; + await service.onSettingsChanged(); + expect(record().move_on_archive).toBe(false); + + settings.archiveFolder = "New/Archive"; + await service.onSettingsChanged(); + expect(record()).toMatchObject({ move_on_archive: false, archive_folder: "New/Archive" }); + + // A restart reads the saved values back from the record. + const restarted = settingsFor("default"); + restarted.moveArchivedTasks = true; + await load(files, vault, restarted); + expect(restarted.moveArchivedTasks).toBe(false); + expect(restarted.archiveFolder).toBe("New/Archive"); + }); +}); + +describe("competing TaskNotes settings records", () => { + it("names sync conflict copies and keeps using the canonical record", async () => { + const { files, vault } = memoryVault(JSON.parse(fs.readFileSync(path.join(FIXTURES, "beta3-default.json"), "utf8")).files); + await load(files, vault, settingsFor("default")); + const canonicalRecord = files.get("_settings/dev.tasknotes.md")!; + files.set("_settings/dev.tasknotes.md", canonicalRecord.replace(/archive_folder: .*/, "archive_folder: Device-A")); + const copy = canonicalRecord.replace(/archive_folder: .*/, "archive_folder: Device-B"); + files.set("_settings/dev.tasknotes (conflicted copy Device-B).md", copy); + files.set("_settings/dev.other.md", "---\napp: dev.other\nscope: collection\n---\n"); + + const settings = settingsFor("default"); + const notices = await load(files, vault, settings); + + const notice = notices.find((message) => message.includes("conflicted copy")); + expect(notice).toContain("_settings/dev.tasknotes (conflicted copy Device-B).md"); + expect(notice).not.toContain("dev.other"); + expect(settings.archiveFolder).toBe("Device-A"); + expect(files.get("_settings/dev.tasknotes (conflicted copy Device-B).md")).toBe(copy); + }); +}); diff --git a/tests/unit/services/mdbaseRound3Robustness.test.ts b/tests/unit/services/mdbaseRound3Robustness.test.ts index 4bfef8f26..c03d01666 100644 --- a/tests/unit/services/mdbaseRound3Robustness.test.ts +++ b/tests/unit/services/mdbaseRound3Robustness.test.ts @@ -9,7 +9,7 @@ import { checkCollection, repairCollectionRecord } from "../../../src/services/m import { addAppCollectionConfig } from "../../../src/services/mdbase/collectionConfig"; import { collectionScope } from "../../../src/services/mdbase/collectionMembership"; import { findExplicitTypeReference } from "../../../src/services/mdbase/ExplicitTypeReferences"; -import { parseMdbaseTaskTypeDocument, validateCanonicalTaskType } from "../../../src/services/mdbaseCanonicalConfig"; +import { parseMdbaseTaskTypeDocument, validateCanonicalTaskType, withCurrentTaskNotesContract } from "../../../src/services/mdbaseCanonicalConfig"; import { recognitionVault } from "../../helpers/mdbaseRecognitionVault"; const root = path.join(__dirname, "../../fixtures/mdbase-upgrades"); @@ -135,7 +135,7 @@ describe("approved repair robustness", () => { const h = harness(capture("round3-glob-array")); h.files.delete("outside.md"); h.files.set("TaskNotes/Tasks/stale.md", "---\ntags: [task]\ntitle: Stale status\nstatus: unknown\ndateCreated: 2026-01-01T00:00:00Z\n---\nBody\n"); const [preview] = await checkCollection(h.plugin); expect(preview.statusValues).toContain("open"); - const type = JSON.parse(JSON.stringify(typeOf(h)).replace(/"open"/g, '"queued"')); + const type = withCurrentTaskNotesContract(JSON.parse(JSON.stringify(typeOf(h)).replace(/"open"/g, '"queued"'))).type; expect(validateCanonicalTaskType(type).valid).toBe(true); setType(h, type); const before = Object.fromEntries(h.files); await expect(repairCollectionRecord(h.plugin, preview, "open")).rejects.toThrow("allowed status"); diff --git a/tests/unit/services/mdbaseSafeWrites.test.ts b/tests/unit/services/mdbaseSafeWrites.test.ts index 3487e64c5..80bbeab18 100644 --- a/tests/unit/services/mdbaseSafeWrites.test.ts +++ b/tests/unit/services/mdbaseSafeWrites.test.ts @@ -77,7 +77,7 @@ describe("safe migration writes and recovery", () => { return new FieldMapper(mapping); } expect(h.plugin.fieldMapper).toBeDefined(); - expect(type(h.files.get("_types/task.md")!).implements[0].version).toBe("0.3.0-rc.5"); + expect(type(h.files.get("_types/task.md")!).implements[0].version).toBe("0.3.0-rc.6"); expect(h.files.has(METADATA_PENDING)).toBe(false); throw new Error("runtime boundary reached"); }) })); @@ -100,7 +100,7 @@ describe("safe migration writes and recovery", () => { expect(h.files.get("mdbase.yaml")).toBe("external"); expect([...h.files.keys()]).toEqual(["mdbase.yaml"]); }); - it.each(["_types/task.md", "_schemas/tasknotes/tasknotes-task.schema.json", "_contracts/tasknotes.task.md", "mdbase.yaml"])("rolls back a beta upgrade when %s fails", async (target) => { + it.each(["_types/task.md", "_schemas/tasknotes/tasknotes-task.schema.json", "_contracts/tasknotes.task/0.3.0-rc.6.md", "_settings/dev.tasknotes.md", "mdbase.yaml"])("rolls back a beta upgrade when %s fails", async (target) => { const before = faultFixture(target); const h = safetyHarness(before); let failed = false; @@ -111,12 +111,14 @@ describe("safe migration writes and recovery", () => { await h.service.initialize(); expect(failed).toBe(true); for (const [p, content] of Object.entries(before)) expect(h.files.get(p)).toBe(content); + // Files the upgrade created (the settings record included) and staging files are removed. + expect([...h.files.keys()].filter((p) => !p.startsWith(".tasknotes/") && !(p in before))).toEqual([]); expect(h.files.has(METADATA_PENDING)).toBe(false); expect(h.notices.join(" ")).toContain("Backups"); expect(h.notices.some((notice) => notice.startsWith("TaskNotes updated"))).toBe(false); unchangedRecords(before, h.files); await new MdbaseSpecService(h.plugin).initialize(); - expect(type(h.files.get("_types/task.md")!).implements[0].version).toBe("0.3.0-rc.5"); + expect(type(h.files.get("_types/task.md")!).implements[0].version).toBe("0.3.0-rc.6"); }); it.each(["_types/task.md", "mdbase.yaml", METADATA_PENDING])("a torn temp write for %s never corrupts the active file", async (target) => { @@ -133,7 +135,7 @@ describe("safe migration writes and recovery", () => { for (const [p, content] of Object.entries(before)) expect(h.files.get(p)).toBe(content); expect([...h.files.keys()].some((p) => p.includes(".tasknotes-stage-"))).toBe(false); await new MdbaseSpecService(h.plugin).initialize(); - expect(type(h.files.get("_types/task.md")!).implements[0].version).toBe("0.3.0-rc.5"); + expect(type(h.files.get("_types/task.md")!).implements[0].version).toBe("0.3.0-rc.6"); unchangedRecords(before, h.files); }); @@ -164,7 +166,7 @@ describe("safe migration writes and recovery", () => { const h = safetyHarness(fixture("beta0-custom")); let fired = false; h.hooks.rename = async (_from, to) => { - if (!fired && to === "_contracts/tasknotes.task.md") { + if (!fired && to === "_contracts/tasknotes.task/0.3.0-rc.6.md") { fired = true; h.files.set("_types/task.md", h.files.get("_types/task.md")! + "\nEXTERNAL CHANGE\n"); throw new Error("Injected contract failure"); @@ -239,7 +241,7 @@ describe("safe migration writes and recovery", () => { try { await h.service.initialize(); expect(h.files.get(orphan)).toBe("RECOVERY COPY FROM EARLIER FAILURE"); - expect(type(h.files.get("_types/task.md")!).implements[0].version).toBe("0.3.0-rc.5"); + expect(type(h.files.get("_types/task.md")!).implements[0].version).toBe("0.3.0-rc.6"); expect(h.files.has(`${folder}-2/manifest.json`)).toBe(true); } finally { iso.mockRestore(); } }); diff --git a/tests/unit/services/mdbaseScope.test.ts b/tests/unit/services/mdbaseScope.test.ts index e15569c10..780e99cd6 100644 --- a/tests/unit/services/mdbaseScope.test.ts +++ b/tests/unit/services/mdbaseScope.test.ts @@ -64,7 +64,7 @@ describe("BOM metadata and record membership", () => { const entries = { ...fixture.files }; entries["_types/task.md"] = "\ufeff" + entries["_types/task.md"].replace(/\r?\n/g, newline); const h = harness(entries); h.plugin.settings.excludedFolders = "Private"; await h.service.initialize(); const type = typeOf(h.files.get("_types/task.md")!); - expect(type.implements[0].version).toBe("0.3.0-rc.5"); + expect(type.implements[0].version).toBe("0.3.0-rc.6"); expect(type.match.expr.$expr).toContain("Private/"); expect(h.notices.join("\n")).not.toMatch(/frontmatter|stopped/); expect([...h.files.values()]).toContain(entries["_types/task.md"]); diff --git a/tests/unit/utils/linkUtils.test.ts b/tests/unit/utils/linkUtils.test.ts index 5646fce5c..a7c0767e5 100644 --- a/tests/unit/utils/linkUtils.test.ts +++ b/tests/unit/utils/linkUtils.test.ts @@ -1,4 +1,4 @@ -import { generateLink, generateLinkWithBasename, generateLinkWithDisplay, getProjectDisplayName, parseLinkToPath } from '../../../src/utils/linkUtils'; +import { generateLink, generateLinkWithBasename, generateLinkWithDisplay, formatResolvedProjectReference, getProjectDisplayName, parseLinkToPath } from '../../../src/utils/linkUtils'; import { MockObsidian } from '../../helpers/obsidian-runtime'; import type { App, TFile } from 'obsidian'; @@ -145,3 +145,26 @@ describe('linkUtils - frontmatter link format', () => { }); }); }); + +describe('linkUtils - TaskNotes link format overrides Obsidian preference', () => { + const projectFile = { path: 'Projects/Project C.md', basename: 'Project C', name: 'Project C.md', extension: 'md' } as TFile; + // Obsidian's API with the global "Use [[Wikilinks]]" preference on. + const app = createMockApp({ + fileManager: { generateMarkdownLink: jest.fn((file: TFile) => `[[${file.basename}]]`) }, + metadataCache: { + fileToLinktext: (file: TFile, _source: string, omitMd = true) => (omitMd ? file.path.replace(/\.md$/, '') : file.path), + getFirstLinkpathDest: (linkPath: string) => (linkPath === 'Project C' || linkPath === projectFile.path ? projectFile : null), + }, + }); + + it('writes a Markdown link when the setting is Markdown, even if Obsidian prefers wikilinks', () => { + expect(generateLink(app, projectFile, 'Tasks/a.md', '', '', true)).toBe('[Project C](Projects/Project%20C.md)'); + }); + + it('rewrites resolved NLP project links to the setting and leaves others unchanged', () => { + expect(formatResolvedProjectReference(app, '[[Project C]]', '', true)).toBe('[Project C](Projects/Project%20C.md)'); + expect(formatResolvedProjectReference(app, '[[Project C]]', '', false)).toBe('[[Project C]]'); + expect(formatResolvedProjectReference(app, '[[Missing]]', '', true)).toBe('[[Missing]]'); + expect(formatResolvedProjectReference(app, 'Project C', '', true)).toBe('Project C'); + }); +}); diff --git a/vendor/README.md b/vendor/README.md index 000f8b812..a61ad1e3d 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -1,12 +1,11 @@ # Canonical TaskNotes resources -`mdbase-contracts/tasknotes.task-0.3.0-rc.17.json` is the same published provision -pinned by TaskNotes App. `scripts/verify-tasknotes-pack.mjs --check` verifies its +`mdbase-contracts/tasknotes.task-0.3.0-rc.19.json` is the mdbase-contracts +tasknotes.task 0.3.0-rc.19 provision that TaskNotes App pins. `scripts/verify-tasknotes-pack.mjs --check` verifies its SHA-256 and byte-for-byte embedded resources before production and development builds. Run the script without `--check` only when intentionally regenerating the embedded resource module. Updating the pack requires an explicit new digest. -`@tasknotes/model@0.3.0-rc.15` is installed from npm and pinned by package-lock.json -integrity; it is byte-identical to the model snapshot vendored by TaskNotes App. It generates the editable, -collection-specific implementing type. Immutable contracts and schemas come -from the catalog provision instead. +`@tasknotes/model@0.3.0-rc.20` (tasknotes.task 0.3.0-rc.6 and the TaskNotes settings record) +comes from npm, and TaskNotes App uses the same version. It generates the editable, collection-specific implementing type and +the settings record. Immutable contracts and schemas come from the catalog provision instead. diff --git a/vendor/mdbase-contracts/tasknotes.task-0.3.0-rc.17.json b/vendor/mdbase-contracts/tasknotes.task-0.3.0-rc.17.json deleted file mode 100644 index 5e5345085..000000000 --- a/vendor/mdbase-contracts/tasknotes.task-0.3.0-rc.17.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "manifest": { - "kind": "mdbase.type-pack", - "id": "tasknotes.task", - "version": "0.3.0-rc.17", - "name": "TaskNotes task type pack", - "description": "Portable tasks whose assignments link to person records, with a reviewed, atomic starter upgrade that keeps each collection's generator bookkeeping.", - "resources": [ - { - "kind": "contract", - "mode": "managed", - "source": "contracts/tasknotes.task/0.3.0-rc.5.md", - "target": "_contracts/tasknotes.task.md", - "digest": "sha256:ecadd00700e249940f4244aaa5b5985a96557d13340463ec491dfa5e89b032a4" - }, - { - "kind": "type", - "mode": "seed", - "source": "types/tasknotes-task/5.md", - "target": "_types/task.md", - "digest": "sha256:b3cf8dbe8885b331c494d2eeb36abfb052890e06b757877d655c269e666487dc", - "upgrade_from": { - "digest": "sha256:b7f8af7b18d77859dab1f1532c1b55c08c7c39879f55e4ef2a94cb7d2d2284eb", - "document": "---\nkind: mdbase.type\nname: task\nversion: 1\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a2\n - none\n - open\n - in-progress\n - done\n - cancelled\n default: open\n priority:\n enum: &a4\n - none\n - low\n - normal\n - high\n default: normal\n due: &a1\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled: *a1\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a3\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.3\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - materialized-occurrences\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a2\n default: open\n completed_values: *a3\n skipped_values:\n - cancelled\n default_skipped: cancelled\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 0\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: open\n label: Open\n color: \"#64748b\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 1\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: in-progress\n label: In progress\n color: \"#3b82f6\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 2\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: done\n label: Done\n color: \"#22c55e\"\n is_completed: true\n is_skipped: false\n exclude_from_cycle: false\n order: 3\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: cancelled\n label: Cancelled\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: true\n exclude_from_cycle: true\n order: 4\n auto_archive: false\n auto_archive_delay_minutes: 5\n priority:\n values: *a4\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n weight: 0\n - value: low\n label: Low\n color: \"#3b82f6\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#f59e0b\"\n weight: 2\n - value: high\n label: High\n color: \"#ef4444\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n reset_body_checkboxes: false\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n write_format: wikilink\n archive:\n archived_tag: archived\n move_on_archive: false\n time_tracking:\n auto_stop_on_complete: false\n templating:\n enabled: false\n occurrence_enabled: false\nx-tasknotes-generator:\n managed_fields:\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n# Task\n\nTask records live under `tasks/`.\n" - } - }, - { - "kind": "schema", - "mode": "managed", - "source": "schemas/tasknotes.task/0.3.0-rc.5.schema.json", - "target": "_schemas/tasknotes/tasknotes-task.schema.json", - "digest": "sha256:aec95578a093be95cbce0fb500964e7aeaeeacc611624fc2893074637dbd4125" - }, - { - "kind": "schema", - "mode": "managed", - "source": "schemas/tasknotes.task.binding/0.3.0-rc.5.schema.json", - "target": "_schemas/tasknotes/tasknotes-task-binding.schema.json", - "digest": "sha256:5b38e8a1c426b7f7fa92d8353fe35dd98e632305cbb15ffccde387473166b267" - } - ] - }, - "resources": [ - { - "source": "contracts/tasknotes.task/0.3.0-rc.5.md", - "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: tasknotes.task\nversion: 0.3.0-rc.5\nname: TaskNotes task\ndescription: Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.5.\nrecord_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task.schema.json\n title: TaskNotes portable task view\n description: The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.5 record contract.\n type: object\n required:\n - status\n - dateCreated\n additionalProperties: false\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n type: string\n minLength: 1\n priority:\n type: string\n minLength: 1\n due:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n pattern: \\S\n description: Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\n attachments:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrenceAnchor:\n enum:\n - scheduled\n - completion\n recurrenceParent:\n type: string\n occurrenceDate:\n type: string\n format: date\n occurrenceMaterialization:\n enum:\n - manual\n - on_completion\n - rolling\n occurrenceNextTrigger:\n enum:\n - completion\n - completion_or_skip\n occurrenceTemplate:\n type: string\n occurrencePastHorizon:\n type: string\n occurrenceFutureHorizon:\n type: string\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n reminders:\n type: array\n items:\n type: object\n blockedBy:\n type: array\n items:\n type: object\n completeInstances:\n type: array\n items:\n type: string\n format: date\n skippedInstances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n sortOrder:\n type: string\nbinding_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\n title: TaskNotes task data-contract binding\n description: Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\n type: object\n required:\n - profiles\n - capabilities\n - title\n - status\n - priority\n - recurrence\n - occurrences\n - links\n - archive\n - time_tracking\n - templating\n properties:\n profiles:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - core-lite\n - recurrence\n - templating\n - materialized-occurrences\n - extended\n capabilities:\n type: array\n uniqueItems: true\n items:\n enum:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - rename\n - archive\n - batch\n - concurrency\n - dry-run\n - migration\n - templating\n title:\n $ref: \"#/$defs/titlePolicy\"\n status:\n $ref: \"#/$defs/statusPolicy\"\n priority:\n $ref: \"#/$defs/priorityPolicy\"\n recurrence:\n $ref: \"#/$defs/recurrencePolicy\"\n occurrences:\n $ref: \"#/$defs/occurrencePolicy\"\n links:\n $ref: \"#/$defs/linkPolicy\"\n archive:\n $ref: \"#/$defs/archivePolicy\"\n time_tracking:\n $ref: \"#/$defs/timeTrackingPolicy\"\n templating:\n $ref: \"#/$defs/templatingPolicy\"\n nlp:\n $ref: \"#/$defs/nlpPolicy\"\n additionalProperties: false\n $defs:\n nonEmptyString:\n type: string\n minLength: 1\n stringSet:\n type: array\n uniqueItems: true\n items:\n $ref: \"#/$defs/nonEmptyString\"\n titlePolicy:\n type: object\n required:\n - storage\n properties:\n storage:\n enum:\n - filename\n - frontmatter\n filename_format:\n enum:\n - title\n - zettel\n - timestamp\n - uuid\n - custom\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n allOf:\n - if:\n properties:\n filename_format:\n const: custom\n required:\n - filename_format\n then:\n properties:\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n required:\n - custom_filename_template\n additionalProperties: true\n statusDefinition:\n type: object\n required:\n - value\n - label\n - color\n - is_completed\n - is_skipped\n - exclude_from_cycle\n - order\n - auto_archive\n - auto_archive_delay_minutes\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n is_completed:\n type: boolean\n is_skipped:\n type: boolean\n exclude_from_cycle:\n type: boolean\n next_status:\n $ref: \"#/$defs/nonEmptyString\"\n order:\n type: number\n auto_archive:\n type: boolean\n auto_archive_delay_minutes:\n type: number\n minimum: 0\n additionalProperties: true\n statusPolicy:\n type: object\n required:\n - values\n - default\n - completed_values\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n completed_values:\n $ref: \"#/$defs/stringSet\"\n skipped_values:\n $ref: \"#/$defs/stringSet\"\n default_skipped:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/statusDefinition\"\n additionalProperties: false\n priorityDefinition:\n type: object\n required:\n - value\n - label\n - color\n - weight\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n weight:\n type: number\n additionalProperties: true\n priorityPolicy:\n type: object\n required:\n - values\n - default\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/priorityDefinition\"\n additionalProperties: false\n recurrencePolicy:\n type: object\n required:\n - syntax\n - maintain_due_date_offset\n - reset_body_checkboxes\n properties:\n syntax:\n const: tasknotes\n maintain_due_date_offset:\n type: boolean\n reset_body_checkboxes:\n type: boolean\n additionalProperties: false\n occurrencePolicy:\n type: object\n required:\n - identity_roles\n - default_materialization\n - default_next_trigger\n properties:\n identity_roles:\n const:\n - recurrenceParent\n - occurrenceDate\n default_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default_next_trigger:\n enum:\n - completion\n - completion_or_skip\n past_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n future_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n linkPolicy:\n type: object\n required:\n - accepted_formats\n - write_format\n properties:\n accepted_formats:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - wikilink\n - markdown\n write_format:\n enum:\n - wikilink\n - markdown\n additionalProperties: false\n archivePolicy:\n type: object\n required:\n - archived_tag\n - move_on_archive\n properties:\n archived_tag:\n $ref: \"#/$defs/nonEmptyString\"\n move_on_archive:\n type: boolean\n folder:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n timeTrackingPolicy:\n type: object\n required:\n - auto_stop_on_complete\n properties:\n auto_stop_on_complete:\n type: boolean\n additionalProperties: false\n templatingPolicy:\n type: object\n required:\n - enabled\n - occurrence_enabled\n properties:\n enabled:\n type: boolean\n template_path:\n $ref: \"#/$defs/nonEmptyString\"\n occurrence_enabled:\n type: boolean\n occurrence_template_path:\n $ref: \"#/$defs/nonEmptyString\"\n failure_mode:\n enum:\n - error\n - warning_fallback\n unknown_variable_policy:\n enum:\n - preserve\n - empty\n additionalProperties: false\n nlpTrigger:\n type: object\n required:\n - property_id\n - trigger\n - enabled\n properties:\n property_id:\n $ref: \"#/$defs/nonEmptyString\"\n trigger:\n $ref: \"#/$defs/nonEmptyString\"\n enabled:\n type: boolean\n additionalProperties: false\n nlpPolicy:\n type: object\n required:\n - triggers\n properties:\n triggers:\n type: array\n items:\n $ref: \"#/$defs/nlpTrigger\"\n additionalProperties: false\n---\n\n# TaskNotes task contract\n\nTypes implement this contract through `implements`; applications consume\nthe normalized contract view rather than assuming frontmatter names.\n" - }, - { - "source": "types/tasknotes-task/5.md", - "document": "---\nkind: mdbase.type\nname: task\nversion: 4\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a2\n - none\n - open\n - in-progress\n - done\n - cancelled\n default: open\n priority:\n enum: &a4\n - none\n - low\n - normal\n - high\n default: normal\n due: &a1\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled: *a1\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n items:\n type: string\n minLength: 1\n pattern: \\S\n uniqueItems: true\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a3\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n assignees[]:\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.5\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n assignees: assignees\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - materialized-occurrences\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a2\n default: open\n completed_values: *a3\n skipped_values:\n - cancelled\n default_skipped: cancelled\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 0\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: open\n label: Open\n color: \"#64748b\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 1\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: in-progress\n label: In progress\n color: \"#3b82f6\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 2\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: done\n label: Done\n color: \"#22c55e\"\n is_completed: true\n is_skipped: false\n exclude_from_cycle: false\n order: 3\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: cancelled\n label: Cancelled\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: true\n exclude_from_cycle: true\n order: 4\n auto_archive: false\n auto_archive_delay_minutes: 5\n priority:\n values: *a4\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n weight: 0\n - value: low\n label: Low\n color: \"#3b82f6\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#f59e0b\"\n weight: 2\n - value: high\n label: High\n color: \"#ef4444\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n reset_body_checkboxes: false\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n write_format: wikilink\n archive:\n archived_tag: archived\n move_on_archive: false\n time_tracking:\n auto_stop_on_complete: false\n templating:\n enabled: false\n occurrence_enabled: false\nx-tasknotes-generator:\n managed_fields:\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n# Task\n\nTask records live under `tasks/`.\n" - }, - { - "source": "schemas/tasknotes.task/0.3.0-rc.5.schema.json", - "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task.schema.json\",\n \"title\": \"TaskNotes portable task view\",\n \"description\": \"The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.5 record contract.\",\n \"type\": \"object\",\n \"required\": [\n \"status\",\n \"dateCreated\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"status\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"priority\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"due\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"scheduled\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"contexts\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"projects\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"assignees\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"pattern\": \"\\\\S\"\n },\n \"description\": \"Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\"\n },\n \"attachments\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"timeEstimate\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"completedDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"dateCreated\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"dateModified\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"recurrence\": {\n \"type\": \"string\"\n },\n \"recurrenceAnchor\": {\n \"enum\": [\n \"scheduled\",\n \"completion\"\n ]\n },\n \"recurrenceParent\": {\n \"type\": \"string\"\n },\n \"occurrenceDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"occurrenceMaterialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"occurrenceNextTrigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"occurrenceTemplate\": {\n \"type\": \"string\"\n },\n \"occurrencePastHorizon\": {\n \"type\": \"string\"\n },\n \"occurrenceFutureHorizon\": {\n \"type\": \"string\"\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"timeEntries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"reminders\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"blockedBy\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"completeInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"skippedInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"icsEventId\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"googleCalendarEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionOriginalScheduled\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"googleCalendarMovedOriginalDates\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"sortOrder\": {\n \"type\": \"string\"\n }\n }\n}\n" - }, - { - "source": "schemas/tasknotes.task.binding/0.3.0-rc.5.schema.json", - "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\",\n \"title\": \"TaskNotes task data-contract binding\",\n \"description\": \"Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\",\n \"type\": \"object\",\n \"required\": [\n \"profiles\",\n \"capabilities\",\n \"title\",\n \"status\",\n \"priority\",\n \"recurrence\",\n \"occurrences\",\n \"links\",\n \"archive\",\n \"time_tracking\",\n \"templating\"\n ],\n \"properties\": {\n \"profiles\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"core-lite\",\n \"recurrence\",\n \"templating\",\n \"materialized-occurrences\",\n \"extended\"\n ]\n }\n },\n \"capabilities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"dependencies\",\n \"reminders\",\n \"attachments\",\n \"links\",\n \"time-tracking\",\n \"materialized-occurrences\",\n \"rename\",\n \"archive\",\n \"batch\",\n \"concurrency\",\n \"dry-run\",\n \"migration\",\n \"templating\"\n ]\n }\n },\n \"title\": {\n \"$ref\": \"#/$defs/titlePolicy\"\n },\n \"status\": {\n \"$ref\": \"#/$defs/statusPolicy\"\n },\n \"priority\": {\n \"$ref\": \"#/$defs/priorityPolicy\"\n },\n \"recurrence\": {\n \"$ref\": \"#/$defs/recurrencePolicy\"\n },\n \"occurrences\": {\n \"$ref\": \"#/$defs/occurrencePolicy\"\n },\n \"links\": {\n \"$ref\": \"#/$defs/linkPolicy\"\n },\n \"archive\": {\n \"$ref\": \"#/$defs/archivePolicy\"\n },\n \"time_tracking\": {\n \"$ref\": \"#/$defs/timeTrackingPolicy\"\n },\n \"templating\": {\n \"$ref\": \"#/$defs/templatingPolicy\"\n },\n \"nlp\": {\n \"$ref\": \"#/$defs/nlpPolicy\"\n }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"nonEmptyString\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"stringSet\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"titlePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"storage\"\n ],\n \"properties\": {\n \"storage\": {\n \"enum\": [\n \"filename\",\n \"frontmatter\"\n ]\n },\n \"filename_format\": {\n \"enum\": [\n \"title\",\n \"zettel\",\n \"timestamp\",\n \"uuid\",\n \"custom\"\n ]\n },\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"allOf\": [\n {\n \"if\": {\n \"properties\": {\n \"filename_format\": {\n \"const\": \"custom\"\n }\n },\n \"required\": [\n \"filename_format\"\n ]\n },\n \"then\": {\n \"properties\": {\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"required\": [\n \"custom_filename_template\"\n ]\n }\n }\n ],\n \"additionalProperties\": true\n },\n \"statusDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"is_completed\",\n \"is_skipped\",\n \"exclude_from_cycle\",\n \"order\",\n \"auto_archive\",\n \"auto_archive_delay_minutes\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"is_completed\": {\n \"type\": \"boolean\"\n },\n \"is_skipped\": {\n \"type\": \"boolean\"\n },\n \"exclude_from_cycle\": {\n \"type\": \"boolean\"\n },\n \"next_status\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"order\": {\n \"type\": \"number\"\n },\n \"auto_archive\": {\n \"type\": \"boolean\"\n },\n \"auto_archive_delay_minutes\": {\n \"type\": \"number\",\n \"minimum\": 0\n }\n },\n \"additionalProperties\": true\n },\n \"statusPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"completed_values\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"completed_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"skipped_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default_skipped\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/statusDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"priorityDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"weight\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"weight\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"priorityPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/priorityDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"recurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"syntax\",\n \"maintain_due_date_offset\",\n \"reset_body_checkboxes\"\n ],\n \"properties\": {\n \"syntax\": {\n \"const\": \"tasknotes\"\n },\n \"maintain_due_date_offset\": {\n \"type\": \"boolean\"\n },\n \"reset_body_checkboxes\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"occurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"identity_roles\",\n \"default_materialization\",\n \"default_next_trigger\"\n ],\n \"properties\": {\n \"identity_roles\": {\n \"const\": [\n \"recurrenceParent\",\n \"occurrenceDate\"\n ]\n },\n \"default_materialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"default_next_trigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"past_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"future_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"linkPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"accepted_formats\",\n \"write_format\"\n ],\n \"properties\": {\n \"accepted_formats\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"write_format\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"archivePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"archived_tag\",\n \"move_on_archive\"\n ],\n \"properties\": {\n \"archived_tag\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"move_on_archive\": {\n \"type\": \"boolean\"\n },\n \"folder\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"timeTrackingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"auto_stop_on_complete\"\n ],\n \"properties\": {\n \"auto_stop_on_complete\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"templatingPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"enabled\",\n \"occurrence_enabled\"\n ],\n \"properties\": {\n \"enabled\": {\n \"type\": \"boolean\"\n },\n \"template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"occurrence_enabled\": {\n \"type\": \"boolean\"\n },\n \"occurrence_template_path\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"failure_mode\": {\n \"enum\": [\n \"error\",\n \"warning_fallback\"\n ]\n },\n \"unknown_variable_policy\": {\n \"enum\": [\n \"preserve\",\n \"empty\"\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"nlpTrigger\": {\n \"type\": \"object\",\n \"required\": [\n \"property_id\",\n \"trigger\",\n \"enabled\"\n ],\n \"properties\": {\n \"property_id\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"trigger\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"enabled\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"nlpPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"triggers\"\n ],\n \"properties\": {\n \"triggers\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/nlpTrigger\"\n }\n }\n },\n \"additionalProperties\": false\n }\n }\n}\n" - } - ], - "provides": [ - { - "id": "tasknotes.task", - "version": "0.3.0-rc.5", - "digest": "sha256:dde71d0d776aa0069ee627fe3c10b1a20f87ba36929a16790598ebfb4e8ebf04" - } - ] -} diff --git a/vendor/mdbase-contracts/tasknotes.task-0.3.0-rc.19.json b/vendor/mdbase-contracts/tasknotes.task-0.3.0-rc.19.json new file mode 100644 index 000000000..0e7dcb7c7 --- /dev/null +++ b/vendor/mdbase-contracts/tasknotes.task-0.3.0-rc.19.json @@ -0,0 +1,167 @@ +{ + "manifest": { + "kind": "mdbase.type-pack", + "id": "tasknotes.task", + "version": "0.3.0-rc.19", + "name": "TaskNotes task type pack", + "description": "Portable tasks whose app behaviour (templates, archive moves, status cycling, time tracking and link format) lives in a TaskNotes settings record, with a reviewed, atomic upgrade from every TaskNotes starter published before.", + "resources": [ + { + "kind": "contract", + "mode": "managed", + "source": "contracts/tasknotes.task/0.3.0-rc.5.md", + "target": "_contracts/tasknotes.task.md", + "digest": "sha256:ecadd00700e249940f4244aaa5b5985a96557d13340463ec491dfa5e89b032a4" + }, + { + "kind": "contract", + "mode": "managed", + "source": "contracts/tasknotes.task/0.3.0-rc.6.md", + "target": "_contracts/tasknotes.task/0.3.0-rc.6.md", + "digest": "sha256:3f4eb90782948ed68112ffcc99575911036020544cf501de3d2265b588b38456" + }, + { + "kind": "type", + "mode": "seed", + "source": "types/tasknotes-task/6.md", + "target": "_types/task.md", + "digest": "sha256:87e52c25b474a06283b29e2983ae4fc6e618fd1a4cbcc5856ff9a67328df605b", + "upgrade_from": [ + { + "digest": "sha256:b3cf8dbe8885b331c494d2eeb36abfb052890e06b757877d655c269e666487dc", + "version": 4, + "document": "---\nkind: mdbase.type\nname: task\nversion: 4\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a2\n - none\n - open\n - in-progress\n - done\n - cancelled\n default: open\n priority:\n enum: &a4\n - none\n - low\n - normal\n - high\n default: normal\n due: &a1\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled: *a1\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n items:\n type: string\n minLength: 1\n pattern: \\S\n uniqueItems: true\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a3\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n assignees[]:\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.5\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n assignees: assignees\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - materialized-occurrences\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a2\n default: open\n completed_values: *a3\n skipped_values:\n - cancelled\n default_skipped: cancelled\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 0\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: open\n label: Open\n color: \"#64748b\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 1\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: in-progress\n label: In progress\n color: \"#3b82f6\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 2\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: done\n label: Done\n color: \"#22c55e\"\n is_completed: true\n is_skipped: false\n exclude_from_cycle: false\n order: 3\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: cancelled\n label: Cancelled\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: true\n exclude_from_cycle: true\n order: 4\n auto_archive: false\n auto_archive_delay_minutes: 5\n priority:\n values: *a4\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n weight: 0\n - value: low\n label: Low\n color: \"#3b82f6\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#f59e0b\"\n weight: 2\n - value: high\n label: High\n color: \"#ef4444\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n reset_body_checkboxes: false\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n write_format: wikilink\n archive:\n archived_tag: archived\n move_on_archive: false\n time_tracking:\n auto_stop_on_complete: false\n templating:\n enabled: false\n occurrence_enabled: false\nx-tasknotes-generator:\n managed_fields:\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n# Task\n\nTask records live under `tasks/`.\n" + }, + { + "digest": "sha256:2b6016c3543c0dfff491cc6262aed3afd4d672360484174042a261acd1d8ff5f", + "version": 4, + "document": "---\nkind: mdbase.type\nname: task\nversion: 4\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a2\n - none\n - open\n - in-progress\n - done\n - cancelled\n default: open\n priority:\n enum: &a4\n - none\n - low\n - normal\n - high\n default: normal\n due: &a1\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled: *a1\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n items:\n type: string\n minLength: 1\n pattern: \\S\n uniqueItems: true\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a3\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n assignees[]:\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.5\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n assignees: assignees\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - materialized-occurrences\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a2\n default: open\n completed_values: *a3\n skipped_values:\n - cancelled\n default_skipped: cancelled\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 0\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: open\n label: Open\n color: \"#64748b\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 1\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: in-progress\n label: In progress\n color: \"#3b82f6\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 2\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: done\n label: Done\n color: \"#22c55e\"\n is_completed: true\n is_skipped: false\n exclude_from_cycle: false\n order: 3\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: cancelled\n label: Cancelled\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: true\n exclude_from_cycle: true\n order: 4\n auto_archive: false\n auto_archive_delay_minutes: 5\n priority:\n values: *a4\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n weight: 0\n - value: low\n label: Low\n color: \"#3b82f6\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#f59e0b\"\n weight: 2\n - value: high\n label: High\n color: \"#ef4444\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n reset_body_checkboxes: false\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n write_format: wikilink\n archive:\n archived_tag: archived\n move_on_archive: false\n time_tracking:\n auto_stop_on_complete: false\n templating:\n enabled: false\n occurrence_enabled: false\nx-tasknotes-generator:\n managed_fields:\n - assignees\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n# Task\n\nTask records live under `tasks/`.\n" + }, + { + "digest": "sha256:02d1a2a04f26937dd7eec3cde3070dd770c73625e06f386d6214044d176788dd", + "version": 3, + "document": "---\nkind: mdbase.type\nname: task\nversion: 3\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a1\n - none\n - open\n - in-progress\n - done\n default: open\n priority:\n enum: &a3\n - none\n - low\n - normal\n - high\n default: normal\n due:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n items:\n type: string\n minLength: 1\n pattern: \\S\n uniqueItems: true\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a2\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n assignees[]:\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.5\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n assignees: assignees\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - templating\n - materialized-occurrences\n - extended\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a1\n default: open\n completed_values: *a2\n skipped_values: []\n definitions:\n - value: none\n label: None\n color: \"#cccccc\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 0\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: open\n label: Open\n color: \"#808080\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 1\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: in-progress\n label: In progress\n color: \"#0066cc\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 2\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: done\n label: Done\n color: \"#00aa00\"\n is_completed: true\n is_skipped: false\n exclude_from_cycle: false\n order: 3\n auto_archive: false\n auto_archive_delay_minutes: 5\n priority:\n values: *a3\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#cccccc\"\n weight: 0\n - value: low\n label: Low\n color: \"#00aa00\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#ffaa00\"\n weight: 2\n - value: high\n label: High\n color: \"#ff0000\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n reset_body_checkboxes: false\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n write_format: wikilink\n archive:\n archived_tag: archived\n move_on_archive: false\n time_tracking:\n auto_stop_on_complete: false\n templating:\n enabled: false\n occurrence_enabled: false\nx-tasknotes-generator:\n managed_fields:\n - assignees\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n\n# Task\n\nThis type definition implements the TaskNotes contract for this mdbase collection.\nIts JSON Schema describes persisted task frontmatter; collection and lifecycle\nmetadata describe generic mdbase behavior; `implements` maps the portable\nTaskNotes task view and supplies TaskNotes behavior.\n\nChanges made here are loaded by TaskNotes. Portable changes made in TaskNotes\nsettings are written back while unknown extensions are preserved.\n" + }, + { + "digest": "sha256:ade71117bd31e571f403511c48d432bab7a05dfd25e730ca2931f3afda7f7cd5", + "version": 2, + "document": "---\nkind: mdbase.type\nname: task\nversion: 2\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a1\n - none\n - open\n - in-progress\n - done\n default: open\n priority:\n enum: &a3\n - none\n - low\n - normal\n - high\n default: normal\n due:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n items:\n type: string\n minLength: 1\n pattern: \\S\n uniqueItems: true\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a2\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.4\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n assignees: assignees\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - templating\n - materialized-occurrences\n - extended\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a1\n default: open\n completed_values: *a2\n skipped_values: []\n definitions:\n - value: none\n label: None\n color: \"#cccccc\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 0\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: open\n label: Open\n color: \"#808080\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 1\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: in-progress\n label: In progress\n color: \"#0066cc\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 2\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: done\n label: Done\n color: \"#00aa00\"\n is_completed: true\n is_skipped: false\n exclude_from_cycle: false\n order: 3\n auto_archive: false\n auto_archive_delay_minutes: 5\n priority:\n values: *a3\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#cccccc\"\n weight: 0\n - value: low\n label: Low\n color: \"#00aa00\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#ffaa00\"\n weight: 2\n - value: high\n label: High\n color: \"#ff0000\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n reset_body_checkboxes: false\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n write_format: wikilink\n archive:\n archived_tag: archived\n move_on_archive: false\n time_tracking:\n auto_stop_on_complete: false\n templating:\n enabled: false\n occurrence_enabled: false\nx-tasknotes-generator:\n managed_fields:\n - assignees\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n\n# Task\n\nThis type definition implements the TaskNotes contract for this mdbase collection.\nIts JSON Schema describes persisted task frontmatter; collection and lifecycle\nmetadata describe generic mdbase behavior; `implements` maps the portable\nTaskNotes task view and supplies TaskNotes behavior.\n\nChanges made here are loaded by TaskNotes. Portable changes made in TaskNotes\nsettings are written back while unknown extensions are preserved.\n" + }, + { + "digest": "sha256:b7f8af7b18d77859dab1f1532c1b55c08c7c39879f55e4ef2a94cb7d2d2284eb", + "version": 1, + "document": "---\nkind: mdbase.type\nname: task\nversion: 1\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a2\n - none\n - open\n - in-progress\n - done\n - cancelled\n default: open\n priority:\n enum: &a4\n - none\n - low\n - normal\n - high\n default: normal\n due: &a1\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled: *a1\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a3\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.3\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - materialized-occurrences\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a2\n default: open\n completed_values: *a3\n skipped_values:\n - cancelled\n default_skipped: cancelled\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 0\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: open\n label: Open\n color: \"#64748b\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 1\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: in-progress\n label: In progress\n color: \"#3b82f6\"\n is_completed: false\n is_skipped: false\n exclude_from_cycle: false\n order: 2\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: done\n label: Done\n color: \"#22c55e\"\n is_completed: true\n is_skipped: false\n exclude_from_cycle: false\n order: 3\n auto_archive: false\n auto_archive_delay_minutes: 5\n - value: cancelled\n label: Cancelled\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: true\n exclude_from_cycle: true\n order: 4\n auto_archive: false\n auto_archive_delay_minutes: 5\n priority:\n values: *a4\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n weight: 0\n - value: low\n label: Low\n color: \"#3b82f6\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#f59e0b\"\n weight: 2\n - value: high\n label: High\n color: \"#ef4444\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n reset_body_checkboxes: false\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n write_format: wikilink\n archive:\n archived_tag: archived\n move_on_archive: false\n time_tracking:\n auto_stop_on_complete: false\n templating:\n enabled: false\n occurrence_enabled: false\nx-tasknotes-generator:\n managed_fields:\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n# Task\n\nTask records live under `tasks/`.\n" + } + ] + }, + { + "kind": "schema", + "mode": "managed", + "source": "schemas/tasknotes.task/0.3.0-rc.6.schema.json", + "target": "_schemas/tasknotes/tasknotes-task.schema.json", + "digest": "sha256:ac78a55f46669d6e8bd08d608b7c25338fec8ba7606dbf54ebd6b8d4e66b7035" + }, + { + "kind": "schema", + "mode": "managed", + "source": "schemas/tasknotes.task.binding/0.3.0-rc.6.schema.json", + "target": "_schemas/tasknotes/tasknotes-task-binding.schema.json", + "digest": "sha256:4f8f3288f593ce06a04525697308173147ff957e7066f83d9a922468d06be659" + }, + { + "kind": "schema", + "mode": "managed", + "source": "schemas/mdbase.app-settings/1.0.0.schema.json", + "target": "schemas/mdbase.app-settings/1.0.0.schema.json", + "digest": "sha256:f37ed43b57fe86465b5bb67216da71d6e351bc1b85bf55a20f95edf886f0749e" + }, + { + "kind": "contract", + "mode": "managed", + "source": "contracts/mdbase.app-settings/1.0.0.md", + "target": "_contracts/mdbase.app-settings/1.0.0.md", + "digest": "sha256:9028d8ccd99cfbb4034399777376315486eb8e699aea7757a6b193743f1084c8" + }, + { + "kind": "schema", + "mode": "managed", + "source": "schemas/tasknotes.settings/1.0.0.schema.json", + "target": "schemas/tasknotes.settings/1.0.0.schema.json", + "digest": "sha256:3a4fc207e14ab1579ca478ce84b25cb8dd85b804b3aa79d01eda08e4554dfbb1" + }, + { + "kind": "contract", + "mode": "managed", + "source": "contracts/tasknotes.settings/1.0.0.md", + "target": "_contracts/tasknotes.settings/1.0.0.md", + "digest": "sha256:e8c25653ef37c032fb4cb1f1c3f7c618092a09c4911a41aba42c0476796fe0d1" + }, + { + "kind": "type", + "mode": "seed", + "source": "types/tasknotes-settings/1.md", + "target": "_types/tasknotes-settings.md", + "digest": "sha256:22ec312c75df356b0b26ba3c46961212efe2803a5d57071fa5753bab7638c2cc" + } + ] + }, + "resources": [ + { + "source": "contracts/tasknotes.task/0.3.0-rc.5.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: tasknotes.task\nversion: 0.3.0-rc.5\nname: TaskNotes task\ndescription: Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.5.\nrecord_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task.schema.json\n title: TaskNotes portable task view\n description: The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.5 record contract.\n type: object\n required:\n - status\n - dateCreated\n additionalProperties: false\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n type: string\n minLength: 1\n priority:\n type: string\n minLength: 1\n due:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n pattern: \\S\n description: Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\n attachments:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrenceAnchor:\n enum:\n - scheduled\n - completion\n recurrenceParent:\n type: string\n occurrenceDate:\n type: string\n format: date\n occurrenceMaterialization:\n enum:\n - manual\n - on_completion\n - rolling\n occurrenceNextTrigger:\n enum:\n - completion\n - completion_or_skip\n occurrenceTemplate:\n type: string\n occurrencePastHorizon:\n type: string\n occurrenceFutureHorizon:\n type: string\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n reminders:\n type: array\n items:\n type: object\n blockedBy:\n type: array\n items:\n type: object\n completeInstances:\n type: array\n items:\n type: string\n format: date\n skippedInstances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n sortOrder:\n type: string\nbinding_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\n title: TaskNotes task data-contract binding\n description: Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.\n type: object\n required:\n - profiles\n - capabilities\n - title\n - status\n - priority\n - recurrence\n - occurrences\n - links\n - archive\n - time_tracking\n - templating\n properties:\n profiles:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - core-lite\n - recurrence\n - templating\n - materialized-occurrences\n - extended\n capabilities:\n type: array\n uniqueItems: true\n items:\n enum:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - rename\n - archive\n - batch\n - concurrency\n - dry-run\n - migration\n - templating\n title:\n $ref: \"#/$defs/titlePolicy\"\n status:\n $ref: \"#/$defs/statusPolicy\"\n priority:\n $ref: \"#/$defs/priorityPolicy\"\n recurrence:\n $ref: \"#/$defs/recurrencePolicy\"\n occurrences:\n $ref: \"#/$defs/occurrencePolicy\"\n links:\n $ref: \"#/$defs/linkPolicy\"\n archive:\n $ref: \"#/$defs/archivePolicy\"\n time_tracking:\n $ref: \"#/$defs/timeTrackingPolicy\"\n templating:\n $ref: \"#/$defs/templatingPolicy\"\n nlp:\n $ref: \"#/$defs/nlpPolicy\"\n additionalProperties: false\n $defs:\n nonEmptyString:\n type: string\n minLength: 1\n stringSet:\n type: array\n uniqueItems: true\n items:\n $ref: \"#/$defs/nonEmptyString\"\n titlePolicy:\n type: object\n required:\n - storage\n properties:\n storage:\n enum:\n - filename\n - frontmatter\n filename_format:\n enum:\n - title\n - zettel\n - timestamp\n - uuid\n - custom\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n allOf:\n - if:\n properties:\n filename_format:\n const: custom\n required:\n - filename_format\n then:\n properties:\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n required:\n - custom_filename_template\n additionalProperties: true\n statusDefinition:\n type: object\n required:\n - value\n - label\n - color\n - is_completed\n - is_skipped\n - exclude_from_cycle\n - order\n - auto_archive\n - auto_archive_delay_minutes\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n is_completed:\n type: boolean\n is_skipped:\n type: boolean\n exclude_from_cycle:\n type: boolean\n next_status:\n $ref: \"#/$defs/nonEmptyString\"\n order:\n type: number\n auto_archive:\n type: boolean\n auto_archive_delay_minutes:\n type: number\n minimum: 0\n additionalProperties: true\n statusPolicy:\n type: object\n required:\n - values\n - default\n - completed_values\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n completed_values:\n $ref: \"#/$defs/stringSet\"\n skipped_values:\n $ref: \"#/$defs/stringSet\"\n default_skipped:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/statusDefinition\"\n additionalProperties: false\n priorityDefinition:\n type: object\n required:\n - value\n - label\n - color\n - weight\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n weight:\n type: number\n additionalProperties: true\n priorityPolicy:\n type: object\n required:\n - values\n - default\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/priorityDefinition\"\n additionalProperties: false\n recurrencePolicy:\n type: object\n required:\n - syntax\n - maintain_due_date_offset\n - reset_body_checkboxes\n properties:\n syntax:\n const: tasknotes\n maintain_due_date_offset:\n type: boolean\n reset_body_checkboxes:\n type: boolean\n additionalProperties: false\n occurrencePolicy:\n type: object\n required:\n - identity_roles\n - default_materialization\n - default_next_trigger\n properties:\n identity_roles:\n const:\n - recurrenceParent\n - occurrenceDate\n default_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default_next_trigger:\n enum:\n - completion\n - completion_or_skip\n past_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n future_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n linkPolicy:\n type: object\n required:\n - accepted_formats\n - write_format\n properties:\n accepted_formats:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - wikilink\n - markdown\n write_format:\n enum:\n - wikilink\n - markdown\n additionalProperties: false\n archivePolicy:\n type: object\n required:\n - archived_tag\n - move_on_archive\n properties:\n archived_tag:\n $ref: \"#/$defs/nonEmptyString\"\n move_on_archive:\n type: boolean\n folder:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n timeTrackingPolicy:\n type: object\n required:\n - auto_stop_on_complete\n properties:\n auto_stop_on_complete:\n type: boolean\n additionalProperties: false\n templatingPolicy:\n type: object\n required:\n - enabled\n - occurrence_enabled\n properties:\n enabled:\n type: boolean\n template_path:\n $ref: \"#/$defs/nonEmptyString\"\n occurrence_enabled:\n type: boolean\n occurrence_template_path:\n $ref: \"#/$defs/nonEmptyString\"\n failure_mode:\n enum:\n - error\n - warning_fallback\n unknown_variable_policy:\n enum:\n - preserve\n - empty\n additionalProperties: false\n nlpTrigger:\n type: object\n required:\n - property_id\n - trigger\n - enabled\n properties:\n property_id:\n $ref: \"#/$defs/nonEmptyString\"\n trigger:\n $ref: \"#/$defs/nonEmptyString\"\n enabled:\n type: boolean\n additionalProperties: false\n nlpPolicy:\n type: object\n required:\n - triggers\n properties:\n triggers:\n type: array\n items:\n $ref: \"#/$defs/nlpTrigger\"\n additionalProperties: false\n---\n\n# TaskNotes task contract\n\nTypes implement this contract through `implements`; applications consume\nthe normalized contract view rather than assuming frontmatter names.\n" + }, + { + "source": "contracts/tasknotes.task/0.3.0-rc.6.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: tasknotes.task\nversion: 0.3.0-rc.6\nname: TaskNotes task\ndescription: Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.6.\nrecord_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task.schema.json\n title: TaskNotes portable task view\n description: The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.6 record contract.\n type: object\n required:\n - status\n - dateCreated\n additionalProperties: false\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n type: string\n minLength: 1\n priority:\n type: string\n minLength: 1\n due:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled:\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n pattern: \\S\n description: Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\n attachments:\n type: array\n uniqueItems: true\n items:\n type: string\n minLength: 1\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrenceAnchor:\n enum:\n - scheduled\n - completion\n recurrenceParent:\n type: string\n occurrenceDate:\n type: string\n format: date\n occurrenceMaterialization:\n enum:\n - manual\n - on_completion\n - rolling\n occurrenceNextTrigger:\n enum:\n - completion\n - completion_or_skip\n occurrenceTemplate:\n type: string\n occurrencePastHorizon:\n type: string\n occurrenceFutureHorizon:\n type: string\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n reminders:\n type: array\n items:\n type: object\n blockedBy:\n type: array\n items:\n type: object\n completeInstances:\n type: array\n items:\n type: string\n format: date\n skippedInstances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n sortOrder:\n type: string\nbinding_schema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n $id: https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\n title: TaskNotes task data-contract binding\n description: Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.6. App behaviour, such as templates, archive moves, status cycling and natural-language triggers, lives in the app's tasknotes.settings record instead.\n type: object\n required:\n - profiles\n - capabilities\n - title\n - status\n - priority\n - recurrence\n - occurrences\n - links\n - archive\n properties:\n profiles:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - core-lite\n - recurrence\n - templating\n - materialized-occurrences\n - extended\n capabilities:\n type: array\n uniqueItems: true\n items:\n enum:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - rename\n - archive\n - batch\n - concurrency\n - dry-run\n - migration\n - templating\n title:\n $ref: \"#/$defs/titlePolicy\"\n status:\n $ref: \"#/$defs/statusPolicy\"\n priority:\n $ref: \"#/$defs/priorityPolicy\"\n recurrence:\n $ref: \"#/$defs/recurrencePolicy\"\n occurrences:\n $ref: \"#/$defs/occurrencePolicy\"\n links:\n $ref: \"#/$defs/linkPolicy\"\n archive:\n $ref: \"#/$defs/archivePolicy\"\n additionalProperties: false\n $defs:\n nonEmptyString:\n type: string\n minLength: 1\n stringSet:\n type: array\n uniqueItems: true\n items:\n $ref: \"#/$defs/nonEmptyString\"\n titlePolicy:\n type: object\n required:\n - storage\n properties:\n storage:\n enum:\n - filename\n - frontmatter\n filename_format:\n enum:\n - title\n - zettel\n - timestamp\n - uuid\n - custom\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n allOf:\n - if:\n properties:\n filename_format:\n const: custom\n required:\n - filename_format\n then:\n properties:\n custom_filename_template:\n $ref: \"#/$defs/nonEmptyString\"\n required:\n - custom_filename_template\n additionalProperties: true\n statusDefinition:\n type: object\n required:\n - value\n - label\n - color\n - is_completed\n - is_skipped\n - order\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n is_completed:\n type: boolean\n is_skipped:\n type: boolean\n order:\n type: number\n additionalProperties: true\n statusPolicy:\n type: object\n required:\n - values\n - default\n - completed_values\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n completed_values:\n $ref: \"#/$defs/stringSet\"\n skipped_values:\n $ref: \"#/$defs/stringSet\"\n default_skipped:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/statusDefinition\"\n additionalProperties: false\n priorityDefinition:\n type: object\n required:\n - value\n - label\n - color\n - weight\n properties:\n value:\n $ref: \"#/$defs/nonEmptyString\"\n label:\n type: string\n color:\n type: string\n icon:\n $ref: \"#/$defs/nonEmptyString\"\n weight:\n type: number\n additionalProperties: true\n priorityPolicy:\n type: object\n required:\n - values\n - default\n - definitions\n properties:\n values:\n $ref: \"#/$defs/stringSet\"\n default:\n $ref: \"#/$defs/nonEmptyString\"\n definitions:\n type: array\n items:\n $ref: \"#/$defs/priorityDefinition\"\n additionalProperties: false\n recurrencePolicy:\n type: object\n required:\n - syntax\n - maintain_due_date_offset\n properties:\n syntax:\n const: tasknotes\n maintain_due_date_offset:\n type: boolean\n additionalProperties: false\n occurrencePolicy:\n type: object\n required:\n - identity_roles\n - default_materialization\n - default_next_trigger\n properties:\n identity_roles:\n const:\n - recurrenceParent\n - occurrenceDate\n default_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default_next_trigger:\n enum:\n - completion\n - completion_or_skip\n past_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n future_horizon:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n linkPolicy:\n type: object\n required:\n - accepted_formats\n properties:\n accepted_formats:\n type: array\n minItems: 1\n uniqueItems: true\n items:\n enum:\n - wikilink\n - markdown\n additionalProperties: false\n archivePolicy:\n type: object\n required:\n - archived_tag\n properties:\n archived_tag:\n $ref: \"#/$defs/nonEmptyString\"\n additionalProperties: false\n---\n\n# TaskNotes task contract\n\nTypes implement this contract through `implements`; applications consume\nthe normalized contract view rather than assuming frontmatter names.\n" + }, + { + "source": "types/tasknotes-task/6.md", + "document": "---\nkind: mdbase.type\nname: task\nversion: 5\ndescription: A task managed by TaskNotes.\nmatch:\n where:\n tags:\n contains: task\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n type: object\n additionalProperties: true\n properties:\n id:\n type: string\n minLength: 1\n title:\n type: string\n minLength: 1\n status:\n enum: &a2\n - none\n - open\n - in-progress\n - done\n - cancelled\n default: open\n priority:\n enum: &a4\n - none\n - low\n - normal\n - high\n default: normal\n due: &a1\n anyOf:\n - type: string\n format: date\n - type: string\n format: date-time\n scheduled: *a1\n contexts:\n type: array\n items:\n type: string\n projects:\n type: array\n items:\n type: string\n assignees:\n type: array\n items:\n type: string\n minLength: 1\n pattern: \\S\n uniqueItems: true\n attachments:\n type: array\n items:\n type: string\n minLength: 1\n uniqueItems: true\n timeEstimate:\n type: integer\n minimum: 0\n completedDate:\n type: string\n format: date\n dateCreated:\n type: string\n format: date-time\n dateModified:\n type: string\n format: date-time\n recurrence:\n type: string\n recurrence_anchor:\n enum:\n - scheduled\n - completion\n default: scheduled\n occurrence_materialization:\n enum:\n - manual\n - on_completion\n - rolling\n default: manual\n occurrence_next_trigger:\n enum:\n - completion\n - completion_or_skip\n default: completion\n occurrence_template:\n type: string\n occurrence_past_horizon:\n type: string\n occurrence_future_horizon:\n type: string\n recurrence_parent:\n type: string\n occurrence_date:\n type: string\n format: date\n tags:\n type: array\n items:\n type: string\n timeEntries:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n startTime:\n type: string\n format: date-time\n endTime:\n type: string\n format: date-time\n description:\n type: string\n duration:\n type: integer\n reminders:\n type: array\n items:\n oneOf:\n - type: object\n required:\n - id\n - type\n - absoluteTime\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: absolute\n description:\n type: string\n absoluteTime:\n type: string\n format: date-time\n - type: object\n required:\n - id\n - type\n - relatedTo\n - offset\n additionalProperties: false\n properties:\n id:\n type: string\n type:\n const: relative\n description:\n type: string\n relatedTo:\n enum:\n - due\n - scheduled\n offset:\n type: string\n blockedBy:\n type: array\n items:\n type: object\n additionalProperties: false\n properties:\n uid:\n type: string\n reltype:\n type: string\n gap:\n type: string\n required:\n - uid\n complete_instances:\n type: array\n items:\n type: string\n format: date\n skipped_instances:\n type: array\n items:\n type: string\n format: date\n icsEventId:\n type: array\n items:\n type: string\n googleCalendarEventId:\n type: string\n googleCalendarExceptionEventId:\n type: string\n googleCalendarExceptionOriginalScheduled:\n type: string\n format: date\n googleCalendarMovedOriginalDates:\n type: array\n items:\n type: string\n format: date\n tasknotes_manual_order:\n type: string\n allOf:\n - if:\n required:\n - status\n properties:\n status:\n enum: &a3\n - done\n not:\n required:\n - recurrence\n then:\n required:\n - completedDate\n required:\n - title\n - status\n - dateCreated\ncollection:\n read_defaults:\n status: open\n priority: normal\n recurrence_anchor: scheduled\n occurrence_materialization: manual\n occurrence_next_trigger: completion\n links:\n projects[]:\n target_type: any\n validate_exists: false\n assignees[]:\n validate_exists: false\n attachments[]:\n validate_exists: false\n occurrence_template:\n target_type: any\n validate_exists: false\n recurrence_parent:\n target_type: task\n validate_exists: false\n blockedBy[].uid:\n target_type: task\n validate_exists: false\n path:\n runtime: tasknotes\n template: \"{{zettel}}\"\n folder: tasks\n generated_by: tasknotes.filename.create\n display:\n name_field: title\n unique:\n - field: id\n scope: type\nlifecycle:\n on_create:\n set:\n id:\n uuid: true\n dateCreated:\n now: true\n dateModified:\n now: true\n on_update:\n set:\n dateModified:\n now: true\nimplements:\n - contract: tasknotes.task\n version: 0.3.0-rc.6\n fields:\n id: id\n title: title\n status: status\n priority: priority\n due: due\n scheduled: scheduled\n contexts: contexts\n projects: projects\n assignees: assignees\n attachments: attachments\n timeEstimate: timeEstimate\n completedDate: completedDate\n dateCreated: dateCreated\n dateModified: dateModified\n recurrence: recurrence\n recurrenceAnchor: recurrence_anchor\n occurrenceMaterialization: occurrence_materialization\n occurrenceNextTrigger: occurrence_next_trigger\n occurrenceTemplate: occurrence_template\n occurrencePastHorizon: occurrence_past_horizon\n occurrenceFutureHorizon: occurrence_future_horizon\n recurrenceParent: recurrence_parent\n occurrenceDate: occurrence_date\n tags: tags\n timeEntries: timeEntries\n reminders: reminders\n blockedBy: blockedBy\n completeInstances: complete_instances\n skippedInstances: skipped_instances\n icsEventId: icsEventId\n googleCalendarEventId: googleCalendarEventId\n googleCalendarExceptionEventId: googleCalendarExceptionEventId\n googleCalendarExceptionOriginalScheduled: googleCalendarExceptionOriginalScheduled\n googleCalendarMovedOriginalDates: googleCalendarMovedOriginalDates\n sortOrder: tasknotes_manual_order\n binding:\n profiles:\n - core-lite\n - recurrence\n - materialized-occurrences\n capabilities:\n - dependencies\n - reminders\n - attachments\n - links\n - time-tracking\n - materialized-occurrences\n - archive\n - templating\n title:\n storage: frontmatter\n filename_format: zettel\n status:\n values: *a2\n default: open\n completed_values: *a3\n skipped_values:\n - cancelled\n default_skipped: cancelled\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: false\n order: 0\n - value: open\n label: Open\n color: \"#64748b\"\n is_completed: false\n is_skipped: false\n order: 1\n - value: in-progress\n label: In progress\n color: \"#3b82f6\"\n is_completed: false\n is_skipped: false\n order: 2\n - value: done\n label: Done\n color: \"#22c55e\"\n is_completed: true\n is_skipped: false\n order: 3\n - value: cancelled\n label: Cancelled\n color: \"#94a3b8\"\n is_completed: false\n is_skipped: true\n order: 4\n priority:\n values: *a4\n default: normal\n definitions:\n - value: none\n label: None\n color: \"#94a3b8\"\n weight: 0\n - value: low\n label: Low\n color: \"#3b82f6\"\n weight: 1\n - value: normal\n label: Normal\n color: \"#f59e0b\"\n weight: 2\n - value: high\n label: High\n color: \"#ef4444\"\n weight: 3\n recurrence:\n syntax: tasknotes\n maintain_due_date_offset: true\n occurrences:\n identity_roles:\n - recurrenceParent\n - occurrenceDate\n default_materialization: manual\n default_next_trigger: completion\n past_horizon: P0D\n future_horizon: P14D\n links:\n accepted_formats:\n - wikilink\n - markdown\n archive:\n archived_tag: archived\nx-tasknotes-generator:\n managed_fields:\n - attachments\n - blockedBy\n - complete_instances\n - completedDate\n - contexts\n - dateCreated\n - dateModified\n - due\n - googleCalendarEventId\n - googleCalendarExceptionEventId\n - googleCalendarExceptionOriginalScheduled\n - googleCalendarMovedOriginalDates\n - icsEventId\n - id\n - occurrence_date\n - occurrence_future_horizon\n - occurrence_materialization\n - occurrence_next_trigger\n - occurrence_past_horizon\n - occurrence_template\n - priority\n - projects\n - recurrence\n - recurrence_anchor\n - recurrence_parent\n - reminders\n - scheduled\n - skipped_instances\n - status\n - tags\n - tasknotes_manual_order\n - timeEntries\n - timeEstimate\n - title\n---\n# Task\n\nTask records live under `tasks/`.\n" + }, + { + "source": "schemas/tasknotes.task/0.3.0-rc.6.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task.schema.json\",\n \"title\": \"TaskNotes portable task view\",\n \"description\": \"The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.6 record contract.\",\n \"type\": \"object\",\n \"required\": [\n \"status\",\n \"dateCreated\"\n ],\n \"additionalProperties\": false,\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"title\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"status\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"priority\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"due\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"scheduled\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n {\n \"type\": \"string\",\n \"format\": \"date-time\"\n }\n ]\n },\n \"contexts\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"projects\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"assignees\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"pattern\": \"\\\\S\"\n },\n \"description\": \"Links to records implementing mdbase.person, such as [[Alex Rivera]]. Implementing types declare this field in collection.links. These references never grant collection access.\"\n },\n \"attachments\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"timeEstimate\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"completedDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"dateCreated\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"dateModified\": {\n \"type\": \"string\",\n \"format\": \"date-time\"\n },\n \"recurrence\": {\n \"type\": \"string\"\n },\n \"recurrenceAnchor\": {\n \"enum\": [\n \"scheduled\",\n \"completion\"\n ]\n },\n \"recurrenceParent\": {\n \"type\": \"string\"\n },\n \"occurrenceDate\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"occurrenceMaterialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"occurrenceNextTrigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"occurrenceTemplate\": {\n \"type\": \"string\"\n },\n \"occurrencePastHorizon\": {\n \"type\": \"string\"\n },\n \"occurrenceFutureHorizon\": {\n \"type\": \"string\"\n },\n \"tags\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"timeEntries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"reminders\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"blockedBy\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\"\n }\n },\n \"completeInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"skippedInstances\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"icsEventId\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"googleCalendarEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionEventId\": {\n \"type\": \"string\"\n },\n \"googleCalendarExceptionOriginalScheduled\": {\n \"type\": \"string\",\n \"format\": \"date\"\n },\n \"googleCalendarMovedOriginalDates\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"format\": \"date\"\n }\n },\n \"sortOrder\": {\n \"type\": \"string\"\n }\n }\n}\n" + }, + { + "source": "schemas/tasknotes.task.binding/0.3.0-rc.6.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json\",\n \"title\": \"TaskNotes task data-contract binding\",\n \"description\": \"Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.6. App behaviour, such as templates, archive moves, status cycling and natural-language triggers, lives in the app's tasknotes.settings record instead.\",\n \"type\": \"object\",\n \"required\": [\n \"profiles\",\n \"capabilities\",\n \"title\",\n \"status\",\n \"priority\",\n \"recurrence\",\n \"occurrences\",\n \"links\",\n \"archive\"\n ],\n \"properties\": {\n \"profiles\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"core-lite\",\n \"recurrence\",\n \"templating\",\n \"materialized-occurrences\",\n \"extended\"\n ]\n }\n },\n \"capabilities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"dependencies\",\n \"reminders\",\n \"attachments\",\n \"links\",\n \"time-tracking\",\n \"materialized-occurrences\",\n \"rename\",\n \"archive\",\n \"batch\",\n \"concurrency\",\n \"dry-run\",\n \"migration\",\n \"templating\"\n ]\n }\n },\n \"title\": {\n \"$ref\": \"#/$defs/titlePolicy\"\n },\n \"status\": {\n \"$ref\": \"#/$defs/statusPolicy\"\n },\n \"priority\": {\n \"$ref\": \"#/$defs/priorityPolicy\"\n },\n \"recurrence\": {\n \"$ref\": \"#/$defs/recurrencePolicy\"\n },\n \"occurrences\": {\n \"$ref\": \"#/$defs/occurrencePolicy\"\n },\n \"links\": {\n \"$ref\": \"#/$defs/linkPolicy\"\n },\n \"archive\": {\n \"$ref\": \"#/$defs/archivePolicy\"\n }\n },\n \"additionalProperties\": false,\n \"$defs\": {\n \"nonEmptyString\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"stringSet\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"items\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"titlePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"storage\"\n ],\n \"properties\": {\n \"storage\": {\n \"enum\": [\n \"filename\",\n \"frontmatter\"\n ]\n },\n \"filename_format\": {\n \"enum\": [\n \"title\",\n \"zettel\",\n \"timestamp\",\n \"uuid\",\n \"custom\"\n ]\n },\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"allOf\": [\n {\n \"if\": {\n \"properties\": {\n \"filename_format\": {\n \"const\": \"custom\"\n }\n },\n \"required\": [\n \"filename_format\"\n ]\n },\n \"then\": {\n \"properties\": {\n \"custom_filename_template\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"required\": [\n \"custom_filename_template\"\n ]\n }\n }\n ],\n \"additionalProperties\": true\n },\n \"statusDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"is_completed\",\n \"is_skipped\",\n \"order\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"is_completed\": {\n \"type\": \"boolean\"\n },\n \"is_skipped\": {\n \"type\": \"boolean\"\n },\n \"order\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"statusPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"completed_values\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"completed_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"skipped_values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default_skipped\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/statusDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"priorityDefinition\": {\n \"type\": \"object\",\n \"required\": [\n \"value\",\n \"label\",\n \"color\",\n \"weight\"\n ],\n \"properties\": {\n \"value\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"label\": {\n \"type\": \"string\"\n },\n \"color\": {\n \"type\": \"string\"\n },\n \"icon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"weight\": {\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": true\n },\n \"priorityPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"values\",\n \"default\",\n \"definitions\"\n ],\n \"properties\": {\n \"values\": {\n \"$ref\": \"#/$defs/stringSet\"\n },\n \"default\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"definitions\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/$defs/priorityDefinition\"\n }\n }\n },\n \"additionalProperties\": false\n },\n \"recurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"syntax\",\n \"maintain_due_date_offset\"\n ],\n \"properties\": {\n \"syntax\": {\n \"const\": \"tasknotes\"\n },\n \"maintain_due_date_offset\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"occurrencePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"identity_roles\",\n \"default_materialization\",\n \"default_next_trigger\"\n ],\n \"properties\": {\n \"identity_roles\": {\n \"const\": [\n \"recurrenceParent\",\n \"occurrenceDate\"\n ]\n },\n \"default_materialization\": {\n \"enum\": [\n \"manual\",\n \"on_completion\",\n \"rolling\"\n ]\n },\n \"default_next_trigger\": {\n \"enum\": [\n \"completion\",\n \"completion_or_skip\"\n ]\n },\n \"past_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n },\n \"future_horizon\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n },\n \"linkPolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"accepted_formats\"\n ],\n \"properties\": {\n \"accepted_formats\": {\n \"type\": \"array\",\n \"minItems\": 1,\n \"uniqueItems\": true,\n \"items\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ]\n }\n }\n },\n \"additionalProperties\": false\n },\n \"archivePolicy\": {\n \"type\": \"object\",\n \"required\": [\n \"archived_tag\"\n ],\n \"properties\": {\n \"archived_tag\": {\n \"$ref\": \"#/$defs/nonEmptyString\"\n }\n },\n \"additionalProperties\": false\n }\n }\n}\n" + }, + { + "source": "schemas/mdbase.app-settings/1.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"mdbase.app-settings 1.0.0 record contract view\",\n \"description\": \"The reserved fields every app settings record shares. Everything else in the record belongs to the app and is described by the app's own settings contract.\",\n \"type\": \"object\",\n \"required\": [\"app\", \"scope\", \"settings_version\"],\n \"properties\": {\n \"app\": {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9]+([.-][a-z0-9]+)*$\",\n \"description\": \"The app whose clients honour these settings, such as tasknotes or dev.mdbase.reader.\"\n },\n \"scope\": {\n \"enum\": [\"collection\", \"user\"],\n \"description\": \"Who the values apply to: everyone using the app in this collection, or one person.\"\n },\n \"user\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"A link to the mdbase.person record the values apply to. Present only when scope is user.\"\n },\n \"settings_version\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"description\": \"The version of the app's settings schema that last wrote the record. A client that does not understand it treats the settings as read-only.\"\n }\n },\n \"allOf\": [\n {\n \"if\": { \"properties\": { \"scope\": { \"const\": \"user\" } }, \"required\": [\"scope\"] },\n \"then\": { \"required\": [\"user\"] },\n \"else\": { \"not\": { \"required\": [\"user\"] } }\n }\n ],\n \"additionalProperties\": true\n}\n" + }, + { + "source": "contracts/mdbase.app-settings/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.app-settings\nversion: 1.0.0\nname: App settings\ndescription: One record per app holding the policy that app's clients honour in this collection.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/mdbase.app-settings/1.0.0.schema.json\n---\n\n# App settings\n\nAn app settings record holds values that every client of one app honours in this\ncollection, such as how new files are named. The values change client behaviour,\nnever what another record means.\n\n## Location and identity\n\nA collection-scope record lives at `_settings/.md`, where `` is the\nrecord's `app` field. Clients read it by that path, without a query. Each\nimplementing type declares `collection.unique` on `app` with `scope: type`, so a\ncollection holds at most one collection-scope record per app.\n\nThe record's `type` field names the app's own settings type, not a shared value.\nGeneric tools find every settings record through this contract.\n\n## Reserved fields\n\n- `app`: the app id.\n- `scope`: `collection`, or `user` for one person's overrides.\n- `user`: a link to an `mdbase.person` record, present only when `scope` is\n `user`.\n- `settings_version`: the version of the app's settings schema that last wrote\n the record.\n\nEvery other frontmatter field belongs to the app and is described by the app's\nown settings contract. The body is free Markdown for people; apps never parse it.\n\n## Client behaviour\n\n- A missing record means every setting has its default. Clients never create the\n record on read.\n- A missing or invalid field takes its default; valid fields are used as\n written. The record is not rewritten until someone saves.\n- A save creates the record at its fixed path the first time, and otherwise\n patches only the fields it changes, guarded by the record's revision.\n- A client that finds a `settings_version` newer than it understands treats the\n settings as read-only and uses them as written.\n\n## Authority\n\nThis artifact is passive. A settings record grants nothing and must never hold\nsecrets: anyone who can read or write records can read or edit it.\n" + }, + { + "source": "schemas/tasknotes.settings/1.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"title\": \"tasknotes.settings 1.0.0\",\n \"description\": \"Collection-wide behaviour that every TaskNotes client honours. Every field is optional and has a default, so a record with none of them behaves like no record at all.\",\n \"type\": \"object\",\n \"required\": [\n \"app\",\n \"scope\",\n \"settings_version\"\n ],\n \"properties\": {\n \"app\": {\n \"const\": \"dev.tasknotes\",\n \"description\": \"Marks this record as TaskNotes' settings.\"\n },\n \"scope\": {\n \"const\": \"collection\",\n \"description\": \"These settings apply to everyone using TaskNotes in this collection.\"\n },\n \"settings_version\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"description\": \"The version of these settings that last wrote the record. TaskNotes writes 1.\"\n },\n \"nlp_triggers\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"property_id\",\n \"trigger\",\n \"enabled\"\n ],\n \"properties\": {\n \"property_id\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"trigger\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"enabled\": {\n \"type\": \"boolean\"\n }\n },\n \"additionalProperties\": false\n },\n \"description\": \"Natural-language capture triggers, such as @ for contexts. Without the field each client uses its own defaults.\"\n },\n \"template_enabled\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Apply template_path to the body of new tasks.\"\n },\n \"template_path\": {\n \"type\": \"string\",\n \"description\": \"The template for new task bodies. Empty or absent means none.\"\n },\n \"occurrence_template_enabled\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Apply occurrence_template_path to new materialized occurrences.\"\n },\n \"occurrence_template_path\": {\n \"type\": \"string\",\n \"description\": \"The template for new occurrence bodies. Empty or absent means none.\"\n },\n \"template_failure_mode\": {\n \"enum\": [\n \"error\",\n \"warning_fallback\"\n ],\n \"description\": \"What a client does when a template cannot be applied.\"\n },\n \"template_unknown_variable_policy\": {\n \"enum\": [\n \"preserve\",\n \"empty\"\n ],\n \"description\": \"What a client writes for a template variable it does not know.\"\n },\n \"move_on_archive\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Move a task's file to archive_folder when it is archived.\"\n },\n \"archive_folder\": {\n \"type\": \"string\",\n \"description\": \"Where archived tasks move to. Empty or absent means the client's default.\"\n },\n \"auto_stop_time_tracking_on_complete\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Stop a running timer when its task is completed.\"\n },\n \"reset_body_checkboxes_on_recurrence\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Uncheck body checkboxes when a recurring task moves to its next occurrence.\"\n },\n \"link_write_format\": {\n \"enum\": [\n \"wikilink\",\n \"markdown\"\n ],\n \"default\": \"wikilink\",\n \"description\": \"How clients write links into task frontmatter. Both formats are always read.\"\n },\n \"status_behavior\": {\n \"type\": \"object\",\n \"description\": \"Behaviour per status value. A status without an entry has no next status and no auto-archive, and is out of the status cycle exactly when it is a skipped status.\",\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"next_status\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"The status the cycle moves to next.\"\n },\n \"exclude_from_cycle\": {\n \"type\": \"boolean\",\n \"description\": \"Leave this status out of the status cycle.\"\n },\n \"auto_archive\": {\n \"type\": \"boolean\",\n \"description\": \"Archive a task some time after it takes this status.\"\n },\n \"auto_archive_delay_minutes\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"description\": \"How long after taking this status a task is archived.\"\n }\n },\n \"additionalProperties\": false\n },\n \"default\": {}\n }\n },\n \"additionalProperties\": true\n}\n" + }, + { + "source": "contracts/tasknotes.settings/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: tasknotes.settings\nversion: 1.0.0\nname: TaskNotes settings\ndescription: Collection-wide TaskNotes behaviour, such as templates, archive moves and status cycling.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/tasknotes.settings/1.0.0.schema.json\n---\n\n# TaskNotes settings\n\nThe collection's settings record for TaskNotes, at `_settings/dev.tasknotes.md`.\nIt also implements `mdbase.app-settings`, whose reserved fields (`app`, `scope`,\n`settings_version`) it shares.\n\nThese settings change what TaskNotes clients do, never what a task record\nmeans: that stays in the `tasknotes.task` binding of each task type.\n`tasknotes.task` 0.3.0-rc.6 moved them here from the binding. A client reads\neach setting from this record, then from an earlier binding that still holds\nit, then uses its default, so a collection whose task type is still on rc.5\nkeeps its behaviour until it is upgraded.\n\n## Fields\n\n- `nlp_triggers`: natural-language capture triggers. Without it, each client\n uses its own defaults.\n- `template_enabled`, `template_path`, `occurrence_template_enabled`,\n `occurrence_template_path`, `template_failure_mode`,\n `template_unknown_variable_policy`: body templates for new tasks and\n materialized occurrences.\n- `move_on_archive`, `archive_folder`: whether archiving moves the file, and\n where to.\n- `auto_stop_time_tracking_on_complete`: stop a running timer on completion.\n- `reset_body_checkboxes_on_recurrence`: uncheck body checkboxes when a\n recurring task advances.\n- `link_write_format`: `wikilink` or `markdown` for links written to task\n frontmatter.\n- `status_behavior`: per status value, `next_status`, `exclude_from_cycle`,\n `auto_archive` and `auto_archive_delay_minutes`. A status without an entry\n has no next status and no auto-archive (5 minute delay), and is out of the\n cycle exactly when it is a skipped status.\n\n## Authority\n\nThis artifact is passive and grants nothing.\n" + }, + { + "source": "types/tasknotes-settings/1.md", + "document": "---\nkind: mdbase.type\nname: tasknotes-settings\nversion: 1\ndescription: TaskNotes' collection-wide settings, one record at _settings/dev.tasknotes.md.\nmatch:\n where:\n app:\n eq: dev.tasknotes\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: tasknotes.settings 1.0.0\n description: Collection-wide behaviour that every TaskNotes client honours. Every field is optional and has a default, so a record with none of them behaves like no record at all.\n type: object\n required:\n - app\n - scope\n - settings_version\n properties:\n app:\n const: dev.tasknotes\n description: Marks this record as TaskNotes' settings.\n scope:\n const: collection\n description: These settings apply to everyone using TaskNotes in this collection.\n settings_version:\n type: integer\n minimum: 1\n description: The version of these settings that last wrote the record. TaskNotes writes 1.\n nlp_triggers:\n type: array\n items:\n type: object\n required:\n - property_id\n - trigger\n - enabled\n properties:\n property_id:\n type: string\n minLength: 1\n trigger:\n type: string\n minLength: 1\n enabled:\n type: boolean\n additionalProperties: false\n description: Natural-language capture triggers, such as @ for contexts. Without the field each client uses its own defaults.\n template_enabled:\n type: boolean\n default: false\n description: Apply template_path to the body of new tasks.\n template_path:\n type: string\n description: The template for new task bodies. Empty or absent means none.\n occurrence_template_enabled:\n type: boolean\n default: false\n description: Apply occurrence_template_path to new materialized occurrences.\n occurrence_template_path:\n type: string\n description: The template for new occurrence bodies. Empty or absent means none.\n template_failure_mode:\n enum:\n - error\n - warning_fallback\n description: What a client does when a template cannot be applied.\n template_unknown_variable_policy:\n enum:\n - preserve\n - empty\n description: What a client writes for a template variable it does not know.\n move_on_archive:\n type: boolean\n default: false\n description: Move a task's file to archive_folder when it is archived.\n archive_folder:\n type: string\n description: Where archived tasks move to. Empty or absent means the client's default.\n auto_stop_time_tracking_on_complete:\n type: boolean\n default: false\n description: Stop a running timer when its task is completed.\n reset_body_checkboxes_on_recurrence:\n type: boolean\n default: false\n description: Uncheck body checkboxes when a recurring task moves to its next occurrence.\n link_write_format:\n enum:\n - wikilink\n - markdown\n default: wikilink\n description: How clients write links into task frontmatter. Both formats are always read.\n status_behavior:\n type: object\n description: Behaviour per status value. A status without an entry has no next status and no auto-archive, and is out of the status cycle exactly when it is a skipped status.\n additionalProperties:\n type: object\n properties:\n next_status:\n type: string\n minLength: 1\n description: The status the cycle moves to next.\n exclude_from_cycle:\n type: boolean\n description: Leave this status out of the status cycle.\n auto_archive:\n type: boolean\n description: Archive a task some time after it takes this status.\n auto_archive_delay_minutes:\n type: number\n minimum: 0\n description: How long after taking this status a task is archived.\n additionalProperties: false\n default: {}\n additionalProperties: true\ncollection:\n unique:\n - field: app\n scope: type\nimplements:\n - contract: mdbase.app-settings\n version: 1.0.0\n fields:\n app: app\n scope: scope\n settings_version: settings_version\n - contract: tasknotes.settings\n version: 1.0.0\n fields:\n app: app\n scope: scope\n settings_version: settings_version\n nlp_triggers: nlp_triggers\n template_enabled: template_enabled\n template_path: template_path\n occurrence_template_enabled: occurrence_template_enabled\n occurrence_template_path: occurrence_template_path\n template_failure_mode: template_failure_mode\n template_unknown_variable_policy: template_unknown_variable_policy\n move_on_archive: move_on_archive\n archive_folder: archive_folder\n auto_stop_time_tracking_on_complete: auto_stop_time_tracking_on_complete\n reset_body_checkboxes_on_recurrence: reset_body_checkboxes_on_recurrence\n link_write_format: link_write_format\n status_behavior: status_behavior\n---\n\n# TaskNotes settings\n\nSettings that every TaskNotes client honours in this collection, such as body\ntemplates, archive moves and status cycling. TaskNotes reads them from\n`_settings/dev.tasknotes.md` and writes only the fields a save changes.\n" + } + ], + "provides": [ + { + "id": "tasknotes.task", + "version": "0.3.0-rc.6", + "digest": "sha256:c400ead6e218b715d86ad31125bb3d4b2e276817da6ba8065449fd0947b24d67" + }, + { + "id": "tasknotes.settings", + "version": "1.0.0", + "digest": "sha256:a61d55535871d27e9599cb3588148a0590d9e2198d6ee7a0bf3197350b60dc53" + }, + { + "id": "mdbase.app-settings", + "version": "1.0.0", + "digest": "sha256:4ae872a56290e2739ff876df1ca8fdac428753c854d65fd8e8e62045bd643a4e" + } + ] +}