diff --git a/BOTS.md b/BOTS.md index 999a43b..6f2da9e 100644 --- a/BOTS.md +++ b/BOTS.md @@ -36,8 +36,9 @@ the PR reviewer decides which screenshots the plugin needs under REVIEW.md. npm package files come from one cached tarball per version, verified against npm's SHA-512 before reading. No package code runs, and files are read to stdout -without extracting paths or links onto disk. Published asset URLs and the -provenance-pinned repository source of author overviews are unchanged. +without extracting paths or links onto disk. npm manifests and overviews come +from that tarball; GitHub files come from the pinned commit and plugin directory. +Published asset URLs still use jsDelivr for npm and raw GitHub URLs for Git. ## Tokens and repository setup diff --git a/README.md b/README.md index 51dea69..38ac230 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,11 @@ HTTPS `media`). The bot writes artifact pins and review dates. The published index combines records with metadata from their pinned manifests. See [plugin metadata](#plugin-metadata) for fields and override precedence. -Authors must keep `OVERVIEW.md` beside `paseo-plugin.json` in the repository at the -pinned source commit. Git monorepos use `artifact.pluginPath`; npm monorepos use -the pinned package's `repository.directory` and proven `repository.commit`. -The author owns this overview. +Authors must ship `OVERVIEW.md` beside `paseo-plugin.json` in the submitted artifact. +For npm, include both files at the published package root. The registry reads them +from the verified tarball at the pinned version; repository metadata and provenance +are not needed to read package files. For GitHub, include both files at the pinned +commit, under `artifact.pluginPath` for a monorepo. The author owns this overview. Approved imports can temporarily use `plugins//.md` in the registry. An unchanged imported artifact keeps this exception while its stopgap exists. diff --git a/REVIEW.md b/REVIEW.md index 9b666a4..8135699 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -123,9 +123,10 @@ overview is read inside Paseo, where the install command already sits at the top page, by someone deciding whether to install. `OVERVIEW.md` next to `paseo-plugin.json` is required. A submission or a bump whose -repository has no `OVERVIEW.md` at the pinned commit fails validation and gets changes -requested naming the file. Records imported from paseo.cafe are the exception: they carry -one written at import at `plugins//.md`, ending with the line +pinned artifact has no `OVERVIEW.md` fails validation and gets changes requested +naming the file. npm reads the verified tarball at the pinned version; GitHub reads +the plugin directory at the pinned commit. Records imported from paseo.cafe are +the exception: they carry one written at import at `plugins//.md`, ending with the line `*This plugin entry was imported from [paseo.cafe](https://paseo.cafe/plugins/).*`. The author may replace that file by pull request, and the author's own `OVERVIEW.md` takes over on the first bump, which removes the registry copy. diff --git a/scripts/lib/artifact-files.ts b/scripts/lib/artifact-files.ts new file mode 100644 index 0000000..a041e60 --- /dev/null +++ b/scripts/lib/artifact-files.ts @@ -0,0 +1,30 @@ +import { readOptional, withGitArtifact } from "./git-artifact.ts"; +import { type NpmClient, resolveVersion } from "./npm.ts"; +import type { PluginRecord } from "./record.ts"; + +export interface ArtifactFiles { + manifest: string | null; + overview: string | null; +} + +/** Read plugin files from the verified install pin, independently of repository metadata. */ +export async function readArtifactFiles( + client: NpmClient, + artifact: PluginRecord["artifact"], +): Promise { + if (artifact.kind === "git") { + return withGitArtifact({ artifact }, (directory) => ({ + manifest: readOptional(directory, "paseo-plugin.json"), + overview: readOptional(directory, "OVERVIEW.md"), + })); + } + const doc = resolveVersion(await client.packument(artifact.package), artifact.version); + if (doc.dist.integrity !== artifact.integrity || doc.dist.tarball !== artifact.resolved) { + throw new Error(`${artifact.package}@${artifact.version} integrity on npm differs from the pinned record`); + } + const [manifest, overview] = await Promise.all([ + client.file(artifact.package, artifact.version, "paseo-plugin.json"), + client.file(artifact.package, artifact.version, "OVERVIEW.md"), + ]); + return { manifest, overview }; +} diff --git a/scripts/lib/bump-review.ts b/scripts/lib/bump-review.ts index d0c8d9d..8265eb3 100644 --- a/scripts/lib/bump-review.ts +++ b/scripts/lib/bump-review.ts @@ -25,7 +25,7 @@ export async function commitBumpForReview(input: { git(["add", recordPath(next.id, directory)], options); const drop = author !== null && existsSync(overviewPath); const note = author === null - ? "This version has no OVERVIEW.md. The registry requires one to update a listing; the bump cannot merge until the repository adds it.\n\n" + ? "This version has no OVERVIEW.md. The registry requires one to update a listing; the bump cannot merge until a release includes it in the submitted artifact.\n\n" : drop ? "This version ships OVERVIEW.md, so the registry's copy is removed in this bump.\n\n" : ""; diff --git a/scripts/lib/git-artifact.ts b/scripts/lib/git-artifact.ts index 8cd21a9..fe06868 100644 --- a/scripts/lib/git-artifact.ts +++ b/scripts/lib/git-artifact.ts @@ -6,26 +6,15 @@ import { run } from "./shell.ts"; import type { PluginRecord } from "./record.ts"; import { parseArtifact } from "./record.ts"; import { deriveId } from "./id.ts"; -import type { RepositorySource } from "./repository.ts"; import { parseSubmissionSource } from "./submission-source.ts"; import { isoDate } from "./dates.ts"; -export function withGitArtifact(record: PluginRecord, consume: (directory: string) => T): T { +export function withGitArtifact(record: Pick, consume: (directory: string) => T): T { const artifact = record.artifact; if (artifact.kind !== "git") throw new Error("Expected git artifact"); return withCheckout(artifact, consume); } -/** Read a source repository at an exact reviewed commit, without running its code. */ -export function withRepositoryCommit( - source: RepositorySource, - commit: string, - consume: (directory: string) => T, -): T { - if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error("Expected a full repository commit"); - return withCheckout({ remote: source.url, commit, pluginPath: source.directory }, consume); -} - function withCheckout( artifact: { remote: string; commit: string; pluginPath?: string; tag?: string }, consume: (directory: string) => T, diff --git a/scripts/lib/listing.test.ts b/scripts/lib/listing.test.ts index 6b8c8fe..d9ae908 100644 --- a/scripts/lib/listing.test.ts +++ b/scripts/lib/listing.test.ts @@ -39,7 +39,7 @@ const record: PluginRecord = { test("publication preserves author, overview, and review dates", async () => { const client: NpmClient = { async packument() { return { name: doc.name, "dist-tags": { latest: doc.version }, versions: { [doc.version]: doc }, time: {} }; }, - async file() { return '{"name":"Dracula"}'; }, + async file(_name, _version, path) { return path === "paseo-plugin.json" ? '{"name":"Dracula"}' : null; }, async provenance() { return null; }, async tarball() { throw new Error("Not needed"); }, }; diff --git a/scripts/lib/listing.ts b/scripts/lib/listing.ts index f156645..f4fd65e 100644 --- a/scripts/lib/listing.ts +++ b/scripts/lib/listing.ts @@ -1,4 +1,5 @@ -import { readAuthorOverview, requireOverview } from "./overview.ts"; +import { readArtifactFiles } from "./artifact-files.ts"; +import { validateOverview, requireOverview } from "./overview.ts"; import type { Category } from "./categories.ts"; import { authorOf, type NpmClient, resolveVersion } from "./npm.ts"; import { resolveMetadata } from "./metadata.ts"; @@ -42,8 +43,9 @@ export async function resolvePlugin( record: PluginRecord, overview: string | null = null, ): Promise { - const readme = requireOverview(await readAuthorOverview(client, record), overview, record.id); - const metadata = await resolveMetadata(client, record); + const files = await readArtifactFiles(client, record.artifact); + const readme = requireOverview(validateOverview(files.overview, `${record.id}/OVERVIEW.md`), overview, record.id); + const metadata = resolveMetadata(files, record); const artifact = record.artifact; const doc = artifact.kind === "npm" ? resolveVersion(await client.packument(artifact.package), artifact.version) diff --git a/scripts/lib/manifest-metadata.test.ts b/scripts/lib/manifest-metadata.test.ts index 9f3c5f9..c02d543 100644 --- a/scripts/lib/manifest-metadata.test.ts +++ b/scripts/lib/manifest-metadata.test.ts @@ -5,6 +5,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test, type TestContext } from "node:test"; +import { readArtifactFiles } from "./artifact-files.ts"; import { resolvePlugin } from "./listing.ts"; import { createNpmClient } from "./npm.ts"; import type { PluginRecord } from "./record.ts"; @@ -25,6 +26,10 @@ function fixture(t: TestContext, kind: "npm" | "git", manifest: unknown, listing const remote = "https://github.com/acme/manifest.git"; const git = (...args: string[]) => execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim(); git("init", "-q"); + if (kind === "npm") { + writeFileSync(join(plugin, "paseo-plugin.json"), '{"name":"Repository name"}'); + writeFileSync(join(plugin, "OVERVIEW.md"), "Repository overview."); + } git("add", "."); git("-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-qm", "published"); const commit = git("rev-parse", "HEAD"); @@ -41,6 +46,7 @@ function fixture(t: TestContext, kind: "npm" | "git", manifest: unknown, listing if (previous[i] === undefined) delete process.env[key]; else process.env[key] = previous[i]; })); writeFileSync(join(plugin, "paseo-plugin.json"), JSON.stringify(manifest)); + writeFileSync(join(plugin, "OVERVIEW.md"), "A theme for focused work."); const archive = execFileSync("tar", ["-czf", "-", "-C", root, pluginPath]); const doc = { name: "@acme/example", version: "1.0.0", description: "Package description", dist: { tarball: "https://registry.npmjs.org/example/-/example-1.0.0.tgz", @@ -66,7 +72,7 @@ function fixture(t: TestContext, kind: "npm" | "git", manifest: unknown, listing : `https://github.com/acme/manifest/raw/${commit}/${pluginPath}/`; const detail = () => resolvePlugin(client, record, "Imported overview"); const validate = () => validateArtifact(client, record, { previous: record, registryOverview: "Imported overview" }); - return { record, detail, validate, base }; + return { record, client, detail, validate, base }; } for (const kind of ["npm", "git"] as const) { @@ -76,6 +82,7 @@ for (const kind of ["npm", "git"] as const) { media: ["assets/demo.mp4", "assets/screen shot.PNG"], futureField: true, }); const detail = await f.detail(); + assert.equal(detail.readme, "A theme for focused work."); assert.equal(detail.name, "Author name"); assert.equal(detail.icon, `${f.base}assets/icon.png`); assert.deepEqual(detail.media, [`${f.base}assets/demo.mp4`, `${f.base}assets/screen%20shot.PNG`]); @@ -142,3 +149,22 @@ for (const kind of ["npm", "git"] as const) { }); } } + +test("npm file reads need only the artifact pin, without repository metadata or provenance", async (t) => { + const { client, record } = fixture(t, "npm", { name: "Package name" }); + const files = await readArtifactFiles(client, record.artifact); + assert.deepEqual(files, { + manifest: '{"name":"Package name"}', + overview: "A theme for focused work.", + }); +}); + +for (const field of ["integrity", "resolved"] as const) { + test(`npm rejects a changed ${field} pin before returning package files`, async (t) => { + const { client, record } = fixture(t, "npm", { name: "Package name" }); + assert.equal(record.artifact.kind, "npm"); + await assert.rejects(readArtifactFiles(client, { + ...record.artifact, [field]: "changed", + }), /differs from the pinned record/); + }); +} diff --git a/scripts/lib/metadata.ts b/scripts/lib/metadata.ts index 21ac1f9..04901e2 100644 --- a/scripts/lib/metadata.ts +++ b/scripts/lib/metadata.ts @@ -1,6 +1,5 @@ -import { readOptional, withGitArtifact } from "./git-artifact.ts"; +import type { ArtifactFiles } from "./artifact-files.ts"; import { mediaKind } from "./media.ts"; -import { type NpmClient, resolveVersion } from "./npm.ts"; import { AuthorError } from "./problems.ts"; import type { PluginRecord } from "./record.ts"; @@ -14,21 +13,12 @@ export interface PluginMetadata { /** Read metadata at the artifact pin and apply the registry's per-field overrides. * Categories do not determine a plugin's type or its content requirements. */ -export async function resolveMetadata(client: NpmClient, record: PluginRecord): Promise { +export function resolveMetadata(files: ArtifactFiles, record: PluginRecord): PluginMetadata { const artifact = record.artifact; - if (artifact.kind === "git") { - return withGitArtifact(record, (directory) => { - const base = `${artifact.remote.replace(/\.git$/, "")}/raw/${artifact.commit}/${artifact.pluginPath ? `${encodePath(artifact.pluginPath)}/` : ""}`; - return readMetadata(readOptional(directory, "paseo-plugin.json"), record, base); - }); - } - const doc = resolveVersion(await client.packument(artifact.package), artifact.version); - if (doc.dist.integrity !== artifact.integrity || doc.dist.tarball !== artifact.resolved) { - throw new Error(`${artifact.package}@${artifact.version} integrity on npm differs from the pinned record`); - } - const manifest = await client.file(artifact.package, artifact.version, "paseo-plugin.json"); - const base = `https://cdn.jsdelivr.net/npm/${artifact.package}@${artifact.version}/`; - return readMetadata(manifest, record, base); + const base = artifact.kind === "git" + ? `${artifact.remote.replace(/\.git$/, "")}/raw/${artifact.commit}/${artifact.pluginPath ? `${encodePath(artifact.pluginPath)}/` : ""}` + : `https://cdn.jsdelivr.net/npm/${artifact.package}@${artifact.version}/`; + return readMetadata(files.manifest, record, base); } function readMetadata(text: string | null, record: PluginRecord, base: string): PluginMetadata { diff --git a/scripts/lib/overview.test.ts b/scripts/lib/overview.test.ts index 108dc45..638bff9 100644 --- a/scripts/lib/overview.test.ts +++ b/scripts/lib/overview.test.ts @@ -13,7 +13,7 @@ import { run } from "./shell.ts"; import { validateArtifact } from "./validate-artifact.ts"; for (const kind of ["npm", "git"] as const) { - test(`${kind}: pinned repository overview is required except for unchanged or approved new imports`, async () => { + test(`${kind}: pinned artifact overview is required except for unchanged or approved new imports`, async () => { const directory = mkdtempSync(join(tmpdir(), "registry-required-overview-")); const pluginPath = "packages/example"; const plugin = join(directory, pluginPath); @@ -45,16 +45,18 @@ for (const kind of ["npm", "git"] as const) { dist: { integrity: "sha512-YWJj", tarball: "https://registry.npmjs.org/example.tgz" }, }; let provenanceCommit = commit; + const overviews = new Map([[doc.version, "Tarball overview"]]); + const expectedOverview = kind === "npm" ? "Tarball overview" : "Author overview"; const client: NpmClient = { async packument() { - return { name: doc.name, "dist-tags": { latest: "9.0.0" }, time: { [doc.version]: "2026-10-06" }, versions: { [doc.version]: doc } }; + return { name: doc.name, "dist-tags": { latest: "9.0.0" }, time: { [doc.version]: "2026-10-06" }, versions: Object.fromEntries([...overviews.keys()].map(version => [version, { ...doc, version }])) }; }, async file(_name, version, path) { - assert.equal(version, doc.version); + assert.ok(overviews.has(version)); // Published README/OVERVIEW content is deliberately different from the repository. return path === "paseo-plugin.json" ? '{"id":"example"}' : path === "paseo-listing.json" ? '{"readme":"README.md"}' - : "Wrong tarball content"; + : overviews.get(version)!; }, async provenance() { return { repositoryUrl: remote.replace(/\.git$/, ""), commit: provenanceCommit }; }, async tarball() { throw new Error("Plugin artifacts must not execute"); }, @@ -69,9 +71,9 @@ for (const kind of ["npm", "git"] as const) { artifact: kind === "git" ? { kind, remote, commit, tag: "v1", pluginPath } : { kind, package: doc.name, version: doc.version, resolved: doc.dist.tarball, integrity: doc.dist.integrity }, }; - assert.equal(await readAuthorOverview(client, record), "Author overview"); + assert.equal(await readAuthorOverview(client, record), expectedOverview); const published = await resolvePlugin(client, record, "Registry stopgap"); - assert.equal(published.readme, "Author overview"); + assert.equal(published.readme, expectedOverview); assert.equal(published.icon, record.listing!.icon); assert.deepEqual(published.media, record.listing!.media); assert.equal(published.publishedAt, "2026-09-20T00:00:00.000Z"); @@ -81,17 +83,25 @@ for (const kind of ["npm", "git"] as const) { assert.deepEqual(withMedia.media, media); assert.equal("screenshots" in withMedia, false); assert.deepEqual(await validateArtifact(client, record), []); + if (record.artifact.kind === "npm") { + overviews.set("2.0.0", null); + // A missing package file must not fall back to the repository, which still has it. + assert.equal(await readAuthorOverview(client, { + ...record, artifact: { ...record.artifact, version: "2.0.0" }, + }), null); + } rmSync(join(plugin, "OVERVIEW.md")); const absentCommit = pin("v2"); + overviews.set("2.0.0", null); provenanceCommit = absentCommit; const absent: PluginRecord = { ...record, repository: { ...record.repository, commit: absentCommit }, artifact: kind === "git" ? { ...record.artifact, kind, remote, commit: absentCommit, tag: "v2", pluginPath } - : { kind, package: doc.name, version: doc.version, resolved: doc.dist.tarball, integrity: doc.dist.integrity }, + : { kind, package: doc.name, version: "2.0.0", resolved: doc.dist.tarball, integrity: doc.dist.integrity }, }; // The old pin must still read its own overview after HEAD loses it. - assert.equal(await readAuthorOverview(client, record), "Author overview"); - assert.equal((await resolvePlugin(client, record, "Registry stopgap")).readme, "Author overview"); + assert.equal(await readAuthorOverview(client, record), expectedOverview); + assert.equal((await resolvePlugin(client, record, "Registry stopgap")).readme, expectedOverview); assert.equal((await resolvePlugin(client, absent, "Registry stopgap")).readme, "Registry stopgap"); assert.equal(await readAuthorOverview(client, absent), null); await assert.rejects(resolvePlugin(client, absent), /OVERVIEW.md.*required/); @@ -109,6 +119,7 @@ for (const kind of ["npm", "git"] as const) { writeFileSync(join(plugin, "OVERVIEW.md"), command); const invalidCommit = pin(`invalid-${index}`); provenanceCommit = invalidCommit; + overviews.set("2.0.0", command); const invalid = { ...absent, repository: { ...absent.repository, commit: invalidCommit }, artifact: kind === "git" ? { ...absent.artifact, kind, remote, commit: invalidCommit, tag: `invalid-${index}`, pluginPath } : absent.artifact }; await assert.rejects(resolvePlugin(client, invalid, "Registry stopgap"), /OVERVIEW.md.*install commands/); await assert.rejects(validateArtifact(client, invalid, { previous: invalid, registryOverview: "Registry stopgap" }), /OVERVIEW.md.*install commands/); diff --git a/scripts/lib/overview.ts b/scripts/lib/overview.ts index 7fd0500..df9dd4b 100644 --- a/scripts/lib/overview.ts +++ b/scripts/lib/overview.ts @@ -1,7 +1,7 @@ import { AuthorError } from "./problems.ts"; -import { readOptional, withGitArtifact, withRepositoryCommit } from "./git-artifact.ts"; -import { type NpmClient, resolveVersion } from "./npm.ts"; -import { parseRepository } from "./repository.ts"; +import { readOptional } from "./git-artifact.ts"; +import type { NpmClient } from "./npm.ts"; +import { readArtifactFiles } from "./artifact-files.ts"; import { type PluginRecord, RECORDS_DIR } from "./record.ts"; /** Registry stopgap for plugins without an author-owned overview. */ @@ -23,19 +23,13 @@ export function validateOverview(overview: string | null, source: string): strin /** The author's OVERVIEW.md as validate resolves it for this record, null when absent. */ export async function readAuthorOverview(client: NpmClient, record: PluginRecord): Promise { - const read = (directory: string) => - validateOverview(readOptional(directory, "OVERVIEW.md"), `${record.id}/OVERVIEW.md`); - if (record.artifact.kind === "git") return withGitArtifact(record, read); - if (!record.repository.commit) return null; - const doc = resolveVersion(await client.packument(record.artifact.package), record.artifact.version); - const source = parseRepository(doc.repository); - if (!source) throw new AuthorError(`${record.id}: pinned npm version has no source repository. Set package.json repository to the public source repository and publish a new version.`); - return withRepositoryCommit(source, record.repository.commit, read); + const files = await readArtifactFiles(client, record.artifact); + return validateOverview(files.overview, `${record.id}/OVERVIEW.md`); } /** The existing protocol field contains only an author overview or an import stopgap. */ export function requireOverview(author: string | null, registry: string | null, id: string): string { const overview = author ?? registry; - if (overview === null) throw new AuthorError(`${id}/OVERVIEW.md is required. Add OVERVIEW.md beside paseo-plugin.json in the source repository and publish a new release. For npm submissions, publish with provenance so the registry can verify the source commit, or submit the tagged GitHub repository instead.`); + if (overview === null) throw new AuthorError(`${id}/OVERVIEW.md is required. Add OVERVIEW.md beside paseo-plugin.json in the submitted artifact and publish a new release. For npm, include it at the published package root; for GitHub, include it in the plugin directory at the pinned commit.`); return overview; } diff --git a/scripts/lib/pin.ts b/scripts/lib/pin.ts index e3b3748..7d689b8 100644 --- a/scripts/lib/pin.ts +++ b/scripts/lib/pin.ts @@ -1,3 +1,4 @@ +import { readArtifactFiles } from "./artifact-files.ts"; import { AuthorError } from "./problems.ts"; import { isoDate } from "./dates.ts"; import { type NpmClient, resolveVersion } from "./npm.ts"; @@ -20,7 +21,11 @@ export async function pinRecord( ): Promise { const packument = await client.packument(submission.package); const doc = resolveVersion(packument, options.version); - const manifest = await client.file(doc.name, doc.version, "paseo-plugin.json"); + const artifact: PluginRecord["artifact"] = { + kind: "npm", package: doc.name, version: doc.version, + resolved: doc.dist.tarball, integrity: doc.dist.integrity, + }; + const { manifest } = await readArtifactFiles(client, artifact); if (manifest === null) { throw new AuthorError( `${doc.name}@${doc.version} does not ship paseo-plugin.json. Include it at the package root and publish a new version.`, @@ -52,13 +57,7 @@ export async function pinRecord( throw new Error("ID owner must match repository owner"); return { id: `${owner}/${slug}`, - artifact: { - kind: "npm", - package: doc.name, - version: doc.version, - resolved: doc.dist.tarball, - integrity: doc.dist.integrity, - }, + artifact, repository: repository!, categories: submission.categories, submittedAt: options.submittedAt ?? today, diff --git a/scripts/lib/validate-artifact.ts b/scripts/lib/validate-artifact.ts index d6dfb66..e436e26 100644 --- a/scripts/lib/validate-artifact.ts +++ b/scripts/lib/validate-artifact.ts @@ -1,5 +1,6 @@ +import { readArtifactFiles } from "./artifact-files.ts"; import { resolveMetadata } from "./metadata.ts"; -import { readAuthorOverview, requireOverview, validateOverview } from "./overview.ts"; +import { requireOverview, validateOverview } from "./overview.ts"; import type { NpmClient } from "./npm.ts"; import { parseArtifact, type PluginRecord } from "./record.ts"; @@ -10,7 +11,8 @@ export async function validateArtifact( context: { previous?: PluginRecord | null; registryOverview?: string | null; allowNewImport?: boolean } = {}, ): Promise { const problems: string[] = []; - const author = await readAuthorOverview(client, record); + const files = await readArtifactFiles(client, record.artifact); + const author = validateOverview(files.overview, `${record.id}/OVERVIEW.md`); const previous = context.previous; const unchanged = previous && JSON.stringify(parseArtifact(previous.artifact)) === JSON.stringify(parseArtifact(record.artifact)) && @@ -18,7 +20,7 @@ export async function validateArtifact( const imported = unchanged || (!previous && context.allowNewImport); const registry = validateOverview(context.registryOverview ?? null, `${record.id}.md`); requireOverview(author, imported ? registry : null, record.id); - await resolveMetadata(client, record); + resolveMetadata(files, record); if (record.artifact.kind === "git") return problems; const artifact = record.artifact; if (record.repository?.commit) {