Skip to content

Commit 8092bde

Browse files
bloveclaude
andcommitted
ci(dx): guard that public dev-facing functions carry a JSDoc summary
Adds scripts/check-dx-coverage.mjs — a TypeDoc walk over the chat/ag-ui/ langgraph/render public surfaces that fails when an exported function lacks a JSDoc summary (so the audited DX bar can't silently regress). Honors @internal as an escape hatch for spec-only/implementation exports. Wired as a step in the `library` CI job. Documented the one straggler it found (extractErrorMessage). Guard reports: "all 65 public functions across chat/ag-ui/langgraph/render have a JSDoc summary." Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d32a43f commit 8092bde

3 files changed

Lines changed: 105 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ jobs:
9595
- run: npx nx run-many -t test --projects=$LIBS --coverage
9696
- run: npx nx run-many -t build --projects=$LIBS --configuration=production
9797
- run: node scripts/verify-release-versions.mjs
98+
- name: DX-coverage — public dev-facing functions must have a JSDoc summary
99+
run: node scripts/check-dx-coverage.mjs
98100

99101
website:
100102
name: Website — lint / build

libs/chat/src/lib/primitives/chat-error/chat-error.component.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ import type { Agent } from '../../agent';
55
import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens';
66
import { CHAT_ERROR_STYLES } from '../../styles/chat-error.styles';
77

8+
/**
9+
* Coerce an unknown error value into a human-readable message string — reads
10+
* `.message` from `Error`s, returns strings as-is, and `String()`-casts the
11+
* rest. Useful when rendering an agent's `error` outside the built-in
12+
* `chat-error` component.
13+
*
14+
* @param error Any caught/agent error value.
15+
* @returns The message text, or `null` when `error` is nullish.
16+
* @example
17+
* ```ts
18+
* const msg = extractErrorMessage(agent.error());
19+
* ```
20+
*/
821
export function extractErrorMessage(error: unknown): string | null {
922
if (!error) return null;
1023
if (error instanceof Error) return error.message;

scripts/check-dx-coverage.mjs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env node
2+
/**
3+
* DX-coverage guard: every public exported FUNCTION in the dev-facing
4+
* `@threadplane/*` libraries must carry a non-empty JSDoc summary so app
5+
* developers get hover guidance on the surface they actually call.
6+
*
7+
* Symbols tagged `@internal` are exempt (spec-only / implementation exports).
8+
* Run: `node scripts/check-dx-coverage.mjs` — exits non-zero on violations.
9+
*/
10+
import { Application, TSConfigReader, ReflectionKind } from 'typedoc';
11+
import fs from 'fs';
12+
import path from 'path';
13+
14+
const LIBRARIES = [
15+
{ slug: 'chat', entryPoints: ['libs/chat/src/public-api.ts'] },
16+
{ slug: 'ag-ui', entryPoints: ['libs/ag-ui/src/public-api.ts'] },
17+
{ slug: 'langgraph', entryPoints: ['libs/langgraph/src/public-api.ts'] },
18+
{ slug: 'render', entryPoints: ['libs/render/src/public-api.ts'] },
19+
];
20+
21+
function summaryText(comment) {
22+
if (!comment?.summary) return '';
23+
return comment.summary.map((p) => p.text ?? '').join('').trim();
24+
}
25+
26+
function isInternal(reflection, signature) {
27+
const tagged = (c) => !!c?.blockTags?.some((t) => t.tag === '@internal') || !!c?.modifierTags?.has?.('@internal');
28+
return tagged(reflection.comment) || tagged(signature?.comment);
29+
}
30+
31+
/** A function is documented if either the reflection or its call signature has a summary. */
32+
function functionHasSummary(reflection) {
33+
const sig = reflection.signatures?.[0];
34+
return summaryText(reflection.comment).length > 0 || summaryText(sig?.comment).length > 0;
35+
}
36+
37+
function* walk(reflections) {
38+
for (const ref of reflections ?? []) {
39+
yield ref;
40+
if (ref.children) yield* walk(ref.children);
41+
}
42+
}
43+
44+
async function main() {
45+
const violations = [];
46+
let checked = 0;
47+
48+
for (const lib of LIBRARIES) {
49+
const missing = lib.entryPoints.filter((p) => !fs.existsSync(p));
50+
if (missing.length) {
51+
console.error(`✗ ${lib.slug}: entry point(s) not found: ${missing.join(', ')}`);
52+
process.exitCode = 1;
53+
continue;
54+
}
55+
const libDir = path.dirname(path.dirname(lib.entryPoints[0]));
56+
const libTsconfig = fs.existsSync(path.join(libDir, 'tsconfig.lib.json'))
57+
? path.join(libDir, 'tsconfig.lib.json')
58+
: undefined;
59+
60+
const app = await Application.bootstrapWithPlugins({
61+
entryPoints: lib.entryPoints,
62+
skipErrorChecking: true,
63+
excludeInternal: true,
64+
...(libTsconfig ? { tsconfig: libTsconfig } : {}),
65+
});
66+
app.options.addReader(new TSConfigReader());
67+
const project = await app.convert();
68+
if (!project) throw new Error(`TypeDoc failed to convert ${lib.slug}`);
69+
70+
for (const ref of walk(project.children)) {
71+
if (ref.kind !== ReflectionKind.Function) continue;
72+
if (isInternal(ref, ref.signatures?.[0])) continue;
73+
checked++;
74+
if (!functionHasSummary(ref)) {
75+
violations.push(`@threadplane/${lib.slug} :: ${ref.name}()`);
76+
}
77+
}
78+
}
79+
80+
if (violations.length) {
81+
console.error(`\n✗ DX-coverage: ${violations.length} public function(s) missing a JSDoc summary:\n`);
82+
for (const v of violations.sort()) console.error(` - ${v}`);
83+
console.error(`\nAdd a one-line summary (and ideally an @example), or mark the symbol @internal if it is not public API.`);
84+
process.exit(1);
85+
}
86+
87+
console.log(`✓ DX-coverage: all ${checked} public functions across chat/ag-ui/langgraph/render have a JSDoc summary.`);
88+
}
89+
90+
main().catch((e) => { console.error(e); process.exit(1); });

0 commit comments

Comments
 (0)