diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index ce1979ff..db84e57c 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -783,6 +783,18 @@ "authentication": "ON_INSTALL" }, "category": "Tooling" + }, + { + "name": "nostics", + "source": { + "source": "local", + "path": "./plugins/nostics" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Tooling" } ] } diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a67182d9..168d892a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -722,6 +722,14 @@ "keywords": ["skill-optimizer", "skills", "evals", "optimization", "benchmark"], "tags": ["skills"], "source": "./plugins/skill-optimizer" + }, + { + "name": "nostics", + "description": "Structured diagnostic code library for JavaScript/TypeScript. Turns errors into typed, machine-readable Diagnostic instances with stable codes, docs URLs, and actionable fixes. Covers defineDiagnostics, reporters, formatters, Vite plugins, and adding new diagnostic codes.", + "category": "tooling", + "keywords": ["nostics", "diagnostics", "error-handling", "typescript", "javascript"], + "tags": ["skills", "vercel"], + "source": "./plugins/nostics" } ] } diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 69d34bd9..38a28236 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -59,5 +59,6 @@ "plugins/mcp-dev": "1.0.0", "plugins/vercel-sandbox": "1.1.0", "plugins/eve": "1.1.0", - "plugins/skill-optimizer": "1.1.0" + "plugins/skill-optimizer": "1.1.0", + "plugins/nostics": "1.0.0" } diff --git a/README.md b/README.md index d0c36eec..b7d8c316 100644 --- a/README.md +++ b/README.md @@ -405,6 +405,12 @@ Measure and improve how well a plugin's skills help agents solve real tasks. Run **Install:** `/plugin install skill-optimizer@pleaseai` | **Source:** [plugins/skill-optimizer](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/skill-optimizer) +#### Nostics + +Structured diagnostic code library for JavaScript/TypeScript. Turns errors into typed, machine-readable Diagnostic instances with stable codes, docs URLs, and actionable fixes. Covers defineDiagnostics, reporters, formatters, Vite plugins, and adding new diagnostic codes. + +**Install:** `/plugin install nostics@pleaseai` | **Source:** [plugins/nostics](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/nostics) + ## Quick Start The fastest way to get started — install the marketplace and let the plugin recommender auto-detect what you need: diff --git a/plugins/nostics/.agents/skills/add-diagnostic/SKILL.md b/plugins/nostics/.agents/skills/add-diagnostic/SKILL.md new file mode 100644 index 00000000..c206057a --- /dev/null +++ b/plugins/nostics/.agents/skills/add-diagnostic/SKILL.md @@ -0,0 +1,28 @@ +--- +name: add-diagnostic +description: 'Add a new diagnostic code following the defineDiagnostics() conventions from nostics' +user-invocable: true +allowed-tools: Read Grep Glob Edit Write +license: MIT +--- + +# Add a New Diagnostic Code + +1. **Find the catalog.** Grep for `defineDiagnostics` to locate the `codes` object the new entry belongs in. With several catalogs, pick by area. +2. **Pick the code.** `PREFIX_XNNNN`: `PREFIX` is the project name uppercased (`NUXT`, `I18N`); `X` is the area letter (`B` build, `R` runtime, `C` config, `D` deprecation); `NNNN` is the next free number. Read existing codes to choose. Never rename or reuse a published code. +3. **Add the entry:** + +```ts +LIB_R0001: { + why: (p: { hook: string }) => `${p.hook}() must run at the top of setup().`, // string or typed fn; becomes Error.message (required) + fix: 'Move the call into setup() or a composable it calls.', // optional, but add whenever the fix is known + docs: 'https://example.com/custom', // optional: overrides docsBase, or `false` to opt out +}, +``` + +- `why` is the only required field. Params from `why` and `fix` are intersected and required at the call site. +- Runtime fields (`cause`, `sources`) are passed at the call site, never in the definition. + +4. **Call it:** `diagnostics.LIB_R0001({ hook })` to report, `throw diagnostics.LIB_R0001({ hook, cause: err })` to raise. Both run the reporters. + +Full API and reporter/formatter/plugin details: the `nostics` skill. diff --git a/plugins/nostics/.agents/skills/nostics/SKILL.md b/plugins/nostics/.agents/skills/nostics/SKILL.md new file mode 100644 index 00000000..c9c1b46e --- /dev/null +++ b/plugins/nostics/.agents/skills/nostics/SKILL.md @@ -0,0 +1,158 @@ +--- +name: nostics +description: "Structured diagnostic code library for JavaScript/TypeScript. Turns errors and other conditions into typed, machine-readable `Diagnostic` instances with stable codes, docs URLs, and actionable fields. Use this skill whenever the project imports `nostics`, or works with `defineDiagnostics`/`defineProdDiagnostics`, the `Diagnostic` class, diagnostic code registries, or structured error handling. Also covers reporters (`createConsoleReporter`, `createFetchReporter` from nostics/reporters/fetch, `createFileReporter` from nostics/reporters/node, `createDevReporter` from nostics/reporters/dev), formatters (`formatDiagnostic`, `ansiFormatter`, `jsonFormatter`), and Vite plugins (`nosticsStrip` from @nostics/unplugin/strip-transform, `nosticsCollector` from @nostics/unplugin/dev-server-collector). Also use when migrating a library's existing `console.warn`/`console.error`/`warn()` helpers or thrown `Error`s to diagnostics: follow `references/migration.md`." +license: MIT +--- + +# nostics + +Every error condition becomes a typed `Diagnostic` (extends `Error`) with a stable code, docs URL, and actionable `fix`. Serializable via `toJSON()`. + +`Diagnostic`: `name` (the code), `message`/`why` (interpolated text), `fix?`, `docs?`, `sources?` (`'file:line:column'`), `cause?`, `toJSON()`. Throw it, catch it with `instanceof Diagnostic`, send `toJSON()` across process boundaries. + +## defineDiagnostics + +Returns one callable handle per code. Calling a handle builds a fresh `Diagnostic`, fires every reporter in order, and returns it. `throw` the return value to raise (reporters still run, so a thrown diagnostic also reports). + +```ts +import { createConsoleReporter, defineDiagnostics } from 'nostics' + +const diagnostics = /*#__PURE__*/ defineDiagnostics({ + docsBase: (code) => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`, + reporters: [/*#__PURE__*/ createConsoleReporter()], + codes: { + NUXT_B1001: { + why: 'Could not compile template.', + fix: 'Check the template for syntax errors.', + }, + NUXT_B2011: { + why: (p: { src: string }) => `Invalid plugin "${p.src}". src option is required.`, + fix: 'Pass a string path or an object with a `src` to `addPlugin()`.', + }, + NUXT_W9001: { why: 'message', docs: false }, // per-code: string overrides docsBase, false opts out + }, +}) +``` + +- **`docsBase`** `string | (code) => string | undefined`: string appends `/${code.toLowerCase()}`; function returns the full URL (or `undefined` to omit). +- **`codes`**: each definition needs `why` (`string | (params) => string`, the only required field, becomes `Error.message`); optional `fix` (`string | (params) => string`) and `docs` (`string | false`). +- **`reporters`**: fired on every call; optional. Their `options` types are intersected; required reporter options become required at the call site. Omit it (or pass `[]`) for a catalog whose codes are only ever `throw`n: the thrown `Diagnostic` already carries the message, so a console reporter would print it once and surface it again from the uncaught error, a visible duplicate. Keep report-only warnings and fatal throws in separate catalogs when one needs a reporter and the other does not. +- **Param inference**: params from `why` and `fix` are intersected and required at the call site. If `why` needs `{ src }` and `fix` needs `{ date }`, the call requires `{ src, date }`. + +## Call sites + +```ts +diagnostics.NUXT_B1001() // no params: report only +diagnostics.NUXT_B2011({ src: '/plugins/bad.ts' }) // params first +diagnostics.NUXT_B2011({ + src, + cause: originalError, + sources: ['nuxt.config.ts:42:3'], +}) // runtime fields merge in +diagnostics.NUXT_B2011({ src }, { method: 'error' }) // reporter options second +throw diagnostics.NUXT_B2011({ src }) // raise +``` + +`cause`/`sources` go in the params object; `sources` matters most for build/config diagnostics where the JS stack points inside the library. Catch with `if (err instanceof Diagnostic)` then read `.name`, `.message`, `.fix`, `.docs`. + +## Reporters + +`(diagnostic: Diagnostic, options?: Opts) => void`. Declaring a required `options` type makes the second call-site argument required and typed. + +| Reporter | Import | Description | +| --------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `createConsoleReporter(options?)` | `nostics` | `console[method](formatter(d))`. `method` defaults `'warn'` (`'log'\|'warn'\|'error'`), `formatter` defaults `formatDiagnostic`; both via options, `method` also overridable per call. | +| `createFetchReporter(url)` | `nostics/reporters/fetch` | POSTs diagnostic JSON to the URL; failures swallowed. | +| `createFileReporter(options?)` | `nostics/reporters/node` | Appends NDJSON to a local file (default `.nostics.log`). | +| `createDevReporter()` | `nostics/reporters/dev` | Sends `toJSON()` to the Vite dev server via `import.meta.hot.send()`. | + +```ts +import type { DiagnosticReporter } from 'nostics' +const sentryReporter: DiagnosticReporter = (d) => + sentry.captureMessage(d.message, { tags: { code: d.name } }) +const audited: DiagnosticReporter<{ priority: number }> = (d, o) => + audit.log({ name: d.name, priority: o.priority }) +// → audited makes diagnostics.X({...}, { priority: 1 }) required and type-checked. +``` + +## Formatters + +| Formatter | Import | Description | +| ----------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `formatDiagnostic` | `nostics` | Plain unicode-decorated string (built-in reporters use it). | +| `ansiFormatter(colors)` | `nostics/formatters/ansi` | Colorized; accepts a `Colors` interface (`red`/`yellow`/`cyan`/`gray`/`bold`/`dim`, each `(s) => string`). | +| `jsonFormatter` | `nostics/formatters/json` | `JSON.stringify(diagnostic)` via `toJSON()`. | + +`formatDiagnostic` output, detail order fixed `fix` → `sources` → `see`, missing fields omitted: + +``` +[NUXT_B2011] Invalid plugin `/plugins/bad.ts`. src option is required. +├▶ fix: Pass a string path or an object with a `src` to `addPlugin()`. +├▶ sources: nuxt.config.ts:42:3 +╰▶ see: https://nuxt.com/e/b2011 +``` + +## Vite plugins (`@nostics/unplugin`, dev dependency) + +`@nostics/unplugin/strip-transform` (library authors, build optimization) and `@nostics/unplugin/dev-server-collector` (app developers, dev-time collection). Both unplugin-based: `.vite()`, `.webpack()`, `.rollup()`, etc. + +- **`nosticsStrip`** marks `defineDiagnostics()` `/*#__PURE__*/` and wraps bare diagnostic expression statements with a `NODE_ENV` guard so they tree-shake out of production. Option `packageName?` (default `'nostics'`). Throws/returns/assignments stay (they are behavior). For tracking: relative imports, export the catalog directly, no factory wrappers or deep barrels. + - The plugin is optional. The same production output happens with no build transform if the catalog is annotated by hand: put `/*#__PURE__*/` before `defineDiagnostics(` and before each reporter factory call inside it (as in every example here), and dev-guard each report-only call site (`process.env.NODE_ENV !== 'production' && diagnostics.CODE(p)`). Always write the annotations in source; reach for the plugin when report-only call sites are unguarded and you want stripping without touching them. +- **`nosticsCollector`** listens for `createDevReporter()` diagnostics over the Vite WebSocket and writes them as NDJSON via `createFileReporter`. Vite-only. Options `logFile?` (default `.nostics.log`), `debug?` (default `!!process.env.DEBUG`). + +```ts +// vite.config.ts +import { nosticsStrip } from '@nostics/unplugin/strip-transform' +import { nosticsCollector } from '@nostics/unplugin/dev-server-collector' +export default defineConfig({ + plugins: [nosticsStrip.vite(), nosticsCollector.vite()], +}) + +// src/diagnostics.ts — pair the collector with createDevReporter() +import { createConsoleReporter, defineDiagnostics } from 'nostics' +import { createDevReporter } from 'nostics/reporters/dev' +export const diagnostics = /*#__PURE__*/ defineDiagnostics({ + reporters: [/*#__PURE__*/ createConsoleReporter(), /*#__PURE__*/ createDevReporter()], + codes: { + /* ... */ + }, +}) +``` + +## Production builds + +- **Report-only** diagnostics (bare `diagnostics.X()`) should disappear: `nosticsStrip` or hand annotations drop them, then the unused catalog tree-shakes. +- **Surviving** diagnostics (`throw`/`return`/assigned/argument) stay, and each keeps the _whole_ catalog reachable, so every `why`/`fix` ships. Not every library throws in production: if yours only reports, stripping is enough, stop here. + +When a library _does_ `throw` in production, pick `defineProdDiagnostics` at definition time with a `NODE_ENV` ternary, so a consumer bundler drops the dev branch (all catalog text): + +```ts +import { defineDiagnostics, defineProdDiagnostics } from 'nostics' +export const diagnostics = + process.env.NODE_ENV === 'production' + ? /*#__PURE__*/ defineProdDiagnostics({ docsBase }) + : /*#__PURE__*/ defineDiagnostics({ + docsBase, + reporters: [ + /* ... */ + ], + codes: { + /* text */ + }, + }) +``` + +The accessed code becomes the `message`, `docs` still derives from `docsBase`, no `why`/`fix` text ships. No `reporters` by default (so a surviving `throw` doesn't also log and then resurface as the uncaught error); pass `reporters` to keep prod telemetry. `nosticsStrip` tracks this ternary like a direct catalog export. + +## Conventions + +- **Codes** are stable, fully-qualified `PREFIX_XNNNN` (`B` build, `R` runtime, `C` config, `D` deprecation). Never reuse or reassign a published code. +- Always provide `why`; provide `fix` whenever the solution is known (the most actionable field for humans and agents). Use parameterized templates for runtime values, not string concatenation outside the factory. +- **`why` is the diagnosis, `fix` is the remedy — split them, don't overlap them.** `why` states only what is wrong; `fix` states only what to do. The reporter prints both, so any wording that appears in both is dead weight. When a single source sentence carries both (`"A hash must start with '#'. Prefix it with '#'."`), cut it in two — diagnosis to `why`, remedy to `fix` — rather than pasting the whole thing into `why` and echoing it in `fix`. `fix` accepts a param function too (`(p) => ...`), so move value-bearing remedies (`use "#${p.hash}"`) into it instead of leaving them in `why`. +- Pass `cause` when re-raising; pass `sources` when the JS stack doesn't reflect the user's source. +- Split large catalogs by domain (`diagnostics/build.ts`, `runtime.ts`, `config.ts`, re-exported from `index.ts`), each `defineDiagnostics()` sharing `docsBase` with its own code range. + +## References + +- **Migrating an existing library** to nostics (replacing `console.warn`/`console.error`/`warn()`/thrown `Error`s with diagnostic codes, without changing runtime behavior): follow `references/migration.md` start to finish. +- Building the error-code documentation site (page template, deployment, agent optimization): `references/documentation-site.md`. diff --git a/plugins/nostics/.agents/skills/nostics/references/documentation-site.md b/plugins/nostics/.agents/skills/nostics/references/documentation-site.md new file mode 100644 index 00000000..5578c860 --- /dev/null +++ b/plugins/nostics/.agents/skills/nostics/references/documentation-site.md @@ -0,0 +1,46 @@ +# Documentation Site and Error Code Registry + +Every published code needs a stable, public documentation page forever. It serves three audiences: **developers** clicking the `see:` URL from their terminal, **AI agents** fetching the page to help when a user pastes an error, and **search engines** indexing `NUXT_B2011` so it's findable. Plan the URL structure to match `docsBase` (string form → `${docsBase}/${code.toLowerCase()}`; function form → full control). + +## Page template + +Each code page (`https://nuxt.com/e/b2011`) follows this structure. Keep it human-readable and agent-parseable: consistent `##` headings, actionable content early, no critical info hidden in tabs/collapsed sections/JS-rendered content. + +```markdown +# {CODE}: {Short title} + +Code: `{CODE}` +Level: {error|warn|suggestion|deprecation} + +## What this error means + +{Plain-language explanation, no assumed context: what the system expected vs received. 1-3 sentences. Agents rely on this to explain the error.} + +## Why this happens + +{Bulleted list of the concrete scenarios that trigger this diagnostic.} + +## How to fix it + +{The most important section: copy-pasteable code showing the wrong pattern and the corrected version.} + +## Additional context + +{Optional: links to related docs, changelog, or related codes.} + +## Example output + +{Optional: the formatted terminal output, so users confirm they're on the right page.} +``` + +## Deployment + +- Host on a public URL matching `docsBase`: a static site generator (VitePress, Nuxt Content) with a catch-all `/e/[code]` route, or a dedicated `/errors` route in existing docs. +- Return 200 for valid codes, 404 for unknown ones, so agents and crawlers distinguish them. +- Add frontmatter/`` structured data (code, level, title). Keep pages lightweight; avoid SPAs that block fetch-based agents. + +## Keep docs in sync with code + +- Store the markdown alongside the diagnostic definitions or in `docs/errors/`, and add the page in the same PR as a new code. +- Generate an index page listing all codes with messages and levels. +- In CI, validate that every code in `defineDiagnostics()` has a corresponding page; fail the build if one is missing. diff --git a/plugins/nostics/.agents/skills/nostics/references/migration.md b/plugins/nostics/.agents/skills/nostics/references/migration.md new file mode 100644 index 00000000..b7cbcdc2 --- /dev/null +++ b/plugins/nostics/.agents/skills/nostics/references/migration.md @@ -0,0 +1,94 @@ +# Migrate errors and warnings to nostics + +Follow this start to finish whenever the task is migrating a library's existing user-facing errors, warnings, and logs (`console.warn`/`console.error`, `warn()` helpers, thrown `Error`s) to nostics diagnostics. Turn ad hoc reporting into a catalog of stable diagnostic codes **without changing runtime behavior**. Full API: the `nostics` skill's `SKILL.md` (this reference lives beside it). + +## What to migrate + +Inventory `console.warn`, `console.error`, `warn(...)` helpers, `throw new Error(...)`, `Promise.reject(new Error(...))`. Skip tests, fixtures, snapshots, and generated output. Plain debug `console.log`s are usually not user-facing; leave them. + +Migrate: + +- dev warnings that report and keep going +- warnings followed by recovery or a fallback (replace only the report, keep the recovery) +- plain user-facing thrown or rejected `Error`s (the diagnostic becomes the thrown/rejected value) +- deprecation notices +- build/config errors caused by a user's file: always pass **both** the original error as `cause` and the file as `sources`, because the JS stack points inside the library and is useless to the user + +Do **not** migrate: + +- **structured errors other code inspects** (type fields, private symbols, `isXxxError()` guards, `instanceof` checks): they are control flow, not reporting. Leave them unchanged. Only if such an error is also deliberately user-facing, add a separate dev-only report with the error as `cause`; never replace the error object itself. +- **catch blocks that only log a native/platform error and fall back** when the library cannot name a likely cause or a concrete fix: the native error is the best available information, keep the plain log. This exception covers platform APIs failing (e.g. `history.pushState`, storage quota), **not** errors caused by the user's own files: a caught parse or config error on a user file should become a diagnostic carrying the original error as `cause` and the file as `sources`. +- anything where the diagnostic would only restate "an operation failed". A diagnostic earns its place by naming a likely user-code problem or a concrete fix. + +## Preserve behavior exactly + +A project's dev guard may be `process.env.NODE_ENV !== 'production'` or its own build-time constant (libraries often define a flag for this; recognize whatever the codebase uses). Written as `DEV` below; treat all forms the same: + +- Keep existing dev guards exactly as they are. nostics stripping is additive and does not replace them. If a throw or reject only happened in dev, it must still only happen in dev. +- Never add a guard the original did not have. A throw or report that fired in production keeps firing in production builds that do not use stripping; note that migrating an unguarded report-only call makes it strippable, so once `nosticsStrip` runs in the build it disappears from production bundles. That is usually the goal of the migration, but if the library deliberately reports in production, surface that decision instead of changing it silently. +- Keep throw vs reject, timing, recovery code, and returned fallbacks. +- Keep structured error shapes (fields, symbols). Migrating a throw replaces the thrown message with the diagnostic's `why`: if tests assert the exact old text, update them deliberately as part of the migration, never weaken the message to dodge a test. + +## Catalog shape + +One catalog file per area of the library (a single `src/diagnostics.ts` is fine for small ones), exported directly. No factory wrappers and no deep barrel re-exports: the strip plugin tracks the export across one relative import. `nostics` is a runtime import: add it to `dependencies`, not `devDependencies` (library bundlers refuse or inline it otherwise). + +```ts +import { createConsoleReporter, defineDiagnostics } from 'nostics' + +export const diagnostics = /*#__PURE__*/ defineDiagnostics({ + docsBase: (code) => `https://example.com/e/${code.toLowerCase()}`, + reporters: [/*#__PURE__*/ createConsoleReporter()], + codes: { + LIB_R0001: { + why: (p: { hook: string }) => `${p.hook}() must be called at the top of a setup function.`, + fix: 'Move the call into setup() or a composable called by setup().', + }, + }, +}) +``` + +- Codes are `PREFIX_XNNNN`. Pick the category letter by **area**, not severity: `B` build, `R` runtime, `C` config, `D` deprecation. A runtime warning is `R`; reserve `D` for deprecations. Published codes are permanent: never rename or reuse one. +- `why` says what happened with runtime values interpolated through typed param functions (both `why` and `fix` accept them; their params are merged and required at the call site). `fix` is the concrete next action, never a restatement of the problem. +- **Split the original warning; never copy it whole into `why`.** Most existing warnings bundle the diagnosis and the remedy in one string (`"A hash must start with '#'. Prefix it with '#'."`). The reporter prints `why` **and** `fix`, so pasting the full sentence into `why` and then writing a `fix` duplicates the remedy on screen. Cut the sentence in two: diagnosis to `why`, remedy to `fix`. If the remedy needs the offending value, make `fix` a param function — its params merge with `why`'s. + + ```ts + // before: warn(`A \`hash\` should start with "#". Replace "${hash}" with "#${hash}".`) + + // ❌ remedy lives in why and is echoed by fix + { why: (p) => `A \`hash\` should start with "#". Replace "${p.hash}" with "#${p.hash}".`, + fix: 'Prefix the hash with "#".' } + + // ✅ diagnosis in why, remedy in fix (a function, because it needs the value) + { why: (p) => `A \`hash\` should start with "#" but received "${p.hash}".`, + fix: (p) => `Prepend "#": use "#${p.hash}".` } + ``` + +- Extra `console.warn`/`console.error` arguments must not be lost: an error value becomes `cause`; data values are interpolated into `why` (e.g. `JSON.stringify(p.value)`). +- `cause` and `sources` (`'file:line:column'` strings pointing at user code) go **inside the params object** (the first argument), merged with the message params. The second argument is reporter options only, e.g. `{ method: 'error' }`. +- `docsBase` is optional. If the project has no documented error-page URL scheme, propose one and surface it to the maintainer rather than inventing pages that do not exist. When pointing per-code `docs` at existing documentation with `#hash` anchors, verify each anchor against the built or published HTML (grep the `id=`) instead of guessing it. A custom slugify can keep heading casing and leave a trailing hyphen, so the real anchor may look like `#Using-the-store-outside-of-setup-`. + +## Call-site patterns + +| Before | After | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `DEV && warn(msg)` | `DEV && diagnostics.LIB_R0001(params)` (same guard) | +| warn, then recover/fallback | diagnostic, then the same recovery | +| warn, then `throw new Error(...)` | `throw diagnostics.LIB_R0002(params)` | +| warn, then `Promise.reject(new Error(...))` | `return Promise.reject(diagnostics.LIB_R0003(params))` | +| `console.error(...)` level | `diagnostics.LIB_B0001(params, { method: 'error' })` | +| caught error tied to a user file | `diagnostics.LIB_B0002({ ...params, cause: err, sources: ['src/file.ts:10:5'] }, { method: 'error' })` | +| structured/internal error | leave unchanged | + +Calling a handle always runs the reporters, so `throw diagnostics.CODE(params)` reports **and** throws. For a warn-then-throw site that is the same double output it already had. For a bare `throw new Error(...)` it adds a console report before the throw, which duplicates the message the uncaught error already shows. When a catalog's codes are only ever thrown (config validators, fatal asserts), give that catalog **no reporters** (`reporters` is optional) so the throw is the only output, and keep warnings in a separate reporter-backed catalog. Dropping the reporter also keeps a strict test harness happy when its `afterEach` fails on any unasserted `console.warn`/`console.error`. + +Report-only calls must stay bare expression statements (`DEV && diagnostics.LIB_R0001(p)` included) so `nosticsStrip` can remove them in production. `throw`/`return`/assigned diagnostics are behavior and stay. + +Dropping diagnostics from production builds takes two pieces: `/*#__PURE__*/` annotations on the catalog (`defineDiagnostics(...)` and each reporter factory call inside it) so an unused catalog tree-shakes away, and a `DEV` guard on every report-only call site. Both can be written manually in source, or the `nosticsStrip` build plugin adds them at build time (`import { nosticsStrip } from '@nostics/unplugin/strip-transform'`, then the matching unplugin adapter: `nosticsStrip.rolldown()`, `.vite()`, `.rollup()`, ...). Decide from context: when every report-only site is already dev-guarded, manual annotations in the catalog file are enough and avoid a build transform; reach for the plugin when call sites are unguarded and stripping is wanted. Either way the behavior rule holds: if the library deliberately reports unguarded in production, do not silence it with a guard or the plugin; ask the maintainer. + +## Verify + +- Tests for warnings, throws, guards, and error shapes still pass; tests asserting exact message text are updated consciously, not accidentally. +- Watch substring assertions when splitting a warning. `toHaveBeenWarned('...')` / `toContain` pin a **fragment**, not the whole message, and a fragment may sit in the remedy half you just moved to `fix`. Before splitting, grep the tests for substrings of each warning: keep pinned **diagnosis** fragments in `why`; when a test pins a **remedy** fragment, update that assertion to the surviving `why` text. The same warning is often pinned by a shared constant duplicated across several spec files — fix every copy. +- Dev-only gates are still present everywhere the source had them, and no new gates were added. +- Report-only diagnostics remain strippable expression statements. Thrown/returned diagnostics keep their message text in production by design: they are behavior, not reports. diff --git a/plugins/nostics/.claude-plugin/plugin.json b/plugins/nostics/.claude-plugin/plugin.json new file mode 100644 index 00000000..fa1d7055 --- /dev/null +++ b/plugins/nostics/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "nostics", + "version": "1.0.0", + "description": "Structured diagnostic code library for JavaScript/TypeScript. Turns errors into typed, machine-readable Diagnostic instances with stable codes, docs URLs, and actionable fixes. Covers defineDiagnostics, reporters, formatters, Vite plugins, and adding new diagnostic codes.", + "author": { + "name": "vercel-labs", + "url": "https://github.com/vercel-labs" + }, + "repository": "https://github.com/vercel-labs/nostics", + "license": "MIT", + "keywords": ["nostics", "diagnostics", "error-handling", "typescript", "javascript"], + "skills": "./.agents/skills/" +} diff --git a/plugins/nostics/.codex-plugin/plugin.json b/plugins/nostics/.codex-plugin/plugin.json new file mode 100644 index 00000000..0100e674 --- /dev/null +++ b/plugins/nostics/.codex-plugin/plugin.json @@ -0,0 +1,31 @@ +{ + "name": "nostics", + "version": "1.0.0", + "description": "Structured diagnostic code library for JavaScript/TypeScript. Turns errors into typed, machine-readable Diagnostic instances with stable codes, docs URLs, and actionable fixes. Covers defineDiagnostics, reporters, formatters, Vite plugins, and adding new diagnostic codes.", + "author": { + "name": "vercel-labs", + "url": "https://github.com/vercel-labs" + }, + "interface": { + "displayName": "Nostics", + "shortDescription": "Structured diagnostic code library for JavaScript/TypeScript", + "longDescription": "Structured diagnostic code library for JavaScript/TypeScript. Turns errors into typed, machine-readable Diagnostic instances with stable codes, docs URLs, and actionable fixes. Covers defineDiagnostics, reporters, formatters, Vite plugins, and adding new diagnostic codes.", + "developerName": "vercel-labs", + "category": "Tooling", + "capabilities": [ + "Skill" + ], + "defaultPrompt": [ + "Help me use Nostics for my current task." + ] + }, + "repository": "https://github.com/vercel-labs/nostics", + "license": "MIT", + "keywords": [ + "nostics", + "diagnostics", + "error-handling", + "typescript", + "javascript" + ] +} diff --git a/plugins/nostics/plugin.json b/plugins/nostics/plugin.json new file mode 100644 index 00000000..111a3acc --- /dev/null +++ b/plugins/nostics/plugin.json @@ -0,0 +1,18 @@ +{ + "name": "nostics", + "version": "1.0.0", + "description": "Structured diagnostic code library for JavaScript/TypeScript. Turns errors into typed, machine-readable Diagnostic instances with stable codes, docs URLs, and actionable fixes. Covers defineDiagnostics, reporters, formatters, Vite plugins, and adding new diagnostic codes.", + "author": { + "name": "vercel-labs", + "url": "https://github.com/vercel-labs" + }, + "repository": "https://github.com/vercel-labs/nostics", + "license": "MIT", + "keywords": [ + "nostics", + "diagnostics", + "error-handling", + "typescript", + "javascript" + ] +} diff --git a/plugins/nostics/skills-lock.json b/plugins/nostics/skills-lock.json new file mode 100644 index 00000000..5151d3e4 --- /dev/null +++ b/plugins/nostics/skills-lock.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "skills": { + "add-diagnostic": { + "source": "vercel-labs/nostics", + "sourceType": "github", + "skillPath": "skills/add-diagnostic/SKILL.md", + "computedHash": "c7b8e4657722027d0ec9099406e05941d33103cc76b4915859e3cd60f94617a0" + }, + "nostics": { + "source": "vercel-labs/nostics", + "sourceType": "github", + "skillPath": "skills/nostics/SKILL.md", + "computedHash": "7689f41ff1841c3cdd7805bcecbb95d89175c7029a8c305dda6b4f9fcfc82e15" + } + } +} diff --git a/release-please-config.json b/release-please-config.json index ecdf123c..6d0c30c6 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -932,6 +932,27 @@ "jsonpath": "$.version" } ] + }, + "plugins/nostics": { + "release-type": "simple", + "component": "nostics", + "extra-files": [ + { + "type": "json", + "path": ".claude-plugin/plugin.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": ".codex-plugin/plugin.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "plugin.json", + "jsonpath": "$.version" + } + ] } }, "release-type": "node",