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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,27 @@ Starter files are named by revision (`types/tasknotes-task/<revision>.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.

## Retained contracts

A type implements one exact contract version, and a type outside a pack's
targets (a renamed or customised starter, or a collection's own type) is never
upgraded by that pack. A pack that stops installing a contract an earlier
version installed leaves such a type invalid, and engines then refuse the whole
upgrade. The catalog therefore keeps a second invariant, checked by
`scripts/contract-retention.test.mjs`: every offered (non-hidden) pack installs
every contract document that any earlier version of the pack shipped,
including unlisted versions.

rc.12 to rc.18 of the TaskNotes pack managed one contract at
`_contracts/tasknotes.task.md` and replaced it in place: rc.3 (rc.12), rc.4
(rc.13, rc.14), then rc.5. rc.19 kept rc.5 there but not the two before it, so
a collection last set up by rc.12 to rc.14 whose task type is not the starter
at `_types/task.md` could not be upgraded at all. rc.20 is rc.19 plus rc.3 and
rc.4 at `_contracts/tasknotes.task/<version>.md`, neither provided; its starter
and baselines are unchanged, and rc.19 is hidden. Such a type stays valid on
the contract it implements. TaskNotes reads rc.6 and rc.5 types only, so it
still needs a type on one of those.

The starter is defined by `@tasknotes/model/starter`, which reproduces the
published starter byte for byte; `scripts/sync-tasknotes-pack.mjs` generates it
from there. The upgrade tests check that a new starter changes nothing beyond
Expand Down
62 changes: 62 additions & 0 deletions dist/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,16 @@
"artifact": "./artifacts/contracts/tasknotes.task/0.3.0-rc.3.md",
"standards": []
},
{
"id": "tasknotes.task",
"version": "0.3.0-rc.4",
"name": "TaskNotes task",
"description": "Portable task data and behavior defined by tasknotes-spec 0.3.0-rc.4.",
"contract_type": "record",
"digest": "sha256:91165b7664f936bba9289aca984e2638901db7fedf68642ab755e8d0b424e734",
"artifact": "./artifacts/contracts/tasknotes.task/0.3.0-rc.4.md",
"standards": []
},
{
"id": "tasknotes.task",
"version": "0.3.0-rc.5",
Expand Down Expand Up @@ -993,6 +1003,58 @@
"TaskNotes compatible"
]
},
"installation": {
"visibility": "hidden",
"recommendation": "integration-managed",
"primary_type": "task",
"caution": "Upgrades the previous starter type while preserving independent customizations. Conflicts block the whole upgrade. TaskNotes moves settings an edited type still holds into its settings record when it upgrades that type.",
"types": [
{
"name": "task",
"label": "Task"
},
{
"name": "tasknotes-settings",
"label": "Tasknotes settings"
}
]
}
},
{
"id": "tasknotes.task",
"version": "0.3.0-rc.20",
"name": "TaskNotes task type pack",
"description": "Portable tasks whose app behaviour (templates, archive moves, status cycling, time tracking and link format) lives in a TaskNotes settings record, with a reviewed, atomic upgrade from every TaskNotes starter published before.",
"digest": "sha256:59c932775e37782d0ea6e152f2534bd77f535f91ad1edd6b6b7471c2213d0995",
"provision": "./packs/tasknotes.task/0.3.0-rc.20.json",
"provides": [
{
"id": "tasknotes.task",
"version": "0.3.0-rc.6",
"digest": "sha256:c400ead6e218b715d86ad31125bb3d4b2e276817da6ba8065449fd0947b24d67"
},
{
"id": "tasknotes.settings",
"version": "1.0.0",
"digest": "sha256:a61d55535871d27e9599cb3588148a0590d9e2198d6ee7a0bf3197350b60dc53"
},
{
"id": "mdbase.app-settings",
"version": "1.0.0",
"digest": "sha256:4ae872a56290e2739ff876df1ca8fdac428753c854d65fd8e8e62045bd643a4e"
}
],
"resource_count": 12,
"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",
Expand Down
189 changes: 189 additions & 0 deletions dist/packs/tasknotes.task/0.3.0-rc.20.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/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"
"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/contract-retention.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",
Expand Down
2 changes: 1 addition & 1 deletion packs/tasknotes.task/0.3.0-rc.19.pack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 block the whole upgrade. TaskNotes moves settings an edited type still holds into its settings record when it upgrades that type.
Expand Down
88 changes: 88 additions & 0 deletions packs/tasknotes.task/0.3.0-rc.20.pack.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
kind: mdbase.catalog-pack
id: tasknotes.task
version: 0.3.0-rc.20
name: TaskNotes task type pack
description: Portable tasks whose app behaviour (templates, archive moves, status cycling, time tracking and link format) lives in a TaskNotes settings record, 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 block the whole upgrade. TaskNotes moves settings an edited type still holds into its settings record when it upgrades that type.
provides:
- id: tasknotes.task
version: 0.3.0-rc.6
digest: sha256:c400ead6e218b715d86ad31125bb3d4b2e276817da6ba8065449fd0947b24d67
- id: tasknotes.settings
version: 1.0.0
- id: mdbase.app-settings
version: 1.0.0
resources:
# Not provided for new installs: every tasknotes.task contract an earlier
# pack installed is retained, so a task type that still implements one
# (customised, renamed, or written by TaskNotes before it upgrades it) keeps
# the exact contract it implements. rc.5 stays where rc.15 to rc.19 managed
# it; rc.3 (rc.12) and rc.4 (rc.13, rc.14) were replaced in place at that
# target and return at versioned ones.
- kind: contract
mode: managed
source: contracts/tasknotes.task/0.3.0-rc.5.md
target: _contracts/tasknotes.task.md
- kind: contract
mode: managed
source: contracts/tasknotes.task/0.3.0-rc.4.md
target: _contracts/tasknotes.task/0.3.0-rc.4.md
- kind: contract
mode: managed
source: contracts/tasknotes.task/0.3.0-rc.3.md
target: _contracts/tasknotes.task/0.3.0-rc.3.md
- kind: contract
mode: managed
source: contracts/tasknotes.task/0.3.0-rc.6.md
target: _contracts/tasknotes.task/0.3.0-rc.6.md
- kind: type
mode: seed
source: types/tasknotes-task/6.md
target: _types/task.md
# Every starter an earlier tasknotes.task pack seeded at _types/task.md.
upgrade_from:
- types/tasknotes-task/5.md
- 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.6.schema.json
target: _schemas/tasknotes/tasknotes-task.schema.json
- kind: schema
mode: managed
source: schemas/tasknotes.task.binding/0.3.0-rc.6.schema.json
target: _schemas/tasknotes/tasknotes-task-binding.schema.json
- kind: schema
mode: managed
source: schemas/mdbase.app-settings/1.0.0.schema.json
target: schemas/mdbase.app-settings/1.0.0.schema.json
- kind: contract
mode: managed
source: contracts/mdbase.app-settings/1.0.0.md
target: _contracts/mdbase.app-settings/1.0.0.md
- kind: schema
mode: managed
source: schemas/tasknotes.settings/1.0.0.schema.json
target: schemas/tasknotes.settings/1.0.0.schema.json
- kind: contract
mode: managed
source: contracts/tasknotes.settings/1.0.0.md
target: _contracts/tasknotes.settings/1.0.0.md
- kind: type
mode: seed
source: types/tasknotes-settings/1.md
target: _types/tasknotes-settings.md
33 changes: 33 additions & 0 deletions scripts/contract-retention.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
import { compareVersions, publishedProvisions } from "./pack-versions.mjs";

// A type implements one exact contract version. A collection set up by any
// earlier version of a pack may hold types that still implement a contract
// that version installed: a customised or renamed starter, or a type of the
// collection's own. If the offered pack no longer installs that contract, the
// type is left invalid and engines refuse the whole upgrade. So the offered
// pack must keep installing every contract document an earlier version shipped.

const dist = new URL("../dist/", import.meta.url);
const catalog = JSON.parse(await readFile(new URL("catalog.json", dist), "utf8"));
const contracts = (provision) => provision.manifest.resources.filter(({ kind }) => kind === "contract");

const all = await publishedProvisions();
const offered = catalog.packs.filter(({ installation }) => installation.visibility !== "hidden");

for (const entry of offered) {
test(`${entry.id} ${entry.version} keeps every contract an earlier version installed`, () => {
const provision = all.get(entry.id).find(({ manifest }) => manifest.version === entry.version);
const kept = new Set(contracts(provision).map(({ digest }) => digest));
const missing = new Set();
for (const earlier of all.get(entry.id)) {
if (compareVersions(earlier.manifest.version, entry.version) >= 0) continue;
for (const shipped of contracts(earlier)) {
if (!kept.has(shipped.digest)) missing.add(`${shipped.source} (${earlier.manifest.version})`);
}
}
assert.deepEqual([...missing], [], `${entry.id} ${entry.version} must also install ${[...missing].join(", ")}`);
});
}
2 changes: 1 addition & 1 deletion scripts/tasknotes-plugin-collections.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.19";
const LATEST = "0.3.0-rc.20";
const fixtures = new URL("./fixtures/tasknotes-5.0/", import.meta.url);

async function reviewedInstall(engine) {
Expand Down
16 changes: 15 additions & 1 deletion scripts/tasknotes-upgrade.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const targets = [
// rc.19 (tasknotes.task rc.6) moves app behaviour out of the binding into
// the TaskNotes settings record; nothing else in the starter changes.
{ pack: "0.3.0-rc.19", contract: /0\.3\.0-rc\.6/, links: /assignees\[\]:/, minimal: true, customMapping: true, settingsRecord: true },
// rc.20 is rc.19 plus the tasknotes.task contracts earlier packs replaced
// in place (rc.3 and rc.4), so a type outside the pack that still implements
// one no longer blocks the upgrade.
{ pack: "0.3.0-rc.20", contract: /0\.3\.0-rc\.6/, links: /assignees\[\]:/, minimal: true, customMapping: true, settingsRecord: true, retainsContracts: true },
];

// The binding settings tasknotes.task rc.6 moved to the TaskNotes settings record.
Expand Down Expand Up @@ -123,13 +127,23 @@ for (const target of targets) for (const scenario of ["managed", "customized", "
// rc.19 changes the starter's status definitions (their behaviour moved
// to the settings record); a collection that edited only their colours
// keeps them, as the lists merge item by item.
const success = !["conflict", "other-reference"].includes(scenario);
// A pack that replaces the contract another type implements leaves that
// type invalid, so the whole upgrade is refused; rc.20 keeps the contract.
const success = scenario !== "conflict" && (scenario !== "other-reference" || target.retainsContracts === true);
assert.equal(result.valid, success, JSON.stringify(result));
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);
} else {
const upgraded = await readFile(typePath, "utf8");
if (scenario === "other-reference") {
// The other type and the exact contract it implements are untouched.
assert.equal(await readFile(join(root, "_types/other.md"), "utf8"), oldType.replace("name: task", "name: other"));
assert.equal(
await readFile(join(root, "_contracts/tasknotes.task/0.3.0-rc.3.md"), "utf8"),
new Map(before).get("_contracts/tasknotes.task.md"),
);
}
assert.match(upgraded, /assignees/);
assert.match(upgraded, target.contract);
if (target.links) assert.match(upgraded, target.links);
Expand Down
Loading