diff --git a/test/__fixtures__/first-use.ts b/test/__fixtures__/first-use.ts
new file mode 100644
index 0000000..88fda4f
--- /dev/null
+++ b/test/__fixtures__/first-use.ts
@@ -0,0 +1,149 @@
+/**
+ * The synthetic CCD the quickstart's first example parses, byte for byte. Every value is invented
+ * (the canonical synthetic patient "Jane Doe", fake OIDs and identifiers). It lives here, embedded
+ * in a `.ts` module like every other fixture in this repository, because no XML document is ever
+ * tracked as a file, and `pnpm phi-scan` reads the XML embedded under `test/__fixtures__/`.
+ */
+export const QUICKSTART_CCD = `
+
+
+
+
+
+
+ Synthetic Continuity of Care Document
+
+
+
+
+
+ JaneQDoe
+
+
+
+
+
+
+
+
+ Problems
+ Essential hypertension
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Medications
+ Lisinopril 10 MG Oral Tablet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Allergies
+ No known allergies.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Results
+ Hemoglobin 13.5 g/dL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Vital Signs
+ Heart rate: 72 /min
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Social History
+ Never smoker
+
+
+
+
+
+
+
+
+
+
+
+
+ Immunizations
+ Influenza, split virus, trivalent, injectable, preservative free
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/test/_helpers/first-use.ts b/test/_helpers/first-use.ts
new file mode 100644
index 0000000..6692efb
--- /dev/null
+++ b/test/_helpers/first-use.ts
@@ -0,0 +1,172 @@
+import { join } from "node:path";
+
+import ts from "typescript";
+
+/**
+ * Readers for the package's first-use path: the first fenced block of
+ * `docs-content/quickstart.md`, the first fenced block under `## Usage` in `README.md`, and the
+ * install command both documents print. Every reader works on the text of the document as it is
+ * on disk at test time, never on a copy held here.
+ */
+
+/**
+ * The compiler options of a new project, as `tsc --init` of the TypeScript this repository pins
+ * writes them: `strict` plus `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes` and
+ * `verbatimModuleSyntax`, NodeNext modules. The emit-only settings it also writes (`sourceMap`,
+ * `declaration`, `declarationMap`, `jsx`) are left out because nothing is emitted, and Node's
+ * types are loaded where `tsc --init` writes `types: []`, because every first-use example is a
+ * Node program.
+ */
+const READER_OPTIONS: ts.CompilerOptions = {
+ module: ts.ModuleKind.NodeNext,
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
+ target: ts.ScriptTarget.ESNext,
+ strict: true,
+ noUncheckedIndexedAccess: true,
+ exactOptionalPropertyTypes: true,
+ verbatimModuleSyntax: true,
+ isolatedModules: true,
+ noUncheckedSideEffectImports: true,
+ moduleDetection: ts.ModuleDetectionKind.Force,
+ skipLibCheck: true,
+ noEmit: true,
+ types: ["node"],
+};
+
+/**
+ * Every TypeScript error a reader's new project reports for one example, as `line N: TSxxxx
+ * message`; empty when the example compiles. The example is compiled as an ES module sitting in
+ * the repository root, with each specifier in `paths` resolved to the single source file the
+ * bundler compiles into that published entry point and its types. Only the example's own errors
+ * are returned.
+ */
+export function compileErrors(
+ root: string,
+ paths: Readonly>,
+ code: string,
+): string[] {
+ const file = join(root, "first-use-example.mts");
+ const options: ts.CompilerOptions = {
+ ...READER_OPTIONS,
+ typeRoots: [join(root, "node_modules", "@types")],
+ paths: Object.fromEntries(Object.entries(paths).map(([pkg, entry]) => [pkg, [entry]])),
+ };
+ const host = ts.createCompilerHost(options, true);
+ const getSourceFile = host.getSourceFile.bind(host);
+ host.getSourceFile = (name, language, onError, create) =>
+ name === file
+ ? ts.createSourceFile(name, code, language, true)
+ : getSourceFile(name, language, onError, create);
+ const fileExists = host.fileExists.bind(host);
+ host.fileExists = (name) => name === file || fileExists(name);
+ const readFile = host.readFile.bind(host);
+ host.readFile = (name) => (name === file ? code : readFile(name));
+ const program = ts.createProgram([file], options, host);
+ return ts
+ .getPreEmitDiagnostics(program, program.getSourceFile(file))
+ .filter((d) => d.file?.fileName === file)
+ .map((d) => {
+ const line = d.file?.getLineAndCharacterOfPosition(d.start ?? 0).line ?? 0;
+ const text = ts.flattenDiagnosticMessageText(d.messageText, " ");
+ return `line ${String(line + 1)}: TS${String(d.code)} ${text}`;
+ });
+}
+
+/** One fenced code block: its info string split into language and tags, and its body. */
+export interface Fence {
+ readonly lang: string;
+ readonly tags: readonly string[];
+ readonly body: string;
+}
+
+/** Every fenced block in a chunk of markdown, in document order. */
+export function fences(markdown: string): Fence[] {
+ const out: Fence[] = [];
+ let open: { marker: string; lang: string; tags: string[] } | undefined;
+ let buf: string[] = [];
+ for (const line of markdown.split("\n")) {
+ if (open === undefined) {
+ const opened = /^(`{3,}|~{3,})\s*(.*)$/.exec(line);
+ if (opened !== null) {
+ const tokens = (opened[2] ?? "").trim().split(/\s+/).filter(Boolean);
+ open = { marker: opened[1] ?? "```", lang: tokens[0] ?? "", tags: tokens.slice(1) };
+ buf = [];
+ }
+ continue;
+ }
+ if (line.trimEnd() === open.marker) {
+ out.push({ lang: open.lang, tags: open.tags, body: buf.join("\n") });
+ open = undefined;
+ continue;
+ }
+ buf.push(line);
+ }
+ if (open !== undefined) throw new Error("unterminated code fence");
+ return out;
+}
+
+/** The lines of one `##` section, from its heading to the next `##` heading. */
+export function section(markdown: string, heading: string): string {
+ const lines = markdown.split("\n");
+ const start = lines.indexOf(heading);
+ if (start === -1) throw new Error(`no ${heading} section`);
+ let end = lines.length;
+ for (let i = start + 1; i < lines.length; i += 1) {
+ if (lines[i]?.startsWith("## ") === true) {
+ end = i;
+ break;
+ }
+ }
+ return lines.slice(start + 1, end).join("\n");
+}
+
+/** Every backtick-quoted literal in a snippet that holds a C-CDA document. */
+export function documentLiterals(code: string): string[] {
+ const literals: string[] = [];
+ for (const match of code.matchAll(/`([^`]* {
+ if (open !== undefined) {
+ if (text.trimEnd() === open) open = undefined;
+ return;
+ }
+ const opened = /^(\s*)(`{3,}|~{3,})\s*(.*)$/.exec(text);
+ if (opened === null) return;
+ open = `${opened[1] ?? ""}${opened[2] ?? "```"}`;
+ const tokens = (opened[3] ?? "").trim().split(/\s+/).filter(Boolean);
+ if (tokens.slice(1).includes("runnable")) out.push({ line: index + 1, lang: tokens[0] ?? "" });
+ });
+ return out;
+}
+
+/**
+ * The package names a document tells a reader to install: the first non-flag argument of every
+ * `npm install`, `npm i`, `npm add`, `pnpm add`, `pnpm install`, `pnpm i`, `yarn add`, `bun add`
+ * or `deno add npm:`, read as a package name, so any `@version` suffix, closing quote or backtick,
+ * or sentence-ending full stop is left off. A local path or a protocol specifier (`file:../pkg`,
+ * `link:`, `git+https:`) installs whatever that path or URL holds rather than resolving a package
+ * name, so it is not a registry specifier and is not read.
+ */
+export function installSpecifiers(markdown: string): string[] {
+ const out: string[] = [];
+ const command =
+ /\b(?:npm (?:install|add|i)|pnpm (?:add|install|i)|yarn add|bun add|deno add)((?:[ \t]+-{1,2}[\w-]+)*)[ \t]+(?:npm:)?(@?\w[\w.-]*(?:\/[\w.-]+)?)(?![\w.:/+-])/g;
+ for (const match of markdown.matchAll(command)) {
+ const spec = match[2];
+ if (spec !== undefined) out.push(spec.replace(/\.+$/, ""));
+ }
+ return out;
+}
diff --git a/test/docs-content.test.ts b/test/docs-content.test.ts
index 1862c57..8194623 100644
--- a/test/docs-content.test.ts
+++ b/test/docs-content.test.ts
@@ -1,10 +1,23 @@
import { execFileSync } from "node:child_process";
-import { readFileSync } from "node:fs";
-import { join } from "node:path";
+import { readFileSync, readdirSync, rmSync } from "node:fs";
+import { join, relative } from "node:path";
-import { beforeAll, describe, expect, test } from "vitest";
+import { afterAll, beforeAll, describe, expect, test } from "vitest";
-import { docSnippetSuite } from "@cosyte/vitest-config/snippets";
+import {
+ docSnippetSuite,
+ extractRunnableSnippets,
+ runSnippet,
+} from "@cosyte/vitest-config/snippets";
+
+import {
+ compileErrors,
+ documentLiterals,
+ fences,
+ runnableTaggedFences,
+ section,
+} from "./_helpers/first-use.js";
+import * as firstUseFixtures from "./__fixtures__/first-use.js";
/**
* Doc/code-agreement gate. Every ```` ```ts runnable ```` block in `docs-content/` **and in
@@ -74,3 +87,171 @@ describe("README status section", () => {
expect(status).toContain(`\`${version ?? ""}\``);
});
});
+
+/**
+ * The two FIRST-USE examples, the quickstart's first block and the first block under the README's
+ * `## Usage`, are the ones a reader runs first. Each must be the block a sweep above executes, each
+ * is run again here with a changed value to prove the run can go red, and every C-CDA document
+ * literal either one prints must be a byte-for-byte copy of a fixture embedded in
+ * `test/__fixtures__/first-use.ts`, whose XML `pnpm phi-scan` reads. Temp modules for these runs live
+ * in their own directory inside the root, as the harness requires, and are removed when the file is
+ * done.
+ */
+const resolveEntry = (specifier: string): string | undefined =>
+ specifier === "@cosyte/ccda" ? ENTRY : undefined;
+const FIRST_USE_TMP = join(root, ".cosyte-first-use-snippets");
+/**
+ * The snippet harness strips types without checking them, so compiling is checked separately, the
+ * way a reader's new TypeScript project compiles the block, against the source entry point the
+ * bundler compiles into the published types. A program over the source takes seconds to check, so
+ * these cases state their own budget.
+ */
+const SOURCE_PATHS = { "@cosyte/ccda": join(root, "src", "index.ts") };
+const COMPILE_TIMEOUT = 60_000;
+/** The embedded first-use fixtures, by value: a document literal must be exactly one of them. */
+const FIRST_USE_FIXTURES: ReadonlySet = new Set(Object.values(firstUseFixtures));
+const QUICKSTART_TEXT = readFileSync(join(root, "docs-content", "quickstart.md"), "utf8");
+const README_TEXT = readFileSync(README, "utf8");
+const FIRST_USE = [
+ {
+ doc: "docs-content/quickstart.md",
+ fence: fences(QUICKSTART_TEXT)[0],
+ snippets: extractRunnableSnippets(QUICKSTART_TEXT),
+ claim: 'doc.getMrn(); // => "MRN-00042"',
+ claimMutated: 'doc.getMrn(); // => "MRN-00043"',
+ },
+ {
+ doc: "README.md",
+ fence: fences(section(README_TEXT, "## Usage"))[0],
+ snippets: extractRunnableSnippets(README_TEXT),
+ claim: 'doc.getMrn(); // => "MRN001"',
+ claimMutated: 'doc.getMrn(); // => "MRN002"',
+ },
+] as const;
+
+afterAll(() => {
+ rmSync(FIRST_USE_TMP, { recursive: true, force: true });
+});
+
+describe("the first-use examples", () => {
+ for (const example of FIRST_USE) {
+ const acRun = example.doc === "README.md" ? "AC-CC2" : "AC-CC1";
+
+ test(`${acRun}: the first block of ${example.doc} is one the sweep executes, and it runs`, async () => {
+ expect(example.fence?.lang).toBe("ts");
+ expect(example.fence?.tags).toContain("runnable");
+ expect(example.fence?.tags).not.toContain("throws");
+ const executed = example.snippets.find((s) => s.code === example.fence?.body);
+ expect(
+ executed,
+ `${example.doc}: the first block is not among the executed ones`,
+ ).toBeDefined();
+ if (executed === undefined) return;
+ await runSnippet(executed, { resolve: resolveEntry, tmpDir: FIRST_USE_TMP });
+ });
+
+ test(
+ `${acRun}: the first block of ${example.doc} compiles in a new TypeScript project`,
+ () => {
+ expect(compileErrors(root, SOURCE_PATHS, example.fence?.body ?? "")).toEqual([]);
+ },
+ COMPILE_TIMEOUT,
+ );
+
+ test(
+ `${acRun}: a first block of ${example.doc} that does not compile is reported, so it turns this suite red`,
+ () => {
+ const code = example.fence?.body ?? "";
+ expect(code.split(example.claim).length - 1).toBe(1);
+ const broken = example.claim.replace("doc.getMrn()", "doc.getMrnn()");
+ expect(compileErrors(root, SOURCE_PATHS, code.replace(example.claim, broken))).toEqual([
+ expect.stringMatching(/TS2551|TS2339/),
+ ]);
+ },
+ COMPILE_TIMEOUT,
+ );
+
+ test(`AC-CC4: a changed claimed value in the first block of ${example.doc} turns it red`, async () => {
+ const code = example.fence?.body ?? "";
+ expect(code.split(example.claim).length - 1).toBe(1);
+ const mutated = code.replace(example.claim, example.claimMutated);
+ await expect(
+ runSnippet(mutated, { resolve: resolveEntry, tmpDir: FIRST_USE_TMP }),
+ ).rejects.toThrow();
+ });
+
+ test(`AC-CC5: every document literal in the first block of ${example.doc} is a committed fixture`, () => {
+ for (const literal of documentLiterals(example.fence?.body ?? "")) {
+ expect(
+ FIRST_USE_FIXTURES.has(literal),
+ `${example.doc}: a document literal is no fixture`,
+ ).toBe(true);
+ }
+ });
+ }
+
+ test("AC-CC4: a changed input value in the first block of README.md turns it red", async () => {
+ const code = FIRST_USE[1].fence?.body ?? "";
+ expect(code.split('mrn: "MRN001"').length - 1).toBe(1);
+ const mutated = code.replace('mrn: "MRN001"', 'mrn: "MRN002"');
+ await expect(
+ runSnippet(mutated, { resolve: resolveEntry, tmpDir: FIRST_USE_TMP }),
+ ).rejects.toThrow();
+ });
+
+ test("AC-CC5: the quickstart's first block reads exactly one document, and a changed value leaves the corpus", async () => {
+ const code = FIRST_USE[0].fence?.body ?? "";
+ expect(documentLiterals(code)).toHaveLength(1);
+ expect(code.split('extension="MRN-00042"').length - 1).toBe(1);
+ const mutated = code.replace('extension="MRN-00042"', 'extension="MRN-00043"');
+ expect(FIRST_USE_FIXTURES.has(documentLiterals(mutated)[0] ?? "")).toBe(false);
+ await expect(
+ runSnippet(mutated, { resolve: resolveEntry, tmpDir: FIRST_USE_TMP }),
+ ).rejects.toThrow();
+ });
+});
+
+/**
+ * A fence tagged `runnable` is a claim that something executes it. The two sweeps above execute
+ * every TypeScript block so tagged in `docs-content/` and `README.md`; a block tagged `runnable` in
+ * any other language is extracted by nothing, so it is named here instead of passing silently.
+ */
+function markdownUnder(dir: string): string[] {
+ const out: string[] = [];
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const full = join(dir, entry.name);
+ if (entry.isDirectory()) out.push(...markdownUnder(full));
+ else if (/\.mdx?$/i.test(entry.name)) out.push(full);
+ }
+ return out;
+}
+
+/** `file:line` for every runnable-tagged fence in `markdown` that the snippet harness does not extract. */
+function unexecutedRunnableFences(file: string, markdown: string): string[] {
+ const tagged = runnableTaggedFences(markdown);
+ const unexecuted = tagged
+ .filter((f) => !["ts", "typescript", "tsx"].includes(f.lang.toLowerCase()))
+ .map((f) => `${file}:${String(f.line)}`);
+ if (unexecuted.length === 0 && tagged.length !== extractRunnableSnippets(markdown).length) {
+ unexecuted.push(
+ `${file}: tagged ${String(tagged.length)}, extracted ${String(extractRunnableSnippets(markdown).length)}`,
+ );
+ }
+ return unexecuted;
+}
+
+describe("a runnable tag is an execution that happens", () => {
+ test("AC-CC3: every runnable-tagged block in README.md and docs-content/ is executed", () => {
+ const files = [README, ...markdownUnder(join(root, "docs-content"))];
+ const unexecuted = files.flatMap((file) =>
+ unexecutedRunnableFences(relative(root, file), readFileSync(file, "utf8")),
+ );
+ expect(unexecuted).toEqual([]);
+ });
+
+ test("AC-CC3: a runnable-tagged block no sweep executes is named by file and line", () => {
+ const fence = "```";
+ const planted = `# Page\n\n${fence}js runnable\nconst x = 1;\n${fence}\n\n${fence}ts runnable\nconst y = 2;\n${fence}\n`;
+ expect(unexecutedRunnableFences("planted.md", planted)).toEqual(["planted.md:3"]);
+ });
+});
diff --git a/test/install-specifier.test.ts b/test/install-specifier.test.ts
new file mode 100644
index 0000000..9c6e7e5
--- /dev/null
+++ b/test/install-specifier.test.ts
@@ -0,0 +1,48 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+import { describe, expect, it } from "vitest";
+
+import { installSpecifiers } from "./_helpers/first-use.js";
+
+/**
+ * The install command a reader copies has to fetch THIS package. The subject is package identity:
+ * each specifier the two first-use documents print is compared with `package.json` `name`, and a
+ * mismatch names both strings.
+ */
+const root = join(import.meta.dirname, "..");
+const { name } = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { name: string };
+
+describe("the documented install specifier", () => {
+ for (const doc of ["docs-content/installation.md", "README.md"]) {
+ it(`AC-CC7: every install command in ${doc} names package.json name`, () => {
+ const specs = installSpecifiers(readFileSync(join(root, doc), "utf8"));
+ expect(specs.length, `${doc} prints no install command`).toBeGreaterThan(0);
+ for (const spec of specs) {
+ expect(spec, `${doc} installs "${spec}", package.json name is "${name}"`).toBe(name);
+ }
+ });
+ }
+
+ it("AC-CC7: a specifier that is not the package name is read as the name it prints", () => {
+ expect(installSpecifiers("npm install @cosyte/ccdb\n`pnpm add -D @cosyte/ccda@0.0.1`")).toEqual(
+ ["@cosyte/ccdb", "@cosyte/ccda"],
+ );
+ });
+
+ it("AC-CC7: every install command form a reader may copy is read, inline code included", () => {
+ const forms = [
+ "pnpm i @cosyte/ccdb",
+ "pnpm install @cosyte/ccdb",
+ "npm add @cosyte/ccdb",
+ "deno add npm:@cosyte/ccdb",
+ "run `npm install @cosyte/ccdb` first",
+ "then run npm install @cosyte/ccdb.",
+ ];
+ for (const form of forms) expect(installSpecifiers(form), form).toEqual(["@cosyte/ccdb"]);
+ expect(installSpecifiers("pnpm install\npnpm install --frozen-lockfile")).toEqual([]);
+ expect(
+ installSpecifiers("pnpm add file:../ccda\nnpm install git+https://x.test/ccda.git"),
+ ).toEqual([]);
+ });
+});