diff --git a/README.md b/README.md index 918cce3..4e7944c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,8 @@ collections where `type` is data). Person v3, Comment v2 and View v2 therefore n longer require or pin `type`, and View v2 accepts unknown top-level fields; each keeps its `match` rule for hand-written records. `mdbase.contact` 1.3.0, `mdbase.comment` 1.0.1 and `mdbase.view` 1.0.1 offer them as reviewed upgrades of -the unmodified previous starter. Pack resources +the unmodified previous starter; `mdbase.contact` 1.4.0 upgrades both earlier +Person starters (v1 from 1.1.0 and v2 from 1.2.0) to v3. Pack resources explicitly use `managed` for schemas/contracts and `seed` for editable starter types; install tests exercise the exact generated payload without supplying missing modes. Associations are ordinary collection data, never @@ -85,7 +86,7 @@ MDBASE_SPEC_DIR=../mdbase-spec npm run sync:runtime Requires Node.js 22+ and a built checkout of [`@callumalpass/mdbase`](https://github.com/callumalpass/mdbase) (mdbase-ts) -0.3.0-rc.8 or later. A sibling `../mdbase` checkout is used by default; set +0.3.0-rc.9 or later (seed upgrade baseline lists). A sibling `../mdbase` checkout is used by default; set `MDBASE_TS_DIR` to override it. CI checks out and builds the tag pinned in `sources.json` (`typescript_implementation.ref`), which is the single source of truth for the mdbase-ts version the catalog is verified against. @@ -127,7 +128,48 @@ MDBASE_VERIFY_CLI=/absolute/path/to/mdbase npm test Installed collections are reopened with mdbase-ts either way. CI runs `npm test` once per engine, with the CLI built from the commits pinned in -`sources.json` (`rust_cli`). +`sources.json` (`rust_cli`). The CLI's engine must include mdbase-rs 056db73 +(seed upgrade baseline lists); `rust_cli.ref` is the mdbase-connect commit that +pins that engine revision. + +## Seed upgrade baselines + +A seed type is the collection's to edit once installed, so a new pack version +replaces it only through an explicit seed-type upgrade (mdbase spec 05A). A pack +definition names the starters it upgrades with `upgrade_from`, either one path +or a list: + +```yaml + - kind: type + mode: seed + source: types/tasknotes-task/5.md + target: _types/task.md + upgrade_from: + - types/tasknotes-task/4.md + - types/tasknotes-task/3.md + - types/tasknotes-task/2.md + - types/tasknotes-task/1.md +``` + +`npm run build` emits a list as `[{ digest, version, document }]`, ordered +newest first, with `version` read from each baseline's frontmatter; the single +form keeps emitting one `{ digest, document }` so published packs stay +byte-identical. The build rejects baselines on anything but a seed type, +duplicate or self baselines, and baselines whose type `kind` or `name` differs +from the desired starter. + +Engines replace a starter equal to any listed baseline with the exact new +starter, and merge an edited starter only against the baseline its lock records +as its `origin_digest`; an edited type with no recorded origin (installed by an +engine before 05A) or an unlisted origin is preserved with a reason. An +unlisted starter is therefore never upgraded, so the catalog keeps an +invariant, checked by `scripts/seed-baselines.test.mjs`: for every offered +(non-hidden) pack and seed target, `upgrade_from` lists every starter that any +earlier version of the pack shipped at that target, including unlisted +(`catalog: false`) versions. When a pack's list changes, publish a new version +and hide the previous one (`installation.visibility: hidden`). +`scripts/seed-upgrade.test.mjs` installs each earlier version through the +selected engine and upgrades it, unedited and customised. ## TaskNotes assignments @@ -147,6 +189,13 @@ rc.17's starter is rc.16's with the generator bookkeeping collection's list follows its own field mapping, so an upgrade that changed it conflicted wherever that mapping was customized. rc.16 is not listed. +rc.18 ships rc.17's starter unchanged and lists every earlier starter as an +upgrade baseline (revisions 4, 3, 2 and 1, from rc.16, rc.15, rc.13/rc.14 and +rc.12), so a collection seeded by any earlier pack upgrades to it. rc.17 is +hidden. A customised task type whose lock records no origin is preserved, and +because it still implements the replaced contract the upgrade is blocked for +review rather than merged against a guessed starter. + Starter files are named by revision (`types/tasknotes-task/.md`); a revision that does not change the task data keeps the type `version`, so collections already at that version upgrade without a version conflict. diff --git a/dist/catalog.json b/dist/catalog.json index 425a12c..e9a079b 100644 --- a/dist/catalog.json +++ b/dist/catalog.json @@ -482,6 +482,48 @@ "Portable person references" ] }, + "installation": { + "visibility": "hidden", + "recommendation": "user", + "primary_type": "person", + "types": [ + { + "name": "person", + "label": "Person" + } + ] + } + }, + { + "id": "mdbase.contact", + "version": "1.4.0", + "name": "People type pack", + "description": "Portable people, editable account identity associations, and familiar contact fields.", + "digest": "sha256:741fcf0f50d13538eda1c9ee32df4d85e32549b8fb730350bb80b3323b09df93", + "provision": "./packs/mdbase.contact/1.4.0.json", + "provides": [ + { + "id": "mdbase.person", + "version": "2.0.0", + "digest": "sha256:f16c462a1fd422f44ed055002f8476ec53c164107647fec782aeb74adeab2f9d" + }, + { + "id": "mdbase.contact", + "version": "1.0.0", + "digest": "sha256:49cfe15403dfc741a693e89a2f4d2857de306f391d02cb03e67bfaf0aa1d6b0d" + } + ], + "resource_count": 7, + "display": { + "name": "People", + "summary": "Reference people across apps and collections without tying records to account membership.", + "category": "people", + "audience": "general", + "icon": "user", + "badges": [ + "Portable person references" + ] + }, "installation": { "visibility": "default", "recommendation": "user", @@ -787,6 +829,44 @@ "TaskNotes compatible" ] }, + "installation": { + "visibility": "hidden", + "recommendation": "integration-managed", + "primary_type": "task", + "caution": "Upgrades the previous starter type while preserving independent customizations. Conflicts or other references to the previous contract block the whole upgrade.", + "types": [ + { + "name": "task", + "label": "Task" + } + ] + } + }, + { + "id": "tasknotes.task", + "version": "0.3.0-rc.18", + "name": "TaskNotes task type pack", + "description": "Portable tasks whose assignments link to person records, with a reviewed, atomic upgrade from every TaskNotes starter published before.", + "digest": "sha256:9a9ebc26e6f63ad2fc3d92f87d38f6c140a3af90408de47d667ae7b9ba0dc9ef", + "provision": "./packs/tasknotes.task/0.3.0-rc.18.json", + "provides": [ + { + "id": "tasknotes.task", + "version": "0.3.0-rc.5", + "digest": "sha256:dde71d0d776aa0069ee627fe3c10b1a20f87ba36929a16790598ebfb4e8ebf04" + } + ], + "resource_count": 4, + "display": { + "name": "TaskNotes tasks", + "summary": "Manage portable Markdown tasks, assignments, dates, recurrence, reminders, and time tracking.", + "category": "work", + "audience": "general", + "icon": "square-check-big", + "badges": [ + "TaskNotes compatible" + ] + }, "installation": { "visibility": "default", "recommendation": "integration-managed", diff --git a/dist/packs/mdbase.contact/1.4.0.json b/dist/packs/mdbase.contact/1.4.0.json new file mode 100644 index 0000000..b58cbb5 --- /dev/null +++ b/dist/packs/mdbase.contact/1.4.0.json @@ -0,0 +1,114 @@ +{ + "manifest": { + "kind": "mdbase.type-pack", + "id": "mdbase.contact", + "version": "1.4.0", + "name": "People type pack", + "description": "Portable people, editable account identity associations, and familiar contact fields.", + "resources": [ + { + "kind": "schema", + "mode": "managed", + "source": "schemas/mdbase.person/1.0.0.schema.json", + "target": "schemas/mdbase.person/1.0.0.schema.json", + "digest": "sha256:3254afaa98c895e84fdc0aba299e67a0f3b594681c5581313ece5fcf4e2c6d93" + }, + { + "kind": "contract", + "mode": "managed", + "source": "contracts/mdbase.person/1.0.0.md", + "target": "_contracts/mdbase.person/1.0.0.md", + "digest": "sha256:e2a9435ad01a2c06c151bf6af62a9d04aa30faa8e6128e4b5fe39378c767465c" + }, + { + "kind": "schema", + "mode": "managed", + "source": "schemas/mdbase.person/2.0.0.schema.json", + "target": "schemas/mdbase.person/2.0.0.schema.json", + "digest": "sha256:f9705e5869a2814086b907323749f63dce60de535728d6725601f02901ae60ac" + }, + { + "kind": "contract", + "mode": "managed", + "source": "contracts/mdbase.person/2.0.0.md", + "target": "_contracts/mdbase.person/2.0.0.md", + "digest": "sha256:2899b149b1602bd4e51ff8e87c82dc92da3f690f4e89b1a6caf5b2b555540fe5" + }, + { + "kind": "schema", + "mode": "managed", + "source": "schemas/mdbase.contact/1.0.0.schema.json", + "target": "schemas/mdbase.contact/1.0.0.schema.json", + "digest": "sha256:bd8a011d18c71164b633f4e82cb33faa5b658cdba4f89312a34c13bf46a350d2" + }, + { + "kind": "contract", + "mode": "managed", + "source": "contracts/mdbase.contact/1.0.0.md", + "target": "_contracts/mdbase.contact/1.0.0.md", + "digest": "sha256:e3634a35737ada5eb62e3e15f1c0c9d2e27c55a3f98dfab94bbc14c40404903d" + }, + { + "kind": "type", + "mode": "seed", + "source": "types/person/3.md", + "target": "_types/person.md", + "digest": "sha256:e74ebbce6c67e195a52d005c29ad63f93ae044800a857d33fb3bbc73135adb72", + "upgrade_from": [ + { + "digest": "sha256:aa81c2964285ecc69fcb96bca4b6ae80bbddeb648908d840d7c7be50df15b362", + "version": 2, + "document": "---\nkind: mdbase.type\nname: person\nversion: 2\ndescription: An individual that other notes link to, with a collection-owned display name and optional contact details and account associations.\nmatch:\n where:\n type: person\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: Person\n description: One person represented by an ordinary note. Contact details and account associations are optional; this record never grants access or membership.\n type: object\n required: [type, name]\n properties:\n type:\n const: person\n description: Identifies this note as a Person. Usually set by the application; changing it changes which type validates the note.\n name:\n type: string\n minLength: 1\n pattern: '\\S'\n description: The display name to use in this collection. Use a full name, preferred name, or single name as appropriate; no first/last-name split is required. It can differ from an account's name and is not automatically synchronised with it.\n identities:\n type: array\n description: Optional accounts associated with this person, each identified by an exact issuer and subject pair. Omit this field for someone without an account. These editable associations help applications find your person record; they are not proof of identity and never grant access. Avoid linking the same account to multiple person notes.\n uniqueItems: true\n items:\n type: object\n description: One account association supplied by its identity service. Both issuer and subject must match exactly; never infer them from a name or email.\n required: [issuer, subject]\n properties:\n issuer:\n type: string\n format: uri\n pattern: '^https?://'\n description: The exact identity-service issuer URL reported for this account. Preserve its scheme, case, path, and trailing slash; do not guess it from the collection's hosting address.\n subject:\n type: string\n pattern: '\\S'\n description: The opaque account identifier reported by that issuer, not a display name, email, password, or access token. Copy it exactly; a different issuer may use the same subject for a different account.\n additionalProperties: false\n kind:\n const: individual\n default: individual\n description: This Person starter represents an individual. The organisation field describes their affiliation, not a different record kind. Existing organisation or group contacts can keep their own Contact-compatible type.\n email:\n type: string\n format: email\n description: The preferred email address for contacting this person. Optional contact information only; it does not establish an account association or grant access.\n phone:\n type: string\n minLength: 1\n description: The preferred telephone number, stored as text so country codes, formatting, and extensions can be preserved. Include a country code when useful; omit if unknown.\n organisation:\n type: string\n minLength: 1\n description: The person's main affiliation, such as an employer, school, or community. A collection-owned display label, not a membership or access-control setting; omit if not relevant.\n birthday:\n type: string\n format: date\n description: A known birthday as a full calendar date in YYYY-MM-DD form. Omit if unknown or if only the month and day are known; do not invent a year. Consider whether this collection needs this personal information.\n additionalProperties: true\ncollection:\n display:\n name_field: name\n description_field: organisation\n icon: user\nimplements:\n - contract: mdbase.person\n version: 2.0.0\n fields:\n name: name\n identities: identities\n - contract: mdbase.contact\n version: 1.0.0\n fields:\n name: name\n kind: kind\n primary_email: email\n primary_phone: phone\n organisation: organisation\n birthday: birthday\n---\n\n# Person\n\nUse one note per individual, whether or not they have a Connect account. This\nsingle type supplies both a Person record and optional Contact details. You do\nnot need a second Contact note for the same person.\n\n## Start with a name\n\n```yaml\ntype: person\nname: Alex Rivera\n```\n\nOther notes refer to this person with an ordinary link to this note, such as\n`[[Alex Rivera]]` in a task's `assignees`. Links to this note appear in its\nbacklinks. Rename or move the note with a tool that updates references, as mdbase\nand Obsidian do, so those links stay current. Reusing a person's file name for\nsomebody else redirects existing links to the wrong person.\n\nThe name is your collection's display label. Full names, preferred names and\nsingle names are equally valid. There is no required first-name/last-name split.\n\n## Add only the contact details you need\n\nEmail, phone, organisation and birthday are optional. Omit unknown values rather\nthan filling them with empty strings or guesses. Store phone numbers as text,\nincluding an international prefix or extension when helpful. Birthday currently\nrequires a complete date; partial dates need a separately configured local field.\nKeep free-form context in the Markdown body and be deliberate about sensitive\npersonal information in shared collections.\n\nThis starter describes individuals. Existing organisation and group records can\ncontinue using their Contact-compatible type; setting someone's affiliation does\nnot turn their Person note into an organisation record.\n\n## Link accounts deliberately\n\nThe editor's **Link this record to me** action uses the authenticated account's\nactual issuer and subject. If configuring these manually, copy both exact values\nfrom the identity service. Do not guess them from an email, display name, or the\nserver hosting this collection. Multiple entries may represent the same person\nat different identity services. Omit `identities` when there is no account to link.\n\nAssociations are ordinary editable data, not authentication or proof of identity.\nLinking never invites someone, grants access, or changes collection membership.\nNever put credentials in a person record. Avoid linking the same account from\nseveral notes: applications should report ambiguity, not guess which\nrecord to use. Account deletion or recreation is not permission to reassign an\nold association automatically.\n\n## Keep the collection's own shape\n\nThe inline schema belongs to this collection. You may add local fields, or rename\nfields and update their explicit `implements` mappings. Apps use those mappings\nfor the Person and Contact contracts; they need not assume your local field names.\nAdditional fields remain part of the note but do not automatically become shared\ncontract fields. Do not edit published contracts to customise this type.\n\nInstalling a newer starter does not overwrite an existing customised type or\nmigrate your address book. Converting an existing individual Contact to Person\nshould be a reviewed edit of that one note, preserving its path, fields and body.\n" + }, + { + "digest": "sha256:18896ac3086c7fdcb64748babd7dd8263dbece4fcc6bcf43a53f74d51bcece5e", + "version": 1, + "document": "---\nkind: mdbase.type\nname: person\nversion: 1\ndescription: A portable person with optional account identities and contact information\nmatch:\n where:\n type: person\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: Person\n type: object\n required: [type, id, name]\n properties:\n type:\n const: person\n id:\n type: string\n pattern: '\\S'\n description: Stable person ID; keep this when moving or renaming the note.\n name:\n type: string\n minLength: 1\n pattern: '\\S'\n identities:\n type: array\n uniqueItems: true\n items:\n type: object\n required: [issuer, subject]\n properties:\n issuer:\n type: string\n format: uri\n pattern: '^https?://'\n subject:\n type: string\n pattern: '\\S'\n additionalProperties: false\n kind:\n const: individual\n default: individual\n email:\n type: string\n format: email\n phone:\n type: string\n minLength: 1\n organisation:\n type: string\n minLength: 1\n birthday:\n type: string\n format: date\n additionalProperties: true\ncollection:\n display:\n name_field: name\n description_field: organisation\n icon: user\n unique:\n - field: id\n scope: collection\nimplements:\n - contract: mdbase.person\n version: 1.0.0\n fields:\n id: id\n name: name\n identities: identities\n - contract: mdbase.contact\n version: 1.0.0\n fields:\n name: name\n kind: kind\n primary_email: email\n primary_phone: phone\n organisation: organisation\n birthday: birthday\n---\n\n# Person\n\nCreate a stable ID when adding a person, for example `person_` followed by a\nrandom UUID. Keep it when copying the note to another collection or renaming\nits file. Apps use this ID for assignments and other person references.\n\n```yaml\ntype: person\nid: person_2c1343ec-cac1-4d58-98c9-41736f4de7db\nname: Callum\nidentities:\n - issuer: https://connect.example\n subject: usr_789\n```\n\nUse the actual issuer and subject supplied by Connect, not the example values.\nOmit `identities` for people without accounts. Email is contact information,\nnot a way to establish an identity association. Linking a person does not\ninvite them or grant access to this collection.\n\nIdentity associations and contact labels are ordinary editable data. They are\nnot proof of authentication. Avoid duplicate person IDs or identity pairs\nacross person notes; applications must report ambiguity rather than guess.\n\nThe inline schema belongs to the collection. Add local fields or rename them\nand update the explicit contract mappings without editing published contracts.\n" + } + ] + } + ] + }, + "resources": [ + { + "source": "schemas/mdbase.person/1.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://mdbase.dev/contracts/schemas/mdbase.person/1.0.0.schema.json\",\n \"title\": \"Person\",\n \"description\": \"A portable person reference with editable account identity associations. Not an authentication or access-control assertion.\",\n \"type\": \"object\",\n \"required\": [\"id\", \"name\"],\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"\\\\S\",\n \"description\": \"Stable person identifier used by references such as task assignments; independent of file path, name, account, and membership.\"\n },\n \"name\": {\n \"type\": \"string\",\n \"pattern\": \"\\\\S\",\n \"description\": \"Collection-owned display name, not a verified account name.\"\n },\n \"identities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"description\": \"Editable identity associations. Missing or empty means this person has no linked account. Match issuer and subject exactly; never match by email or name.\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"issuer\", \"subject\"],\n \"properties\": {\n \"issuer\": {\n \"type\": \"string\",\n \"format\": \"uri\",\n \"pattern\": \"^https?://\",\n \"description\": \"Stable issuer URL supplied by the identity service. Copy exactly; do not derive from a collection authority or relay URL.\"\n },\n \"subject\": {\n \"type\": \"string\",\n \"pattern\": \"\\\\S\",\n \"description\": \"Opaque, case-sensitive, account-wide identifier supplied by the issuer. Not an email, membership ID, or credential.\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n}\n" + }, + { + "source": "contracts/mdbase.person/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.person\nversion: 1.0.0\nname: Person\ndescription: A portable person reference with editable account identity associations.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/mdbase.person/1.0.0.schema.json\n---\n\n# Person\n\nA person is collection data, independent of an account or collection membership.\nApplications reference the stable `id`, not a name, file path, email address,\naccount subject, or membership ID. Collections choose their own local type and\nfield names through explicit `implements` mappings.\n\n`name` is a collection-owned display label. `identities` optionally associates\nthis person with one or more accounts through issuer/subject pairs. A person\nwithout a Connect account is valid. The same record can travel between hosted,\nrelay, local, and exported collections without rewriting its identity references.\n\n## Identity matching\n\nAn identity service supplies the authenticated caller's issuer and subject.\nSubjects are opaque, account-wide identifiers, stable across email changes,\ncollection memberships, and removal/rejoining. Different identity issuers may\nuse the same subject; both fields must match exactly. Consumers must not trim,\ncase-fold, resolve redirects, or otherwise normalize either field when matching.\nCopy the issuer value supplied by the service, including its trailing-slash\nconvention. Never derive the issuer from the current collection authority URL.\nHTTP issuers support explicitly configured development and self-hosted instances;\nproduction deployments should use HTTPS.\n\nAn application resolves an identity across all implementing person records:\n\n- No matching person: unlinked, not anonymous authentication and not an error.\n- Exactly one matching record with an unambiguous person ID: linked.\n- Multiple matching records, or multiple records with that person's ID:\n ambiguous. Do not pick the first match, merge records, or overwrite a link.\n\nA partial or failed query cannot establish that a match is unique. Cross-record\nuniqueness is a collection/application invariant, not something this JSON Schema\ncan prove. Consumers must handle ambiguity even if a starter type has a local\nuniqueness constraint: several local types can implement this contract.\n\n## Editable data, not security authority\n\nIdentity associations have exactly the same trust as other editable collection\ndata. Editing one may change an application's \"Assigned to me\" view. It must\nnever change authentication, membership, permissions, authenticated audit\nattribution, or proof of identity to another service. Schema conformance proves\nshape, not that an identity association is truthful.\n\n\"Link this person to me\" is an ordinary record update using the authenticated\ncaller's identity. Never infer a link from a matching email or display name.\nMembership and current account display names come from the identity service,\nnot frontmatter. A person association must never invite someone or grant access.\n\n## Lifecycle and portability\n\nRenaming or moving a record does not change its ID. Removing a member does not\ndelete their person record or task assignments. Deleting a record leaves\nunresolved references; consumers should preserve these rather than silently\nunassigning work. Reusing an ID for a different person changes every reference\nand should not be treated as a routine rename.\n\nCopying records preserves account associations, but never copies collection\naccess. Multiple issuer/subject entries let one person represent accounts at\ndifferent identity services. Account deletion or an issuer migration may leave\nan unresolved identity association; do not automatically rebind it to a newly\ncreated account. Stored names and identity references remain collection data\nsubject to that collection's retention and editing policies.\n\nAccount-wide subjects deliberately allow correlation across collections. This\nis a portability trade-off, not an anonymity guarantee. Never store passwords,\naccess tokens, refresh tokens, or other credentials in these fields.\n\n## Contact information\n\nThis contract does not define another address book. A local type can also\nimplement `mdbase.contact` for compact contact semantics, or\n`mdbase.jscontact.card` when it intentionally stores that interchange shape.\nThe starter Person type implements both Person and Contact.\n" + }, + { + "source": "schemas/mdbase.person/2.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://mdbase.dev/contracts/schemas/mdbase.person/2.0.0.schema.json\",\n \"title\": \"Person\",\n \"description\": \"A person represented by a record, referenced by links to that record, with editable account identity associations. Not an authentication or access-control assertion.\",\n \"type\": \"object\",\n \"required\": [\n \"name\"\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"pattern\": \"\\\\S\",\n \"description\": \"Collection-owned display name, not a verified account name.\"\n },\n \"identities\": {\n \"type\": \"array\",\n \"uniqueItems\": true,\n \"description\": \"Editable identity associations. Missing or empty means this person has no linked account. Match issuer and subject exactly; never match by email or name.\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\n \"issuer\",\n \"subject\"\n ],\n \"properties\": {\n \"issuer\": {\n \"type\": \"string\",\n \"format\": \"uri\",\n \"pattern\": \"^https?://\",\n \"description\": \"Stable issuer URL supplied by the identity service. Copy exactly; do not derive from a collection authority or relay URL.\"\n },\n \"subject\": {\n \"type\": \"string\",\n \"pattern\": \"\\\\S\",\n \"description\": \"Opaque, case-sensitive, account-wide identifier supplied by the issuer. Not an email, membership ID, or credential.\"\n }\n },\n \"additionalProperties\": false\n }\n }\n },\n \"additionalProperties\": false\n}\n" + }, + { + "source": "contracts/mdbase.person/2.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.person\nversion: 2.0.0\nname: Person\ndescription: A person record, referenced by links, with editable account identity associations.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/mdbase.person/2.0.0.schema.json\n---\n\n# Person\n\nA person is collection data, independent of an account or collection membership.\nOther records refer to a person with an ordinary mdbase link to the person's\nrecord, such as `\"[[Alex Rivera]]\"`, declared in the referring type's\n`collection.links` and resolved with the collection's own link rules. Never\nrefer to a person by name text, email address, account subject, or membership\nID. Collections choose their own local type and field names through explicit\n`implements` mappings.\n\nVersion 2.0.0 removes the separate `id` field of 1.0.0. Link resolution already\nidentifies the record, and rename reference updates keep links current, so a\nsecond identifier only created a way for the two to disagree.\n\n`name` is a collection-owned display label. `identities` optionally associates\nthis person with one or more accounts through issuer/subject pairs. A person\nwithout a Connect account is valid. The same record can travel between hosted,\nrelay, local, and exported collections without rewriting its identity references.\n\n## Identity matching\n\nAn identity service supplies the authenticated caller's issuer and subject.\nSubjects are opaque, account-wide identifiers, stable across email changes,\ncollection memberships, and removal/rejoining. Different identity issuers may\nuse the same subject; both fields must match exactly. Consumers must not trim,\ncase-fold, resolve redirects, or otherwise normalize either field when matching.\nCopy the issuer value supplied by the service, including its trailing-slash\nconvention. Never derive the issuer from the current collection authority URL.\nHTTP issuers support explicitly configured development and self-hosted instances;\nproduction deployments should use HTTPS.\n\nAn application resolves an identity across all implementing person records:\n\n- No matching person: unlinked, not anonymous authentication and not an error.\n- Exactly one matching record: linked.\n- Multiple matching records: ambiguous. Do not pick the first match, merge\n records, or overwrite a link.\n\nA partial or failed query cannot establish that a match is unique. Consumers\nmust handle ambiguity: several records, possibly of several local types, can\nclaim the same identity.\n\n## Editable data, not security authority\n\nIdentity associations have exactly the same trust as other editable collection\ndata. Editing one may change an application's \"Assigned to me\" view. It must\nnever change authentication, membership, permissions, authenticated audit\nattribution, or proof of identity to another service. Schema conformance proves\nshape, not that an identity association is truthful.\n\n\"Link this person to me\" is an ordinary record update using the authenticated\ncaller's identity. Never infer a link from a matching email or display name.\nMembership and current account display names come from the identity service,\nnot frontmatter. A person association must never invite someone or grant access.\n\n## Lifecycle and portability\n\nRenaming or moving a person record through mdbase or an editor that updates\nreferences keeps links to it current. Removing a member does not delete their\nperson record or task assignments. Deleting a record leaves unresolved links;\nconsumers should preserve these rather than silently unassigning work. Giving a\ndifferent person's record the old record's name or path redirects existing\nlinks and should not be treated as a routine rename.\n\nCopying records preserves account associations, but never copies collection\naccess. Multiple issuer/subject entries let one person represent accounts at\ndifferent identity services. Account deletion or an issuer migration may leave\nan unresolved identity association; do not automatically rebind it to a newly\ncreated account. Stored names and identity references remain collection data\nsubject to that collection's retention and editing policies.\n\nAccount-wide subjects deliberately allow correlation across collections. This\nis a portability trade-off, not an anonymity guarantee. Never store passwords,\naccess tokens, refresh tokens, or other credentials in these fields.\n\n## Contact information\n\nThis contract does not define another address book. A local type can also\nimplement `mdbase.contact` for compact contact semantics, or\n`mdbase.jscontact.card` when it intentionally stores that interchange shape.\nThe starter Person type implements both Person and Contact.\n" + }, + { + "source": "schemas/mdbase.contact/1.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://mdbase.dev/contracts/schemas/mdbase.contact/1.0.0.schema.json\",\n \"title\": \"Contact\",\n \"description\": \"A compact application-facing contact view.\",\n \"type\": \"object\",\n \"required\": [\"name\"],\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"The name applications should use when presenting this contact.\"\n },\n \"kind\": {\n \"type\": \"string\",\n \"enum\": [\"individual\", \"organisation\", \"group\"],\n \"description\": \"The broad kind of contact when the collection distinguishes it.\"\n },\n \"primary_email\": {\n \"type\": \"string\",\n \"format\": \"email\",\n \"description\": \"The preferred email address for this contact.\"\n },\n \"primary_phone\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"The preferred telephone number for this contact.\"\n },\n \"organisation\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"The organisation most closely associated with this contact.\"\n },\n \"birthday\": {\n \"type\": \"string\",\n \"format\": \"date\",\n \"description\": \"The contact's birthday when known.\"\n }\n },\n \"additionalProperties\": false\n}\n" + }, + { + "source": "contracts/mdbase.contact/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.contact\nversion: 1.0.0\nname: Contact\ndescription: A compact application-facing view of a person or organisation.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/mdbase.contact/1.0.0.schema.json\n---\n\n# Contact\n\nThis contract exposes the small set of contact semantics commonly shared by\naddress books, relationship tools, and other applications. A collection keeps\nits own field names and may store richer local data; the implementing type maps\nonly the fields it wants applications to see.\n\nThe contract is deliberately not an interchange format. Exporters can convert\nthis view to JSContact, vCard, or another wire representation without requiring\nMarkdown records to store that representation directly.\n" + }, + { + "source": "types/person/3.md", + "document": "---\nkind: mdbase.type\nname: person\nversion: 3\ndescription: An individual that other notes link to, with a collection-owned display name and optional contact details and account associations.\nmatch:\n where:\n type: person\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: Person\n description: One person represented by an ordinary note. Contact details and account associations are optional; this record never grants access or membership.\n type: object\n required: [name]\n properties:\n name:\n type: string\n minLength: 1\n pattern: '\\S'\n description: The display name to use in this collection. Use a full name, preferred name, or single name as appropriate; no first/last-name split is required. It can differ from an account's name and is not automatically synchronised with it.\n identities:\n type: array\n description: Optional accounts associated with this person, each identified by an exact issuer and subject pair. Omit this field for someone without an account. These editable associations help applications find your person record; they are not proof of identity and never grant access. Avoid linking the same account to multiple person notes.\n uniqueItems: true\n items:\n type: object\n description: One account association supplied by its identity service. Both issuer and subject must match exactly; never infer them from a name or email.\n required: [issuer, subject]\n properties:\n issuer:\n type: string\n format: uri\n pattern: '^https?://'\n description: The exact identity-service issuer URL reported for this account. Preserve its scheme, case, path, and trailing slash; do not guess it from the collection's hosting address.\n subject:\n type: string\n pattern: '\\S'\n description: The opaque account identifier reported by that issuer, not a display name, email, password, or access token. Copy it exactly; a different issuer may use the same subject for a different account.\n additionalProperties: false\n kind:\n const: individual\n default: individual\n description: This Person starter represents an individual. The organisation field describes their affiliation, not a different record kind. Existing organisation or group contacts can keep their own Contact-compatible type.\n email:\n type: string\n format: email\n description: The preferred email address for contacting this person. Optional contact information only; it does not establish an account association or grant access.\n phone:\n type: string\n minLength: 1\n description: The preferred telephone number, stored as text so country codes, formatting, and extensions can be preserved. Include a country code when useful; omit if unknown.\n organisation:\n type: string\n minLength: 1\n description: The person's main affiliation, such as an employer, school, or community. A collection-owned display label, not a membership or access-control setting; omit if not relevant.\n birthday:\n type: string\n format: date\n description: A known birthday as a full calendar date in YYYY-MM-DD form. Omit if unknown or if only the month and day are known; do not invent a year. Consider whether this collection needs this personal information.\n additionalProperties: true\ncollection:\n display:\n name_field: name\n description_field: organisation\n icon: user\nimplements:\n - contract: mdbase.person\n version: 2.0.0\n fields:\n name: name\n identities: identities\n - contract: mdbase.contact\n version: 1.0.0\n fields:\n name: name\n kind: kind\n primary_email: email\n primary_phone: phone\n organisation: organisation\n birthday: birthday\n---\n\n# Person\n\nUse one note per individual, whether or not they have a Connect account. This\nsingle type supplies both a Person record and optional Contact details. You do\nnot need a second Contact note for the same person.\n\n## Start with a name\n\n```yaml\ntype: person\nname: Alex Rivera\n```\n\nOther notes refer to this person with an ordinary link to this note, such as\n`[[Alex Rivera]]` in a task's `assignees`. Links to this note appear in its\nbacklinks. Rename or move the note with a tool that updates references, as mdbase\nand Obsidian do, so those links stay current. Reusing a person's file name for\nsomebody else redirects existing links to the wrong person.\n\nThe name is your collection's display label. Full names, preferred names and\nsingle names are equally valid. There is no required first-name/last-name split.\n\n## Add only the contact details you need\n\nEmail, phone, organisation and birthday are optional. Omit unknown values rather\nthan filling them with empty strings or guesses. Store phone numbers as text,\nincluding an international prefix or extension when helpful. Birthday currently\nrequires a complete date; partial dates need a separately configured local field.\nKeep free-form context in the Markdown body and be deliberate about sensitive\npersonal information in shared collections.\n\nThis starter describes individuals. Existing organisation and group records can\ncontinue using their Contact-compatible type; setting someone's affiliation does\nnot turn their Person note into an organisation record.\n\n## Link accounts deliberately\n\nThe editor's **Link this record to me** action uses the authenticated account's\nactual issuer and subject. If configuring these manually, copy both exact values\nfrom the identity service. Do not guess them from an email, display name, or the\nserver hosting this collection. Multiple entries may represent the same person\nat different identity services. Omit `identities` when there is no account to link.\n\nAssociations are ordinary editable data, not authentication or proof of identity.\nLinking never invites someone, grants access, or changes collection membership.\nNever put credentials in a person record. Avoid linking the same account from\nseveral notes: applications should report ambiguity, not guess which\nrecord to use. Account deletion or recreation is not permission to reassign an\nold association automatically.\n\n## Keep the collection's own shape\n\nThe inline schema belongs to this collection. You may add local fields, or rename\nfields and update their explicit `implements` mappings. Apps use those mappings\nfor the Person and Contact contracts; they need not assume your local field names.\nAdditional fields remain part of the note but do not automatically become shared\ncontract fields. Do not edit published contracts to customise this type.\n\nInstalling a newer starter does not overwrite an existing customised type or\nmigrate your address book. Converting an existing individual Contact to Person\nshould be a reviewed edit of that one note, preserving its path, fields and body.\n" + } + ], + "provides": [ + { + "id": "mdbase.person", + "version": "2.0.0", + "digest": "sha256:f16c462a1fd422f44ed055002f8476ec53c164107647fec782aeb74adeab2f9d" + }, + { + "id": "mdbase.contact", + "version": "1.0.0", + "digest": "sha256:49cfe15403dfc741a693e89a2f4d2857de306f391d02cb03e67bfaf0aa1d6b0d" + } + ] +} diff --git a/dist/packs/tasknotes.task/0.3.0-rc.18.json b/dist/packs/tasknotes.task/0.3.0-rc.18.json new file mode 100644 index 0000000..504c65a --- /dev/null +++ b/dist/packs/tasknotes.task/0.3.0-rc.18.json @@ -0,0 +1,86 @@ +{ + "manifest": { + "kind": "mdbase.type-pack", + "id": "tasknotes.task", + "version": "0.3.0-rc.18", + "name": "TaskNotes task type pack", + "description": "Portable tasks whose assignments link to person records, 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": "type", + "mode": "seed", + "source": "types/tasknotes-task/5.md", + "target": "_types/task.md", + "digest": "sha256:b3cf8dbe8885b331c494d2eeb36abfb052890e06b757877d655c269e666487dc", + "upgrade_from": [ + { + "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.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/package.json b/package.json index 32a0b59..fa01b6b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "node scripts/build.mjs", "expand:type": "node scripts/expand-type.mjs", "verify": "node scripts/verify.mjs", - "test": "node --test scripts/schema-expansion.test.mjs scripts/person-contract.test.mjs scripts/comment-contract.test.mjs scripts/starter-type-keys.test.mjs && npm run build && npm run verify && node --test scripts/person-pack.test.mjs scripts/tasknotes-upgrade.test.mjs scripts/tasknotes-plugin-collections.test.mjs" + "test": "node --test scripts/schema-expansion.test.mjs scripts/person-contract.test.mjs scripts/comment-contract.test.mjs scripts/starter-type-keys.test.mjs && npm run build && npm run verify && node --test scripts/seed-baselines.test.mjs scripts/seed-upgrade.test.mjs scripts/person-pack.test.mjs scripts/tasknotes-upgrade.test.mjs scripts/tasknotes-plugin-collections.test.mjs" }, "dependencies": { "ajv": "^8.20.0", diff --git a/packs/mdbase.contact/1.3.0.pack.yaml b/packs/mdbase.contact/1.3.0.pack.yaml index b13182a..f69933e 100644 --- a/packs/mdbase.contact/1.3.0.pack.yaml +++ b/packs/mdbase.contact/1.3.0.pack.yaml @@ -12,7 +12,7 @@ display: badges: - Portable person references installation: - visibility: default + visibility: hidden recommendation: user primary_type: person expand_local_refs: true diff --git a/packs/mdbase.contact/1.4.0.pack.yaml b/packs/mdbase.contact/1.4.0.pack.yaml new file mode 100644 index 0000000..2dc74af --- /dev/null +++ b/packs/mdbase.contact/1.4.0.pack.yaml @@ -0,0 +1,58 @@ +kind: mdbase.catalog-pack +id: mdbase.contact +version: 1.4.0 +name: People type pack +description: Portable people, editable account identity associations, and familiar contact fields. +display: + name: People + summary: Reference people across apps and collections without tying records to account membership. + category: people + audience: general + icon: user + badges: + - Portable person references +installation: + visibility: default + recommendation: user + primary_type: person +expand_local_refs: true +provides: + - id: mdbase.person + version: 2.0.0 + - id: mdbase.contact + version: 1.0.0 +resources: + # Not provided for new installs: retained so Person types customised under + # 1.1.0 keep the exact contract they implement. + - kind: schema + mode: managed + source: schemas/mdbase.person/1.0.0.schema.json + target: schemas/mdbase.person/1.0.0.schema.json + - kind: contract + mode: managed + source: contracts/mdbase.person/1.0.0.md + target: _contracts/mdbase.person/1.0.0.md + - kind: schema + mode: managed + source: schemas/mdbase.person/2.0.0.schema.json + target: schemas/mdbase.person/2.0.0.schema.json + - kind: contract + mode: managed + source: contracts/mdbase.person/2.0.0.md + target: _contracts/mdbase.person/2.0.0.md + - kind: schema + mode: managed + source: schemas/mdbase.contact/1.0.0.schema.json + target: schemas/mdbase.contact/1.0.0.schema.json + - kind: contract + mode: managed + source: contracts/mdbase.contact/1.0.0.md + target: _contracts/mdbase.contact/1.0.0.md + - kind: type + mode: seed + source: types/person/3.md + target: _types/person.md + # Every starter an earlier mdbase.contact pack seeded at _types/person.md. + upgrade_from: + - types/person/2.md + - types/person/1.md diff --git a/packs/tasknotes.task/0.3.0-rc.17.pack.yaml b/packs/tasknotes.task/0.3.0-rc.17.pack.yaml index 96f844d..89172b7 100644 --- a/packs/tasknotes.task/0.3.0-rc.17.pack.yaml +++ b/packs/tasknotes.task/0.3.0-rc.17.pack.yaml @@ -12,7 +12,7 @@ display: badges: - TaskNotes compatible installation: - visibility: default + visibility: hidden recommendation: integration-managed primary_type: task caution: Upgrades the previous starter type while preserving independent customizations. Conflicts or other references to the previous contract block the whole upgrade. diff --git a/packs/tasknotes.task/0.3.0-rc.18.pack.yaml b/packs/tasknotes.task/0.3.0-rc.18.pack.yaml new file mode 100644 index 0000000..ecc5c90 --- /dev/null +++ b/packs/tasknotes.task/0.3.0-rc.18.pack.yaml @@ -0,0 +1,45 @@ +kind: mdbase.catalog-pack +id: tasknotes.task +version: 0.3.0-rc.18 +name: TaskNotes task type pack +description: Portable tasks whose assignments link to person records, with a reviewed, atomic upgrade from every TaskNotes starter published before. +display: + name: TaskNotes tasks + summary: Manage portable Markdown tasks, assignments, dates, recurrence, reminders, and time tracking. + category: work + audience: general + icon: square-check-big + badges: + - TaskNotes compatible +installation: + visibility: default + recommendation: integration-managed + primary_type: task + caution: Upgrades the previous starter type while preserving independent customizations. Conflicts or other references to the previous contract block the whole upgrade. +provides: + - id: tasknotes.task + version: 0.3.0-rc.5 + digest: sha256:dde71d0d776aa0069ee627fe3c10b1a20f87ba36929a16790598ebfb4e8ebf04 +resources: + - kind: contract + mode: managed + source: contracts/tasknotes.task/0.3.0-rc.5.md + target: _contracts/tasknotes.task.md + - kind: type + mode: seed + source: types/tasknotes-task/5.md + target: _types/task.md + # Every starter an earlier tasknotes.task pack seeded at _types/task.md. + upgrade_from: + - types/tasknotes-task/4.md + - types/tasknotes-task/3.md + - types/tasknotes-task/2.md + - types/tasknotes-task/1.md + - kind: schema + mode: managed + source: schemas/tasknotes.task/0.3.0-rc.5.schema.json + target: _schemas/tasknotes/tasknotes-task.schema.json + - kind: schema + mode: managed + source: schemas/tasknotes.task.binding/0.3.0-rc.5.schema.json + target: _schemas/tasknotes/tasknotes-task-binding.schema.json diff --git a/scripts/build.mjs b/scripts/build.mjs index 7166149..2964c93 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -46,18 +46,9 @@ for (const packFile of packFiles) { assertInside(root, sourcePath, "resource source"); const document = await readFile(sourcePath, "utf8"); const resourceDigest = digest(document); - let upgradeFrom; - if (resource.upgrade_from !== undefined) { - if (resource.kind !== "type" || resource.mode !== "seed") { - fail("Only seed types may declare an upgrade baseline."); - } - assertSafePath(resource.upgrade_from, "upgrade baseline"); - const baselinePath = resolve(root, resource.upgrade_from); - assertInside(root, baselinePath, "upgrade baseline"); - const baseline = await readFile(baselinePath, "utf8"); - upgradeFrom = { digest: digest(baseline), document: baseline }; - } - + const upgradeFrom = resource.upgrade_from === undefined + ? undefined + : await upgradeBaselines(resource, document, resourceDigest, relative(root, packFile)); resources.push({ source: resource.source, document }); manifestResources.push({ kind: resource.kind, @@ -174,6 +165,49 @@ console.log( `Built ${catalog.contracts.length} contract and ${catalog.packs.length} pack into ${relative(root, dist)}.`, ); +/** + * A seed type's upgrade baselines (mdbase spec 05A). The single-path form + * keeps emitting one `{ digest, document }` baseline, so published packs stay + * byte-identical; a list emits `[{ digest, version, document }]`, newest first. + */ +async function upgradeBaselines(resource, desired, desiredDigest, label) { + const at = `${label} ${resource.target} upgrade_from`; + if (resource.kind !== "type" || resource.mode !== "seed") { + fail(`${at}: only seed types may declare an upgrade baseline.`); + } + const list = Array.isArray(resource.upgrade_from); + const paths = list ? resource.upgrade_from : [resource.upgrade_from]; + if (paths.length === 0) fail(`${at}: a baseline list must not be empty.`); + const desiredType = matter(desired).data; + const seen = new Set(); + const baselines = []; + for (const path of paths) { + assertSafePath(path, "upgrade baseline"); + const baselinePath = resolve(root, path); + assertInside(root, baselinePath, "upgrade baseline"); + const document = await readFile(baselinePath, "utf8"); + const baselineDigest = digest(document); + if (baselineDigest === desiredDigest) fail(`${at}: ${path} is the resource's own document.`); + if (seen.has(baselineDigest)) fail(`${at}: ${path} repeats an earlier baseline.`); + seen.add(baselineDigest); + const frontmatter = matter(document).data; + if (frontmatter.kind !== desiredType.kind || frontmatter.name !== desiredType.name) { + fail(`${at}: ${path} must have the same type kind and name as ${resource.source}.`); + } + if (frontmatter.version !== undefined && !Number.isInteger(frontmatter.version)) { + fail(`${at}: ${path} must declare an integer type version.`); + } + baselines.push({ + digest: baselineDigest, + ...(frontmatter.version === undefined ? {} : { version: frontmatter.version }), + document, + }); + } + if (!list) return { digest: baselines[0].digest, document: baselines[0].document }; + // Newest first; Array.prototype.sort is stable, so equal versions keep their order. + return baselines.sort((left, right) => (right.version ?? -Infinity) - (left.version ?? -Infinity)); +} + function registerContract(source, document, resourceDigest) { const frontmatter = matter(document).data; if (frontmatter.kind !== "mdbase.contract") { diff --git a/scripts/pack-versions.mjs b/scripts/pack-versions.mjs new file mode 100644 index 0000000..7faf4e3 --- /dev/null +++ b/scripts/pack-versions.mjs @@ -0,0 +1,70 @@ +import { readdir, readFile } from "node:fs/promises"; + +const dist = new URL("../dist/", import.meta.url); + +/** Every published provision by pack id, listed or not (`catalog: false` URLs stay installable). */ +export async function publishedProvisions() { + const byId = new Map(); + for (const id of (await readdir(new URL("packs/", dist))).sort()) { + const versions = []; + for (const file of await readdir(new URL(`packs/${id}/`, dist))) { + versions.push(JSON.parse(await readFile(new URL(`packs/${id}/${file}`, dist), "utf8"))); + } + byId.set(id, versions.sort((left, right) => compareVersions(left.manifest.version, right.manifest.version))); + } + return byId; +} + +/** Seed type resources; legacy packs without modes install types as seeds. */ +export function seedTypes(provision) { + return provision.manifest.resources.filter((resource) => + resource.kind === "type" && (resource.mode ?? "seed") === "seed"); +} + +/** A seed type's upgrade baselines: one, or a list (mdbase spec 05A). */ +export function baselines(resource) { + if (resource.upgrade_from === undefined) return []; + return Array.isArray(resource.upgrade_from) ? resource.upgrade_from : [resource.upgrade_from]; +} + +/** The provision with the modes legacy engines defaulted (types seed, others managed). */ +export function withLegacyResourceModes(provision) { + if (provision.manifest.resources.every(({ mode }) => mode !== undefined)) return provision; + return { + ...provision, + manifest: { + ...provision.manifest, + resources: provision.manifest.resources.map((resource) => ({ + ...resource, + mode: resource.mode ?? (resource.kind === "type" ? "seed" : "managed"), + })), + }, + }; +} + +/** SemVer 2.0.0 precedence, including numeric prerelease identifiers (rc.9 < rc.12). */ +export function compareVersions(left, right) { + const split = (version) => { + const [core, prerelease] = version.split("+")[0].split(/-(.*)/su); + return { core: core.split(".").map(Number), prerelease: prerelease ? prerelease.split(".") : [] }; + }; + const a = split(left); + const b = split(right); + for (let index = 0; index < 3; index += 1) { + if (a.core[index] !== b.core[index]) return a.core[index] - b.core[index]; + } + if (a.prerelease.length === 0 || b.prerelease.length === 0) { + return b.prerelease.length - a.prerelease.length; + } + const numeric = /^\d+$/u; + for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) { + const x = a.prerelease[index]; + const y = b.prerelease[index]; + if (x === undefined || y === undefined) return x === undefined ? -1 : 1; + if (x === y) continue; + if (numeric.test(x) && numeric.test(y)) return Number(x) - Number(y); + if (numeric.test(x) !== numeric.test(y)) return numeric.test(x) ? -1 : 1; + return x < y ? -1 : 1; + } + return 0; +} diff --git a/scripts/person-pack.test.mjs b/scripts/person-pack.test.mjs index 9da92a8..494105f 100644 --- a/scripts/person-pack.test.mjs +++ b/scripts/person-pack.test.mjs @@ -6,6 +6,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import test from "node:test"; +import matter from "gray-matter"; const root = fileURLToPath(new URL("../", import.meta.url)); const { Collection } = mdbase; @@ -13,7 +14,7 @@ const { Collection } = mdbase; test("the catalog offers only the single-type People starter", async () => { const catalog = JSON.parse(await readFile(join(root, "dist/catalog.json"), "utf8")); const packs = catalog.packs.filter(({ id, installation }) => id === "mdbase.contact" && installation.visibility !== "hidden"); - assert.deepEqual(packs.map(({ version }) => version), ["1.3.0"]); + assert.deepEqual(packs.map(({ version }) => version), ["1.4.0"]); assert.deepEqual(packs[0].installation.types.map(({ name }) => name), ["person"]); assert.equal(packs[0].installation.primary_type, "person"); }); @@ -23,6 +24,7 @@ test("previously distributed Contact provisions remain byte-identical", async () ["1.0.0", "70c3ab048da407a56fe265a840c83a2d1df90aa351fde0d50ce1459a155cc4e9"], ["1.1.0", "14bba55df0574401b46ae08e5ef1e41e615d1477f70c3cc202eb9ff179287864"], ["1.2.0", "dbbb3a874cfc5d69a2a5793c86dab801c16438d58a270b48bc7376fc8bbdfa2d"], + ["1.3.0", "48fc070ae00c61ab5b20b9ffb385d20d2a468328da07e45e67c99602b05d02e8"], ]) { const bytes = await readFile(join(root, "dist/packs/mdbase.contact", `${version}.json`)); assert.equal(createHash("sha256").update(bytes).digest("hex"), digest); @@ -56,9 +58,13 @@ async function install(collectionRoot, version) { finally { await engine.close(); } } -// 1.2.0 and 1.3.0 offer only the Person starter; 1.3.0 upgrades an unmodified v2 to v3. -const singleType = (version) => version === "1.2.0" || version === "1.3.0"; -for (const versions of [["1.1.0"], ["1.0.0", "1.1.0"], ["1.2.0"], ["1.0.0", "1.2.0"], ["1.1.0", "1.2.0"], ["1.3.0"], ["1.2.0", "1.3.0"]]) { +// 1.2.0 and later offer only the Person starter; 1.3.0 upgrades an unmodified v2 to v3, +// and 1.4.0 upgrades every earlier Person starter (v1 and v2) to v3. +const singleType = (version) => ["1.2.0", "1.3.0", "1.4.0"].includes(version); +for (const versions of [ + ["1.1.0"], ["1.0.0", "1.1.0"], ["1.2.0"], ["1.0.0", "1.2.0"], ["1.1.0", "1.2.0"], ["1.3.0"], ["1.2.0", "1.3.0"], + ["1.4.0"], ["1.0.0", "1.4.0"], ["1.1.0", "1.4.0"], ["1.2.0", "1.4.0"], ["1.3.0", "1.4.0"], +]) { test(`one fresh Person type, legacy contacts preserved: ${versions.join(" then ")}`, async () => { const collectionRoot = await mkdtemp(join(tmpdir(), "mdbase-person-pack-")); let collection; @@ -70,8 +76,11 @@ for (const versions of [["1.1.0"], ["1.0.0", "1.1.0"], ["1.2.0"], ["1.0.0", "1.2 const legacyContact = "---\ntype: contact\nname: Existing contact\nid: legacy_contact\nprivate_notes: Keep me\n---\nOriginal **contact** body.\n"; const legacyPerson = "---\ntype: person\nname: Existing person\nid: legacy_person\n---\nOriginal **person** body.\n"; const hasContact = versions.some((version) => !singleType(version)); - // A Person type customized under 1.1.0 is a user-owned seed and is kept. - const personV2 = !versions.includes("1.1.0"); + // A Person type customized under 1.1.0 is a user-owned seed: packs before + // 1.4.0 keep it as it is; 1.4.0 lists its v1 starter as a baseline, so it + // merges the customization into Person v3 (mdbase.person 2.0.0). + const mergedPerson = versions.includes("1.1.0") && versions.includes("1.4.0"); + const personV2 = !versions.includes("1.1.0") || mergedPerson; const personContract = personV2 ? "2.0.0" : "1.0.0"; for (const version of versions) { await install(collectionRoot, version); @@ -92,7 +101,14 @@ for (const versions of [["1.1.0"], ["1.0.0", "1.1.0"], ["1.2.0"], ["1.0.0", "1.2 assert.equal(await readFile(join(collectionRoot, "_types/contact.md"), "utf8"), customizedContact, "pack upgrade must preserve the user's customized Contact type"); } - if (customizedPerson) assert.equal(await readFile(join(collectionRoot, "_types/person.md"), "utf8"), customizedPerson); + if (mergedPerson) { + const person = await readFile(join(collectionRoot, "_types/person.md"), "utf8"); + assert.ok(person.endsWith("\nMy collection-owned Person documentation.\n")); + assert.deepEqual(matter(person).data.implements.map(({ contract, version }) => `${contract} ${version}`).sort(), + ["mdbase.contact 1.0.0", "mdbase.person 2.0.0"]); + } else if (customizedPerson) { + assert.equal(await readFile(join(collectionRoot, "_types/person.md"), "utf8"), customizedPerson); + } if (hasContact) assert.equal(await readFile(join(collectionRoot, "legacy-contact.md"), "utf8"), legacyContact); if (customizedPerson) assert.equal(await readFile(join(collectionRoot, "legacy-person.md"), "utf8"), legacyPerson); if (!hasContact) await assert.rejects(readFile(join(collectionRoot, "_types/contact.md")), { code: "ENOENT" }); diff --git a/scripts/seed-baselines.test.mjs b/scripts/seed-baselines.test.mjs new file mode 100644 index 0000000..cd92486 --- /dev/null +++ b/scripts/seed-baselines.test.mjs @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { test } from "node:test"; +import matter from "gray-matter"; +import { baselines, compareVersions, publishedProvisions, seedTypes } from "./pack-versions.mjs"; + +// A seed type's upgrade_from (mdbase spec 05A) lists the starters it upgrades. +// A collection seeded by any earlier version of a pack may install the offered +// version, so the offered pack must list every starter an earlier version ever +// shipped at the same target: an unlisted starter is never upgraded. + +const dist = new URL("../dist/", import.meta.url); +const catalog = JSON.parse(await readFile(new URL("catalog.json", dist), "utf8")); +const sha256 = (document) => `sha256:${createHash("sha256").update(document).digest("hex")}`; + +test("SemVer precedence orders TaskNotes release candidates numerically", () => { + assert.ok(compareVersions("0.3.0-rc.9", "0.3.0-rc.12") < 0); + assert.ok(compareVersions("0.3.0-rc.18", "0.3.0") < 0); + assert.ok(compareVersions("1.4.0", "1.3.0") > 0); +}); + +const all = await publishedProvisions(); +const offered = catalog.packs.filter(({ installation }) => installation.visibility !== "hidden"); + +for (const entry of offered) { + const offeredProvision = all.get(entry.id).find(({ manifest }) => manifest.version === entry.version); + for (const resource of seedTypes(offeredProvision)) { + test(`${entry.id} ${entry.version} upgrades every earlier starter at ${resource.target}`, () => { + const listed = new Set(baselines(resource).map(({ digest }) => digest)); + const missing = []; + for (const earlier of all.get(entry.id)) { + if (compareVersions(earlier.manifest.version, entry.version) >= 0) continue; + for (const shipped of seedTypes(earlier)) { + if (shipped.target !== resource.target || shipped.digest === resource.digest) continue; + if (!listed.has(shipped.digest)) missing.push(`${shipped.source} (${earlier.manifest.version})`); + } + } + assert.deepEqual(missing, [], `${resource.target} upgrade_from must also list ${missing.join(", ")}`); + }); + } +} + +for (const [id, versions] of all) { + for (const provision of versions) { + for (const resource of provision.manifest.resources.filter((r) => r.upgrade_from !== undefined)) { + test(`${id} ${provision.manifest.version} ${resource.target} baselines follow spec 05A`, () => { + assert.equal(resource.kind, "type"); + assert.equal(resource.mode, "seed"); + const desired = matter(provision.resources.find(({ source }) => source === resource.source).document).data; + const list = baselines(resource); + assert.ok(list.length > 0); + assert.equal(new Set(list.map(({ digest }) => digest)).size, list.length, "digests must be distinct"); + for (const baseline of list) { + assert.equal(sha256(baseline.document), baseline.digest); + assert.notEqual(baseline.digest, resource.digest, "a baseline cannot be the desired document"); + const frontmatter = matter(baseline.document).data; + assert.equal(frontmatter.kind, desired.kind); + assert.equal(frontmatter.name, desired.name); + if (baseline.version !== undefined) assert.equal(baseline.version, frontmatter.version); + } + if (Array.isArray(resource.upgrade_from)) { + const order = list.map(({ version }) => version); + assert.deepEqual(order, [...order].sort((a, b) => b - a), "lists are ordered newest first"); + } + }); + } + } +} diff --git a/scripts/seed-upgrade.test.mjs b/scripts/seed-upgrade.test.mjs new file mode 100644 index 0000000..a867d98 --- /dev/null +++ b/scripts/seed-upgrade.test.mjs @@ -0,0 +1,125 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import matter from "gray-matter"; +import { parse, stringify } from "yaml"; +import { packEngine } from "./pack-engine.mjs"; +import { compareVersions, publishedProvisions, seedTypes, withLegacyResourceModes } from "./pack-versions.mjs"; + +// Every pack whose seed type lists its upgrade baselines must upgrade a +// collection seeded by any earlier version of that pack, through each engine: +// an unedited starter becomes the exact new starter, and an edited one merges +// against the starter its lock records as its origin. + +const sha256 = (document) => `sha256:${createHash("sha256").update(document).digest("hex")}`; +const all = await publishedProvisions(); + +// Publishers' own starters change other managed resources too (TaskNotes moves +// its contract to a new source), which a reviewed install adopts explicitly. +async function reviewedInstall(engine) { + let assessment = await engine.assess(); + assert.equal(assessment.valid, true, JSON.stringify(assessment)); + const adoptions = Object.fromEntries(assessment.result.resources + .filter((r) => r.mode === "managed" && r.action === "conflict" && r.installed_digest === undefined) + .map((r) => [r.target, r.current_digest])); + assessment = await engine.assess(adoptions); + assert.equal(assessment.valid, true, JSON.stringify(assessment)); + const applied = await engine.apply(assessment.result.assessment_digest, adoptions); + assert.equal(applied.valid, true, JSON.stringify(applied)); + return applied.result; +} + +async function withCollection(run) { + const root = await mkdtemp(join(tmpdir(), "mdbase-seed-upgrade-")); + const engines = []; + try { + await writeFile(join(root, "mdbase.yaml"), "spec_version: 0.3.0\nsettings:\n validation: error\n"); + await run(root, async (provision) => { + const engine = await packEngine(root, withLegacyResourceModes(provision), "dev.mdbase.tests"); + engines.push(engine); + return reviewedInstall(engine); + }); + } finally { + await Promise.all(engines.map((engine) => engine.close())); + await rm(root, { recursive: true, force: true }); + } +} + +async function originDigest(root, id, target) { + const lock = parse(await readFile(join(root, "mdbase.lock.yaml"), "utf8")); + return lock.packs.find((pack) => pack.id === id).resources.find((resource) => resource.target === target) + .origin_digest; +} + +// A collection's own edits: a documented body line and an extra schema property. +function customize(document) { + const { content } = matter(document); + // gray-matter caches parsed documents; edit a copy. + const data = structuredClone(matter(document).data); + data.schema.value.properties.local_note = { type: "string", description: "This collection's own field." }; + return `---\n${stringify(data)}---\n${content}\nThis collection's own documentation.\n`; +} + +for (const [id, versions] of all) { + for (const provision of versions) { + for (const resource of seedTypes(provision).filter(({ upgrade_from: from }) => Array.isArray(from))) { + const desired = provision.resources.find(({ source }) => source === resource.source).document; + const desiredType = matter(desired).data; + const earlier = versions.filter((candidate) => + compareVersions(candidate.manifest.version, provision.manifest.version) < 0 + && seedTypes(candidate).some(({ target }) => target === resource.target)); + assert.ok(earlier.length > 0, `${id} ${provision.manifest.version} lists baselines but has no earlier starter`); + + for (const previous of earlier) { + const shipped = seedTypes(previous).find(({ target }) => target === resource.target); + const starter = previous.resources.find(({ source }) => source === shipped.source).document; + const label = `${id} ${previous.manifest.version} to ${provision.manifest.version} (${shipped.source})`; + + test(`${label}: an unedited starter becomes the exact new starter`, async () => { + await withCollection(async (root, install) => { + await install(previous); + const typePath = join(root, resource.target); + assert.equal(await readFile(typePath, "utf8"), starter); + const result = await install(provision); + const planned = result.resources.find(({ target }) => target === resource.target); + if (starter === desired) { + assert.equal(planned.action, "preserve"); + } else { + assert.equal(planned.action, "update", JSON.stringify(planned)); + assert.deepEqual(planned.upgrade_baseline, { + digest: sha256(starter), version: matter(starter).data.version, + }); + } + assert.equal(await readFile(typePath, "utf8"), desired); + assert.equal(await originDigest(root, id, resource.target), resource.digest); + }); + }); + + if (starter === desired) continue; + test(`${label}: an edited starter merges against its own baseline`, async () => { + await withCollection(async (root, install) => { + await install(previous); + const typePath = join(root, resource.target); + await writeFile(typePath, customize(starter)); + const result = await install(provision); + const planned = result.resources.find(({ target }) => target === resource.target); + assert.equal(planned.action, "update", JSON.stringify(planned)); + assert.equal(planned.upgrade_baseline.digest, sha256(starter)); + assert.equal(planned.upgrade_baseline.version, matter(starter).data.version); + const upgraded = await readFile(typePath, "utf8"); + const type = structuredClone(matter(upgraded).data); + assert.ok(upgraded.endsWith("\nThis collection's own documentation.\n"), "the body edit is kept"); + assert.equal(type.schema.value.properties.local_note.type, "string", "the added property is kept"); + // Everything else is the new starter: the edits touched nothing it changes. + delete type.schema.value.properties.local_note; + assert.deepEqual(type, desiredType); + assert.equal(await originDigest(root, id, resource.target), resource.digest); + }); + }); + } + } + } +} diff --git a/scripts/tasknotes-plugin-collections.test.mjs b/scripts/tasknotes-plugin-collections.test.mjs index 933f4ee..58c4ecb 100644 --- a/scripts/tasknotes-plugin-collections.test.mjs +++ b/scripts/tasknotes-plugin-collections.test.mjs @@ -9,7 +9,7 @@ import { packEngine } from "./pack-engine.mjs"; // TaskNotes 4.13.6 and the 5.0 betas, with default and customized settings). // Every TaskNotes pack must install over them, as TaskNotes App does through // Connect, without conflicts and without dropping statuses or touching records. -const LATEST = "0.3.0-rc.17"; +const LATEST = "0.3.0-rc.18"; const fixtures = new URL("./fixtures/tasknotes-5.0/", import.meta.url); async function reviewedInstall(engine) { @@ -40,6 +40,21 @@ for (const name of (await readdir(fixtures)).filter((file) => file.endsWith(".js const result = await reviewedInstall(engine); assert.equal(result.valid, true, JSON.stringify(result)); const type = await readFile(join(root, "_types/task.md"), "utf8"); + // These collections have no lock, so no recorded seed origin: a task type + // equal to a published starter is replaced with the new starter, and any + // other is the collection's own and is preserved with a reason, never + // merged against a guessed baseline (mdbase spec 05A). + const seed = provision.manifest.resources.find(({ target }) => target === "_types/task.md"); + const planned = result.result.resources.find(({ target }) => target === "_types/task.md"); + const starter = seed.upgrade_from.find(({ document }) => document === files["_types/task.md"]); + if (starter) { + assert.equal(planned.action, "update"); + assert.equal(type, provision.resources.find(({ source }) => source === seed.source).document); + } else { + assert.equal(planned.action, "preserve"); + assert.match(planned.reason, /no upgrade baseline applies/); + assert.equal(type, files["_types/task.md"]); + } if (files["_types/task.md"].includes("- cancelled")) assert.match(type, /- cancelled/); assert.match(type, /version: 0\.3\.0-rc\.5/); for (const path of Object.keys(files).filter((path) => !path.startsWith("_") && !path.startsWith("mdbase"))) { diff --git a/scripts/tasknotes-upgrade.test.mjs b/scripts/tasknotes-upgrade.test.mjs index 5b9eeb0..7259e6c 100644 --- a/scripts/tasknotes-upgrade.test.mjs +++ b/scripts/tasknotes-upgrade.test.mjs @@ -28,6 +28,8 @@ const targets = [ // rc.17 also leaves the collection's generator bookkeeping alone, which // follows its field mapping (rc.16 conflicted with a customized list). { pack: "0.3.0-rc.17", contract: /0\.3\.0-rc\.5/, links: /assignees\[\]:/, minimal: true, customMapping: true }, + // rc.18 ships rc.17's starter and lists every earlier starter as a baseline. + { pack: "0.3.0-rc.18", contract: /0\.3\.0-rc\.5/, links: /assignees\[\]:/, minimal: true, customMapping: true }, ]; // Everything an upgrade may change in the starter type: assignees and versions. @@ -42,7 +44,7 @@ function withoutAssignees(document) { type["x-tasknotes-generator"].managed_fields = type["x-tasknotes-generator"].managed_fields.filter((field) => field !== "assignees"); return type; } -for (const target of targets) for (const scenario of ["managed", "customized", "generator-defaults", "custom-mapping", "unmanaged", "conflict", "other-reference"]) { +for (const target of targets) for (const scenario of ["managed", "customized", "customized-without-origin", "generator-defaults", "custom-mapping", "unmanaged", "conflict", "other-reference"]) { if (scenario === "generator-defaults" && !target.minimal) continue; if (scenario === "custom-mapping" && !target.customMapping) continue; test(`TaskNotes rc.12 to ${target.pack} upgrade: ${scenario}`, async () => { @@ -59,7 +61,7 @@ for (const target of targets) for (const scenario of ["managed", "customized", " assert.equal((await reviewedInstall(await engine(old))).valid, true); const typePath = join(root, "_types/task.md"); const oldType = await readFile(typePath, "utf8"); - if (scenario === "customized") { + if (scenario === "customized" || scenario === "customized-without-origin") { const type = parse(oldType.slice(4, oldType.indexOf("\n---", 4))); type.schema.value.properties.heading = type.schema.value.properties.title; delete type.schema.value.properties.title; @@ -79,6 +81,13 @@ for (const target of targets) for (const scenario of ["managed", "customized", " if (scenario === "conflict") await writeFile(typePath, oldType.replace("version: 1\n", "version: 99\n")); if (scenario === "other-reference") await writeFile(join(root, "_types/other.md"), oldType.replace("name: task", "name: other")); if (scenario === "unmanaged") await rm(join(root, "mdbase.lock.yaml")); + if (scenario === "customized-without-origin") { + // Engines before mdbase spec 05A recorded no seed origin in the lock. + const lockPath = join(root, "mdbase.lock.yaml"); + const lock = parse(await readFile(lockPath, "utf8")); + for (const pack of lock.packs) for (const resource of pack.resources) delete resource.origin_digest; + await writeFile(lockPath, stringify(lock)); + } const task = "---\ntype: task\ntitle: Keep\nheading: Keep\n---\nOriginal body.\n"; await writeFile(join(root, "task.md"), task); const paths = old.manifest.resources.map((r) => r.target); @@ -90,8 +99,15 @@ for (const target of targets) for (const scenario of ["managed", "customized", " for (const [path, document] of before) assert.equal(await readFile(join(root, path), "utf8"), document); } const result = await reviewedInstall(await engine(await load(target.pack))); - const success = !["conflict", "other-reference"].includes(scenario); + // An edited seed with no recorded origin (locks written before mdbase spec + // 05A) is never merged against a guessed baseline. It is preserved, and as + // it still requires the replaced contract the whole upgrade is blocked, + // exactly as for any other reference to that contract. + const success = !["conflict", "other-reference", "customized-without-origin"].includes(scenario); assert.equal(result.valid, success, JSON.stringify(result)); + if (scenario === "customized-without-origin") { + assert.match(JSON.stringify(result.diagnostics), /tasknotes\.task.{1,4}0\.3\.0-rc\.3/); + } assert.equal(await readFile(join(root, "task.md"), "utf8"), task); if (!success) { for (const [path, document] of before) assert.equal(await readFile(join(root, path), "utf8"), document); diff --git a/sources.json b/sources.json index 2de0a6c..3f6b984 100644 --- a/sources.json +++ b/sources.json @@ -5,12 +5,12 @@ }, "typescript_implementation": { "repository": "callumalpass/mdbase", - "ref": "v0.3.0-rc.8" + "ref": "v0.3.0-rc.9" }, "rust_cli": { "repository": "mdbase-dev/mdbase-connect", - "ref": "62242c21e754d27fee068e32ace02408bb572906", + "ref": "ab936db5395bc546b7e618c46e77326f81c10581", "engine_repository": "callumalpass/mdbase-rs", - "engine_ref": "c456e71b578c527448501ef7aec25825f13a9bea" + "engine_ref": "056db7334a0f49f10373225ae54c1abaedc14e37" } }