@opsydyn/elysia-spectral 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.2](https://github.com/opsydyn/elysia-spectral/compare/v1.5.1...v1.5.2) (2026-05-12)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * complete self-describing lint reports ([b0917ce](https://github.com/opsydyn/elysia-spectral/commit/b0917ce4c977a1378ba851efc643058a70fea70b))
9
+
10
+ ## [1.5.1](https://github.com/opsydyn/elysia-spectral/compare/v1.5.0...v1.5.1) (2026-05-12)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * harden spectral import startup path ([024fedf](https://github.com/opsydyn/elysia-spectral/commit/024fedf7fe294fc7919f9d17d7727029e2a04fae))
16
+
3
17
  ## [1.5.0](https://github.com/opsydyn/elysia-spectral/compare/v1.4.0...v1.5.0) (2026-04-28)
4
18
 
5
19
 
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  Thin Elysia plugin that lints the OpenAPI document generated by `@elysiajs/openapi` with Spectral.
8
8
 
9
+ `@opsydyn/elysia-spectral` does not generate OpenAPI documents by itself. Your app must expose an OpenAPI JSON document — most commonly by mounting `@elysiajs/openapi`, or by configuring `source.specPath` / `source.baseUrl` to point at an equivalent route.
10
+
9
11
  ## What Is Elysia?
10
12
 
11
13
  Elysia is a fast, ergonomic TypeScript web framework for Bun. It uses a plugin model for composing functionality and integrates with `@elysiajs/openapi` to generate OpenAPI documentation directly from route schemas — no separate spec file or annotation layer required.
@@ -56,6 +58,7 @@ Current package scope:
56
58
  - resolver pipeline for advanced ruleset loading
57
59
  - console output
58
60
  - JSON report output
61
+ - self-describing JSON report metadata (`failOn`, `durationMs`, relative artifact paths)
59
62
  - JUnit report output
60
63
  - SARIF report output
61
64
  - OpenAPI snapshot output
@@ -109,6 +112,8 @@ bun add elysia @elysiajs/openapi @opsydyn/elysia-spectral
109
112
  npm install elysia @elysiajs/openapi @opsydyn/elysia-spectral
110
113
  ```
111
114
 
115
+ `@elysiajs/openapi` is the recommended generator because it exposes the default `/openapi/json` route this package expects. If your app uses a different OpenAPI generator or serves the JSON at a different path, configure `source.specPath` accordingly.
116
+
112
117
  2. Add `@elysiajs/openapi` and `spectralPlugin` to your app with the `strict` preset.
113
118
 
114
119
  ```ts
@@ -217,6 +222,34 @@ spectralPlugin({ ruleset: strict })
217
222
 
218
223
  When `preset` is set, autodiscovered `spectral.yaml` overrides merge on top of the preset rather than the package default. This lets you tighten or loosen individual rules without losing the preset baseline.
219
224
 
225
+ ### Provide an OpenAPI JSON route
226
+
227
+ `@opsydyn/elysia-spectral` needs a public OpenAPI JSON document to lint. By default it looks for `/openapi/json` via `app.handle(new Request(...))`.
228
+
229
+ The recommended setup is to mount `@elysiajs/openapi`:
230
+
231
+ ```ts
232
+ import { Elysia } from 'elysia'
233
+ import { openapi } from '@elysiajs/openapi'
234
+ import { spectralPlugin } from '@opsydyn/elysia-spectral'
235
+
236
+ new Elysia()
237
+ .use(openapi())
238
+ .use(spectralPlugin())
239
+ ```
240
+
241
+ If your app exposes the OpenAPI JSON document somewhere else, point the plugin at that route:
242
+
243
+ ```ts
244
+ spectralPlugin({
245
+ source: {
246
+ specPath: '/docs/openapi.json'
247
+ }
248
+ })
249
+ ```
250
+
251
+ If the route is missing, the runtime throws an actionable provider error explaining that an OpenAPI generator (for example `@elysiajs/openapi`) must be installed and mounted, or that `source.specPath` should be updated.
252
+
220
253
  ## How-to Guides
221
254
 
222
255
  ### Use a repo-level ruleset
@@ -386,9 +419,10 @@ What it surfaces:
386
419
 
387
420
  Keyboard shortcuts: `r` re-runs, `/` focuses the filter, `Enter`/`Space` toggles the focused severity chip.
388
421
 
389
- Three dark themes ship with the dashboard, selectable from the header dropdown and persisted per-browser via `localStorage`:
422
+ Four dark themes ship with the dashboard, selectable from the header dropdown and persisted per-browser via `localStorage`:
390
423
 
391
424
  - **Astro Houston** (default) — purple / blue gradient
425
+ - **Elysia** — pink on deep purple, sourced from the Scalar palette
392
426
  - **Tron Legacy** — cyan neon on near-black
393
427
  - **Detroit 808** — TR-808 amber, red, and yellow
394
428
 
@@ -523,6 +557,8 @@ Use `'warn'` for local development and `'error'` when artifact generation is req
523
557
 
524
558
  Use `createOpenApiLintRuntime` to run a standalone lint check in CI without starting an HTTP server. Import your Elysia app instance directly — the runtime uses `app.handle()` in-process to retrieve the generated OpenAPI document. No port binding required.
525
559
 
560
+ Before using the runtime, make sure the app instance mounts an OpenAPI generator (typically `@elysiajs/openapi`) or otherwise serves a reachable OpenAPI JSON route.
561
+
526
562
  Create a script at `scripts/lint-openapi.ts`:
527
563
 
528
564
  ```ts
@@ -760,7 +796,9 @@ type SpectralPluginOptions = {
760
796
  sinks?: OpenApiLintSink[]
761
797
  }
762
798
  source?: {
799
+ /** Public OpenAPI JSON route. Defaults to '/openapi/json'. Mount @elysiajs/openapi or serve an equivalent route yourself. */
763
800
  specPath?: string
801
+ /** Optional base URL fallback when the OpenAPI document is only reachable over HTTP. */
764
802
  baseUrl?: string
765
803
  }
766
804
  /**
@@ -809,6 +847,10 @@ type LintRunResult = {
809
847
  generatedAt: string
810
848
  /** Where the lint run was triggered from. */
811
849
  source: LintRunSource
850
+ /** The configured threshold that produced this result. */
851
+ failOn: SeverityThreshold
852
+ /** Duration of the completed lint run in milliseconds. */
853
+ durationMs: number | null
812
854
  summary: {
813
855
  error: number
814
856
  warn: number
@@ -971,6 +1013,8 @@ Example successful response:
971
1013
  "ok": true,
972
1014
  "generatedAt": "2026-04-06T12:00:00.000Z",
973
1015
  "source": "startup",
1016
+ "failOn": "error",
1017
+ "durationMs": 42,
974
1018
  "summary": {
975
1019
  "error": 0,
976
1020
  "warn": 0,
@@ -1000,7 +1044,7 @@ Example successful response:
1000
1044
  - startup mode `enforce` throws on threshold failures
1001
1045
  - startup mode `report` prints the same lint report but allows boot to continue on threshold failures
1002
1046
  - startup mode `off` skips startup lint
1003
- - bad `source.specPath` or invalid spec JSON produces an actionable provider error
1047
+ - missing OpenAPI generator / missing OpenAPI JSON route, bad `source.specPath`, or invalid spec JSON produces an actionable provider error
1004
1048
  - artifact writes warn by default and can be made fatal with `output.artifactWriteFailures: 'error'`
1005
1049
 
1006
1050
  ### Output model
@@ -1012,6 +1056,8 @@ The current output model has two layers:
1012
1056
 
1013
1057
  The convenience options compile down to built-in sinks so the current API stays simple while the internal output model becomes extensible.
1014
1058
 
1059
+ Persisted JSON reports are self-describing: they embed the configured `failOn` threshold, the completed `durationMs`, and relative artifact paths so the same report shape is portable across CI runners and developer machines.
1060
+
1015
1061
  ## Explanation
1016
1062
 
1017
1063
  ### Why this package exists
@@ -1022,6 +1068,8 @@ The convenience options compile down to built-in sinks so the current API stays
1022
1068
 
1023
1069
  The plugin does not inspect private `@elysiajs/openapi` internals. It resolves the generated OpenAPI JSON document through Elysia's public `app.handle(new Request(...))` API, using `source.specPath` or the default `/openapi/json`.
1024
1070
 
1071
+ `@elysiajs/openapi` is the default and recommended source for that document, but it is not the only supported source. Any equivalent OpenAPI JSON route works as long as `source.specPath` and, when needed, `source.baseUrl` point to it.
1072
+
1025
1073
  If `source.baseUrl` is configured, the provider can also fall back to loopback HTTP fetch. This keeps spec resolution on public surfaces rather than framework internals.
1026
1074
 
1027
1075
  ### Why startup and healthcheck are separate
@@ -1057,4 +1105,4 @@ Production-grade linting needs more than a pass/fail boolean. The runtime tracks
1057
1105
 
1058
1106
  ### Project status
1059
1107
 
1060
- The package is actively developed toward a stable `v1`. Milestones 0.2 through 0.6 are complete. Ongoing work is tracked in [roadmap.md](../../roadmap.md).
1108
+ The package is published to npm and used in production. The current pre-`1.0` feature roadmap is functionally complete: startup/runtime flows, presets, output sinks, CI workflows, dashboard support, and self-describing result artifacts are all shipped. Remaining work is primarily `1.0` stabilization — public API/package boundaries, backwards-compatibility audit, and migration notes — tracked in [roadmap.md](../../roadmap.md).
@@ -1,2 +1,2 @@
1
- import { a as createOpenApiLintRuntime, c as ResolvedRulesetCandidate, d as RulesetResolverContext, f as RulesetResolverInput, g as lintOpenApi, h as loadRuleset, i as OpenApiLintArtifactWriteError, l as RulesetLoadError, m as loadResolvedRuleset, n as enforceThreshold, o as LoadResolvedRulesetOptions, p as defaultRulesetResolvers, r as shouldFail, s as LoadedRuleset, t as OpenApiLintThresholdError, u as RulesetResolver } from "../index-CyJXdIRT.mjs";
1
+ import { a as createOpenApiLintRuntime, c as LoadedRuleset, d as RulesetResolverContext, f as RulesetResolverInput, g as lintOpenApi, h as loadRuleset, i as OpenApiLintArtifactWriteError, l as ResolvedRulesetCandidate, m as loadResolvedRuleset, n as enforceThreshold, o as RulesetLoadError, p as defaultRulesetResolvers, r as shouldFail, s as LoadResolvedRulesetOptions, t as OpenApiLintThresholdError, u as RulesetResolver } from "../index-DzJWrqPA.mjs";
2
2
  export { LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintThresholdError, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, shouldFail };
@@ -1,2 +1,5 @@
1
- import { a as enforceThreshold, d as RulesetLoadError, f as defaultRulesetResolvers, g as lintOpenApi, i as OpenApiLintThresholdError, m as loadRuleset, n as createOpenApiLintRuntime, o as shouldFail, p as loadResolvedRuleset, t as OpenApiLintArtifactWriteError } from "../core-BLJeXQ15.mjs";
1
+ import { t as RulesetLoadError } from "../ruleset-load-error-CogUOC7W.mjs";
2
+ import { a as enforceThreshold, i as OpenApiLintThresholdError, n as createOpenApiLintRuntime, o as shouldFail, t as OpenApiLintArtifactWriteError } from "../runtime-PGHAFx-E.mjs";
3
+ import { n as loadResolvedRuleset, r as loadRuleset, t as defaultRulesetResolvers } from "../load-ruleset-CiikrzWx.mjs";
4
+ import { t as lintOpenApi } from "../lint-openapi-D76sC7S5.mjs";
2
5
  export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, shouldFail };
@@ -1,5 +1,5 @@
1
- import { RulesetDefinition } from "@stoplight/spectral-core";
2
1
  import { AnyElysia } from "elysia";
2
+ import { RulesetDefinition } from "@stoplight/spectral-core";
3
3
 
4
4
  //#region src/types.d.ts
5
5
  type PresetName = 'recommended' | 'server' | 'strict';
@@ -147,14 +147,16 @@ type LoadResolvedRulesetOptions = {
147
147
  mergeAutodiscoveredWithDefault?: boolean; /** Override the ruleset used as the merge base for autodiscovery and the fallback when no ruleset is configured. Defaults to the package default (recommended preset). */
148
148
  defaultRuleset?: RulesetDefinition;
149
149
  };
150
+ declare const loadRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<RulesetDefinition>;
151
+ declare const loadResolvedRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<LoadedRuleset>;
152
+ declare const defaultRulesetResolvers: RulesetResolver[];
153
+ //#endregion
154
+ //#region src/core/ruleset-load-error.d.ts
150
155
  declare class RulesetLoadError extends Error {
151
156
  constructor(message: string, options?: {
152
157
  cause?: unknown;
153
158
  });
154
159
  }
155
- declare const loadRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<RulesetDefinition>;
156
- declare const loadResolvedRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<LoadedRuleset>;
157
- declare const defaultRulesetResolvers: RulesetResolver[];
158
160
  //#endregion
159
161
  //#region src/core/runtime.d.ts
160
162
  declare const createOpenApiLintRuntime: (options?: SpectralPluginOptions) => OpenApiLintRuntime;
@@ -173,4 +175,4 @@ declare class OpenApiLintThresholdError extends Error {
173
175
  declare const shouldFail: (result: LintRunResult, threshold: SeverityThreshold) => boolean;
174
176
  declare const enforceThreshold: (result: LintRunResult, threshold: SeverityThreshold) => void;
175
177
  //#endregion
176
- export { SpectralLogger as A, OpenApiLintRuntime as C, OpenApiLintSinkContext as D, OpenApiLintSink as E, StartupLintMode as M, PresetName as O, OpenApiLintArtifacts as S, OpenApiLintRuntimeStatus as T, ArtifactWriteFailureMode as _, createOpenApiLintRuntime as a, LintRunSource as b, ResolvedRulesetCandidate as c, RulesetResolverContext as d, RulesetResolverInput as f, lintOpenApi as g, loadRuleset as h, OpenApiLintArtifactWriteError as i, SpectralPluginOptions as j, SeverityThreshold as k, RulesetLoadError as l, loadResolvedRuleset as m, enforceThreshold as n, LoadResolvedRulesetOptions as o, defaultRulesetResolvers as p, shouldFail as r, LoadedRuleset as s, OpenApiLintThresholdError as t, RulesetResolver as u, LintFinding as v, OpenApiLintRuntimeFailure as w, LintSeverity as x, LintRunResult as y };
178
+ export { SpectralLogger as A, OpenApiLintRuntime as C, OpenApiLintSinkContext as D, OpenApiLintSink as E, StartupLintMode as M, PresetName as O, OpenApiLintArtifacts as S, OpenApiLintRuntimeStatus as T, ArtifactWriteFailureMode as _, createOpenApiLintRuntime as a, LintRunSource as b, LoadedRuleset as c, RulesetResolverContext as d, RulesetResolverInput as f, lintOpenApi as g, loadRuleset as h, OpenApiLintArtifactWriteError as i, SpectralPluginOptions as j, SeverityThreshold as k, ResolvedRulesetCandidate as l, loadResolvedRuleset as m, enforceThreshold as n, RulesetLoadError as o, defaultRulesetResolvers as p, shouldFail as r, LoadResolvedRulesetOptions as s, OpenApiLintThresholdError as t, RulesetResolver as u, LintFinding as v, OpenApiLintRuntimeFailure as w, LintSeverity as x, LintRunResult as y };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { A as SpectralLogger, C as OpenApiLintRuntime, D as OpenApiLintSinkContext, E as OpenApiLintSink, M as StartupLintMode, O as PresetName, S as OpenApiLintArtifacts, T as OpenApiLintRuntimeStatus, _ as ArtifactWriteFailureMode, a as createOpenApiLintRuntime, b as LintRunSource, c as ResolvedRulesetCandidate, d as RulesetResolverContext, f as RulesetResolverInput, g as lintOpenApi, h as loadRuleset, i as OpenApiLintArtifactWriteError, j as SpectralPluginOptions, k as SeverityThreshold, l as RulesetLoadError, m as loadResolvedRuleset, n as enforceThreshold, o as LoadResolvedRulesetOptions, p as defaultRulesetResolvers, r as shouldFail, s as LoadedRuleset, t as OpenApiLintThresholdError, u as RulesetResolver, v as LintFinding, w as OpenApiLintRuntimeFailure, x as LintSeverity, y as LintRunResult } from "./index-CyJXdIRT.mjs";
2
- import { RulesetDefinition } from "@stoplight/spectral-core";
1
+ import { A as SpectralLogger, C as OpenApiLintRuntime, D as OpenApiLintSinkContext, E as OpenApiLintSink, M as StartupLintMode, O as PresetName, S as OpenApiLintArtifacts, T as OpenApiLintRuntimeStatus, _ as ArtifactWriteFailureMode, a as createOpenApiLintRuntime, b as LintRunSource, c as LoadedRuleset, d as RulesetResolverContext, f as RulesetResolverInput, i as OpenApiLintArtifactWriteError, j as SpectralPluginOptions, k as SeverityThreshold, l as ResolvedRulesetCandidate, n as enforceThreshold, o as RulesetLoadError, r as shouldFail, s as LoadResolvedRulesetOptions, t as OpenApiLintThresholdError, u as RulesetResolver, v as LintFinding, w as OpenApiLintRuntimeFailure, x as LintSeverity, y as LintRunResult } from "./index-DzJWrqPA.mjs";
3
2
  import { Elysia } from "elysia";
3
+ import { RulesetDefinition } from "@stoplight/spectral-core";
4
4
 
5
5
  //#region src/plugin.d.ts
6
6
  declare const spectralPlugin: (options?: SpectralPluginOptions) => Elysia<"", {
@@ -72,4 +72,9 @@ declare const strict: RulesetDefinition;
72
72
  //#region src/presets/index.d.ts
73
73
  declare const presets: Record<PresetName, RulesetDefinition>;
74
74
  //#endregion
75
- export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintRunSource, LintSeverity, LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, PresetName, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, SeverityThreshold, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
75
+ //#region src/index.d.ts
76
+ declare const loadRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<RulesetDefinition>;
77
+ declare const loadResolvedRuleset: (input?: RulesetResolverInput, baseDirOrOptions?: string | LoadResolvedRulesetOptions) => Promise<LoadedRuleset>;
78
+ declare const lintOpenApi: (spec: Record<string, unknown>, ruleset: RulesetDefinition) => Promise<LintRunResult>;
79
+ //#endregion
80
+ export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintRunSource, LintSeverity, type LoadResolvedRulesetOptions, type LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, PresetName, type ResolvedRulesetCandidate, RulesetLoadError, type RulesetResolver, type RulesetResolverContext, type RulesetResolverInput, SeverityThreshold, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
package/dist/index.mjs CHANGED
@@ -1,4 +1,7 @@
1
- import { a as enforceThreshold, c as strict, d as RulesetLoadError, f as defaultRulesetResolvers, g as lintOpenApi, h as recommended, i as OpenApiLintThresholdError, l as server, m as loadRuleset, n as createOpenApiLintRuntime, o as shouldFail, p as loadResolvedRuleset, r as resolveStartupMode, s as presets, t as OpenApiLintArtifactWriteError, u as resolveReporter } from "./core-BLJeXQ15.mjs";
1
+ import { t as RulesetLoadError } from "./ruleset-load-error-CogUOC7W.mjs";
2
+ import { a as enforceThreshold, i as OpenApiLintThresholdError, n as createOpenApiLintRuntime, o as shouldFail, r as resolveStartupMode, s as resolveReporter, t as OpenApiLintArtifactWriteError } from "./runtime-PGHAFx-E.mjs";
3
+ import { t as recommended } from "./recommended-DgrTqq-3.mjs";
4
+ import { i as server, r as strict, t as presets } from "./presets-CCfU_diN.mjs";
2
5
  import { Elysia } from "elysia";
3
6
  //#region \0inline-text:3.mjs
4
7
  var _inline_text_3_default = "(() => {\n const THEME_KEY = 'elysia-spectral-theme';\n const THEMES = ['astro', 'elysia', 'tron', '808'];\n let stored = null;\n try {\n stored = localStorage.getItem(THEME_KEY);\n } catch {}\n const initial = THEMES.includes(stored) ? stored : 'astro';\n document.documentElement.dataset.theme = initial;\n\n const themeSel = document.querySelector('[data-theme-switcher]');\n if (themeSel) {\n themeSel.value = initial;\n themeSel.addEventListener('change', () => {\n const next = THEMES.includes(themeSel.value) ? themeSel.value : 'astro';\n document.documentElement.dataset.theme = next;\n try {\n localStorage.setItem(THEME_KEY, next);\n } catch {}\n });\n }\n\n const rel = (iso) => {\n const t = Date.parse(iso);\n if (Number.isNaN(t)) return '';\n const s = Math.round((Date.now() - t) / 1000);\n if (s < 60) return s + 's ago';\n if (s < 3600) return Math.round(s / 60) + 'm ago';\n if (s < 86400) return Math.round(s / 3600) + 'h ago';\n return Math.round(s / 86400) + 'd ago';\n };\n for (const el of document.querySelectorAll('[data-relative-time]')) {\n el.textContent = rel(el.getAttribute('data-relative-time'));\n }\n\n const rows = Array.from(document.querySelectorAll('[data-findings] tr'));\n const search = document.querySelector('[data-search]');\n const empty = document.querySelector('[data-empty-findings]');\n const chips = Array.from(document.querySelectorAll('[data-filter]'));\n let activeSeverity = 'all';\n let query = '';\n\n const apply = () => {\n let visible = 0;\n for (const tr of rows) {\n const sev = tr.getAttribute('data-severity');\n const hay = tr.getAttribute('data-haystack') || '';\n const sevOk = activeSeverity === 'all' || sev === activeSeverity;\n const qOk = !query || hay.includes(query);\n const show = sevOk && qOk;\n tr.classList.toggle('is-hidden', !show);\n if (show) visible += 1;\n }\n if (empty)\n empty.classList.toggle('hidden', visible !== 0 || rows.length === 0);\n };\n\n for (const chip of chips) {\n const select = () => {\n activeSeverity = chip.getAttribute('data-filter') || 'all';\n for (const c of chips) c.classList.toggle('is-active', c === chip);\n apply();\n };\n chip.addEventListener('click', select);\n chip.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n select();\n }\n });\n }\n\n if (search) {\n search.addEventListener('input', () => {\n query = search.value.trim().toLowerCase();\n apply();\n });\n }\n\n for (const btn of document.querySelectorAll('[data-copy]')) {\n btn.addEventListener('click', async () => {\n const value = btn.getAttribute('data-copy') || '';\n try {\n await navigator.clipboard.writeText(value);\n btn.classList.add('copied');\n btn.textContent = 'copied';\n setTimeout(() => {\n btn.classList.remove('copied');\n btn.textContent = 'copy';\n }, 1200);\n } catch {}\n });\n }\n\n document.addEventListener('keydown', (e) => {\n if (\n e.target &&\n (e.target.tagName === 'INPUT' ||\n e.target.tagName === 'TEXTAREA' ||\n e.target.tagName === 'SELECT')\n )\n return;\n if (e.key === 'r') {\n const link = document.querySelector('[data-refresh]');\n if (link) link.click();\n }\n if (e.key === '/' && search) {\n e.preventDefault();\n search.focus();\n }\n });\n})();\n";
@@ -239,4 +242,15 @@ const spectralPlugin = (options = {}) => {
239
242
  return plugin;
240
243
  };
241
244
  //#endregion
242
- export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
245
+ //#region src/index.ts
246
+ const loadRuleset = async (input, baseDirOrOptions = process.cwd()) => {
247
+ return await (await import("./load-ruleset-CiikrzWx.mjs").then((n) => n.i)).loadRuleset(input, baseDirOrOptions);
248
+ };
249
+ const loadResolvedRuleset = async (input, baseDirOrOptions = process.cwd()) => {
250
+ return await (await import("./load-ruleset-CiikrzWx.mjs").then((n) => n.i)).loadResolvedRuleset(input, baseDirOrOptions);
251
+ };
252
+ const lintOpenApi = async (spec, ruleset) => {
253
+ return await (await import("./lint-openapi-D76sC7S5.mjs").then((n) => n.n)).lintOpenApi(spec, ruleset);
254
+ };
255
+ //#endregion
256
+ export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, presets, recommended, server, shouldFail, spectralPlugin, strict };
@@ -0,0 +1,122 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
+ import { a as getSpectralConstructor, r as loadRuleset } from "./load-ruleset-CiikrzWx.mjs";
3
+ //#region src/core/finding-guidance.ts
4
+ const guidanceByCode = {
5
+ "elysia-operation-summary": "Add detail.summary to the Elysia route options so generated docs and clients have a short operation label.",
6
+ "elysia-operation-tags": "Add detail.tags with at least one stable tag, for example ['Users'] or ['Dev'].",
7
+ "operation-description": "Add detail.description with a short user-facing explanation of what the route does.",
8
+ "operation-tags": "Add a non-empty detail.tags array on the route so the OpenAPI operation is grouped consistently.",
9
+ "operation-operationId": "Add detail.operationId with a unique camelCase identifier so generated clients and SDKs have stable method names.",
10
+ "operation-success-response": "Add at least one 2xx response schema to the route, for example response: { 200: t.Object(...) }.",
11
+ "oas3-api-servers": "Add a servers array to the OpenAPI documentation config with at least one base URL.",
12
+ "info-contact": "Add an info.contact object to the OpenAPI documentation config with a name and url or email.",
13
+ "rfc9457-problem-details": "Add an \"application/problem+json\" content entry to the error response. See RFC 9457 for the Problem Details schema."
14
+ };
15
+ const getFindingRecommendation = (code, message) => {
16
+ const direct = guidanceByCode[code];
17
+ if (direct) return direct;
18
+ if (code === "oas3-schema" && message.includes("required property \"responses\"")) return "Add a response schema to the route, for example response: { 200: t.Object(...) } or response: { 200: t.Array(...) }.";
19
+ if (code.startsWith("operation-")) return "Add the missing operation metadata under detail on the Elysia route options.";
20
+ };
21
+ //#endregion
22
+ //#region src/core/normalize-findings.ts
23
+ const httpMethods = new Set([
24
+ "get",
25
+ "put",
26
+ "post",
27
+ "delete",
28
+ "options",
29
+ "head",
30
+ "patch",
31
+ "trace"
32
+ ]);
33
+ const normalizeFindings = (diagnostics, spec) => {
34
+ const findings = diagnostics.map((diagnostic) => normalizeFinding(diagnostic, spec));
35
+ const summary = findings.reduce((current, finding) => {
36
+ current[finding.severity] += 1;
37
+ current.total += 1;
38
+ return current;
39
+ }, {
40
+ error: 0,
41
+ warn: 0,
42
+ info: 0,
43
+ hint: 0,
44
+ total: 0
45
+ });
46
+ return {
47
+ ok: summary.error === 0,
48
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
49
+ source: "manual",
50
+ failOn: "error",
51
+ durationMs: null,
52
+ summary,
53
+ findings
54
+ };
55
+ };
56
+ const normalizeFinding = (diagnostic, spec) => {
57
+ const path = [...diagnostic.path];
58
+ const finding = {
59
+ code: String(diagnostic.code),
60
+ message: diagnostic.message,
61
+ severity: toLintSeverity(diagnostic.severity),
62
+ path,
63
+ documentPointer: toDocumentPointer(path)
64
+ };
65
+ const recommendation = getFindingRecommendation(String(diagnostic.code), diagnostic.message);
66
+ if (recommendation) finding.recommendation = recommendation;
67
+ if (diagnostic.source !== void 0) finding.source = diagnostic.source;
68
+ if (diagnostic.range) finding.range = {
69
+ start: diagnostic.range.start,
70
+ end: diagnostic.range.end
71
+ };
72
+ const operation = inferOperation(path, spec);
73
+ if (operation) finding.operation = operation;
74
+ return finding;
75
+ };
76
+ const toLintSeverity = (severity) => {
77
+ switch (severity) {
78
+ case 0: return "error";
79
+ case 1: return "warn";
80
+ case 2: return "info";
81
+ default: return "hint";
82
+ }
83
+ };
84
+ const toDocumentPointer = (path) => {
85
+ if (path.length === 0) return "";
86
+ return `/${path.map((segment) => String(segment).replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`;
87
+ };
88
+ const inferOperation = (path, spec) => {
89
+ if (path[0] !== "paths") return;
90
+ const routePath = typeof path[1] === "string" ? path[1] : void 0;
91
+ const method = typeof path[2] === "string" && httpMethods.has(path[2]) ? path[2] : void 0;
92
+ if (!routePath && !method) return;
93
+ const operationRecord = routePath && method ? getNestedValue(spec, [
94
+ "paths",
95
+ routePath,
96
+ method
97
+ ]) : void 0;
98
+ const operation = {};
99
+ if (routePath !== void 0) operation.path = routePath;
100
+ if (method !== void 0) operation.method = method;
101
+ if (operationRecord && typeof operationRecord === "object" && "operationId" in operationRecord) operation.operationId = String(operationRecord.operationId);
102
+ return operation;
103
+ };
104
+ const getNestedValue = (value, path) => {
105
+ let current = value;
106
+ for (const segment of path) {
107
+ if (current === null || typeof current !== "object") return;
108
+ current = current[segment];
109
+ }
110
+ return current;
111
+ };
112
+ //#endregion
113
+ //#region src/core/lint-openapi.ts
114
+ var lint_openapi_exports = /* @__PURE__ */ __exportAll({ lintOpenApi: () => lintOpenApi });
115
+ const lintOpenApi = async (spec, ruleset) => {
116
+ const [Spectral, resolvedRuleset] = await Promise.all([getSpectralConstructor(), loadRuleset(ruleset, { baseDir: process.cwd() })]);
117
+ const spectral = new Spectral();
118
+ spectral.setRuleset(resolvedRuleset);
119
+ return normalizeFindings(await spectral.run(spec, { ignoreUnknownFormat: false }), spec);
120
+ };
121
+ //#endregion
122
+ export { lint_openapi_exports as n, lintOpenApi as t };