From a0f888d3da1705adb555c2ebb7e2fa0fc6035d30 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 13 Jul 2026 15:25:08 -0700 Subject: [PATCH 1/2] feat: wire DAWN_E codes into dawn verify's runtime preflight Add DAWN_E5101 ("Node version below the supported floor") to the @dawn-ai/sdk error registry, and surface it (or the existing DAWN_E2002 for an unreachable sandbox daemon) on a failed `dawn verify` runtime check. RuntimeCheckResult.node/docker carry an optional `code` field, and the CliError thrown on a failed verify gains the matching code so text mode prints `[DAWN_E5101] See ...` and --json mode exposes it on the runtime check. Co-Authored-By: Claude Opus 4.8 --- .changeset/dx-tidyups.md | 11 +++++++ apps/web/content/docs/errors.mdx | 1 + packages/cli/src/commands/verify.ts | 21 ++++++++++++-- packages/cli/src/lib/verify/check-runtime.ts | 30 ++++++++++++++++---- packages/cli/test/check-runtime.test.ts | 8 +++++- packages/cli/test/verify-command.test.ts | 21 ++++++++++++++ packages/sdk/src/errors.ts | 4 +++ 7 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 .changeset/dx-tidyups.md diff --git a/.changeset/dx-tidyups.md b/.changeset/dx-tidyups.md new file mode 100644 index 00000000..c4825155 --- /dev/null +++ b/.changeset/dx-tidyups.md @@ -0,0 +1,11 @@ +--- +"@dawn-ai/sdk": patch +"@dawn-ai/cli": patch +--- + +Wire `DAWN_E` error codes into `dawn verify`'s runtime preflight. Add +`DAWN_E5101` ("Node version below the supported floor") to the error-code +registry, and surface it (or `DAWN_E2002` for an unreachable sandbox daemon) +on a failed `dawn verify` runtime check — in both the CLI's `[CODE] See ` +line and the `--json` output's `runtime.node.code` / `runtime.docker.code` +fields. diff --git a/apps/web/content/docs/errors.mdx b/apps/web/content/docs/errors.mdx index 4f18dd2e..7dc5487a 100644 --- a/apps/web/content/docs/errors.mdx +++ b/apps/web/content/docs/errors.mdx @@ -23,3 +23,4 @@ A code may omit a docs link and still be a valid, searchable identifier. | `DAWN_E4002` | Unknown model id | [/docs/configuration](/docs/configuration) | | `DAWN_E5001` | Import or export mismatch | — | | `DAWN_E5002` | Tool file has the wrong shape | [/docs/tools](/docs/tools) | +| `DAWN_E5101` | Node version below the supported floor | — | diff --git a/packages/cli/src/commands/verify.ts b/packages/cli/src/commands/verify.ts index 311982b5..59a47e30 100644 --- a/packages/cli/src/commands/verify.ts +++ b/packages/cli/src/commands/verify.ts @@ -8,6 +8,7 @@ import { loadDawnConfig, renderDawnTypes, } from "@dawn-ai/core" +import type { DawnErrorCode } from "@dawn-ai/sdk" import type { SandboxProvider } from "@dawn-ai/workspace" import { type Command, CommanderError } from "commander" @@ -177,7 +178,8 @@ export async function runVerifyCommand(options: VerifyOptions, io: CommandIo): P return } - throw new CliError(`Verify failed: ${getFailureMessage(result)}`) + const code = getFailureCode(result) + throw new CliError(`Verify failed: ${getFailureMessage(result)}`, 1, code ? { code } : {}) } async function verifyApp(options: VerifyOptions): Promise { @@ -348,16 +350,29 @@ function getFailureMessage(result: VerifyFailureResult): string { function runtimeFailureMessage(runtime: RuntimeCheckResult): string { const reasons: string[] = [] if (!runtime.node.ok) { - // TODO(error-codes): tag with DAWN_E5101 once the registry lands (#357). reasons.push(`Node ${runtime.node.version} is below the required floor ${runtime.node.floor}.`) } if (runtime.docker && !runtime.docker.ok) { - // TODO(error-codes): tag with DAWN_E2002 once the registry lands (#357). reasons.push(`Docker sandbox unavailable: ${runtime.docker.detail}.`) } return reasons.join(" ") || "Runtime check failed." } +/** + * The DAWN_E code for a failed verify result, when the failure is + * attributable to a registered code. Only the runtime check currently + * carries codes; a stale Node takes priority over an unreachable sandbox + * daemon since it is reported first in `runtimeFailureMessage`. + */ +function getFailureCode(result: VerifyFailureResult): DawnErrorCode | undefined { + const failedCheck = [...result.checks].reverse().find((check) => check.status === FAILED_STATUS) + + if (failedCheck?.name === "runtime") { + return failedCheck.node.code ?? failedCheck.docker?.code + } + return undefined +} + function inferFailureAppRoot(options: VerifyOptions, message: string): string { const fromMessage = /^Invalid Dawn app at (.+?)\. Missing: /u.exec(message)?.[1] diff --git a/packages/cli/src/lib/verify/check-runtime.ts b/packages/cli/src/lib/verify/check-runtime.ts index dd7049d8..149c01a7 100644 --- a/packages/cli/src/lib/verify/check-runtime.ts +++ b/packages/cli/src/lib/verify/check-runtime.ts @@ -1,3 +1,4 @@ +import type { DawnErrorCode } from "@dawn-ai/sdk" import type { SandboxProvider } from "@dawn-ai/workspace" /** @@ -10,9 +11,20 @@ const NODE_FLOOR = "22.13.0" export interface RuntimeCheckResult { readonly name: "runtime" - readonly node: { readonly version: string; readonly ok: boolean; readonly floor: string } + readonly node: { + readonly version: string + readonly ok: boolean + readonly floor: string + /** Present only when `ok` is false — DAWN_E5101 (Node below the supported floor). */ + readonly code?: DawnErrorCode + } /** Present only when a sandbox provider is configured. */ - readonly docker?: { readonly ok: boolean; readonly detail: string } + readonly docker?: { + readonly ok: boolean + readonly detail: string + /** Present only when `ok` is false — DAWN_E2002 (sandbox preflight failed). */ + readonly code?: DawnErrorCode + } readonly status: "passed" | "warning" | "failed" } @@ -43,17 +55,25 @@ export async function checkRuntime(input: { }): Promise { const version = input.nodeVersion ?? process.versions.node const nodeOk = gte(version, NODE_FLOOR) - const node = { version, ok: nodeOk, floor: NODE_FLOOR } + const node: RuntimeCheckResult["node"] = { + version, + ok: nodeOk, + floor: NODE_FLOOR, + ...(nodeOk ? {} : { code: "DAWN_E5101" as const }), + } let docker: RuntimeCheckResult["docker"] if (input.sandboxProvider?.preflight) { // Reuse the provider preflight contract ({ ok, detail?, warnings? }) — the // same one `dawn check` runs via collect-sandbox-errors. Surface detail verbatim. const result = await input.sandboxProvider.preflight() - docker = { ok: result.ok, detail: result.detail ?? (result.ok ? "reachable" : "unreachable") } + docker = { + ok: result.ok, + detail: result.detail ?? (result.ok ? "reachable" : "unreachable"), + ...(result.ok ? {} : { code: "DAWN_E2002" as const }), + } } - // TODO(error-codes): tag with DAWN_E5101 (Node too old) / DAWN_E2002 (Docker unreachable) once the registry lands (#357). const failed = !nodeOk || docker?.ok === false return { name: "runtime", diff --git a/packages/cli/test/check-runtime.test.ts b/packages/cli/test/check-runtime.test.ts index c9947f66..9d21093e 100644 --- a/packages/cli/test/check-runtime.test.ts +++ b/packages/cli/test/check-runtime.test.ts @@ -25,12 +25,14 @@ describe("checkRuntime", () => { expect(result.node.ok).toBe(false) expect(result.node.floor).toBe("22.13.0") expect(result.node.version).toBe("22.12.5") + expect(result.node.code).toBe("DAWN_E5101") expect(result.status).toBe("failed") }) test("Node at/above floor → passed", async () => { const result = await checkRuntime({ nodeVersion: "22.14.0" }) expect(result.node.ok).toBe(true) + expect(result.node.code).toBeUndefined() expect(result.status).toBe("passed") }) @@ -47,7 +49,11 @@ describe("checkRuntime", () => { preflight: async () => ({ ok: false, detail: "daemon unreachable" }), }, }) - expect(result.docker).toEqual({ ok: false, detail: "daemon unreachable" }) + expect(result.docker).toEqual({ + ok: false, + detail: "daemon unreachable", + code: "DAWN_E2002", + }) expect(result.status).toBe("failed") }) diff --git a/packages/cli/test/verify-command.test.ts b/packages/cli/test/verify-command.test.ts index 743b95db..049b9752 100644 --- a/packages/cli/test/verify-command.test.ts +++ b/packages/cli/test/verify-command.test.ts @@ -88,6 +88,27 @@ describe("dawn verify", () => { expect(result.exitCode).toBe(1) expect(result.stderr).toMatch(/^Verify failed:/) expect(result.stderr).toContain("22.13.0") + expect(result.stderr).toContain("[DAWN_E5101]") + } finally { + if (descriptor) Object.defineProperty(process.versions, "node", descriptor) + } + }) + + test("exposes DAWN_E5101 on the runtime check in json mode when the Node runtime is below the floor", async () => { + const appRoot = await createFixtureApp({ + "src/app/hello/index.ts": "export async function workflow() { return {} }\n", + }) + + const descriptor = Object.getOwnPropertyDescriptor(process.versions, "node") + Object.defineProperty(process.versions, "node", { value: "20.11.0", configurable: true }) + try { + const result = await invoke(["verify", "--cwd", appRoot, "--json"]) + expect(result.exitCode).toBe(1) + const parsed = JSON.parse(result.stdout) + expect(parsed.status).toBe("failed") + const runtimeCheck = parsed.checks.find((check: { name: string }) => check.name === "runtime") + expect(runtimeCheck.node.code).toBe("DAWN_E5101") + expect(runtimeCheck.status).toBe("failed") } finally { if (descriptor) Object.defineProperty(process.versions, "node", descriptor) } diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index 844102ab..76e50322 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -77,6 +77,10 @@ export const DAWN_ERRORS = { title: "Tool file has the wrong shape", docsPath: "/docs/tools", }, + DAWN_E5101: { + code: "DAWN_E5101", + title: "Node version below the supported floor", + }, } as const satisfies Record /** The union of all registered error codes. Producers cannot invent codes. */ From d4873ca6fbf0f416db1e88c56017ed41a14a2cc7 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 13 Jul 2026 15:25:14 -0700 Subject: [PATCH 2/2] chore(docs): enforce gpt-5-family OpenAI example ids in check-docs Add a narrow check-docs.mjs rule flagging an OpenAI legacy model id (gpt-4*, gpt-3*, o1*) used as an example `model:` value anywhere under apps/web/content/docs, excluding api.mdx (the model-id reference page, which intentionally lists legacy ids across every provider). Non-OpenAI provider ids (llama, claude, gemini, ...) are never flagged. Confirmed the rule passes against current docs and fails when a violation is injected. Co-Authored-By: Claude Opus 4.8 --- scripts/check-docs.mjs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs index 11725526..93992059 100644 --- a/scripts/check-docs.mjs +++ b/scripts/check-docs.mjs @@ -155,6 +155,31 @@ if (sdkEntry?.DAWN_ERRORS && docsBundle?.parseNav) { } } +// gpt-5-family example check — Dawn's docs convention is that OpenAI examples +// use only the gpt-5 family (canonical default gpt-5-mini); legacy OpenAI ids +// (gpt-4*, gpt-3*, o1*) must not appear as an example `model:` value. This is +// intentionally narrow: it only matches an OpenAI legacy id used as a +// `model:` value, so non-OpenAI provider ids (llama, claude, gemini, ...) are +// never flagged. `api.mdx` is the model-id REFERENCE page and intentionally +// lists legacy ids across every provider (for readers picking a provider), so +// it is excluded entirely. +const OPENAI_LEGACY_MODEL_RE = /model:\s*["'](gpt-4|gpt-3|o1)[^"']*["']/g +const docsContentDir = resolve(repoRoot, "apps/web/content/docs") +const apiMdxPath = resolve(docsContentDir, "api.mdx") +const docsMdxFiles = walkFiles(docsContentDir, (file) => file.endsWith(".mdx")) + +for (const filePath of docsMdxFiles) { + if (filePath === apiMdxPath) { + continue + } + const source = readFileSync(filePath, "utf8") + for (const match of source.matchAll(OPENAI_LEGACY_MODEL_RE)) { + failures.push( + `${relativeToRoot(filePath)} uses an OpenAI legacy model id as an example (\`${match[0]}\`) — docs examples must use the gpt-5 family (canonical default gpt-5-mini)`, + ) + } +} + // CLI surface check — drive from the commander registry to catch docs drift. // Every user-facing command name and every long option must be referenced in // cli.mdx. The internal `dev-child` command is excluded (not user-facing).