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
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ Prefix: **`DF`**. Codes are sequential 4-digit numbers (e.g. `DF0033`). Check th
import { diagnostics } from './diagnostics'

// For thrown errors — always prefix with `throw` for TypeScript control flow:
throw diagnostics.DF0033.throw({ name })
throw diagnostics.DF0033({ id, reason })

// For reported warnings/errors (not thrown). The default console method is `warn`;
// override with the 2nd-arg reporter options when needed:
diagnostics.DF0033.report({ name }) // console.warn
diagnostics.DF0033.report({ name }, { method: 'error' }) // console.error
diagnostics.DF0033.report({ name, cause: error }, { method: 'warn' }) // attach cause
diagnostics.DF0033({ id, reason }) // console.warn
diagnostics.DF0033({ id, reason }, { method: 'error' }) // console.error
diagnostics.DF0033({ id, reason, cause: error }, { method: 'warn' }) // attach cause
Comment on lines 66 to +75
```

3. **Create a docs page** at `docs/errors/DF0033.md` (when `docs/` lands):
Expand Down
20 changes: 10 additions & 10 deletions docs/guide/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function MyPlugin(): PluginWithDevTools {
ctx.diagnostics.register(myDiagnostics)

// Emit through the host's shared reporter:
myDiagnostics.MYP0002.report()
myDiagnostics.MYP0002()
},
},
}
Expand All @@ -76,26 +76,26 @@ Each definition supports a `why` (string or function — the message) and an opt

## Emit a diagnostic

Each registered code becomes a `DiagnosticHandle` on the typed result of `defineDiagnostics()` (and through the shared `ctx.diagnostics.logger` lookup). Handles expose `.report()` and `.throw()`.
Each registered code becomes a `DiagnosticHandle` on the typed result of `defineDiagnostics()` (and through the shared `ctx.diagnostics.logger` lookup). Each handle is a callable — invoke it to report (returns the `Diagnostic`), or prefix with `throw` to raise.

```ts
// Throw — control flow stops here
throw myDiagnostics.MYP0001.throw({ name: 'foo' })
throw myDiagnostics.MYP0001({ name: 'foo' })

// Report without throwing (default console method: `warn`)
myDiagnostics.MYP0002.report()
myDiagnostics.MYP0002()

// Override the console method per call
myDiagnostics.MYP0002.report({}, { method: 'error' })
myDiagnostics.MYP0002({}, { method: 'error' })

// Attach a `cause` — merged into the params object
myDiagnostics.MYP0001.throw({ name: 'foo', cause: error })
throw myDiagnostics.MYP0001({ name: 'foo', cause: error })
```

`.throw()` is typed `never`, so TypeScript treats the line after as unreachable. Prefix the call with `throw` for control-flow narrowing:
The callable returns a `Diagnostic` (which extends `Error`). Prefix with `throw` so TypeScript narrows the lines after as unreachable:

```ts
throw myDiagnostics.MYP0001.throw({ name })
throw myDiagnostics.MYP0001({ name })
```

## Typed handle reference
Expand All @@ -114,7 +114,7 @@ const myDiagnostics = ctx.diagnostics.defineDiagnostics({
ctx.diagnostics.register(myDiagnostics)

// Use the typed handle directly for autocompletion
myDiagnostics.MYP0001.report({ name: 'foo' })
myDiagnostics.MYP0001({ name: 'foo' })
```

The host's `defineDiagnostics()` pre-wires its ANSI console reporter, so both the typed handle and the shared lookup produce the same output.
Expand All @@ -134,7 +134,7 @@ Each page covers the message, cause, example, and fix — see any [DF code page]

## When to use what

- **`ctx.diagnostics`** — coded conditions worth looking up: misconfiguration, deprecations, validation failures, internal invariants. Always docs-backed. Often `.throw()`.
- **`ctx.diagnostics`** — coded conditions worth looking up: misconfiguration, deprecations, validation failures, internal invariants. Always docs-backed. Often thrown.
- **`ctx.messages`** — user-facing activity surfaces in the DevTools UI: progress indicators, audit results, "URL copied" toasts. Just a message and a level.

