diff --git a/README.md b/README.md index 2cc941d..0a71ffa 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ The package build emits ESM, CommonJS, and TypeScript declaration output under ` The conformance adapter reports its claimed TaskNotes profile and implements the official `core-lite` operation surface. Its fixture run currently passes 4,974 cases with zero failures; the extended-profile fixture remains outside -that claim. The mdbase generator emits the `tasknotes.task 0.2.0` contract, +that claim. The mdbase generator emits the `tasknotes.task 0.3.0-rc.1` record contract, its two JSON Schemas, and a type whose `implements` entry contains field mappings and TaskNotes behavior. Round-trip tests resolve those resources back into model configuration so filesystem-backed hosts can treat the type as a diff --git a/package-lock.json b/package-lock.json index 311eb11..95f2dfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tasknotes/model", - "version": "0.3.0-rc.3", + "version": "0.3.0-rc.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tasknotes/model", - "version": "0.3.0-rc.3", + "version": "0.3.0-rc.4", "license": "MIT", "dependencies": { "@callumalpass/mdbase-runtime": "0.1.0-rc.1", diff --git a/package.json b/package.json index 11172ab..6d73118 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tasknotes/model", - "version": "0.3.0-rc.3", + "version": "0.3.0-rc.4", "description": "TaskNotes model, mapping, validation, recurrence, and operation-planning reference implementation.", "license": "MIT", "type": "module", diff --git a/scripts/sync-tasknotes-contract.mjs b/scripts/sync-tasknotes-contract.mjs index f98df78..a07285c 100644 --- a/scripts/sync-tasknotes-contract.mjs +++ b/scripts/sync-tasknotes-contract.mjs @@ -9,8 +9,12 @@ const generatedPath = path.resolve( "../src/generated/tasknotes-data-contract.ts" ); -const [taskSchema, bindingSchema] = await Promise.all( - ["tasknotes-task.schema.json", "tasknotes-task-binding.schema.json"].map( +const [taskSchema, bindingSchema, completedEventSchema] = await Promise.all( + [ + "tasknotes-task.schema.json", + "tasknotes-task-binding.schema.json", + "tasknotes-task-completed.schema.json", + ].map( async (name) => JSON.parse( await readFile(path.join(specRoot, "schemas", name), "utf8") @@ -24,6 +28,8 @@ const source = `// Generated by scripts/sync-tasknotes-contract.mjs from tasknot export const TASKNOTES_TASK_SCHEMA = ${JSON.stringify(taskSchema, null, "\t")} as const; export const TASKNOTES_TASK_BINDING_SCHEMA = ${JSON.stringify(bindingSchema, null, "\t")} as const; + +export const TASKNOTES_TASK_COMPLETED_EVENT_SCHEMA = ${JSON.stringify(completedEventSchema, null, "\t")} as const; `; await mkdir(path.dirname(generatedPath), { recursive: true }); diff --git a/src/generated/tasknotes-data-contract.ts b/src/generated/tasknotes-data-contract.ts index 4b2c0ff..dea06dc 100644 --- a/src/generated/tasknotes-data-contract.ts +++ b/src/generated/tasknotes-data-contract.ts @@ -5,7 +5,7 @@ export const TASKNOTES_TASK_SCHEMA = { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://tasknotes.dev/schemas/tasknotes-task.schema.json", "title": "TaskNotes portable task view", - "description": "The storage-neutral record view exposed by the tasknotes.task 0.2.0 data contract.", + "description": "The storage-neutral record view exposed by the tasknotes.task 0.3.0-rc.1 record contract.", "type": "object", "required": [ "status", @@ -174,7 +174,7 @@ export const TASKNOTES_TASK_BINDING_SCHEMA = { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://tasknotes.dev/schemas/tasknotes-task-binding.schema.json", "title": "TaskNotes task data-contract binding", - "description": "Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.2.0.", + "description": "Semantic configuration supplied by an mdbase type that implements tasknotes.task 0.3.0-rc.1.", "type": "object", "required": [ "profiles", @@ -642,3 +642,45 @@ export const TASKNOTES_TASK_BINDING_SCHEMA = { } } } as const; + +export const TASKNOTES_TASK_COMPLETED_EVENT_SCHEMA = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://tasknotes.dev/schemas/tasknotes-task-completed.schema.json", + "title": "TaskNotes task-completed event data", + "description": "The minimal portable signal emitted when a task enters a completed status.", + "type": "object", + "required": [ + "task_id", + "task_path", + "title", + "status", + "completed_at" + ], + "additionalProperties": false, + "properties": { + "task_id": { + "type": "string", + "minLength": 1, + "description": "Stable task identity when available, otherwise the normalized task path." + }, + "task_path": { + "type": "string", + "minLength": 1, + "description": "Normalized vault-relative task path at the time of the transition." + }, + "title": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "minLength": 1, + "description": "The completed status entered by the task." + }, + "completed_at": { + "type": "string", + "format": "date-time", + "description": "When TaskNotes observed the completion transition." + } + } +} as const; diff --git a/src/mdbase.ts b/src/mdbase.ts index 282e2ce..0630494 100644 --- a/src/mdbase.ts +++ b/src/mdbase.ts @@ -2,6 +2,7 @@ import YAML from "yaml"; import { resolveModelConfig } from "./config"; import { TASKNOTES_TASK_BINDING_SCHEMA, + TASKNOTES_TASK_COMPLETED_EVENT_SCHEMA, TASKNOTES_TASK_SCHEMA, } from "./generated/tasknotes-data-contract"; import { TASKNOTES_SPEC_VERSION } from "./types"; @@ -14,6 +15,22 @@ import type { } from "./types"; export const MDBASE_SPEC_VERSION = "0.3.0"; +export const TASKNOTES_TASK_CONTRACT_VERSION = TASKNOTES_SPEC_VERSION; +export const TASKNOTES_TASK_COMPLETED_EVENT_VERSION = "1.0.0"; + +export const TASKNOTES_TASK_COMPLETED_EVENT_CONTRACT = { + kind: "mdbase.contract", + contract_type: "event", + id: "tasknotes.task.completed", + version: TASKNOTES_TASK_COMPLETED_EVENT_VERSION, + name: "TaskNotes task completed", + description: + "A TaskNotes task moved from a non-completed status to a completed status.", + data_schema: { + dialect: "json-schema-2020-12", + value: TASKNOTES_TASK_COMPLETED_EVENT_SCHEMA, + }, +} as const; export const DEFAULT_TASKNOTES_MDBASE_PROFILES = [ "core-lite", @@ -727,11 +744,12 @@ export function buildTaskNotesMdbaseResources( ) as Record; const contract: Record = { kind: "mdbase.contract", + contract_type: "record", id: "tasknotes.task", version: TASKNOTES_SPEC_VERSION, name: "TaskNotes task", description: `Portable task data and behavior defined by tasknotes-spec ${TASKNOTES_SPEC_VERSION}.`, - schema: { + record_schema: { dialect: "json-schema-2020-12", ref: relativeResourceReference( `${contractsFolder}/tasknotes.task.md`, diff --git a/src/types.ts b/src/types.ts index 9be9957..737e544 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -export const TASKNOTES_SPEC_VERSION = "0.2.0"; +export const TASKNOTES_SPEC_VERSION = "0.3.0-rc.1"; export type JsonPrimitive = string | number | boolean | null; export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]; diff --git a/test/mdbase.test.mjs b/test/mdbase.test.mjs index d2e46e8..13e89cf 100644 --- a/test/mdbase.test.mjs +++ b/test/mdbase.test.mjs @@ -11,7 +11,7 @@ import { function implementation(type) { return type.implements.find( (entry) => - entry.contract === "tasknotes.task" && entry.version === "0.2.0" + entry.contract === "tasknotes.task" && entry.version === "0.3.0-rc.1" ); } @@ -29,9 +29,11 @@ test("builds one canonical TaskNotes and mdbase collection contract", () => { assert.equal(resources.config.spec_version, "0.3.0"); assert.equal(resources.config.settings.contracts_folder, "_contracts"); assert.equal(resources.contract.id, "tasknotes.task"); - assert.equal(resources.contract.version, "0.2.0"); + assert.equal(resources.contract.contract_type, "record"); + assert.equal(resources.contract.version, "0.3.0-rc.1"); + assert.ok(resources.contract.record_schema); assert.equal(taskImplementation.contract, "tasknotes.task"); - assert.equal(taskImplementation.version, "0.2.0"); + assert.equal(taskImplementation.version, "0.3.0-rc.1"); assert.deepEqual(extension.profiles, [ "core-lite", "recurrence", @@ -75,7 +77,7 @@ test("packages the contract, implementation, and schemas as one digest-pinned ty const pack = await buildTaskNotesMdbaseTypePack(resources); assert.deepEqual(pack.provides, [ - { id: "tasknotes.task", version: "0.2.0" }, + { id: "tasknotes.task", version: "0.3.0-rc.1" }, ]); assert.equal(pack.manifest.kind, "mdbase.type-pack"); assert.equal(pack.manifest.id, "tasknotes.task");