Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The package exports both the root module and focused subpath modules:
| `@tasknotes/model/operations` | Host-independent task mutation planning, including materialized occurrence plans |
| `@tasknotes/model/frontmatter` | Markdown task document parse/serialize helpers |
| `@tasknotes/model/mdbase` | Canonical mdbase v0.3 config, contract, schema, and implementing-type generation |
| `@tasknotes/model/starter` | The published TaskNotes starter type (`tasknotes.task` pack seed), pinned to mdbase-contracts by the test suite. Use it, not the plugin defaults in `defaults`, to build starter types |
| `@tasknotes/model/conformance` | tasknotes-spec conformance operation dispatcher |

## Examples
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tasknotes/model",
"version": "0.3.0-rc.13",
"version": "0.3.0-rc.14",
"description": "TaskNotes model, mapping, validation, recurrence, and operation-planning reference implementation.",
"license": "MIT",
"type": "module",
Expand Down Expand Up @@ -96,6 +96,11 @@
"import": "./dist/esm/mdbase.js",
"require": "./dist/cjs/mdbase.cjs"
},
"./starter": {
"types": "./dist/types/starter.d.ts",
"import": "./dist/esm/starter.js",
"require": "./dist/cjs/starter.cjs"
},
"./conformance": {
"types": "./dist/types/conformance.d.ts",
"import": "./dist/esm/conformance.js",
Expand Down
1 change: 1 addition & 0 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const entries = [
"operations",
"frontmatter",
"mdbase",
"starter",
"conformance",
];

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export * from "./validation";
export * from "./operations";
export * from "./frontmatter";
export * from "./mdbase";
export * from "./starter";
export * from "./conformance";
2 changes: 1 addition & 1 deletion src/mdbase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export interface TaskNotesMdbaseTypePack {
}

/** Version of the generated definition bundle, independent of contract version. */
export const TASKNOTES_MDBASE_PACK_VERSION = "0.3.0-rc.15";
export const TASKNOTES_MDBASE_PACK_VERSION = "0.3.0-rc.16";
export const TASKNOTES_CONTRACT_DIGEST =
"sha256:dde71d0d776aa0069ee627fe3c10b1a20f87ba36929a16790598ebfb4e8ebf04";

Expand Down
110 changes: 110 additions & 0 deletions src/starter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { cloneDefaultModelConfig } from "./defaults";
import { serializeMarkdownDocument } from "./frontmatter";
import {
buildTaskNotesMdbaseResources,
type TaskNotesMdbaseOptions,
type TaskNotesMdbaseResources,
} from "./mdbase";
import { TASKNOTES_SPEC_VERSION, type TaskNotesModelConfig } from "./types";

/**
* The published TaskNotes starter: the `tasknotes.task` pack's seed type that
* applications install into new collections (mdbase-contracts
* `types/tasknotes-task/<TASKNOTES_STARTER_TYPE_VERSION>.md`).
*
* It differs from the plugin defaults in `DEFAULT_STATUSES`: it adds the
* skipped `cancelled` status, uses its own palette and declares only the
* profiles its binding implements. Collections keep whatever they customise,
* because pack upgrades merge against the previous starter; so any change
* 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_PROFILES = [
"core-lite",
"recurrence",
"materialized-occurrences",
] as const;

export const TASKNOTES_STARTER_STATUS_COLORS: Readonly<Record<string, string>> = {
none: "#94a3b8",
open: "#64748b",
"in-progress": "#3b82f6",
done: "#22c55e",
cancelled: "#94a3b8",
};

export const TASKNOTES_STARTER_PRIORITY_COLORS: Readonly<Record<string, string>> = {
none: "#94a3b8",
low: "#3b82f6",
normal: "#f59e0b",
high: "#ef4444",
};

/** Model configuration that the published starter type is generated from. */
export function cloneTaskNotesStarterModelConfig(): TaskNotesModelConfig {
const modelConfig = cloneDefaultModelConfig();
modelConfig.statuses = modelConfig.statuses.map((status) => ({
...status,
color: TASKNOTES_STARTER_STATUS_COLORS[status.value] ?? status.color,
}));
modelConfig.priorities = modelConfig.priorities.map((priority) => ({
...priority,
color: TASKNOTES_STARTER_PRIORITY_COLORS[priority.value] ?? priority.color,
}));
modelConfig.statuses.push({
id: "cancelled",
value: "cancelled",
label: "Cancelled",
color: TASKNOTES_STARTER_STATUS_COLORS.cancelled,
isCompleted: false,
isSkipped: true,
excludeFromCycle: true,
order: modelConfig.statuses.length,
autoArchive: false,
autoArchiveDelay: 5,
});
return modelConfig;
}

/** The published starter's type, schemas and contract resources. */
export function buildTaskNotesStarterResources(
options: Omit<TaskNotesMdbaseOptions, "modelConfig" | "profiles"> = {},
): TaskNotesMdbaseResources {
const modelConfig = cloneTaskNotesStarterModelConfig();
const resources = buildTaskNotesMdbaseResources({
...options,
profiles: TASKNOTES_STARTER_PROFILES,
modelConfig,
});
const type = structuredClone(resources.type) as Record<string, any>;
const implementation = type.implements.find(
(candidate: { contract: string; version: string }) =>
candidate.contract === "tasknotes.task" && candidate.version === TASKNOTES_SPEC_VERSION,
);
if (!implementation) {
throw new Error(`The generated type does not implement tasknotes.task ${TASKNOTES_SPEC_VERSION}.`);
}
type.version = TASKNOTES_STARTER_TYPE_VERSION;
// One shared date schema, so due and scheduled cannot drift apart.
const taskDateSchema = {
anyOf: [
{ type: "string", format: "date" },
{ type: "string", format: "date-time" },
],
};
type.schema.value.properties[implementation.fields.due] = taskDateSchema;
type.schema.value.properties[implementation.fields.scheduled] = taskDateSchema;
implementation.binding.status = {
...implementation.binding.status,
skipped_values: ["cancelled"],
default_skipped: "cancelled",
};
const body = `# Task\n\nTask records live under \`${resources.paths.records}/\`.\n`;
return {
...resources,
type,
typeDocument: serializeMarkdownDocument(type, body),
};
}
41 changes: 41 additions & 0 deletions test/starter.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
import test from "node:test";
import { 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
// 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 = {
"_contracts/tasknotes.task.md":
"sha256:ecadd00700e249940f4244aaa5b5985a96557d13340463ec491dfa5e89b032a4",
"_types/task.md":
"sha256:2b6016c3543c0dfff491cc6262aed3afd4d672360484174042a261acd1d8ff5f",
"_schemas/tasknotes/tasknotes-task.schema.json":
"sha256:aec95578a093be95cbce0fb500964e7aeaeeacc611624fc2893074637dbd4125",
"_schemas/tasknotes/tasknotes-task-binding.schema.json":
"sha256:5b38e8a1c426b7f7fa92d8353fe35dd98e632305cbb15ffccde387473166b267",
};

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,
);
});

test("the starter keeps cancelled as a skipped status and declares only implemented profiles", () => {
const { type } = buildTaskNotesStarterResources();
const binding = type.implements.find((entry) => entry.contract === "tasknotes.task").binding;
assert.equal(type.version, TASKNOTES_STARTER_TYPE_VERSION);
assert.ok(type.schema.value.properties.status.enum.includes("cancelled"));
assert.deepEqual(binding.status.skipped_values, ["cancelled"]);
assert.equal(binding.status.default_skipped, "cancelled");
assert.deepEqual(binding.profiles, ["core-lite", "recurrence", "materialized-occurrences"]);
});
Loading