Diagnostics target tool authors and CI; messages target the human in front of the DevTools panel.
4 changes: 2 additions & 2 deletions packages/devframe/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "devframe",
"type": "module",
"version": "0.3.0",
"version": "0.4.0",
"description": "Framework for building generic DevTools",
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
"license": "MIT",
Expand Down Expand Up @@ -54,7 +54,7 @@
"./utils/when": "./dist/utils/when.mjs",
"./package.json": "./package.json"
},
"types": "./dist/index.d.ts",
"types": "./dist/index.d.mts",
"files": [
"dist",
"skills"
Expand Down
4 changes: 2 additions & 2 deletions packages/devframe/src/adapters/mcp/build-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export async function createMcpServer(
): Promise<McpServerHandle> {
const transport = options.transport ?? 'stdio'
if (transport !== 'stdio')
throw diagnostics.DF0017.throw({ transport, reason: 'Only stdio transport is supported in this release.' })
throw diagnostics.DF0017({ transport, reason: 'Only stdio transport is supported in this release.' })

const host: DevToolsHost = {
mountStatic: () => { /* MCP has no static surface */ },
Expand Down Expand Up @@ -132,7 +132,7 @@ export async function createMcpServer(
}
catch (error) {
const reason = error instanceof Error ? error.message : String(error)
throw diagnostics.DF0017.throw({ transport, reason, cause: error })
throw diagnostics.DF0017({ transport, reason, cause: error })
}

options.onReady?.({ transport: 'stdio' })
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio
})
}
catch (e) {
diagnostics.DF0033.report({ id: d.id, reason: String(e), cause: e as Error }, { method: 'warn' })
diagnostics.DF0033({ id: d.id, reason: String(e), cause: e as Error }, { method: 'warn' })
return
}

