diff --git a/apps/writer/mdbase/baselines/1.0.0-beta.1/types/reader-source.md b/apps/writer/mdbase/baselines/1.0.0-beta.1/types/reader-source.md new file mode 100644 index 0000000..683c326 --- /dev/null +++ b/apps/writer/mdbase/baselines/1.0.0-beta.1/types/reader-source.md @@ -0,0 +1,100 @@ +--- +kind: mdbase.type +name: reader-source +version: 1 +description: A source and user-authored literature note for mdbase Reader. +schema: + dialect: json-schema-2020-12 + value: + $schema: https://json-schema.org/draft/2020-12/schema + type: object + additionalProperties: true + required: [type, id, title, kind, saved_at] + properties: + type: { const: reader-source } + id: { type: string, minLength: 1 } + title: { type: string, minLength: 1 } + kind: { type: string, minLength: 1 } + authors: + type: array + items: { type: string } + published: + oneOf: + - type: string + - type: number + url: { type: string } + description: { type: string } + language: { type: string } + saved_at: { type: string, format: date-time } + documents: + type: array + items: + type: object + additionalProperties: true + required: [file] + properties: + file_id: { type: string } + file: { type: string, minLength: 1 } + role: { type: string } + revision: { type: string, pattern: "^sha256:[0-9a-f]{64}$" } + format: { type: string } + media_type: { type: string } + label: { type: string } + reading: + type: object + additionalProperties: true + properties: + status: { type: string } + progress: { type: number, minimum: 0, maximum: 1 } + tags: + type: array + items: + type: [string, number, boolean] + relations: + type: array + items: + type: object + additionalProperties: true + required: [relation, target] + properties: + relation: { type: string } + target: { type: string } + csl: { type: object } +match: + path_glob: sources/**/*.md + fields_present: [id, title, kind] +collection: + unique: + - field: id + scope: type + links: + documents[].file: + target_type: any + validate_exists: true + relations[].target: + target_type: reader-source + validate_exists: false +implements: + - contract: dev.mdbase.reader.source + version: 1.0.0-beta.1 + fields: + id: id + title: title + kind: kind + authors: authors + published: published + url: url + description: description + language: language + saved_at: saved_at + documents: documents + reading: reading + tags: tags + relations: relations + csl: csl +--- + +# Reader source + +The source body is the user's literature note. Reader never regenerates it +from annotations. diff --git a/apps/writer/mdbase/baselines/1.0.0-beta.2/types/reader-source.md b/apps/writer/mdbase/baselines/1.0.0-beta.2/types/reader-source.md new file mode 100644 index 0000000..af30d3c --- /dev/null +++ b/apps/writer/mdbase/baselines/1.0.0-beta.2/types/reader-source.md @@ -0,0 +1,110 @@ +--- +kind: mdbase.type +name: reader-source +version: 1 +description: A source and user-authored literature note for mdbase Reader. +schema: + dialect: json-schema-2020-12 + value: + $schema: https://json-schema.org/draft/2020-12/schema + type: object + additionalProperties: true + required: [type, id, title, kind, saved_at] + properties: + type: { const: reader-source } + id: { type: string, minLength: 1 } + title: { type: string, minLength: 1 } + kind: { type: string, minLength: 1 } + authors: + type: array + items: { type: string } + published: + oneOf: + - type: string + - type: number + url: { type: string } + original_url: { type: string } + description: { type: string } + language: { type: string } + site: { type: string } + image: { type: string } + capture: + type: object + additionalProperties: true + saved_at: { type: string, format: date-time } + documents: + type: array + items: + type: object + additionalProperties: true + required: [file] + properties: + file_id: { type: string } + file: { type: string, minLength: 1 } + role: { type: string } + revision: { type: string, pattern: "^sha256:[0-9a-f]{64}$" } + format: { type: string } + media_type: { type: string } + label: { type: string } + reading: + type: object + additionalProperties: true + properties: + status: { type: string } + progress: { type: number, minimum: 0, maximum: 1 } + tags: + type: array + items: + type: [string, number, boolean] + relations: + type: array + items: + type: object + additionalProperties: true + required: [relation, target] + properties: + relation: { type: string } + target: { type: string } + csl: { type: object } +match: + path_glob: sources/**/*.md + fields_present: [id, title, kind] +collection: + unique: + - field: id + scope: type + links: + documents[].file: + target_type: any + validate_exists: true + relations[].target: + target_type: reader-source + validate_exists: false +implements: + - contract: dev.mdbase.reader.source + version: 1.0.0-beta.2 + fields: + id: id + title: title + kind: kind + authors: authors + published: published + url: url + original_url: original_url + description: description + language: language + site: site + image: image + capture: capture + saved_at: saved_at + documents: documents + reading: reading + tags: tags + relations: relations + csl: csl +--- + +# Reader source + +The source body is the user's literature note. Reader never regenerates it +from annotations. diff --git a/apps/writer/package.json b/apps/writer/package.json index 15333c4..9e413aa 100644 --- a/apps/writer/package.json +++ b/apps/writer/package.json @@ -31,8 +31,8 @@ "@codemirror/view": "^6.43.8", "@lezer/highlight": "^1.2.3", "@lezer/markdown": "^1.4.3", - "@mdbase-dev/connect": "0.1.0-beta.124", - "@mdbase-dev/ui": "0.1.0-beta.124", + "@mdbase-dev/connect": "0.1.0-beta.125", + "@mdbase-dev/ui": "0.1.0-beta.125", "@mdbase-writer/core": "workspace:*", "@myriaddreamin/typst-ts-renderer": "0.7.0", "@myriaddreamin/typst-ts-web-compiler": "0.7.0", @@ -44,8 +44,8 @@ }, "devDependencies": { "@callumalpass/mdbase": "0.3.0-rc.5", - "@mdbase-dev/connect-dev": "0.1.0-beta.124", - "@mdbase-dev/connect-testing": "0.1.0-beta.124", + "@mdbase-dev/connect-dev": "0.1.0-beta.125", + "@mdbase-dev/connect-testing": "0.1.0-beta.125", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", diff --git a/apps/writer/scripts/manifest.test.mjs b/apps/writer/scripts/manifest.test.mjs index 8a4f73a..8a3e6db 100644 --- a/apps/writer/scripts/manifest.test.mjs +++ b/apps/writer/scripts/manifest.test.mjs @@ -15,7 +15,7 @@ const root = resolve(import.meta.dirname, ".."); test("the Reader resources are byte-identical to Reader's own", { skip: !existsSync(resolve(root, "../../../mdbase-reader")) }, () => { for (const r of packResources.filter((p) => p.source.includes("reader"))) { - for (const source of [r.source, r.upgradeFrom].filter(Boolean)) { + for (const source of [r.source, ...(r.upgradeFrom ?? [])]) { const ours = readFileSync(resolve(root, "mdbase", source), "utf8"); const theirs = readFileSync(resolve(root, "../../../mdbase-reader/apps/reader/mdbase", source), "utf8"); assert.equal(ours, theirs, `${source} differs from Reader's copy`); @@ -43,14 +43,19 @@ test("identity is optional: comments are unsigned without it", async () => { // Published identities must not change. Add a new version/digest instead of updating this pin. // Pack digests are SHA-256 over the canonical (sorted-key) JSON of the whole pack manifest, -// `upgrade_from` included; beta.4's pin was confirmed with the mdbase CLI's `packs assess`. +// `upgrade_from` included; the beta.4 and beta.5 pins were confirmed with the mdbase CLI's `packs assess`. const releasedDigests = { "1.0.0-beta.4": "sha256:b8334c4a7b602b7a84737450c33a5de645f4c5c15e49f445dbfc568ec6542689", + "1.0.0-beta.5": "sha256:21dfd856f03330244d2675ef0dc983461adf6ad71262a570256c7dd706dcf4e4", }; -// Exact bytes of the version-1 reader-source starter released in Writer's beta.3 (and -// Reader's beta.3), which beta.4 upgrades from. -const beta3SeedDigests = { - "types/reader-source.md": "sha256:82869c12234662297cc273aab993732d35827b8a2ba8ad74bec18975cadf0c1a", +// Every earlier reader-source starter, newest first, by the Reader pack that first shipped it: +// Reader's (and Writer's) beta.3, Reader's beta.2, and Reader's beta.1 (Writer's beta.1 and beta.2). +const earlierStarters = { + "types/reader-source.md": [ + "sha256:82869c12234662297cc273aab993732d35827b8a2ba8ad74bec18975cadf0c1a", + "sha256:54f46e7c3bb70e1cea5a37afa458406a0acf2be7f195c8893160782d87254990", + "sha256:e8a3fca643d335e684810c2e83bc9e9c3fc702ecf5a23d29283615902e0cfa55", + ], }; function sha256(value) { @@ -88,16 +93,37 @@ test("the released Writer pack digest stays immutable and is independent of depl assert.deepEqual(staging.provisions.type_packs[0], pack); }); -test("the reader-source seed upgrades from the exact beta.3 starter", async () => { +test("the reader-source seed lists every earlier starter, newest first, as exact bytes", async () => { const pack = await writerPack(); + const documents = new Map(pack.resources.map(({ source, document }) => [source, document])); const seeds = pack.manifest.resources.filter((resource) => resource.upgrade_from); - assert.deepEqual(seeds.map((seed) => seed.source), Object.keys(beta3SeedDigests)); + assert.deepEqual(seeds.map((seed) => seed.source), Object.keys(earlierStarters)); for (const seed of seeds) { + assert.equal(seed.kind, "type"); assert.equal(seed.mode, "seed"); - assert.equal(seed.upgrade_from.digest, beta3SeedDigests[seed.source]); - assert.equal(sha256(seed.upgrade_from.document), seed.upgrade_from.digest); - assert.equal(frontmatter(seed.upgrade_from.document).version, 1); + assert.deepEqual(seed.upgrade_from.map(({ digest }) => digest), earlierStarters[seed.source]); + const type = frontmatter(documents.get(seed.source)); + for (const { digest, document, version } of seed.upgrade_from) { + assert.equal(sha256(document), digest); + assert.notEqual(digest, seed.digest); + const starter = frontmatter(document); + assert.equal(starter.kind, type.kind); + assert.equal(starter.name, type.name); + assert.equal(version, starter.version); + assert.equal(version, 1); + } + } +}); + +// Rebuilt from its baseline, beta.4 (one baseline object, no version) keeps its released pin. +test("the beta.4 pack is reproducible from the listed beta.3 baseline", async () => { + const pack = structuredClone((await writerPack()).manifest); + pack.version = "1.0.0-beta.4"; + for (const resource of pack.resources.filter(({ upgrade_from }) => upgrade_from)) { + const { digest, document } = resource.upgrade_from[0]; + resource.upgrade_from = { digest, document }; } + assert.equal(sha256(canonicalJson(pack)), releasedDigests["1.0.0-beta.4"]); }); test("starter types neither declare nor require the type key", async () => { diff --git a/apps/writer/scripts/writer-manifest.mjs b/apps/writer/scripts/writer-manifest.mjs index 8e37686..14f71ce 100644 --- a/apps/writer/scripts/writer-manifest.mjs +++ b/apps/writer/scripts/writer-manifest.mjs @@ -14,9 +14,15 @@ // and required `type`, which fails in collections whose // settings.explicit_type_keys record the type elsewhere (such as // `[mdbase_type]`). Version 2 drops the pin, matching Reader's pack beta.4, and -// names the exact beta.3 bytes it replaces (`upgrade_from`) so Connect can offer -// a reviewed three-way upgrade that keeps collection edits. Baselines under -// mdbase/baselines/ are released bytes: never edit them. +// lists the exact bytes of every earlier reader-source starter, newest first +// (`upgrade_from`), so Connect can offer a reviewed upgrade from any of them: an +// unedited starter is replaced, and an edited one is merged three-way against +// the starter it was installed from, keeping collection edits. The list is +// Reader's own, byte for byte, kept under the same paths: baselines are named +// for the Reader pack version that first shipped them (Writer's beta.1 and +// beta.2 shipped Reader's beta.1 starter, its beta.3 Reader's beta.3 starter; +// Reader's beta.2 starter is listed because a collection may hold it). They are +// released bytes: never edit them. import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; @@ -24,14 +30,25 @@ import { resolve } from "node:path"; import { dataContractDigest } from "@callumalpass/mdbase"; import { parse as parseYaml } from "yaml"; -export const WRITER_TYPE_PACK_VERSION = "1.0.0-beta.4"; +// beta.5 ships the same starters as beta.4 and lists every earlier starter as an upgrade baseline. +export const WRITER_TYPE_PACK_VERSION = "1.0.0-beta.5"; const projectRoot = resolve(import.meta.dirname, ".."); export const packResources = [ { kind: "contract", mode: "managed", source: "contracts/dev.mdbase.writer.manuscript.md", target: "_contracts/dev.mdbase.writer.manuscript.md" }, { kind: "contract", mode: "managed", source: "contracts/dev.mdbase.reader.source.md", target: "_contracts/dev.mdbase.reader.source.md" }, { kind: "type", mode: "seed", source: "types/writer-manuscript.md", target: "_types/writer-manuscript.md" }, - { kind: "type", mode: "seed", source: "types/reader-source.md", target: "_types/reader-source.md", upgradeFrom: "baselines/1.0.0-beta.3/types/reader-source.md" }, + { + kind: "type", + mode: "seed", + source: "types/reader-source.md", + target: "_types/reader-source.md", + upgradeFrom: [ + "baselines/1.0.0-beta.3/types/reader-source.md", + "baselines/1.0.0-beta.2/types/reader-source.md", + "baselines/1.0.0-beta.1/types/reader-source.md", + ], + }, ]; /** The published comment pack: `{ manifest, resources, provides }`, as mdbase contracts builds it. */ @@ -42,12 +59,17 @@ export async function buildWriterManifest({ origin = "https://writer.mdbase.dev" const resources = await Promise.all( packResources.map(async (resource) => { const document = await readFile(resolve(projectRoot, "mdbase", resource.source), "utf8"); - const baseline = resource.upgradeFrom ? await readFile(resolve(projectRoot, "mdbase", resource.upgradeFrom), "utf8") : undefined; + const baselines = await Promise.all( + (resource.upgradeFrom ?? []).map(async (path) => { + const baseline = await readFile(resolve(projectRoot, "mdbase", path), "utf8"); + return { digest: sha256(baseline), version: parseFrontmatter(baseline).version, document: baseline }; + }), + ); return { ...resource, digest: sha256(document), document, - ...(baseline === undefined ? {} : { upgrade_from: { digest: sha256(baseline), document: baseline } }), + ...(baselines.length === 0 ? {} : { upgrade_from: baselines }), ...(resource.kind === "contract" ? { contractDigest: dataContractDigest(parseFrontmatter(document)) } : {}), }; }), @@ -107,7 +129,7 @@ function sha256(value) { function parseFrontmatter(document) { const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(document); - if (!match?.[1]) throw new Error("Contract resource has no YAML frontmatter."); + if (!match?.[1]) throw new Error("Pack resource has no YAML frontmatter."); return parseYaml(match[1]); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 626a7ae..de75dee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,11 +51,11 @@ importers: specifier: ^1.4.3 version: 1.7.2 '@mdbase-dev/connect': - specifier: 0.1.0-beta.124 - version: 0.1.0-beta.124 + specifier: 0.1.0-beta.125 + version: 0.1.0-beta.125 '@mdbase-dev/ui': - specifier: 0.1.0-beta.124 - version: 0.1.0-beta.124(@codemirror/view@6.43.13)(react@19.3.0) + specifier: 0.1.0-beta.125 + version: 0.1.0-beta.125(@codemirror/view@6.43.13)(react@19.3.0) '@mdbase-writer/core': specifier: workspace:* version: link:../../packages/core @@ -85,11 +85,11 @@ importers: specifier: 0.3.0-rc.5 version: 0.3.0-rc.5 '@mdbase-dev/connect-dev': - specifier: 0.1.0-beta.124 - version: 0.1.0-beta.124 + specifier: 0.1.0-beta.125 + version: 0.1.0-beta.125 '@mdbase-dev/connect-testing': - specifier: 0.1.0-beta.124 - version: 0.1.0-beta.124 + specifier: 0.1.0-beta.125 + version: 0.1.0-beta.125 '@types/react': specifier: ^19.2.14 version: 19.3.0 @@ -311,21 +311,21 @@ packages: '@marijn/find-cluster-break@1.0.4': resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} - '@mdbase-dev/connect-dev@0.1.0-beta.124': - resolution: {integrity: sha512-QWvJg6SnNShu+yRz0e4Ac30mak9W9Hz9hQqdKtGjTG92oWopaH+bOEqOAiKci21wjmm8MivkKkGLwp2zrJSlxg==} + '@mdbase-dev/connect-dev@0.1.0-beta.125': + resolution: {integrity: sha512-fBYTD3+Fcl+I1FBvMiZ9tAGlvnaab+NRZ+J6pcuu3PdesZfbyanFDB5QRdpuXV+HVo8lbaXG145qQCpV2Wo4ZQ==} hasBin: true - '@mdbase-dev/connect-protocol@0.1.0-beta.124': - resolution: {integrity: sha512-p1jA+AqpqfhnV+foOXtoQg8oVK9oSe+W40MwzbW3x7kEeQvxRRxtmGDeVaqBlob3Fn+4Owx0G1KSRu3BzlnphA==} + '@mdbase-dev/connect-protocol@0.1.0-beta.125': + resolution: {integrity: sha512-q6aEZ1+dVAXvoN84FgDLMhqCnCdlsXhp0K5M+Wcp2x2ICUIoE/lA+eDg4AJqDqdWZ3Nq2mkiczvat6HG2y8EeQ==} - '@mdbase-dev/connect-testing@0.1.0-beta.124': - resolution: {integrity: sha512-RsbbtqaP8jEPDw7C+qVAoYLETZMzs6lfabcyRoYMkifB6/1+p26zThPPfv37OFJj8sXCLOfBKkD0a+tz5UeZbA==} + '@mdbase-dev/connect-testing@0.1.0-beta.125': + resolution: {integrity: sha512-o9tJZNPRsClsbP6n7OqzDZ0Ijm3xrlXmVmZ1LniK4e6Yed2asYU+7QYJuIbCp7iXFyfuwdiFj9Z+qMG00Rxvlg==} - '@mdbase-dev/connect@0.1.0-beta.124': - resolution: {integrity: sha512-lV53SpYZjMqYD+tD5NDONsQAIPRHoFDm9OzF+SoG0hibqX5cPMu47LD9igZQIb63XFqvy+MOzjFz4SyGtlRWSg==} + '@mdbase-dev/connect@0.1.0-beta.125': + resolution: {integrity: sha512-vEH4NQfu3dBQgMw0YQxG7LDtOOLxDyxET4DQhrWuHqtgr9qoch/H0QLzel9tFSqkyH+QWxuvW3Bet6ae1+qVyQ==} - '@mdbase-dev/ui@0.1.0-beta.124': - resolution: {integrity: sha512-75xiQ9PhKjVyk9R+xaE0APpNn5MprfrkXwXhVQM5eT3uTCCZ0YwFCTeIAFsjEB6MNItp4pujoyebSIPQKwXg7w==} + '@mdbase-dev/ui@0.1.0-beta.125': + resolution: {integrity: sha512-K2UAAzwW4yuPJxx65LofVDQXKLgs4lOgrAB61YMuiNwV/jOc1yuNhGugy7Fx++VwxBx5bZyG+Pk7tMQlOat+tg==} peerDependencies: '@codemirror/view': ^6.0.0 react: ^19.2.0 @@ -1297,28 +1297,29 @@ snapshots: '@marijn/find-cluster-break@1.0.4': {} - '@mdbase-dev/connect-dev@0.1.0-beta.124': + '@mdbase-dev/connect-dev@0.1.0-beta.125': dependencies: - '@mdbase-dev/connect': 0.1.0-beta.124 - '@mdbase-dev/connect-protocol': 0.1.0-beta.124 + '@mdbase-dev/connect': 0.1.0-beta.125 + '@mdbase-dev/connect-protocol': 0.1.0-beta.125 ajv: 8.20.0 yaml: 2.9.1 - '@mdbase-dev/connect-protocol@0.1.0-beta.124': + '@mdbase-dev/connect-protocol@0.1.0-beta.125': dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) + yaml: 2.9.1 - '@mdbase-dev/connect-testing@0.1.0-beta.124': + '@mdbase-dev/connect-testing@0.1.0-beta.125': dependencies: - '@mdbase-dev/connect': 0.1.0-beta.124 - '@mdbase-dev/connect-protocol': 0.1.0-beta.124 + '@mdbase-dev/connect': 0.1.0-beta.125 + '@mdbase-dev/connect-protocol': 0.1.0-beta.125 - '@mdbase-dev/connect@0.1.0-beta.124': + '@mdbase-dev/connect@0.1.0-beta.125': dependencies: - '@mdbase-dev/connect-protocol': 0.1.0-beta.124 + '@mdbase-dev/connect-protocol': 0.1.0-beta.125 - '@mdbase-dev/ui@0.1.0-beta.124(@codemirror/view@6.43.13)(react@19.3.0)': + '@mdbase-dev/ui@0.1.0-beta.125(@codemirror/view@6.43.13)(react@19.3.0)': dependencies: '@fontsource/atkinson-hyperlegible': 5.3.0 '@fontsource/azeret-mono': 5.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 32593d0..8f65a47 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,11 +3,11 @@ packages: - packages/* minimumReleaseAgeExclude: - - '@mdbase-dev/connect-protocol@0.1.0-beta.124' - - '@mdbase-dev/connect-testing@0.1.0-beta.124' - - '@mdbase-dev/connect@0.1.0-beta.124' - - '@mdbase-dev/connect-dev@0.1.0-beta.124' - - '@mdbase-dev/ui@0.1.0-beta.124' + - '@mdbase-dev/connect-protocol@0.1.0-beta.125' + - '@mdbase-dev/connect-testing@0.1.0-beta.125' + - '@mdbase-dev/connect@0.1.0-beta.125' + - '@mdbase-dev/connect-dev@0.1.0-beta.125' + - '@mdbase-dev/ui@0.1.0-beta.125' onlyBuiltDependencies: - esbuild