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
100 changes: 100 additions & 0 deletions apps/writer/mdbase/baselines/1.0.0-beta.1/types/reader-source.md
Original file line number Diff line number Diff line change
@@ -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.
110 changes: 110 additions & 0 deletions apps/writer/mdbase/baselines/1.0.0-beta.2/types/reader-source.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 4 additions & 4 deletions apps/writer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
48 changes: 37 additions & 11 deletions apps/writer/scripts/manifest.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 () => {
Expand Down
38 changes: 30 additions & 8 deletions apps/writer/scripts/writer-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,41 @@
// 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";

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. */
Expand All @@ -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)) } : {}),
};
}),
Expand Down Expand Up @@ -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]);
}

Expand Down
Loading