Expand Down
10 changes: 5 additions & 5 deletions packages/devframe/src/node/host-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class DevToolsAgentHost implements DevToolsAgentHostType {

registerResource(input: AgentResourceInput): AgentHandle {
if (this.resources.has(input.id))
throw diagnostics.DF0016.throw({ id: input.id })
throw diagnostics.DF0016({ id: input.id })

const resource: AgentResource = {
id: input.id,
Expand Down Expand Up @@ -154,16 +154,16 @@ export class DevToolsAgentHost implements DevToolsAgentHostType {

private _validateToolId(id: string): void {
if (this.tools.has(id))
throw diagnostics.DF0015.throw({ id })
throw diagnostics.DF0015({ id })
// Collision with an RPC function that already carries an `agent` field.
const rpcDef = this.context.rpc.definitions.get(id)
if (rpcDef?.agent)
throw diagnostics.DF0015.throw({ id })
throw diagnostics.DF0015({ id })
}

private _projectTool(input: AgentToolInput): AgentTool {
if (!input.description || typeof input.description !== 'string')
throw diagnostics.DF0014.throw({ name: input.id })
throw diagnostics.DF0014({ name: input.id })

return {
id: input.id,
Expand All @@ -185,7 +185,7 @@ export class DevToolsAgentHost implements DevToolsAgentHostType {
if (!agent)
continue
if (!agent.description || typeof agent.description !== 'string')
throw diagnostics.DF0014.throw({ name })
throw diagnostics.DF0014({ name })

const type: RpcFunctionType = def.type ?? 'query'
const safety = agent.safety ?? inferSafety(type)
Expand Down
4 changes: 2 additions & 2 deletions packages/devframe/src/node/host-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class RpcFunctionsHost extends RpcFunctionsCollectorBase<DevToolsRpcServe
...args: Args
): Promise<Awaited<ReturnType<DevToolsRpcServerFunctions[T]>>> {
if (!this.definitions.has(method as string)) {
throw diagnostics.DF0006.throw({ name: String(method) })
throw diagnostics.DF0006({ name: String(method) })
}

const handler = await this.getHandler(method)
Expand Down Expand Up @@ -80,7 +80,7 @@ export class RpcFunctionsHost extends RpcFunctionsCollectorBase<DevToolsRpcServe

getCurrentRpcSession(): DevToolsNodeRpcSession | undefined {
if (!this._asyncStorage)
throw diagnostics.DF0007.throw()
throw diagnostics.DF0007()
return this._asyncStorage.getStore()
}
}
2 changes: 1 addition & 1 deletion packages/devframe/src/node/host-views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class DevToolsViewHost implements DevToolsViewHostType {

hostStatic(baseUrl: string, distDir: string) {
if (!existsSync(distDir)) {
throw diagnostics.DF0008.throw({ distDir })
throw diagnostics.DF0008({ distDir })
}

this.buildStaticDirs.push({ baseUrl, distDir })
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/node/rpc-shared-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function createRpcSharedStateServerHost(
return sharedState.get(key)!
}
if (options?.initialValue === undefined && options?.sharedState === undefined) {
throw diagnostics.DF0013.throw({ key })
throw diagnostics.DF0013({ key })
}
debug('new-state', key)
const state = options.sharedState ?? createSharedState<T>({
Expand Down
8 changes: 4 additions & 4 deletions packages/devframe/src/node/rpc-streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ export function createRpcStreamingServerHost(rpc: RpcFunctionsHost): RpcStreamin
handler(channelName: string, id: string, opts?: { afterSeq?: number }) {
const state = channels.get(channelName)
if (!state) {
diagnostics.DF0030.report({ channel: channelName, id }, { method: 'error' })
diagnostics.DF0030({ channel: channelName, id }, { method: 'error' })
return
}
const record = state.streams.get(id)
if (!record) {
diagnostics.DF0030.report({ channel: channelName, id }, { method: 'error' })
diagnostics.DF0030({ channel: channelName, id }, { method: 'error' })
return
}
const session = rpc.getCurrentRpcSession()
Expand Down Expand Up @@ -181,7 +181,7 @@ export function createRpcStreamingServerHost(rpc: RpcFunctionsHost): RpcStreamin
const state = channels.get(channelName)
const record = state?.inbound.get(id)
if (!record) {
diagnostics.DF0030.report({ channel: channelName, id }, { method: 'error' })
diagnostics.DF0030({ channel: channelName, id }, { method: 'error' })
return
}
// Lock the inbound to the first session that writes; subsequent
Expand Down Expand Up @@ -218,7 +218,7 @@ export function createRpcStreamingServerHost(rpc: RpcFunctionsHost): RpcStreamin

function createChannel<T>(name: string, opts: RpcStreamingChannelOptions = {}): RpcStreamingChannel<T> {
if (channels.has(name))
throw diagnostics.DF0032.throw({ channel: name })
throw diagnostics.DF0032({ channel: name })

const replayWindow = opts.replayWindow ?? 0
const state: ChannelState<T> = {
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/node/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function createStorage<T extends object>(options: CreateStorageOptions<T>
initialValue = mergeInitialValue ? mergeInitialValue(options.initialValue, savedValue) : savedValue
}
catch (error) {
diagnostics.DF0012.report({ filepath: options.filepath, cause: error }, { method: 'warn' })
diagnostics.DF0012({ filepath: options.filepath, cause: error }, { method: 'warn' })
initialValue = options.initialValue
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/devframe/src/rpc/collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class RpcFunctionsCollectorBase<

register(fn: RpcFunctionDefinition<string, any, any, any, any, any, SetupContext>, force = false): void {
if (this.definitions.has(fn.name) && !force) {
throw diagnostics.DF0021.throw({ name: fn.name })
throw diagnostics.DF0021({ name: fn.name })
}
assertAgentJsonSerializable(fn)
this.definitions.set(fn.name, fn)
Expand All @@ -50,7 +50,7 @@ export class RpcFunctionsCollectorBase<

update(fn: RpcFunctionDefinition<string, any, any, any, any, any, SetupContext>, force = false): void {
if (!this.definitions.has(fn.name) && !force) {
throw diagnostics.DF0022.throw({ name: fn.name })
throw diagnostics.DF0022({ name: fn.name })
}
assertAgentJsonSerializable(fn)
this.definitions.set(fn.name, fn)
Expand All @@ -74,7 +74,7 @@ export class RpcFunctionsCollectorBase<
getSchema<T extends keyof LocalFunctions>(name: T): { args: RpcArgsSchema | undefined, returns: RpcReturnSchema | undefined } {
const definition = this.definitions.get(name as string)
if (!definition)
throw diagnostics.DF0023.throw({ name: String(name) })
throw diagnostics.DF0023({ name: String(name) })
return {
args: definition.args,
returns: definition.returns,
Expand All @@ -98,5 +98,5 @@ function assertAgentJsonSerializable(
fn: RpcFunctionDefinition<string, any, any, any, any, any, any>,
): void {
if (fn.agent && fn.jsonSerializable !== true)
throw diagnostics.DF0019.throw({ name: fn.name })
throw diagnostics.DF0019({ name: fn.name })
}
6 changes: 3 additions & 3 deletions packages/devframe/src/rpc/dump/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function dumpFunctions<

const handler = setupResult.handler || definition.handler
if (!handler) {
throw diagnostics.DF0024.throw({ name: definition.name })
throw diagnostics.DF0024({ name: definition.name })
}

let dump = setupResult.dump ?? definition.dump
Expand Down Expand Up @@ -211,7 +211,7 @@ export function createClientFromDump<T extends Record<string, any>>(
const client = new Proxy({} as T, {
get(_, functionName: string) {
if (!(functionName in store.definitions)) {
throw diagnostics.DF0025.throw({ name: functionName })
throw diagnostics.DF0025({ name: functionName })
}

return async (...args: any[]) => {
Expand Down Expand Up @@ -248,7 +248,7 @@ export function createClientFromDump<T extends Record<string, any>>(
return fallbackRecord.output
}

throw diagnostics.DF0026.throw({ name: functionName, args: JSON.stringify(args) })
throw diagnostics.DF0026({ name: functionName, args: JSON.stringify(args) })
}
},
has(_, functionName: string) {
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/src/rpc/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function getRpcHandler<
}
const result = await getRpcResolvedSetupResult(definition, context)
if (!result.handler) {
throw diagnostics.DF0024.throw({ name: definition.name })
throw diagnostics.DF0024({ name: definition.name })
}
return result.handler
}
2 changes: 1 addition & 1 deletion packages/devframe/src/rpc/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function strictJsonStringify(value: unknown, fnName: string = ''): string

function nonJsonAt(fnName: string, type: string, parent: unknown, key: string): Error {
const path = formatPath(parent, key)
return diagnostics.DF0020.throw({ name: fnName || '<anonymous>', type, path })
return diagnostics.DF0020({ name: fnName || '<anonymous>', type, path })
}

function formatPath(parent: unknown, key: string): string {
Expand Down
4 changes: 2 additions & 2 deletions packages/devframe/src/rpc/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export function validateDefinitions(definitions: readonly RpcFunctionDefinitionA
const type = definition.type || 'query'

if ((type === 'action' || type === 'event') && definition.dump) {
throw diagnostics.DF0027.throw({ name: definition.name, type })
throw diagnostics.DF0027({ name: definition.name, type })
}

if (definition.snapshot && type !== 'query') {
throw diagnostics.DF0028.throw({ name: definition.name, type })
throw diagnostics.DF0028({ name: definition.name, type })
}
}
}
Expand Down
18 changes: 10 additions & 8 deletions packages/devframe/src/types/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ export type DevToolsDiagnosticsDefinition = ReturnType<typeof defineDiagnostics<

/**
* The shared diagnostics lookup exposed by the host. A `Proxy` that resolves
* any registered code name to its `nostics` handle (with `.report()` and
* `.throw()` methods). Typed loosely because it spans heterogeneous
* definitions registered by different integrations.
* any registered code name to its `nostics` handle (a callable that builds
* a diagnostic and routes it through registered reporters). Typed loosely
* because it spans heterogeneous definitions registered by different
* integrations.
*/
export type DevToolsDiagnosticsLogger = Record<string, any>

Expand Down Expand Up @@ -45,18 +46,19 @@ export interface DevToolsDefineDiagnosticsOptions<Codes extends Record<string, D
* ctx.diagnostics.register(myDiagnostics)
*
* // Through the shared lookup (loose typing):
* ctx.diagnostics.logger.MYP0001.throw()
* throw ctx.diagnostics.logger.MYP0001()
*
* // Or directly on the typed handle returned from `defineDiagnostics`:
* myDiagnostics.MYP0001.throw()
* throw myDiagnostics.MYP0001()
* ```
*/
export interface DevToolsDiagnosticsHost {
/**
* Proxy-backed lookup of every registered diagnostic handle by code name.
* Resolves to a `nostics` `DiagnosticHandle` with `.report()` / `.throw()`.
* Loosely typed — for autocompletion, keep a reference to the typed
* result of `defineDiagnostics()` instead.
* Resolves to a `nostics` `DiagnosticHandle` — a callable that builds a
* diagnostic and routes it through registered reporters; prefix with
* `throw` to raise. Loosely typed — for autocompletion, keep a reference
* to the typed result of `defineDiagnostics()` instead.
*/
readonly logger: DevToolsDiagnosticsLogger

Expand Down
Loading
Loading