diff --git a/package.json b/package.json index db71f1b86..92217c8df 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "worktree:setup": "node scripts/worktree.mjs setup", "cloudflare:token": "node scripts/refresh-cloudflare-tunnel-token.mjs", "prepare": "simple-git-hooks", - "lint": "pnpm --filter @sentry/junior lint && pnpm --filter @sentry/junior-memory lint && pnpm ast-grep:lint && pnpm package:lint", + "lint": "pnpm --filter @sentry/junior lint && pnpm --filter @sentry/junior-memory lint && pnpm ast-grep:lint && node scripts/checks.mjs && pnpm package:lint", "lint:fix": "pnpm --filter @sentry/junior lint:fix", "ast-grep:lint": "ast-grep scan", "lint-staged": "lint-staged", diff --git a/scripts/checks.mjs b/scripts/checks.mjs new file mode 100644 index 000000000..e52e35e7b --- /dev/null +++ b/scripts/checks.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import { runPublicApiDocsCheck } from "./lib/checks/public-api-docs.mjs"; + +const checks = [ + { + name: "public-api-docs", + run: runPublicApiDocsCheck, + }, +]; + +let failed = false; + +for (const check of checks) { + const result = check.run(); + + if (result.ok) { + console.log(`${check.name}: OK - ${result.summary}`); + continue; + } + + failed = true; + console.error(`${check.name}: failed`); + + for (const detail of result.details ?? []) { + console.error(` ${detail}`); + } +} + +if (failed) { + process.exit(1); +} diff --git a/scripts/lib/checks/public-api-docs-baseline.json b/scripts/lib/checks/public-api-docs-baseline.json new file mode 100644 index 000000000..29a07110b --- /dev/null +++ b/scripts/lib/checks/public-api-docs-baseline.json @@ -0,0 +1,138 @@ +[ + "packages/junior-dashboard/src/app.ts:JuniorDashboardOptions:InterfaceDeclaration", + "packages/junior-dashboard/src/index.ts:JuniorDashboardPluginOptions:InterfaceDeclaration", + "packages/junior-dashboard/src/nitro.ts:JuniorDashboardNitroOptions:InterfaceDeclaration", + "packages/junior-github/index.d.ts:GitHubAppPermissionLevel:TypeAliasDeclaration", + "packages/junior-memory/src/store.ts:ArchiveMemoryInput:TypeAliasDeclaration", + "packages/junior-memory/src/store.ts:CreateMemoryInput:TypeAliasDeclaration", + "packages/junior-memory/src/store.ts:ListMemoriesInput:TypeAliasDeclaration", + "packages/junior-memory/src/store.ts:MemoryDb:TypeAliasDeclaration", + "packages/junior-memory/src/store.ts:MemoryEmbeddingProvider:InterfaceDeclaration", + "packages/junior-memory/src/store.ts:MemoryRecord:TypeAliasDeclaration", + "packages/junior-memory/src/store.ts:MemoryStoreOptions:InterfaceDeclaration", + "packages/junior-memory/src/store.ts:SearchMemoriesInput:TypeAliasDeclaration", + "packages/junior-memory/src/types.ts:MemoryRuntimeContext:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:Destination:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:InvocationContext:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:LocalDestination:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:LocalInvocationContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/context.ts:LocalRequester:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:LocalSource:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:PluginContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/context.ts:PluginEmbedder:InterfaceDeclaration", + "packages/junior-plugin-api/src/context.ts:PluginLogger:InterfaceDeclaration", + "packages/junior-plugin-api/src/context.ts:PluginMetadata:InterfaceDeclaration", + "packages/junior-plugin-api/src/context.ts:PluginModel:InterfaceDeclaration", + "packages/junior-plugin-api/src/context.ts:Requester:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:SlackDestination:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:SlackInvocationContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/context.ts:SlackRequester:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:SlackSource:TypeAliasDeclaration", + "packages/junior-plugin-api/src/context.ts:Source:TypeAliasDeclaration", + "packages/junior-plugin-api/src/credentials.ts:EgressHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/credentials.ts:EgressResponseHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/credentials.ts:IssueCredentialHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/credentials.ts:PluginCredentialActor:TypeAliasDeclaration", + "packages/junior-plugin-api/src/credentials.ts:PluginCredentialResult:TypeAliasDeclaration", + "packages/junior-plugin-api/src/credentials.ts:PluginCredentialSubject:TypeAliasDeclaration", + "packages/junior-plugin-api/src/credentials.ts:PluginEgressResponse:InterfaceDeclaration", + "packages/junior-plugin-api/src/credentials.ts:PluginGrantAccess:TypeAliasDeclaration", + "packages/junior-plugin-api/src/credentials.ts:PluginResolvedCredentialUser:InterfaceDeclaration", + "packages/junior-plugin-api/src/credentials.ts:PluginStoredTokens:InterfaceDeclaration", + "packages/junior-plugin-api/src/credentials.ts:PluginTokenStore:InterfaceDeclaration", + "packages/junior-plugin-api/src/credentials.ts:PluginUserTokenSlot:InterfaceDeclaration", + "packages/junior-plugin-api/src/credentials.ts:ResolveOAuthAccountHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/dispatch.ts:Dispatch:InterfaceDeclaration", + "packages/junior-plugin-api/src/dispatch.ts:DispatchOptions:TypeAliasDeclaration", + "packages/junior-plugin-api/src/dispatch.ts:DispatchResult:InterfaceDeclaration", + "packages/junior-plugin-api/src/hooks.ts:PluginHooks:InterfaceDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginCredentials:TypeAliasDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginEnvVarDeclaration:InterfaceDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginManifest:InterfaceDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginMcpConfig:InterfaceDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginNpmRuntimeDependency:InterfaceDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginOAuthBearerCredentials:InterfaceDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginOAuthConfig:InterfaceDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginRuntimeDependency:TypeAliasDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginRuntimePostinstallCommand:InterfaceDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginSystemRuntimeDependency:InterfaceDeclaration", + "packages/junior-plugin-api/src/manifest.ts:PluginSystemRuntimeDependencyFromUrl:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:HeartbeatHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:HeartbeatResult:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:OperationalReportHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginConversationStatus:TypeAliasDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginConversationSummary:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginConversations:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginOperationalField:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginOperationalMetric:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginOperationalRecord:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginOperationalRecordSet:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginOperationalReport:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginOperationalReportContent:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginOperationalTone:TypeAliasDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginRoute:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginRouteHandler:TypeAliasDeclaration", + "packages/junior-plugin-api/src/operations.ts:PluginRouteMethod:TypeAliasDeclaration", + "packages/junior-plugin-api/src/operations.ts:RouteRegistrationHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:SlackConversationLink:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:SlackConversationLinkHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:StorageMigrationContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/operations.ts:StorageMigrationResult:InterfaceDeclaration", + "packages/junior-plugin-api/src/registration.ts:PluginRegistration:InterfaceDeclaration", + "packages/junior-plugin-api/src/registration.ts:PluginRegistrationInput:TypeAliasDeclaration", + "packages/junior-plugin-api/src/state.ts:PluginReadState:InterfaceDeclaration", + "packages/junior-plugin-api/src/state.ts:PluginState:InterfaceDeclaration", + "packages/junior-plugin-api/src/tools.ts:BeforeToolExecuteHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/tools.ts:PluginDecision:InterfaceDeclaration", + "packages/junior-plugin-api/src/tools.ts:PluginEnv:InterfaceDeclaration", + "packages/junior-plugin-api/src/tools.ts:PluginSandbox:InterfaceDeclaration", + "packages/junior-plugin-api/src/tools.ts:PluginToolDefinition:InterfaceDeclaration", + "packages/junior-plugin-api/src/tools.ts:PluginToolExecute:TypeAliasDeclaration", + "packages/junior-plugin-api/src/tools.ts:PluginToolExecuteOptions:InterfaceDeclaration", + "packages/junior-plugin-api/src/tools.ts:SandboxPrepareHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/tools.ts:SlackToolRegistrationHookContext:InterfaceDeclaration", + "packages/junior-plugin-api/src/tools.ts:ToolRegistrationHookContext:TypeAliasDeclaration", + "packages/junior-scheduler/src/schedule-tools.ts:SchedulerToolContext:InterfaceDeclaration", + "packages/junior-scheduler/src/store.ts:SchedulerDb:TypeAliasDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledCalendarFrequency:TypeAliasDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledLocalTime:InterfaceDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledRun:InterfaceDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledRunStatus:TypeAliasDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledTask:InterfaceDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledTaskConversationAccess:InterfaceDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledTaskExecutionActor:InterfaceDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledTaskPrincipal:InterfaceDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledTaskRecurrence:InterfaceDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledTaskSchedule:InterfaceDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledTaskSpec:InterfaceDeclaration", + "packages/junior-scheduler/src/types.ts:ScheduledTaskStatus:TypeAliasDeclaration", + "packages/junior/src/app.ts:JuniorAppOptions:InterfaceDeclaration", + "packages/junior/src/nitro.ts:JuniorNitroOptions:InterfaceDeclaration", + "packages/junior/src/nitro.ts:JuniorNitroPluginSource:TypeAliasDeclaration", + "packages/junior/src/nitro.ts:JuniorPluginModuleReference:InterfaceDeclaration", + "packages/junior/src/plugins.ts:JuniorPluginInput:TypeAliasDeclaration", + "packages/junior/src/plugins.ts:JuniorPluginSetOptions:InterfaceDeclaration", + "packages/junior/src/reporting.ts:HealthReport:InterfaceDeclaration", + "packages/junior/src/reporting.ts:JuniorReporting:InterfaceDeclaration", + "packages/junior/src/reporting.ts:PluginOperationalReportFeed:InterfaceDeclaration", + "packages/junior/src/reporting.ts:PluginPackageContentItemReport:InterfaceDeclaration", + "packages/junior/src/reporting.ts:PluginPackageContentReport:InterfaceDeclaration", + "packages/junior/src/reporting.ts:PluginReport:InterfaceDeclaration", + "packages/junior/src/reporting.ts:RuntimeInfoReport:InterfaceDeclaration", + "packages/junior/src/reporting.ts:SkillReport:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:ConversationFeed:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:ConversationReport:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:ConversationReportStatus:TypeAliasDeclaration", + "packages/junior/src/reporting/conversations.ts:ConversationRunReport:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:ConversationStatsItem:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:ConversationStatsReport:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:ConversationSummaryReport:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:ConversationSurface:TypeAliasDeclaration", + "packages/junior/src/reporting/conversations.ts:ConversationUsage:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:RequesterIdentity:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:TranscriptMessage:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:TranscriptPart:InterfaceDeclaration", + "packages/junior/src/reporting/conversations.ts:TranscriptPartType:TypeAliasDeclaration", + "packages/junior/src/reporting/conversations.ts:TranscriptRole:TypeAliasDeclaration", + "packages/junior/src/vercel.ts:JuniorVercelConfigOptions:InterfaceDeclaration" +] diff --git a/scripts/lib/checks/public-api-docs.mjs b/scripts/lib/checks/public-api-docs.mjs new file mode 100644 index 000000000..039bb3dbc --- /dev/null +++ b/scripts/lib/checks/public-api-docs.mjs @@ -0,0 +1,406 @@ +import fs from "node:fs"; +import path from "node:path"; +import { createRequire } from "node:module"; + +const root = process.cwd(); +const requireFromJunior = createRequire( + path.join(root, "packages/junior/package.json"), +); +const ts = requireFromJunior("typescript"); +const baselinePath = "scripts/lib/checks/public-api-docs-baseline.json"; + +const CHECKED_KINDS = new Set([ + ts.SyntaxKind.ClassDeclaration, + ts.SyntaxKind.EnumDeclaration, + ts.SyntaxKind.FunctionDeclaration, + ts.SyntaxKind.InterfaceDeclaration, + ts.SyntaxKind.TypeAliasDeclaration, +]); + +function readJson(relativePath) { + return JSON.parse(fs.readFileSync(path.join(root, relativePath), "utf8")); +} + +function exists(relativePath) { + return fs.existsSync(path.join(root, relativePath)); +} + +function normalizeRelativePath(filePath) { + return path.relative(root, filePath).split(path.sep).join("/"); +} + +function resolveSourceEntrypoint(packageDir, exportedTypesPath) { + if (typeof exportedTypesPath !== "string") { + return null; + } + + const withoutDot = exportedTypesPath.replace(/^\.\//, ""); + + if (withoutDot.endsWith(".d.ts")) { + if (!withoutDot.startsWith("dist/")) { + const relativePath = `${packageDir}/${withoutDot}`; + return exists(relativePath) ? relativePath : null; + } + + const sourcePath = `src/${withoutDot.slice("dist/".length, -".d.ts".length)}.ts`; + const relativePath = `${packageDir}/${sourcePath}`; + return exists(relativePath) ? relativePath : null; + } + + if (!withoutDot.endsWith(".ts") || !withoutDot.startsWith("src/")) { + return null; + } + + const relativePath = `${packageDir}/${withoutDot}`; + return exists(relativePath) ? relativePath : null; +} + +function exportedTypesPaths(exportsField) { + if (!exportsField) { + return []; + } + + if (typeof exportsField === "string") { + return [exportsField]; + } + + if (typeof exportsField.types === "string") { + return [exportsField.types]; + } + + return Object.values(exportsField).flatMap((entry) => { + if (typeof entry === "string") { + return [entry]; + } + + if (entry && typeof entry === "object" && typeof entry.types === "string") { + return [entry.types]; + } + + return []; + }); +} + +function collectEntrypoints() { + const packagesDir = path.join(root, "packages"); + + return fs + .readdirSync(packagesDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .flatMap((entry) => { + const packageDir = `packages/${entry.name}`; + const packageJsonPath = `${packageDir}/package.json`; + + if (!exists(packageJsonPath)) { + return []; + } + + const packageJson = readJson(packageJsonPath); + + if (packageJson.private === true) { + return []; + } + + return exportedTypesPaths(packageJson.exports) + .map((typesPath) => resolveSourceEntrypoint(packageDir, typesPath)) + .filter((entrypoint) => entrypoint !== null); + }) + .filter((entrypoint, index, entrypoints) => { + return entrypoints.indexOf(entrypoint) === index; + }) + .sort(); +} + +const sourceFileCache = new Map(); + +function readSourceFile(filePath) { + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(root, filePath); + const normalizedPath = normalizeRelativePath(absolutePath); + const cached = sourceFileCache.get(normalizedPath); + + if (cached) { + return cached; + } + + const sourceFile = ts.createSourceFile( + absolutePath, + fs.readFileSync(absolutePath, "utf8"), + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + + sourceFileCache.set(normalizedPath, sourceFile); + return sourceFile; +} + +function hasExportModifier(node) { + return Boolean( + node.modifiers?.some((modifier) => { + return modifier.kind === ts.SyntaxKind.ExportKeyword; + }), + ); +} + +function declarationName(node) { + if (node.name && ts.isIdentifier(node.name)) { + return node.name.text; + } + + return null; +} + +function isCheckedDeclaration(node) { + return CHECKED_KINDS.has(node.kind); +} + +function declarationHasJSDoc(node) { + return ts.getJSDocCommentsAndTags(node).some((commentOrTag) => { + const comment = + "comment" in commentOrTag && typeof commentOrTag.comment === "string" + ? commentOrTag.comment.trim() + : ""; + + return comment.length > 0 || ts.isJSDoc(commentOrTag); + }); +} + +function collectLocalDeclarations(sourceFile) { + const declarations = new Map(); + + for (const statement of sourceFile.statements) { + if (!isCheckedDeclaration(statement)) { + continue; + } + + const name = declarationName(statement); + + if (name) { + declarations.set(name, statement); + } + } + + return declarations; +} + +function resolveModuleSpecifier(sourceFile, moduleSpecifier) { + if (!moduleSpecifier.startsWith(".")) { + return null; + } + + const sourceDir = path.dirname(sourceFile.fileName); + const targetBase = path.resolve(sourceDir, moduleSpecifier); + const candidates = [ + `${targetBase}.ts`, + `${targetBase}.tsx`, + path.join(targetBase, "index.ts"), + path.join(targetBase, "index.tsx"), + ]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return normalizeRelativePath(candidate); + } + } + + return null; +} + +function exportNameFromDeclaration(statement) { + if ( + statement.modifiers?.some((modifier) => { + return modifier.kind === ts.SyntaxKind.DefaultKeyword; + }) + ) { + return "default"; + } + + return declarationName(statement); +} + +function buildExportMap(relativePath, seen = new Set()) { + const sourceFile = readSourceFile(relativePath); + const normalizedPath = normalizeRelativePath(sourceFile.fileName); + + if (seen.has(normalizedPath)) { + return new Map(); + } + + seen.add(normalizedPath); + + const localDeclarations = collectLocalDeclarations(sourceFile); + const exports = new Map(); + + for (const statement of sourceFile.statements) { + if (isCheckedDeclaration(statement) && hasExportModifier(statement)) { + const exportName = exportNameFromDeclaration(statement); + + if (exportName) { + exports.set(exportName, { + declaration: statement, + sourceFile, + exportName, + }); + } + + continue; + } + + if (!ts.isExportDeclaration(statement)) { + continue; + } + + const modulePath = statement.moduleSpecifier + ? resolveModuleSpecifier(sourceFile, statement.moduleSpecifier.text) + : normalizedPath; + const sourceExports = + modulePath === normalizedPath + ? new Map( + [...localDeclarations].map(([name, declaration]) => [ + name, + { declaration, sourceFile, exportName: name }, + ]), + ) + : modulePath + ? buildExportMap(modulePath, seen) + : new Map(); + + if (!statement.exportClause) { + for (const [name, declaration] of sourceExports) { + exports.set(name, declaration); + } + + continue; + } + + if (!ts.isNamedExports(statement.exportClause)) { + continue; + } + + for (const element of statement.exportClause.elements) { + const localName = element.propertyName?.text ?? element.name.text; + const exportedName = element.name.text; + const exportedDeclaration = sourceExports.get(localName); + + if (exportedDeclaration) { + exports.set(exportedName, { + ...exportedDeclaration, + exportName: exportedName, + }); + } + } + } + + seen.delete(normalizedPath); + return exports; +} + +function lineAndColumn(sourceFile, node) { + const position = sourceFile.getLineAndCharacterOfPosition(node.getStart()); + return { + line: position.line + 1, + column: position.character + 1, + }; +} + +export function runPublicApiDocsCheck() { + const entrypoints = collectEntrypoints(); + const failures = []; + const checkedDeclarations = new Set(); + const currentMissingDocs = new Set(); + const baseline = exists(baselinePath) + ? new Set(readJson(baselinePath)) + : new Set(); + + for (const entrypoint of entrypoints) { + for (const declarationInfo of buildExportMap(entrypoint).values()) { + const sourcePath = normalizeRelativePath( + declarationInfo.sourceFile.fileName, + ); + const name = declarationName(declarationInfo.declaration); + + if (!name) { + continue; + } + + const key = `${sourcePath}:${declarationInfo.declaration.pos}:${name}`; + + if (checkedDeclarations.has(key)) { + continue; + } + + checkedDeclarations.add(key); + + const baselineKey = `${sourcePath}:${name}:${ts.SyntaxKind[declarationInfo.declaration.kind]}`; + + if (declarationHasJSDoc(declarationInfo.declaration)) { + continue; + } + + currentMissingDocs.add(baselineKey); + + if (baseline.has(baselineKey)) { + continue; + } + + const location = lineAndColumn( + declarationInfo.sourceFile, + declarationInfo.declaration, + ); + + failures.push({ + kind: ts.SyntaxKind[declarationInfo.declaration.kind], + line: location.line, + name, + sourcePath, + }); + } + } + + const staleBaseline = [...baseline].filter((entry) => { + return !currentMissingDocs.has(entry); + }); + + if (process.env.UPDATE_PUBLIC_API_DOCS_BASELINE === "1") { + fs.writeFileSync( + path.join(root, baselinePath), + `${JSON.stringify([...currentMissingDocs].sort(), null, 2)}\n`, + ); + return { + ok: true, + summary: `updated ${baselinePath}: ${currentMissingDocs.size} missing public API docs baselined`, + }; + } + + if (failures.length > 0 || staleBaseline.length > 0) { + const details = []; + + for (const failure of failures) { + details.push( + `${failure.sourcePath}:${failure.line} ${failure.name} (${failure.kind}) needs a JSDoc comment.`, + ); + } + + for (const entry of staleBaseline) { + details.push( + `${baselinePath} has stale entry ${entry}. Remove it from the baseline.`, + ); + } + + details.push( + "Add brief JSDoc explaining public API intent, not a restatement of the declaration.", + ); + + return { + ok: false, + details, + }; + } + + return { + ok: true, + summary: `checked ${checkedDeclarations.size} declarations from ${entrypoints.length} entrypoints (${baseline.size} baselined)`, + }; +}