diff --git a/README.md b/README.md index 80faacb..26077e6 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,17 @@ records of their own, anchored to a quote of the commented record's Markdown body and linked to `mdbase.person` authors. Apps that comment embed the published provision unchanged, as mdbase writer does. +The `mdbase.daily-note` pack defines one record per local calendar day, +identified by its `date` (an RFC 3339 full-date), never by its path. Apps find +daily notes through the contract's type implementations and treat a date held +by several records as ambiguous. The **Daily note** starter stores the date +only in the file name, `Daily/YYYY-MM-DD.md`, with `collection.path.fields` +(path-stored fields, mdbase 0.3.0-rc.6), and matches only date-named files in +that folder so other notes there keep their own types. The pack needs an engine +with path-stored fields: the pinned mdbase-ts and Rust CLI reject the starter +until `sources.json` moves to engines that implement them. See +[`mdbase.daily-note` 1.0.0](contracts/mdbase.daily-note/1.0.0.md). + The `tasknotes.task` pack is the canonical application-provisioned TaskNotes contract bundle. TaskNotes clients embed the published provision byte-for-byte and pin its catalog digest so independently deployed clients cannot drift onto diff --git a/contracts/mdbase.daily-note/1.0.0.md b/contracts/mdbase.daily-note/1.0.0.md new file mode 100644 index 0000000..3018bcd --- /dev/null +++ b/contracts/mdbase.daily-note/1.0.0.md @@ -0,0 +1,66 @@ +--- +kind: mdbase.contract +contract_type: record +id: mdbase.daily-note +version: 1.0.0 +name: Daily note +description: One record per local calendar day, identified by its date. +record_schema: + dialect: json-schema-2020-12 + ref: ../../schemas/mdbase.daily-note/1.0.0.schema.json +--- + +# Daily note + +A record exposed through `mdbase.daily-note` represents one local calendar day. +`date` is an RFC 3339 full-date such as `2026-10-30`: a civil day with no time +and no time zone. It names the day the user lived, not an instant, so it does +not change when the user or the collection changes time zone. + +## Identity + +A daily note is identified by its `date` value, never by its path or file name. +An implementing type may store the date in the path, for example with +`collection.path.fields`, but consumers read the contract field and never parse +paths or file names themselves. + +## Discovery + +Implementing this contract is how a collection declares which of its records +are daily notes. An application discovers daily notes through this contract's +type implementations, not through a folder, a file-name format, or a reserved +type name, path, or frontmatter value. + +## One record per day + +A collection should hold at most one record per date across all types that +implement this contract. To find the note for a day, an application queries +every implementing type for that date: + +- No record: the day has no daily note. This is not an error. +- Exactly one record: that record is the day's note. +- Several records: the date is ambiguous. Applications MUST report the + ambiguity and MUST NOT silently pick one of the records, merge them, or + write to one of them as the day's note. + +A partial or failed query cannot establish that a day has no note or exactly +one note. Implementing types may declare `collection.unique` on their date +field so the engine reports duplicates within the type. + +## Creating a daily note + +An application creates a daily note through the implementing type's ordinary +create operation and path policy. File names, folders, templates and the body +belong to the type and the collection; the contract does not prescribe them. +An application that opens the note for a day, such as today in the user's +time zone, first looks for an existing record for that date and creates one +only when there is none. + +## Other day data + +Other applications may attach their own data to a day, such as time blocks or +completed pomodoros, through separate contracts implemented by the same type. +This contract defines only the day itself. + +This artifact is passive. Implementing it grants no authority to read, create, +or modify any record. diff --git a/dist/artifacts/contracts/mdbase.daily-note/1.0.0.md b/dist/artifacts/contracts/mdbase.daily-note/1.0.0.md new file mode 100644 index 0000000..3018bcd --- /dev/null +++ b/dist/artifacts/contracts/mdbase.daily-note/1.0.0.md @@ -0,0 +1,66 @@ +--- +kind: mdbase.contract +contract_type: record +id: mdbase.daily-note +version: 1.0.0 +name: Daily note +description: One record per local calendar day, identified by its date. +record_schema: + dialect: json-schema-2020-12 + ref: ../../schemas/mdbase.daily-note/1.0.0.schema.json +--- + +# Daily note + +A record exposed through `mdbase.daily-note` represents one local calendar day. +`date` is an RFC 3339 full-date such as `2026-10-30`: a civil day with no time +and no time zone. It names the day the user lived, not an instant, so it does +not change when the user or the collection changes time zone. + +## Identity + +A daily note is identified by its `date` value, never by its path or file name. +An implementing type may store the date in the path, for example with +`collection.path.fields`, but consumers read the contract field and never parse +paths or file names themselves. + +## Discovery + +Implementing this contract is how a collection declares which of its records +are daily notes. An application discovers daily notes through this contract's +type implementations, not through a folder, a file-name format, or a reserved +type name, path, or frontmatter value. + +## One record per day + +A collection should hold at most one record per date across all types that +implement this contract. To find the note for a day, an application queries +every implementing type for that date: + +- No record: the day has no daily note. This is not an error. +- Exactly one record: that record is the day's note. +- Several records: the date is ambiguous. Applications MUST report the + ambiguity and MUST NOT silently pick one of the records, merge them, or + write to one of them as the day's note. + +A partial or failed query cannot establish that a day has no note or exactly +one note. Implementing types may declare `collection.unique` on their date +field so the engine reports duplicates within the type. + +## Creating a daily note + +An application creates a daily note through the implementing type's ordinary +create operation and path policy. File names, folders, templates and the body +belong to the type and the collection; the contract does not prescribe them. +An application that opens the note for a day, such as today in the user's +time zone, first looks for an existing record for that date and creates one +only when there is none. + +## Other day data + +Other applications may attach their own data to a day, such as time blocks or +completed pomodoros, through separate contracts implemented by the same type. +This contract defines only the day itself. + +This artifact is passive. Implementing it grants no authority to read, create, +or modify any record. diff --git a/dist/artifacts/schemas/mdbase.daily-note/1.0.0.schema.json b/dist/artifacts/schemas/mdbase.daily-note/1.0.0.schema.json new file mode 100644 index 0000000..5186210 --- /dev/null +++ b/dist/artifacts/schemas/mdbase.daily-note/1.0.0.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mdbase.dev/contracts/schemas/mdbase.daily-note/1.0.0.schema.json", + "title": "Daily note", + "description": "A record that represents one local calendar day, identified by its date.", + "type": "object", + "required": [ + "date" + ], + "properties": { + "date": { + "type": "string", + "format": "date", + "description": "The calendar day this record represents, as an RFC 3339 full-date (YYYY-MM-DD). A civil date with no time or time zone." + } + }, + "additionalProperties": false +} diff --git a/dist/artifacts/types/daily-note/1.md b/dist/artifacts/types/daily-note/1.md new file mode 100644 index 0000000..b3250d2 --- /dev/null +++ b/dist/artifacts/types/daily-note/1.md @@ -0,0 +1,78 @@ +--- +kind: mdbase.type +name: daily-note +version: 1 +description: One note per calendar day, named by its date in the Daily folder. +match: + path_glob: 'Daily/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].md' +schema: + dialect: json-schema-2020-12 + value: + $schema: https://json-schema.org/draft/2020-12/schema + title: Daily note + description: One local calendar day represented by an ordinary note. The date is stored in the file name, not in frontmatter. + type: object + required: + - date + properties: + date: + type: string + format: date + description: The day this note is for, as a full calendar date in YYYY-MM-DD form, with no time or time zone. It is stored in the file name (Daily/2026-10-30.md), so changing it renames the note; do not also write it in frontmatter. + additionalProperties: true +collection: + path: + pattern: 'Daily/{date}.md' + fields: + - date + unique: + - field: date + scope: type + display: + name_field: date + icon: calendar +implements: + - contract: mdbase.daily-note + version: 1.0.0 + fields: + date: date +--- + +# Daily note + +One note per day, at `Daily/YYYY-MM-DD.md`. The date lives only in the file +name: apps read it from the path, and creating a note for a date puts it at +that date's path. The rest of the note is yours: write in the body and add any +frontmatter fields you like. + +```text +Daily/2026-10-30.md +``` + +Only files in `Daily/` whose name is a date belong to this type, so other notes +in the folder are left alone. A collection should have one note per date; two +notes with the same date are reported, and apps treat that day as ambiguous +rather than choosing one. + +## Use an existing daily-notes folder + +To adopt notes another tool already writes, such as Obsidian's daily notes, +change the folder in both `match.path_glob` and `collection.path.pattern`. For +notes in `Journal/`: + +```yaml +match: + path_glob: 'Journal/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].md' +collection: + path: + pattern: 'Journal/{date}.md' + fields: [date] +``` + +Notes are recognised by their path, so existing files need no frontmatter +changes. File names must use the `YYYY-MM-DD` form. + +This type belongs to your collection once installed. Apps find daily notes +through the `mdbase.daily-note` contract, not through this folder or file name, +so you can move them, or add other types that implement the contract, as long +as each date has one note. diff --git a/dist/catalog.json b/dist/catalog.json index c96c070..2c40f91 100644 --- a/dist/catalog.json +++ b/dist/catalog.json @@ -39,6 +39,16 @@ "artifact": "./artifacts/contracts/mdbase.contact/1.0.0.md", "standards": [] }, + { + "id": "mdbase.daily-note", + "version": "1.0.0", + "name": "Daily note", + "description": "One record per local calendar day, identified by its date.", + "contract_type": "record", + "digest": "sha256:797ad463925497c07a42d364e513116ad3495792927087370be776e73bbe65b7", + "artifact": "./artifacts/contracts/mdbase.daily-note/1.0.0.md", + "standards": [] + }, { "id": "mdbase.person", "version": "1.0.0", @@ -566,6 +576,44 @@ ] } }, + { + "id": "mdbase.daily-note", + "version": "1.0.0", + "name": "Daily notes", + "description": "The mdbase.daily-note contract and an editable Daily note type with one note per calendar day.", + "digest": "sha256:cb93c368c2bdbeb99dfb92a19d535da42221715a665d2047ed1fff3ef30d8c9b", + "provision": "./packs/mdbase.daily-note/1.0.0.json", + "provides": [ + { + "id": "mdbase.daily-note", + "version": "1.0.0", + "digest": "sha256:8ef734d7eeee420fd9c5c1b25f26bfde8bb72ab9e96bc5c3f9923ba251d69ea6" + } + ], + "resource_count": 3, + "display": { + "name": "Daily notes", + "summary": "Keep one note per day that any daily-note-aware app can find and open.", + "category": "calendar", + "audience": "general", + "icon": "calendar", + "badges": [ + "Portable daily notes" + ] + }, + "installation": { + "visibility": "default", + "recommendation": "optional", + "primary_type": "daily-note", + "caution": "Requires an mdbase engine with path-stored fields (mdbase 0.3.0-rc.6). Older engines reject the Daily note type, which keeps each note's date in its file name.", + "types": [ + { + "name": "daily-note", + "label": "Daily note" + } + ] + } + }, { "id": "mdbase.runtime.standard", "version": "0.2.0", diff --git a/dist/packs/mdbase.daily-note/1.0.0.json b/dist/packs/mdbase.daily-note/1.0.0.json new file mode 100644 index 0000000..bde5202 --- /dev/null +++ b/dist/packs/mdbase.daily-note/1.0.0.json @@ -0,0 +1,53 @@ +{ + "manifest": { + "kind": "mdbase.type-pack", + "id": "mdbase.daily-note", + "version": "1.0.0", + "name": "Daily notes", + "description": "The mdbase.daily-note contract and an editable Daily note type with one note per calendar day.", + "resources": [ + { + "kind": "schema", + "mode": "managed", + "source": "schemas/mdbase.daily-note/1.0.0.schema.json", + "target": "schemas/mdbase.daily-note/1.0.0.schema.json", + "digest": "sha256:a71471a2a941fe39ca6052ecf9189ac8156d76127d8792be49c29ab35d385080" + }, + { + "kind": "contract", + "mode": "managed", + "source": "contracts/mdbase.daily-note/1.0.0.md", + "target": "_contracts/mdbase.daily-note/1.0.0.md", + "digest": "sha256:797ad463925497c07a42d364e513116ad3495792927087370be776e73bbe65b7" + }, + { + "kind": "type", + "mode": "seed", + "source": "types/daily-note/1.md", + "target": "_types/daily-note.md", + "digest": "sha256:e9800bb1c091b0e0c6df01f773afdb4346473d379b5738b22d0c31139d18635e" + } + ] + }, + "resources": [ + { + "source": "schemas/mdbase.daily-note/1.0.0.schema.json", + "document": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://mdbase.dev/contracts/schemas/mdbase.daily-note/1.0.0.schema.json\",\n \"title\": \"Daily note\",\n \"description\": \"A record that represents one local calendar day, identified by its date.\",\n \"type\": \"object\",\n \"required\": [\n \"date\"\n ],\n \"properties\": {\n \"date\": {\n \"type\": \"string\",\n \"format\": \"date\",\n \"description\": \"The calendar day this record represents, as an RFC 3339 full-date (YYYY-MM-DD). A civil date with no time or time zone.\"\n }\n },\n \"additionalProperties\": false\n}\n" + }, + { + "source": "contracts/mdbase.daily-note/1.0.0.md", + "document": "---\nkind: mdbase.contract\ncontract_type: record\nid: mdbase.daily-note\nversion: 1.0.0\nname: Daily note\ndescription: One record per local calendar day, identified by its date.\nrecord_schema:\n dialect: json-schema-2020-12\n ref: ../../schemas/mdbase.daily-note/1.0.0.schema.json\n---\n\n# Daily note\n\nA record exposed through `mdbase.daily-note` represents one local calendar day.\n`date` is an RFC 3339 full-date such as `2026-10-30`: a civil day with no time\nand no time zone. It names the day the user lived, not an instant, so it does\nnot change when the user or the collection changes time zone.\n\n## Identity\n\nA daily note is identified by its `date` value, never by its path or file name.\nAn implementing type may store the date in the path, for example with\n`collection.path.fields`, but consumers read the contract field and never parse\npaths or file names themselves.\n\n## Discovery\n\nImplementing this contract is how a collection declares which of its records\nare daily notes. An application discovers daily notes through this contract's\ntype implementations, not through a folder, a file-name format, or a reserved\ntype name, path, or frontmatter value.\n\n## One record per day\n\nA collection should hold at most one record per date across all types that\nimplement this contract. To find the note for a day, an application queries\nevery implementing type for that date:\n\n- No record: the day has no daily note. This is not an error.\n- Exactly one record: that record is the day's note.\n- Several records: the date is ambiguous. Applications MUST report the\n ambiguity and MUST NOT silently pick one of the records, merge them, or\n write to one of them as the day's note.\n\nA partial or failed query cannot establish that a day has no note or exactly\none note. Implementing types may declare `collection.unique` on their date\nfield so the engine reports duplicates within the type.\n\n## Creating a daily note\n\nAn application creates a daily note through the implementing type's ordinary\ncreate operation and path policy. File names, folders, templates and the body\nbelong to the type and the collection; the contract does not prescribe them.\nAn application that opens the note for a day, such as today in the user's\ntime zone, first looks for an existing record for that date and creates one\nonly when there is none.\n\n## Other day data\n\nOther applications may attach their own data to a day, such as time blocks or\ncompleted pomodoros, through separate contracts implemented by the same type.\nThis contract defines only the day itself.\n\nThis artifact is passive. Implementing it grants no authority to read, create,\nor modify any record.\n" + }, + { + "source": "types/daily-note/1.md", + "document": "---\nkind: mdbase.type\nname: daily-note\nversion: 1\ndescription: One note per calendar day, named by its date in the Daily folder.\nmatch:\n path_glob: 'Daily/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].md'\nschema:\n dialect: json-schema-2020-12\n value:\n $schema: https://json-schema.org/draft/2020-12/schema\n title: Daily note\n description: One local calendar day represented by an ordinary note. The date is stored in the file name, not in frontmatter.\n type: object\n required:\n - date\n properties:\n date:\n type: string\n format: date\n description: The day this note is for, as a full calendar date in YYYY-MM-DD form, with no time or time zone. It is stored in the file name (Daily/2026-10-30.md), so changing it renames the note; do not also write it in frontmatter.\n additionalProperties: true\ncollection:\n path:\n pattern: 'Daily/{date}.md'\n fields:\n - date\n unique:\n - field: date\n scope: type\n display:\n name_field: date\n icon: calendar\nimplements:\n - contract: mdbase.daily-note\n version: 1.0.0\n fields:\n date: date\n---\n\n# Daily note\n\nOne note per day, at `Daily/YYYY-MM-DD.md`. The date lives only in the file\nname: apps read it from the path, and creating a note for a date puts it at\nthat date's path. The rest of the note is yours: write in the body and add any\nfrontmatter fields you like.\n\n```text\nDaily/2026-10-30.md\n```\n\nOnly files in `Daily/` whose name is a date belong to this type, so other notes\nin the folder are left alone. A collection should have one note per date; two\nnotes with the same date are reported, and apps treat that day as ambiguous\nrather than choosing one.\n\n## Use an existing daily-notes folder\n\nTo adopt notes another tool already writes, such as Obsidian's daily notes,\nchange the folder in both `match.path_glob` and `collection.path.pattern`. For\nnotes in `Journal/`:\n\n```yaml\nmatch:\n path_glob: 'Journal/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].md'\ncollection:\n path:\n pattern: 'Journal/{date}.md'\n fields: [date]\n```\n\nNotes are recognised by their path, so existing files need no frontmatter\nchanges. File names must use the `YYYY-MM-DD` form.\n\nThis type belongs to your collection once installed. Apps find daily notes\nthrough the `mdbase.daily-note` contract, not through this folder or file name,\nso you can move them, or add other types that implement the contract, as long\nas each date has one note.\n" + } + ], + "provides": [ + { + "id": "mdbase.daily-note", + "version": "1.0.0", + "digest": "sha256:8ef734d7eeee420fd9c5c1b25f26bfde8bb72ab9e96bc5c3f9923ba251d69ea6" + } + ] +} diff --git a/dist/schemas/mdbase.daily-note/1.0.0.schema.json b/dist/schemas/mdbase.daily-note/1.0.0.schema.json new file mode 100644 index 0000000..5186210 --- /dev/null +++ b/dist/schemas/mdbase.daily-note/1.0.0.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mdbase.dev/contracts/schemas/mdbase.daily-note/1.0.0.schema.json", + "title": "Daily note", + "description": "A record that represents one local calendar day, identified by its date.", + "type": "object", + "required": [ + "date" + ], + "properties": { + "date": { + "type": "string", + "format": "date", + "description": "The calendar day this record represents, as an RFC 3339 full-date (YYYY-MM-DD). A civil date with no time or time zone." + } + }, + "additionalProperties": false +} diff --git a/package.json b/package.json index fa01b6b..0522e39 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/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" + "test": "node --test scripts/schema-expansion.test.mjs scripts/person-contract.test.mjs scripts/comment-contract.test.mjs scripts/daily-note-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.daily-note/1.0.0.pack.yaml b/packs/mdbase.daily-note/1.0.0.pack.yaml new file mode 100644 index 0000000..2006331 --- /dev/null +++ b/packs/mdbase.daily-note/1.0.0.pack.yaml @@ -0,0 +1,34 @@ +kind: mdbase.catalog-pack +id: mdbase.daily-note +version: 1.0.0 +name: Daily notes +description: The mdbase.daily-note contract and an editable Daily note type with one note per calendar day. +display: + name: Daily notes + summary: Keep one note per day that any daily-note-aware app can find and open. + category: calendar + audience: general + icon: calendar + badges: + - Portable daily notes +installation: + visibility: default + recommendation: optional + primary_type: daily-note + caution: Requires an mdbase engine with path-stored fields (mdbase 0.3.0-rc.6). Older engines reject the Daily note type, which keeps each note's date in its file name. +provides: + - id: mdbase.daily-note + version: 1.0.0 +resources: + - kind: schema + mode: managed + source: schemas/mdbase.daily-note/1.0.0.schema.json + target: schemas/mdbase.daily-note/1.0.0.schema.json + - kind: contract + mode: managed + source: contracts/mdbase.daily-note/1.0.0.md + target: _contracts/mdbase.daily-note/1.0.0.md + - kind: type + mode: seed + source: types/daily-note/1.md + target: _types/daily-note.md diff --git a/schemas/mdbase.daily-note/1.0.0.schema.json b/schemas/mdbase.daily-note/1.0.0.schema.json new file mode 100644 index 0000000..5186210 --- /dev/null +++ b/schemas/mdbase.daily-note/1.0.0.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mdbase.dev/contracts/schemas/mdbase.daily-note/1.0.0.schema.json", + "title": "Daily note", + "description": "A record that represents one local calendar day, identified by its date.", + "type": "object", + "required": [ + "date" + ], + "properties": { + "date": { + "type": "string", + "format": "date", + "description": "The calendar day this record represents, as an RFC 3339 full-date (YYYY-MM-DD). A civil date with no time or time zone." + } + }, + "additionalProperties": false +} diff --git a/scripts/daily-note-contract.test.mjs b/scripts/daily-note-contract.test.mjs new file mode 100644 index 0000000..588a2b0 --- /dev/null +++ b/scripts/daily-note-contract.test.mjs @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; +import Ajv2020 from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; +import matter from "gray-matter"; + +const root = new URL("../", import.meta.url); +const contractSchema = JSON.parse(await readFile(new URL( + "schemas/mdbase.daily-note/1.0.0.schema.json", root, +), "utf8")); +const starter = matter(await readFile(new URL("types/daily-note/1.md", root), "utf8")).data; +const ajv = new Ajv2020({ allErrors: true, strict: true }); +addFormats(ajv); +const validateContract = ajv.compile(contractSchema); +const validateStarter = ajv.compile(starter.schema.value); + +test("a daily note is one civil calendar date", () => { + for (const validate of [validateContract, validateStarter]) { + assert.equal(validate({ date: "2026-10-30" }), true, ajv.errorsText(validate.errors)); + assert.equal(validate({}), false); + for (const date of ["2026-10-30T09:00:00Z", "2026-10-30+10:00", "2026-02-30", "30-10-2026", 20261030]) { + assert.equal(validate({ date }), false, `accepted ${JSON.stringify(date)}`); + } + } +}); + +test("the contract view is only the date; the starter keeps the note's other fields", () => { + assert.equal(validateContract({ date: "2026-10-30", mood: "good" }), false); + assert.equal(validateStarter({ date: "2026-10-30", mood: "good" }), true); +}); + +test("the starter stores the date in the path and maps it to the contract", () => { + const { path, unique } = starter.collection; + assert.deepEqual(path.fields, ["date"]); + assert.ok(path.pattern.includes("{date}")); + assert.deepEqual(unique, [{ field: "date", scope: "type" }]); + assert.deepEqual(starter.implements, [ + { contract: "mdbase.daily-note", version: "1.0.0", fields: { date: "date" } }, + ]); +}); + +test("the starter matches only date-named notes in its pattern's folder", () => { + const glob = starter.match.path_glob; + const folder = path => path.slice(0, path.lastIndexOf("/") + 1); + assert.equal(folder(glob), folder(starter.collection.path.pattern)); + const pattern = new RegExp(`^${glob.replace(/[.]/g, "\\.").replace(/\*/g, "[^/]*")}$`); + assert.match("Daily/2026-10-30.md", pattern); + for (const path of ["Daily/Ideas.md", "Daily/2026-10-30 draft.md", "2026-10-30.md", "Daily/2026/2026-10-30.md"]) { + assert.doesNotMatch(path, pattern); + } +}); diff --git a/scripts/starter-type-keys.test.mjs b/scripts/starter-type-keys.test.mjs index 5f65ee8..126b350 100644 --- a/scripts/starter-type-keys.test.mjs +++ b/scripts/starter-type-keys.test.mjs @@ -9,7 +9,7 @@ const root = new URL("../", import.meta.url); // configured explicit type key (`type` by default, `mdbase_type` in some collections). // A starter that requires or pins its own name under `type`, or rejects unknown // top-level fields, fails in every collection whose key is something else. -for (const source of ["types/person/3.md", "types/comment/2.md", "types/view/2.md"]) { +for (const source of ["types/person/3.md", "types/comment/2.md", "types/view/2.md", "types/daily-note/1.md"]) { test(`${source} leaves the type key to the collection`, async () => { const type = matter(await readFile(new URL(source, root), "utf8")).data; const schema = type.schema.value; diff --git a/types/daily-note/1.md b/types/daily-note/1.md new file mode 100644 index 0000000..b3250d2 --- /dev/null +++ b/types/daily-note/1.md @@ -0,0 +1,78 @@ +--- +kind: mdbase.type +name: daily-note +version: 1 +description: One note per calendar day, named by its date in the Daily folder. +match: + path_glob: 'Daily/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].md' +schema: + dialect: json-schema-2020-12 + value: + $schema: https://json-schema.org/draft/2020-12/schema + title: Daily note + description: One local calendar day represented by an ordinary note. The date is stored in the file name, not in frontmatter. + type: object + required: + - date + properties: + date: + type: string + format: date + description: The day this note is for, as a full calendar date in YYYY-MM-DD form, with no time or time zone. It is stored in the file name (Daily/2026-10-30.md), so changing it renames the note; do not also write it in frontmatter. + additionalProperties: true +collection: + path: + pattern: 'Daily/{date}.md' + fields: + - date + unique: + - field: date + scope: type + display: + name_field: date + icon: calendar +implements: + - contract: mdbase.daily-note + version: 1.0.0 + fields: + date: date +--- + +# Daily note + +One note per day, at `Daily/YYYY-MM-DD.md`. The date lives only in the file +name: apps read it from the path, and creating a note for a date puts it at +that date's path. The rest of the note is yours: write in the body and add any +frontmatter fields you like. + +```text +Daily/2026-10-30.md +``` + +Only files in `Daily/` whose name is a date belong to this type, so other notes +in the folder are left alone. A collection should have one note per date; two +notes with the same date are reported, and apps treat that day as ambiguous +rather than choosing one. + +## Use an existing daily-notes folder + +To adopt notes another tool already writes, such as Obsidian's daily notes, +change the folder in both `match.path_glob` and `collection.path.pattern`. For +notes in `Journal/`: + +```yaml +match: + path_glob: 'Journal/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].md' +collection: + path: + pattern: 'Journal/{date}.md' + fields: [date] +``` + +Notes are recognised by their path, so existing files need no frontmatter +changes. File names must use the `YYYY-MM-DD` form. + +This type belongs to your collection once installed. Apps find daily notes +through the `mdbase.daily-note` contract, not through this folder or file name, +so you can move them, or add other types that implement the contract, as long +as each date has one note.