From 47c587379e084f9bda50c15820c8591fbe39ce8f Mon Sep 17 00:00:00 2001 From: callumalpass Date: Mon, 28 Sep 2026 15:07:40 +1000 Subject: [PATCH] Align generated types with the published starter (rc.15) - TASKNOTES_MDBASE_TYPE_VERSION (4) is the generated type version, equal to the published starter's, so types written by TaskNotes and by the pack agree. - assignees is never legacy-nullable: v0.2 collections cannot contain it, and a nullable variant conflicted with the starter on pack upgrade. - The starter publishes the first starter's generator bookkeeping unchanged (TASKNOTES_STARTER_MANAGED_FIELDS) and a starter revision (5); it reproduces tasknotes.task pack rc.17 byte for byte. Co-Authored-By: Claude --- package-lock.json | 4 +-- package.json | 2 +- src/mdbase.ts | 14 +++++++--- src/starter.ts | 57 ++++++++++++++++++++++++++++++++++++++- test/assignments.test.mjs | 2 +- test/starter.test.mjs | 22 ++++++++++----- 6 files changed, 87 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index e8cc085..274a6ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tasknotes/model", - "version": "0.3.0-rc.14", + "version": "0.3.0-rc.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tasknotes/model", - "version": "0.3.0-rc.14", + "version": "0.3.0-rc.15", "license": "MIT", "dependencies": { "rrule": "^2.8.1", diff --git a/package.json b/package.json index 27ff9fc..9fd3579 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tasknotes/model", - "version": "0.3.0-rc.14", + "version": "0.3.0-rc.15", "description": "TaskNotes model, mapping, validation, recurrence, and operation-planning reference implementation.", "license": "MIT", "type": "module", diff --git a/src/mdbase.ts b/src/mdbase.ts index c7a1b3f..39833ff 100644 --- a/src/mdbase.ts +++ b/src/mdbase.ts @@ -133,8 +133,15 @@ export interface TaskNotesMdbaseTypePack { provides: Array<{ id: "tasknotes.task"; version: string; digest: string }>; } +/** + * Version of the generated implementing type. It equals the published starter + * type's version, so types written by TaskNotes and by the tasknotes.task pack + * agree and pack upgrades do not see a conflicting version change. + */ +export const TASKNOTES_MDBASE_TYPE_VERSION = 4; + /** Version of the generated definition bundle, independent of contract version. */ -export const TASKNOTES_MDBASE_PACK_VERSION = "0.3.0-rc.16"; +export const TASKNOTES_MDBASE_PACK_VERSION = "0.3.0-rc.17"; export const TASKNOTES_CONTRACT_DIGEST = "sha256:dde71d0d776aa0069ee627fe3c10b1a20f87ba36929a16790598ebfb4e8ebf04"; @@ -482,7 +489,8 @@ export function buildTaskNotesMdbaseResources( "assignees", mapping.assignees, { type: "array", items: { type: "string", minLength: 1, pattern: "\\S" }, uniqueItems: true }, - { links: [{ suffix: "[]" }] } + // Not a v0.2 field, so no legacy nulls to accept; matches the published starter. + { links: [{ suffix: "[]" }], legacyNullable: false } ); addField( "attachments", @@ -739,7 +747,7 @@ export function buildTaskNotesMdbaseResources( const type: Record = { kind: "mdbase.type", name: typeName, - version: 3, + version: TASKNOTES_MDBASE_TYPE_VERSION, description: "A task managed by TaskNotes.", match: buildMatch(modelConfig), schema: { diff --git a/src/starter.ts b/src/starter.ts index 7bef75f..56edc89 100644 --- a/src/starter.ts +++ b/src/starter.ts @@ -2,6 +2,7 @@ import { cloneDefaultModelConfig } from "./defaults"; import { serializeMarkdownDocument } from "./frontmatter"; import { buildTaskNotesMdbaseResources, + TASKNOTES_MDBASE_TYPE_VERSION, type TaskNotesMdbaseOptions, type TaskNotesMdbaseResources, } from "./mdbase"; @@ -19,7 +20,57 @@ import { TASKNOTES_SPEC_VERSION, type TaskNotesModelConfig } from "./types"; * here is a new starter version and must be deliberate. The test suite checks * this output against the published type. */ -export const TASKNOTES_STARTER_TYPE_VERSION = 4; +export const TASKNOTES_STARTER_TYPE_VERSION = TASKNOTES_MDBASE_TYPE_VERSION; + +/** + * Revision of the published starter document (mdbase-contracts + * `types/tasknotes-task/.md`). A revision that does not change the + * type's data keeps TASKNOTES_STARTER_TYPE_VERSION, so collections that already + * have that version do not see a conflicting version change on upgrade. + */ +export const TASKNOTES_STARTER_REVISION = 5; + +/** + * The generator bookkeeping published in the first starter. Collections keep + * their own list (it follows their field mapping), so pack upgrades must not + * change it: the starter always publishes this list. + */ +export const TASKNOTES_STARTER_MANAGED_FIELDS: readonly string[] = [ + "attachments", + "blockedBy", + "complete_instances", + "completedDate", + "contexts", + "dateCreated", + "dateModified", + "due", + "googleCalendarEventId", + "googleCalendarExceptionEventId", + "googleCalendarExceptionOriginalScheduled", + "googleCalendarMovedOriginalDates", + "icsEventId", + "id", + "occurrence_date", + "occurrence_future_horizon", + "occurrence_materialization", + "occurrence_next_trigger", + "occurrence_past_horizon", + "occurrence_template", + "priority", + "projects", + "recurrence", + "recurrence_anchor", + "recurrence_parent", + "reminders", + "scheduled", + "skipped_instances", + "status", + "tags", + "tasknotes_manual_order", + "timeEntries", + "timeEstimate", + "title", +]; export const TASKNOTES_STARTER_PROFILES = [ "core-lite", @@ -87,6 +138,10 @@ export function buildTaskNotesStarterResources( throw new Error(`The generated type does not implement tasknotes.task ${TASKNOTES_SPEC_VERSION}.`); } type.version = TASKNOTES_STARTER_TYPE_VERSION; + type["x-tasknotes-generator"] = { + ...type["x-tasknotes-generator"], + managed_fields: [...TASKNOTES_STARTER_MANAGED_FIELDS], + }; // One shared date schema, so due and scheduled cannot drift apart. const taskDateSchema = { anyOf: [ diff --git a/test/assignments.test.mjs b/test/assignments.test.mjs index 095851e..6ec6ee0 100644 --- a/test/assignments.test.mjs +++ b/test/assignments.test.mjs @@ -21,7 +21,7 @@ test("ordinary task updates retain unresolved assignments and explicitly clear t test("the task contract declares assignments as links resolved by the collection", () => { const resources = buildTaskNotesMdbaseResources(); assert.equal(resources.contract.version, "0.3.0-rc.5"); - assert.equal(resources.type.version, 3); + assert.equal(resources.type.version, 4); assert.equal(resources.taskSchema.properties.assignees.uniqueItems, true); assert.equal(resources.type.implements[0].fields.assignees, "assignees"); // No target_type: several local types may implement mdbase.person. diff --git a/test/starter.test.mjs b/test/starter.test.mjs index 863b681..33ae4b6 100644 --- a/test/starter.test.mjs +++ b/test/starter.test.mjs @@ -1,21 +1,21 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { buildTaskNotesMdbaseTypePack } from "../dist/esm/mdbase.js"; +import { buildTaskNotesMdbaseResources, buildTaskNotesMdbaseTypePack } from "../dist/esm/mdbase.js"; import { buildTaskNotesStarterResources, TASKNOTES_STARTER_TYPE_VERSION, } from "../dist/esm/starter.js"; -// The published tasknotes.task 0.3.0-rc.16 pack (mdbase-contracts 9cda221e, -// https://mdbase.dev/contracts/packs/tasknotes.task/0.3.0-rc.16.json). If the +// The published tasknotes.task 0.3.0-rc.17 pack +// (https://mdbase.dev/contracts/packs/tasknotes.task/0.3.0-rc.17.json). If the // starter changes, publish a new starter type version in mdbase-contracts and // update these digests together; collections upgrade by merging against the // previous starter, so an unintended change here reaches every collection. -const PUBLISHED_RC16_RESOURCES = { +const PUBLISHED_RC17_RESOURCES = { "_contracts/tasknotes.task.md": "sha256:ecadd00700e249940f4244aaa5b5985a96557d13340463ec491dfa5e89b032a4", "_types/task.md": - "sha256:2b6016c3543c0dfff491cc6262aed3afd4d672360484174042a261acd1d8ff5f", + "sha256:b3cf8dbe8885b331c494d2eeb36abfb052890e06b757877d655c269e666487dc", "_schemas/tasknotes/tasknotes-task.schema.json": "sha256:aec95578a093be95cbce0fb500964e7aeaeeacc611624fc2893074637dbd4125", "_schemas/tasknotes/tasknotes-task-binding.schema.json": @@ -26,7 +26,7 @@ test("the starter reproduces the published TaskNotes pack byte for byte", async const pack = await buildTaskNotesMdbaseTypePack(buildTaskNotesStarterResources()); assert.deepEqual( Object.fromEntries(pack.manifest.resources.map((resource) => [resource.target, resource.digest])), - PUBLISHED_RC16_RESOURCES, + PUBLISHED_RC17_RESOURCES, ); }); @@ -39,3 +39,13 @@ test("the starter keeps cancelled as a skipped status and declares only implemen assert.equal(binding.status.default_skipped, "cancelled"); assert.deepEqual(binding.profiles, ["core-lite", "recurrence", "materialized-occurrences"]); }); + +test("types written for migrated v0.2 collections declare assignees as the starter does", () => { + const starter = buildTaskNotesStarterResources().type; + const legacy = buildTaskNotesMdbaseResources({ legacyCompatibility: true }).type; + assert.equal(legacy.version, starter.version); + assert.deepEqual( + legacy.schema.value.properties.assignees, + starter.schema.value.properties.assignees, + ); +});