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
4 changes: 3 additions & 1 deletion apps/website/content/docs/ag-ui/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,9 @@
"type": "Provider[]",
"description": ""
},
"examples": []
"examples": [
"```ts\nTestBed.configureTestingModule({\n providers: [provideFakeAgent({ responses: ['Hello from the fake agent'] })],\n});\n```"
]
},
{
"name": "toAgent",
Expand Down
16 changes: 12 additions & 4 deletions apps/website/content/docs/chat/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -8181,7 +8181,9 @@
"type": "AngularRegistry",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst registry = toRenderRegistry(views({ metric: MetricCardComponent }));\n```"
]
},
{
"name": "validateArgs",
Expand Down Expand Up @@ -8258,7 +8260,9 @@
"type": "ViewRegistry",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst registry = views({ metric: MetricCardComponent, chart: ChartComponent });\n// providers: [provideViews(registry)]\n```"
]
},
{
"name": "withoutViews",
Expand All @@ -8283,7 +8287,9 @@
"type": "ViewRegistry",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst trimmed = withoutViews(a2uiBasicCatalog(), 'Video', 'AudioPlayer');\n```"
]
},
{
"name": "withViews",
Expand All @@ -8308,7 +8314,9 @@
"type": "ViewRegistry",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst extended = withViews(a2uiBasicCatalog(), { MyWidget: MyWidgetComponent });\n```"
]
},
{
"name": "AbstractEvent",
Expand Down
8 changes: 6 additions & 2 deletions apps/website/content/docs/langgraph/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2474,7 +2474,9 @@
"type": "MockLangGraphAgent<>",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst agent = mockLangGraphAgent({ isThreadLoading: true });\nagent.messages.set([{ id: '1', role: 'assistant', content: 'Hi' }]);\n```"
]
},
{
"name": "provideAgent",
Expand Down Expand Up @@ -2521,7 +2523,9 @@
"type": "Provider[]",
"description": ""
},
"examples": []
"examples": [
"```ts\nTestBed.configureTestingModule({\n providers: [provideFakeAgent({ responses: ['Hi from the fake LangGraph agent'] })],\n});\n```"
]
},
{
"name": "refreshOnRunEnd",
Expand Down
26 changes: 19 additions & 7 deletions apps/website/content/docs/render/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -723,14 +723,16 @@
{
"name": "injectRenderHost",
"kind": "function",
"description": "Obtain the element-scoped RenderHost from inside a mounted view component.",
"description": "Obtain the element-scoped RenderHost from inside a mounted view\ncomponent — use it to write state, emit events, or announce a result back to\nthe render engine.",
"signature": "injectRenderHost(): RenderHost",
"params": [],
"returns": {
"type": "RenderHost",
"description": ""
},
"examples": []
"examples": [
"```ts\n\\@Component({ ... })\nclass ApproveButton {\n private host = injectRenderHost();\n approve() { this.host.result('approved'); }\n}\n```"
]
},
{
"name": "overrideViews",
Expand All @@ -755,7 +757,9 @@
"type": "ViewRegistry",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst themed = overrideViews(a2uiBasicCatalog(), { Card: MyCardComponent });\n```"
]
},
{
"name": "provideRender",
Expand Down Expand Up @@ -837,7 +841,9 @@
"type": "AngularRegistry",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst registry = toRenderRegistry(views({ metric: MetricCardComponent }));\n```"
]
},
{
"name": "views",
Expand All @@ -856,7 +862,9 @@
"type": "ViewRegistry",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst registry = views({ metric: MetricCardComponent, chart: ChartComponent });\n// providers: [provideViews(registry)]\n```"
]
},
{
"name": "withoutViews",
Expand All @@ -881,7 +889,9 @@
"type": "ViewRegistry",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst trimmed = withoutViews(a2uiBasicCatalog(), 'Video', 'AudioPlayer');\n```"
]
},
{
"name": "withViews",
Expand All @@ -906,6 +916,8 @@
"type": "ViewRegistry",
"description": ""
},
"examples": []
"examples": [
"```ts\nconst extended = withViews(a2uiBasicCatalog(), { MyWidget: MyWidgetComponent });\n```"
]
}
]
7 changes: 7 additions & 0 deletions libs/ag-ui/src/lib/testing/provide-fake-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import { FakeAgent } from './fake-agent';
*
* Use for offline demos and development. Drop-in replacement for
* provideAgent({ url }) when no real backend is available.
*
* @example
* ```ts
* TestBed.configureTestingModule({
* providers: [provideFakeAgent({ responses: ['Hello from the fake agent'] })],
* });
* ```
*/
export function provideFakeAgent(config: FakeAgentConfig = {}): Provider[] {
return [
Expand Down
6 changes: 6 additions & 0 deletions libs/langgraph/src/lib/testing/mock-langgraph-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ export interface MockLangGraphAgent extends LangGraphAgent<any, any> {
*
* Neutral `Agent`-contract signals come from {@link mockAgent}; LangGraph-specific
* signals are declared here and layered on top.
*
* @example
* ```ts
* const agent = mockLangGraphAgent({ isThreadLoading: true });
* agent.messages.set([{ id: '1', role: 'assistant', content: 'Hi' }]);
* ```
*/
export function mockLangGraphAgent(
initial: MockAgentOptions & {
Expand Down
7 changes: 7 additions & 0 deletions libs/langgraph/src/lib/testing/provide-fake-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import { FakeStreamTransport } from './fake-stream.transport';
* advanced manual scripting (tool calls, interrupts, multi-batch), provide
* the agent yourself with
* `provideAgent({ assistantId, transport: new MockAgentTransport(...) })`.
*
* @example
* ```ts
* TestBed.configureTestingModule({
* providers: [provideFakeAgent({ responses: ['Hi from the fake LangGraph agent'] })],
* });
* ```
*/
export function provideFakeAgent(config: FakeAgentConfig = {}): Provider[] {
return provideAgent({
Expand Down
15 changes: 14 additions & 1 deletion libs/render/src/lib/contexts/render-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,20 @@ export interface RenderHost {

export const RENDER_HOST = new InjectionToken<RenderHost>('RENDER_HOST');

/** Obtain the element-scoped RenderHost from inside a mounted view component. */
/**
* Obtain the element-scoped {@link RenderHost} from inside a mounted view
* component — use it to write state, emit events, or announce a result back to
* the render engine.
*
* @example
* ```ts
* \@Component({ ... })
* class ApproveButton {
* private host = injectRenderHost();
* approve() { this.host.result('approved'); }
* }
* ```
*/
export function injectRenderHost(): RenderHost {
return inject(RENDER_HOST);
}
26 changes: 26 additions & 0 deletions libs/render/src/lib/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export type ViewRegistry = Readonly<Record<string, Type<unknown> | RenderViewEnt

/**
* Creates a view registry from a name → component map.
*
* @example
* ```ts
* const registry = views({ metric: MetricCardComponent, chart: ChartComponent });
* // providers: [provideViews(registry)]
* ```
*/
export function views(map: Record<string, Type<unknown> | RenderViewEntry>): ViewRegistry {
return Object.freeze({ ...map });
Expand All @@ -20,6 +26,11 @@ export function views(map: Record<string, Type<unknown> | RenderViewEntry>): Vie
/**
* Adds views to a registry without overwriting existing entries.
* New keys are added; keys that already exist in `base` are preserved.
*
* @example
* ```ts
* const extended = withViews(a2uiBasicCatalog(), { MyWidget: MyWidgetComponent });
* ```
*/
export function withViews(
base: ViewRegistry,
Expand All @@ -32,6 +43,11 @@ export function withViews(
* Replaces views in a registry. Keys in `overrides` win over `base`.
* Use this to swap an existing renderer; use `withViews` to add NEW
* node types without touching existing entries.
*
* @example
* ```ts
* const themed = overrideViews(a2uiBasicCatalog(), { Card: MyCardComponent });
* ```
*/
export function overrideViews(
base: ViewRegistry,
Expand All @@ -42,6 +58,11 @@ export function overrideViews(

/**
* Removes views from a registry by name.
*
* @example
* ```ts
* const trimmed = withoutViews(a2uiBasicCatalog(), 'Video', 'AudioPlayer');
* ```
*/
export function withoutViews(
base: ViewRegistry,
Expand All @@ -54,6 +75,11 @@ export function withoutViews(

/**
* Converts a ViewRegistry to an AngularRegistry for use with RenderSpecComponent.
*
* @example
* ```ts
* const registry = toRenderRegistry(views({ metric: MetricCardComponent }));
* ```
*/
export function toRenderRegistry(registry: ViewRegistry): AngularRegistry {
return defineAngularRegistry(registry);
Expand Down
49 changes: 38 additions & 11 deletions scripts/check-dx-coverage.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/usr/bin/env node
/**
* DX-coverage guard: every public exported FUNCTION in the dev-facing
* `@threadplane/*` libraries must carry a non-empty JSDoc summary so app
* developers get hover guidance on the surface they actually call.
* DX-coverage guard for the dev-facing `@threadplane/*` libraries:
* 1. Every public exported FUNCTION must carry a non-empty JSDoc summary.
* 2. Functions on the AUTHORING SURFACE — the ones developers call to wire/use
* the framework (`provide*`/`inject*`/`mock*`, the client-tool builders,
* and the view-registry helpers) — must additionally carry an `@example`.
*
* Symbols tagged `@internal` are exempt (spec-only / implementation exports).
* Run: `node scripts/check-dx-coverage.mjs` — exits non-zero on violations.
Expand All @@ -11,6 +13,14 @@ import { Application, TSConfigReader, ReflectionKind } from 'typedoc';
import fs from 'fs';
import path from 'path';

/** Functions a developer calls to wire/use the framework — these must have an @example. */
const AUTHORING_EXACT = new Set([
'tools', 'action', 'view', 'ask',
'views', 'withViews', 'withoutViews', 'overrideViews', 'toRenderRegistry',
'defineAngularRegistry', 'createAgentRef', 'signalStateStore',
]);
const isAuthoringSurface = (name) => /^(provide|inject|mock)/.test(name) || AUTHORING_EXACT.has(name);

const LIBRARIES = [
{ slug: 'chat', entryPoints: ['libs/chat/src/public-api.ts'] },
{ slug: 'ag-ui', entryPoints: ['libs/ag-ui/src/public-api.ts'] },
Expand All @@ -34,6 +44,12 @@ function functionHasSummary(reflection) {
return summaryText(reflection.comment).length > 0 || summaryText(sig?.comment).length > 0;
}

const hasExampleTag = (comment) => !!comment?.blockTags?.some((t) => t.tag === '@example');
/** Whether the function (reflection or its call signature) carries an `@example` tag. */
function functionHasExample(reflection) {
return hasExampleTag(reflection.comment) || hasExampleTag(reflection.signatures?.[0]?.comment);
}

function* walk(reflections) {
for (const ref of reflections ?? []) {
yield ref;
Expand All @@ -42,8 +58,10 @@ function* walk(reflections) {
}

async function main() {
const violations = [];
const summaryViolations = [];
const exampleViolations = [];
let checked = 0;
let authoring = 0;

for (const lib of LIBRARIES) {
const missing = lib.entryPoints.filter((p) => !fs.existsSync(p));
Expand Down Expand Up @@ -71,20 +89,29 @@ async function main() {
if (ref.kind !== ReflectionKind.Function) continue;
if (isInternal(ref, ref.signatures?.[0])) continue;
checked++;
if (!functionHasSummary(ref)) {
violations.push(`@threadplane/${lib.slug} :: ${ref.name}()`);
const label = `@threadplane/${lib.slug} :: ${ref.name}()`;
if (!functionHasSummary(ref)) summaryViolations.push(label);
if (isAuthoringSurface(ref.name)) {
authoring++;
if (!functionHasExample(ref)) exampleViolations.push(label);
}
}
}

if (violations.length) {
console.error(`\n✗ DX-coverage: ${violations.length} public function(s) missing a JSDoc summary:\n`);
for (const v of violations.sort()) console.error(` - ${v}`);
console.error(`\nAdd a one-line summary (and ideally an @example), or mark the symbol @internal if it is not public API.`);
if (summaryViolations.length || exampleViolations.length) {
if (summaryViolations.length) {
console.error(`\n✗ DX-coverage: ${summaryViolations.length} public function(s) missing a JSDoc summary:\n`);
for (const v of summaryViolations.sort()) console.error(` - ${v}`);
}
if (exampleViolations.length) {
console.error(`\n✗ DX-coverage: ${exampleViolations.length} authoring-surface function(s) missing an @example:\n`);
for (const v of exampleViolations.sort()) console.error(` - ${v}`);
}
console.error(`\nAdd the missing JSDoc (summary, and @example for authoring-surface APIs), or mark the symbol @internal if it is not public API.`);
process.exit(1);
}

console.log(`✓ DX-coverage: all ${checked} public functions across chat/ag-ui/langgraph/render have a JSDoc summary.`);
console.log(`✓ DX-coverage: all ${checked} public functions have a summary; all ${authoring} authoring-surface functions have an @example (chat/ag-ui/langgraph/render).`);
}

main().catch((e) => { console.error(e); process.exit(1); });
Loading