diff --git a/00-overview.md b/00-overview.md index 06f2196..b1f50f1 100644 --- a/00-overview.md +++ b/00-overview.md @@ -33,7 +33,7 @@ The tasknotes plugin is the primary reference for how this spec was derived, but ## 0.1 Status and authority -This document is part of `tasknotes-spec` version `0.3.0-rc.5`. +This document is part of `tasknotes-spec` version `0.3.0-rc.6`. This specification is normative for implementations that claim conformance. No single implementation is normative; implementations are expected to conform to the specification. diff --git a/12-mdbase-v0.3-binding.md b/12-mdbase-v0.3-binding.md index 1e8454c..6fdd32a 100644 --- a/12-mdbase-v0.3-binding.md +++ b/12-mdbase-v0.3-binding.md @@ -2,7 +2,7 @@ **Status:** Draft -**Record contract:** `tasknotes.task` `0.3.0-rc.5` +**Record contract:** `tasknotes.task` `0.3.0-rc.6` This revision adds optional `assignees`: links to records implementing `mdbase.person`, resolved by ordinary mdbase link resolution. Unresolved links @@ -10,6 +10,12 @@ survive person deletion or membership removal. Assignment and editable person identity associations never grant collection access. See [the contract](mdbase/tasknotes.task.md). +rc.6 moves app behaviour out of the binding into the collection's TaskNotes +settings record (§12.8.1): natural-language triggers, templating, archive moves, +time-tracking auto-stop, body-checkbox reset on recurrence, the link write +format, and each status's next status, cycle exclusion and auto-archive. The +binding keeps what gives task records their meaning. + **Event contract:** `tasknotes.task.completed` `1.0.0` **Target:** mdbase `0.3.x` @@ -50,7 +56,7 @@ can implement and several applications can consume. ```text personal_task ─┐ -work_task ─────┼─ implements tasknotes.task 0.3.0-rc.5 ── TaskNotes +work_task ─────┼─ implements tasknotes.task 0.3.0-rc.6 ── TaskNotes issue_task ────┘ ├─ automation └─ another task app ``` @@ -94,7 +100,7 @@ An mdbase TaskNotes type declares one implementation: ```yaml implements: - contract: tasknotes.task - version: 0.3.0-rc.5 + version: 0.3.0-rc.6 fields: title: summary status: state @@ -227,8 +233,9 @@ The following consistency rules apply: and - definition semantics MUST agree with completed/skipped membership. -Status and priority labels, colors, and icons are presentation hints. Status -cycle, completion, skipping, auto-archive, and priority weight are semantic. +Status and priority labels, colors, and icons are presentation hints. +Completion, skipping, order and priority weight are semantic. Status cycling +and auto-archive are app behaviour and live in the settings record (§12.8.1). ## 12.8 Title, recurrence, links, and operations @@ -237,16 +244,13 @@ defined elsewhere in this specification: - `title` follows §9.13 and MUST agree with `collection.display` and `collection.path`; -- `recurrence` follows §4; +- `recurrence` follows §4 (`syntax` and `maintain_due_date_offset`); - `occurrences` follows §2.6.6, §4.18, §5.20, and §9.17; -- `links` follows §11; +- `links` follows §11 (`accepted_formats`); - attachment membership is the portable `attachments` contract field; binary MIME type, digest, byte size, revision, and timestamps remain authoritative - in mdbase file descriptors and are not projected into task frontmatter; -- `archive` follows §5.12; -- `time_tracking` follows §5.19 and §9.16; -- `templating` follows §5.3.5 and §9.14; and -- optional `nlp` triggers follow §9.18. + in mdbase file descriptors and are not projected into task frontmatter; and +- `archive` follows §5.12 (`archived_tag`). Archive always operates on the portable `tags` contract field, so the binding does not repeat a record-specific `tags_field`. The implementation's `fields` @@ -255,10 +259,37 @@ map determines the actual frontmatter property. Application-local notification preferences and generator bookkeeping MUST NOT be placed in the binding. +### 12.8.1 App settings record + +App behaviour that every TaskNotes client honours, but that does not change what +a task record means, lives in one record at `_settings/dev.tasknotes.md`. The +record implements `mdbase.app-settings` 1.0.0 and `tasknotes.settings` 1.0.0 +(both published in mdbase-contracts) with `app: dev.tasknotes`, +`scope: collection` and `settings_version: 1`. Its flat frontmatter holds: + +| Field | Was in the rc.5 binding | +| --- | --- | +| `nlp_triggers` | `nlp.triggers` | +| `template_enabled`, `template_path` | `templating.enabled`, `templating.template_path` | +| `occurrence_template_enabled`, `occurrence_template_path` | `templating.occurrence_enabled`, `templating.occurrence_template_path` | +| `template_failure_mode`, `template_unknown_variable_policy` | `templating.failure_mode`, `templating.unknown_variable_policy` | +| `move_on_archive`, `archive_folder` | `archive.move_on_archive`, `archive.folder` | +| `auto_stop_time_tracking_on_complete` | `time_tracking.auto_stop_on_complete` | +| `reset_body_checkboxes_on_recurrence` | `recurrence.reset_body_checkboxes` | +| `link_write_format` | `links.write_format` | +| `status_behavior.` | `status.definitions[].next_status`, `exclude_from_cycle`, `auto_archive`, `auto_archive_delay_minutes` | + +Clients MUST read each setting from the record first, then from an +implementation of an earlier contract version that still carries it in its +binding, then use the default. A missing record means defaults; clients MUST +NOT create or rewrite the record on read. A status without a +`status_behavior` entry has no next status, no auto-archive (delay 5 minutes), +and is excluded from the cycle exactly when it is a skipped status. + ## 12.9 Multiple implementations A collection may contain any number of types implementing -`tasknotes.task 0.3.0-rc.5`. TaskNotes-aware readers MUST: +`tasknotes.task 0.3.0-rc.6`. TaskNotes-aware readers MUST: 1. resolve the exact local contract; 2. enumerate every implementation in canonical type-name order; @@ -362,7 +393,7 @@ lifecycle: created_at: { now: true } implements: - contract: tasknotes.task - version: 0.3.0-rc.5 + version: 0.3.0-rc.6 fields: title: summary status: state @@ -389,21 +420,13 @@ implements: recurrence: syntax: tasknotes maintain_due_date_offset: false - reset_body_checkboxes: false occurrences: identity_roles: [recurrenceParent, occurrenceDate] default_materialization: manual default_next_trigger: completion links: accepted_formats: [wikilink, markdown] - write_format: wikilink archive: archived_tag: archived - move_on_archive: false - time_tracking: - auto_stop_on_complete: true - templating: - enabled: false - occurrence_enabled: false --- ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d8dc54..9e8e102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.3.0-rc.6 - Unreleased + +- Move app behaviour out of the `tasknotes.task` binding into the collection's + TaskNotes settings record (`_settings/dev.tasknotes.md`, contract + `tasknotes.settings` 1.0.0): `nlp`, `templating`, `time_tracking`, + `archive.move_on_archive`, `archive.folder`, `recurrence.reset_body_checkboxes`, + `links.write_format`, and each status definition's `next_status`, + `exclude_from_cycle`, `auto_archive` and `auto_archive_delay_minutes`. +- Readers take each moved setting from the record, then from an rc.5 binding, + then its default, so a collection still on rc.5 keeps its behaviour. + ## 0.3.0-rc.5 - Unreleased - Make `assignees` links to `mdbase.person` records, declared in diff --git a/conformance/tests/mdbase-data-contract.test.mjs b/conformance/tests/mdbase-data-contract.test.mjs index ffc0a9a..7435a43 100644 --- a/conformance/tests/mdbase-data-contract.test.mjs +++ b/conformance/tests/mdbase-data-contract.test.mjs @@ -91,7 +91,6 @@ test("publishes valid JSON Schemas for the portable task view, binding, and comp recurrence: { syntax: "tasknotes", maintain_due_date_offset: false, - reset_body_checkboxes: false, }, occurrences: { identity_roles: ["recurrenceParent", "occurrenceDate"], @@ -100,16 +99,28 @@ test("publishes valid JSON Schemas for the portable task view, binding, and comp }, links: { accepted_formats: ["wikilink", "markdown"], - write_format: "wikilink", }, archive: { archived_tag: "archived", - move_on_archive: false, }, - time_tracking: { auto_stop_on_complete: true }, - templating: { enabled: false, occurrence_enabled: false }, }; assert.equal(validateBinding(binding), true, validateBinding.errors); + assert.equal( + validateBinding({ + ...binding, + time_tracking: { auto_stop_on_complete: true }, + }), + false, + "app behaviour lives in the tasknotes.settings record, not the binding" + ); + assert.equal( + validateBinding({ + ...binding, + archive: { archived_tag: "archived", move_on_archive: false }, + }), + false, + "archive moves are app behaviour" + ); assert.equal( validateBinding({ ...binding, contract: "tasknotes.task" }), false, @@ -137,7 +148,7 @@ test("the published contract uses one exact identity and the canonical schemas", assert.match(contract, /^kind: mdbase\.contract$/m); assert.match(contract, /^id: tasknotes\.task$/m); assert.match(contract, /^contract_type: record$/m); - assert.match(contract, /^version: 0\.3\.0-rc\.3$/m); + assert.match(contract, /^version: 0\.3\.0-rc\.6$/m); assert.match(contract, /^record_schema:$/m); assert.match(contract, /ref: \.\.\/schemas\/tasknotes-task\.schema\.json/); assert.match( diff --git a/mdbase/tasknotes.task.md b/mdbase/tasknotes.task.md index 00366fb..9ffbca9 100644 --- a/mdbase/tasknotes.task.md +++ b/mdbase/tasknotes.task.md @@ -2,9 +2,9 @@ kind: mdbase.contract contract_type: record id: tasknotes.task -version: 0.3.0-rc.5 +version: 0.3.0-rc.6 name: TaskNotes task -description: Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.5. +description: Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.6. record_schema: dialect: json-schema-2020-12 ref: ../schemas/tasknotes-task.schema.json @@ -17,7 +17,10 @@ binding_schema: Types implement this contract with an `implements` entry in their mdbase type file. The `fields` map adapts custom frontmatter names to this portable view; -the `binding` object supplies TaskNotes behavior. +the `binding` object supplies what task records mean, such as status and +priority vocabularies. App behaviour that does not change that meaning, such as +templates, archive moves and status cycling, lives in the collection's +`tasknotes.settings` record at `_settings/dev.tasknotes.md`. `assignees` is an optional unique list of links to records implementing `mdbase.person`, for example `"[[Alex Rivera]]"` or `"[[people/alex-rivera]]"`. diff --git a/package-lock.json b/package-lock.json index 637b571..1c9f8dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tasknotes-spec", - "version": "0.3.0-rc.5", + "version": "0.3.0-rc.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tasknotes-spec", - "version": "0.3.0-rc.5", + "version": "0.3.0-rc.6", "license": "MIT", "devDependencies": { "ajv": "^8.20.0", diff --git a/package.json b/package.json index 068a8b7..7db2cb5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tasknotes-spec", - "version": "0.3.0-rc.5", + "version": "0.3.0-rc.6", "description": "Specification and conformance fixtures for TaskNotes-compatible markdown task files.", "license": "MIT", "type": "module", diff --git a/schemas/tasknotes-task-binding.schema.json b/schemas/tasknotes-task-binding.schema.json index f077b76..a8a1f5a 100644 --- a/schemas/tasknotes-task-binding.schema.json +++ b/schemas/tasknotes-task-binding.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json", "title": "TaskNotes task data-contract binding", - "description": "Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.3.", + "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.", "type": "object", "required": [ "profiles", @@ -13,9 +13,7 @@ "recurrence", "occurrences", "links", - "archive", - "time_tracking", - "templating" + "archive" ], "properties": { "profiles": { @@ -73,15 +71,6 @@ }, "archive": { "$ref": "#/$defs/archivePolicy" - }, - "time_tracking": { - "$ref": "#/$defs/timeTrackingPolicy" - }, - "templating": { - "$ref": "#/$defs/templatingPolicy" - }, - "nlp": { - "$ref": "#/$defs/nlpPolicy" } }, "additionalProperties": false, @@ -141,10 +130,7 @@ "color", "is_completed", "is_skipped", - "exclude_from_cycle", - "order", - "auto_archive", - "auto_archive_delay_minutes" + "order" ], "properties": { "value": { @@ -165,21 +151,8 @@ "is_skipped": { "type": "boolean" }, - "exclude_from_cycle": { - "type": "boolean" - }, - "next_status": { - "$ref": "#/$defs/nonEmptyString" - }, "order": { "type": "number" - }, - "auto_archive": { - "type": "boolean" - }, - "auto_archive_delay_minutes": { - "type": "number", - "minimum": 0 } }, "additionalProperties": true @@ -255,16 +228,13 @@ }, "recurrencePolicy": { "type": "object", - "required": ["syntax", "maintain_due_date_offset", "reset_body_checkboxes"], + "required": ["syntax", "maintain_due_date_offset"], "properties": { "syntax": { "const": "tasknotes" }, "maintain_due_date_offset": { "type": "boolean" - }, - "reset_body_checkboxes": { - "type": "boolean" } }, "additionalProperties": false @@ -293,7 +263,7 @@ }, "linkPolicy": { "type": "object", - "required": ["accepted_formats", "write_format"], + "required": ["accepted_formats"], "properties": { "accepted_formats": { "type": "array", @@ -302,89 +272,16 @@ "items": { "enum": ["wikilink", "markdown"] } - }, - "write_format": { - "enum": ["wikilink", "markdown"] } }, "additionalProperties": false }, "archivePolicy": { "type": "object", - "required": ["archived_tag", "move_on_archive"], + "required": ["archived_tag"], "properties": { "archived_tag": { "$ref": "#/$defs/nonEmptyString" - }, - "move_on_archive": { - "type": "boolean" - }, - "folder": { - "$ref": "#/$defs/nonEmptyString" - } - }, - "additionalProperties": false - }, - "timeTrackingPolicy": { - "type": "object", - "required": ["auto_stop_on_complete"], - "properties": { - "auto_stop_on_complete": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "templatingPolicy": { - "type": "object", - "required": ["enabled", "occurrence_enabled"], - "properties": { - "enabled": { - "type": "boolean" - }, - "template_path": { - "$ref": "#/$defs/nonEmptyString" - }, - "occurrence_enabled": { - "type": "boolean" - }, - "occurrence_template_path": { - "$ref": "#/$defs/nonEmptyString" - }, - "failure_mode": { - "enum": ["error", "warning_fallback"] - }, - "unknown_variable_policy": { - "enum": ["preserve", "empty"] - } - }, - "additionalProperties": false - }, - "nlpTrigger": { - "type": "object", - "required": ["property_id", "trigger", "enabled"], - "properties": { - "property_id": { - "$ref": "#/$defs/nonEmptyString" - }, - "trigger": { - "$ref": "#/$defs/nonEmptyString" - }, - "enabled": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "nlpPolicy": { - "type": "object", - "required": ["triggers"], - "properties": { - "triggers": { - "type": "array", - "items": { - "$ref": "#/$defs/nlpTrigger" - } } }, "additionalProperties": false diff --git a/schemas/tasknotes-task.schema.json b/schemas/tasknotes-task.schema.json index 7646c2a..0c9f2d7 100644 --- a/schemas/tasknotes-task.schema.json +++ b/schemas/tasknotes-task.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://tasknotes.dev/schemas/tasknotes-task.schema.json", "title": "TaskNotes portable task view", - "description": "The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.5 record contract.", + "description": "The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.6 record contract.", "type": "object", "required": ["status", "dateCreated"], "additionalProperties": false,