@shipworthy/openapi-validator 0.0.0-stage → 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shipworthy AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,310 @@
1
- # Temporary Holding Version
1
+ # @shipworthy/openapi-validator
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Lint an OpenAPI 3.x document with [Spectral](https://github.com/stoplightio/spectral)'s
4
+ built-in `spectral:oas` ruleset, exposed as an [Effect](https://effect.website) service.
5
+ Pass a JSON string, a YAML string, or an already-parsed object. The linter prints
6
+ each finding to the console. It fails with a typed `LintError` when the document has
7
+ blocking problems: a duplicate `operationId`, a duplicate tag name, or an unresolved
8
+ `$ref`. It reports other style findings as warnings and does not fail on them. Use it
9
+ to gate a spec in CI or before code generation. It powers the `openapi lint` command
10
+ in `@shipworthy/openapi-cli`. If you want a command-line linter, use that CLI instead
11
+ of calling this package.
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ bun add @shipworthy/openapi-validator effect@^4.0.0
17
+ ```
18
+
19
+ ### Requirements
20
+
21
+ - **Effect 4.** The package depends on `effect` `^4.0.0`. The public API is an Effect
22
+ `Context.Service` and an Effect `Schema` error. Add a compatible version to your
23
+ app so every package resolves to one Effect instance.
24
+ - **ESM only.** The package is `"type": "module"` and has no CommonJS build.
25
+ - **Node.js `>=22.19.0` or Bun.** The package ships compiled JavaScript and `.d.ts`
26
+ declarations in `dist/`. No TypeScript loader or bundler is needed. Its public
27
+ API is exported from `@shipworthy/openapi-validator`.
28
+ - These dependencies are installed with the package (you do not install them):
29
+ `@stoplight/spectral-core@1.24.0`, `@stoplight/spectral-formats@1.9.0`,
30
+ `@stoplight/spectral-parsers@1.0.5`, `@stoplight/spectral-rulesets@1.23.0`.
31
+
32
+ ## Quick start
33
+
34
+ ```ts
35
+ import { Effect } from "effect";
36
+ import { Linter } from "@shipworthy/openapi-validator";
37
+
38
+ const spec = `openapi: 3.1.0
39
+ info:
40
+ title: Pets
41
+ version: 1.0.0
42
+ paths:
43
+ /pets:
44
+ get:
45
+ operationId: listPets
46
+ responses:
47
+ "200":
48
+ description: ok
49
+ /pets/{id}:
50
+ get:
51
+ operationId: listPets
52
+ responses:
53
+ "200":
54
+ description: ok
55
+ `;
56
+
57
+ const program = Effect.gen(function* () {
58
+ const linter = yield* Linter;
59
+ yield* linter.lintDocument(spec, { sourceUrl: "file:///openapi.yaml" });
60
+ console.log("Spec is valid");
61
+ }).pipe(
62
+ Effect.catchTag("LintError", (error) =>
63
+ Effect.sync(() => {
64
+ for (const d of error.diagnostics) console.log(d.code, d.path.join("."));
65
+ }),
66
+ ),
67
+ Effect.provide(Linter.layer),
68
+ );
69
+
70
+ await Effect.runPromise(program);
71
+ ```
72
+
73
+ Output (warnings go to `console.warn`, errors to `console.error`):
74
+
75
+ ```
76
+ ⚠ OpenAPI "servers" must be present and non-empty array.
77
+ ⚠ Info object must have "contact" object. at #/info
78
+ ...
79
+ ⚠ Operation must define parameter "{id}" as expected by path "/pets/{id}". at #/paths//pets/{id}/get
80
+ ✘ Every operation must have unique "operationId". at #/paths//pets/{id}/get/operationId
81
+ operation-operationId-unique paths./pets/{id}.get.operationId
82
+ ```
83
+
84
+ Only the duplicate `operationId` fails the run. The other findings are warnings.
85
+
86
+ ## API
87
+
88
+ The package exports two names: `Linter` and `LintError`.
89
+
90
+ ### `Linter`
91
+
92
+ ```ts
93
+ class Linter extends Context.Service<
94
+ Linter,
95
+ {
96
+ readonly lintDocument: (
97
+ source: string | Record<string, unknown>,
98
+ options?: {
99
+ readonly sourceUrl?: string;
100
+ readonly silent?: boolean;
101
+ readonly strict?: boolean;
102
+ },
103
+ ) => Effect.Effect<void, LintError>;
104
+ }
105
+ >()("@shipworthy/openapi-validator/Linter") {
106
+ static readonly layer: Layer.Layer<Linter>;
107
+ }
108
+ ```
109
+
110
+ - **`Linter.layer`**: a `Layer` with no requirements. It builds one configured
111
+ Spectral instance per mode (default and strict), and you can reuse them for many
112
+ `lintDocument` calls.
113
+ Provide it with `Effect.provide(Linter.layer)`.
114
+ - **`Linter.use(fn)`**: the standard `Context.Service` accessor. Use it for one-off
115
+ calls:
116
+
117
+ ```ts
118
+ await Effect.runPromise(
119
+ Linter.use((linter) => linter.lintDocument(source)).pipe(Effect.provide(Linter.layer)),
120
+ );
121
+ ```
122
+
123
+ #### `lintDocument(source, options?)`
124
+
125
+ | Parameter | Type | Default | Description |
126
+ | ------------------- | ----------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
127
+ | `source` | `string \| Record<string, unknown>` | (required) | The document. A string whose first non-whitespace character is `{` is parsed as JSON. Any other string is parsed as YAML. An object is linted as-is. |
128
+ | `options.sourceUrl` | `string` | `undefined` | The document's URI (for example `file:///abs/openapi.yaml` or `https://…/openapi.yaml`). Spectral uses it to identify the document when it resolves `$ref`s. |
129
+ | `options.silent` | `boolean` | `false` | Suppresses warning output. Errors are always printed. |
130
+ | `options.strict` | `boolean` | `false` | Keeps Spectral's original `spectral:oas` severities and fails on YAML/JSON syntax errors. See [Strict mode](#strict-mode). |
131
+
132
+ **Returns:** `Effect<void, LintError>`. The effect succeeds with `undefined` when no
133
+ blocking diagnostics are found, even if the linter printed warnings.
134
+
135
+ **Fails with `LintError` when any of the following is true:**
136
+
137
+ - The input is not an OpenAPI 3.x document: it is not a plain object with an `openapi`
138
+ field whose major version is `3`. Swagger 2.0, arrays, and `{}` are all rejected. The
139
+ error has `message: "Expected an OpenAPI 3.x document"` and one diagnostic,
140
+ `{ code: "unsupported-format", path: [] }`.
141
+ - At least one blocking diagnostic remains after filtering (see
142
+ [Rules](#rules-and-severities)). `message` is every diagnostic message joined with
143
+ `"\n"`.
144
+ - Building the document or running Spectral throws. `message` is the thrown error's
145
+ message, `diagnostics` is `[]`, and `cause` holds the original error.
146
+
147
+ **Console output:** the linter writes each finding through Effect's `Console` service.
148
+ Blocking diagnostics go to `Console.error` with the prefix `✘ `. Non-blocking ones
149
+ go to `Console.warn` with the prefix `⚠ `. A message ends with ` at #/<path>` when it
150
+ has a location. Output is colored red or yellow only when `process.stdout.isTTY` is
151
+ true and `NO_COLOR` is unset. To capture or redirect output, provide your own
152
+ `Console` (for example `TestConsole` from `effect/testing` in tests).
153
+
154
+ ### `LintError`
155
+
156
+ ```ts
157
+ class LintError extends Schema.TaggedError<LintError>()("LintError", {
158
+ message: Schema.String,
159
+ diagnostics: Schema.Array(
160
+ Schema.Struct({
161
+ code: Schema.Union([Schema.String, Schema.Finite]),
162
+ message: Schema.String,
163
+ path: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
164
+ }),
165
+ ),
166
+ cause: Schema.optional(Schema.Defect()),
167
+ }) {}
168
+ ```
169
+
170
+ - `_tag`: `"LintError"`. Handle it with `Effect.catchTag("LintError", …)`.
171
+ - `diagnostics[].code`: the Spectral rule ID (for example
172
+ `"operation-operationId-unique"`, `"openapi-tags-uniqueness"`, or `"invalid-ref"`),
173
+ or `"unsupported-format"`.
174
+ - `diagnostics[].message`: the rule message plus ` at #/<path>` when a location exists.
175
+ - `diagnostics[].path`: JSON path segments into the document. Segments are strings,
176
+ including array indexes and status codes: `["tags", "1", "name"]`.
177
+
178
+ Example `diagnostics` for a duplicate tag and a missing schema reference:
179
+
180
+ ```json
181
+ [
182
+ {
183
+ "code": "openapi-tags-uniqueness",
184
+ "message": "\"tags\" object contains duplicate tag name \"a\". at #/tags/1/name",
185
+ "path": ["tags", "1", "name"]
186
+ },
187
+ {
188
+ "code": "invalid-ref",
189
+ "message": "'#/components/schemas/Missing' does not exist at #/paths//x/get/responses/200/content/application/json/schema/$ref",
190
+ "path": [
191
+ "paths",
192
+ "/x",
193
+ "get",
194
+ "responses",
195
+ "200",
196
+ "content",
197
+ "application/json",
198
+ "schema",
199
+ "$ref"
200
+ ]
201
+ }
202
+ ]
203
+ ```
204
+
205
+ ## Rules and severities
206
+
207
+ The linter extends Spectral's `spectral:oas` ruleset in its default ("recommended")
208
+ mode, restricted to the OpenAPI 3.x format. It changes Spectral's behavior in these
209
+ ways:
210
+
211
+ | Change | Effect |
212
+ | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
213
+ | `oas3-schema` is **off** | The linter does not validate the document's structure against the OpenAPI JSON Schema, so partial documents can be linted. |
214
+ | `spectral:oas` rules that default to `error` are downgraded to **warn** | This applies to `path-params`, `array-items`, `no-$ref-siblings`, `oas3-server-variables`, `oas3-valid-media-example`, and `oas3-valid-schema-example`. They are printed but do not fail. |
215
+ | `operation-operationId-unique` keeps **error** severity | A duplicate `operationId` fails the run. |
216
+ | Parser option `incompatibleValues` is off | YAML numeric keys such as `200:` are accepted. |
217
+
218
+ Some findings are always reported as warnings, even when Spectral marks them as errors:
219
+
220
+ - `parser` diagnostics, such as YAML or JSON syntax errors. A malformed YAML string is
221
+ printed as warnings and does **not** fail the run.
222
+ - `invalid-ref` diagnostics whose message starts with `ReadError:` and whose path
223
+ includes `examples`. These are example payloads that use `$ref` as data.
224
+
225
+ **Diagnostics that fail the run:** `operation-operationId-unique`,
226
+ `openapi-tags-uniqueness`, and `invalid-ref` for unresolved `$ref`s. Any other
227
+ diagnostic that Spectral reports with error severity also fails, except the two cases
228
+ listed above.
229
+
230
+ **Common warnings (non-blocking):** `oas3-api-servers`, `info-contact`,
231
+ `info-description`, `operation-description`, `operation-tags`, `operation-tag-defined`,
232
+ `operation-success-response`, `oas3-unused-component`, `duplicated-entry-in-enum`,
233
+ `path-params`, and the other recommended `spectral:oas` rules.
234
+
235
+ ### Strict mode
236
+
237
+ Pass `strict: true` to turn off the lenient overrides:
238
+
239
+ - Every `spectral:oas` rule keeps its original severity. Rules that default to `error`
240
+ (`path-params`, `array-items`, `no-$ref-siblings`, and the others listed above) fail
241
+ the run.
242
+ - `parser` diagnostics (YAML or JSON syntax errors, such as duplicate keys) fail the run.
243
+ - `oas3-schema` stays **off**, and `invalid-ref` diagnostics for `$ref`s inside
244
+ `examples` are still warnings.
245
+
246
+ ```ts
247
+ yield * linter.lintDocument(spec, { sourceUrl: "file:///openapi.yaml", strict: true });
248
+ ```
249
+
250
+ Apart from `strict`, you cannot change the rules. The package does not support a `.spectral.yaml` file,
251
+ custom rulesets, or per-rule overrides.
252
+
253
+ ## Supported OpenAPI versions
254
+
255
+ - **Supported:** OpenAPI 3.0.x, 3.1.x, and 3.2.x. The check is
256
+ `parseInt(document.openapi) === 3`.
257
+ - **Rejected:** Swagger / OpenAPI 2.0 and any non-object input. Both fail with code
258
+ `unsupported-format`.
259
+
260
+ ## Caveats
261
+
262
+ - **The linter does not validate the document's structure.** Because `oas3-schema` is
263
+ off, a document with no `paths` or a wrongly typed `info` still passes. Spectral only
264
+ prints warnings for those problems.
265
+ - **YAML syntax errors do not fail by default.** They appear only as `⚠` warnings.
266
+ Pass `strict: true` to make them fail the run.
267
+ - **JSON detection relies on the first character.** A string is treated as JSON only
268
+ if its first non-whitespace character is `{`. Every other string is parsed as YAML.
269
+ JSON is a subset of YAML, so most JSON still lints correctly through the YAML path.
270
+ - **The document is linted as given.** The linter does not bundle or normalize it.
271
+ Diagnostics point at paths in your original document.
272
+ - **The linter writes to the console.** `silent: true` hides warnings only. To
273
+ suppress everything, provide a no-op `Console` service.
274
+
275
+ ## Troubleshooting
276
+
277
+ - **`Expected an OpenAPI 3.x document`**: the root object has no `openapi: 3.x` field.
278
+ This also happens with a Swagger 2.0 file or an empty object.
279
+ - **The run fails but you only see warnings above the error**: only the `✘` lines
280
+ are blocking. Read `error.diagnostics` for the exact set.
281
+ - **Unexpected `invalid-ref` errors**: check that every local `$ref` target exists
282
+ under `#/components/...`. When you lint a file or URL, pass its location as
283
+ `sourceUrl`, as the `openapi lint` CLI does.
284
+ - **Escape codes in logs**: set `NO_COLOR=1`, or run without a TTY.
285
+
286
+ ## Key facts (for AI agents)
287
+
288
+ - Exports: `Linter` (an Effect `Context.Service` with `Linter.layer`) and `LintError`
289
+ (an Effect `Schema.TaggedError`, `_tag: "LintError"`). There are no other exports.
290
+ - Call pattern:
291
+ `Linter.use((l) => l.lintDocument(source, { sourceUrl?, silent?, strict? })).pipe(Effect.provide(Linter.layer))`.
292
+ - `source` can be a JSON string, a YAML string, or a plain object. The call returns
293
+ `Effect<void, LintError>`.
294
+ - Inputs that are not OpenAPI 3.x fail with diagnostic code `unsupported-format`.
295
+ - The ruleset is `spectral:oas` (recommended) with `oas3-schema` off. Error-level oas
296
+ rules are downgraded to warnings, except `operation-operationId-unique`.
297
+ `openapi-tags-uniqueness` and `invalid-ref` also fail. Parser errors only warn.
298
+ - `strict: true` keeps Spectral's original `oas` severities and makes parser
299
+ (syntax) diagnostics fail. `oas3-schema` stays off in both modes. Nothing else is
300
+ configurable.
301
+ - Output goes through Effect `Console` (`error` for blocking diagnostics, `warn`
302
+ otherwise). Color is used only on a TTY without `NO_COLOR`.
303
+ - Requirements: ESM only, compiled JavaScript and `.d.ts` declarations in `dist/`,
304
+ Node.js `>=22.19.0` or Bun, and a dependency on `effect@^4.0.0`.
305
+ - The CLI equivalent is `openapi lint [--strict] <file-or-url>` from
306
+ `@shipworthy/openapi-cli`.
307
+
308
+ ## License
309
+
310
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,18 @@
1
+ import { Schema } from "effect";
2
+ export declare const Diagnostic: Schema.Struct<{
3
+ readonly code: Schema.Union<readonly [Schema.String, Schema.Finite]>;
4
+ readonly message: Schema.String;
5
+ readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
6
+ }>;
7
+ declare const LintError_base: Schema.Class<LintError, Schema.TaggedStruct<"LintError", {
8
+ readonly message: Schema.String;
9
+ readonly diagnostics: Schema.$Array<Schema.Struct<{
10
+ readonly code: Schema.Union<readonly [Schema.String, Schema.Finite]>;
11
+ readonly message: Schema.String;
12
+ readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
13
+ }>>;
14
+ readonly cause: Schema.optional<Schema.Defect>;
15
+ }>, import("effect/Cause").YieldableError>;
16
+ export declare class LintError extends LintError_base {
17
+ }
18
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,12 @@
1
+ import { Schema } from "effect";
2
+ export const Diagnostic = Schema.Struct({
3
+ code: Schema.Union([Schema.String, Schema.Finite]),
4
+ message: Schema.String,
5
+ path: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
6
+ });
7
+ export class LintError extends Schema.TaggedError()("LintError", {
8
+ message: Schema.String,
9
+ diagnostics: Schema.Array(Diagnostic),
10
+ cause: Schema.optional(Schema.Defect()),
11
+ }) {
12
+ }
@@ -0,0 +1,2 @@
1
+ export { LintError } from "./errors.ts";
2
+ export { Linter } from "./lint.ts";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { LintError } from "./errors.js";
2
+ export { Linter } from "./lint.js";
package/dist/lint.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { LintError } from "./errors.ts";
3
+ interface LintOptions {
4
+ readonly sourceUrl?: string;
5
+ readonly silent?: boolean;
6
+ /** Keep Spectral's `oas` severities and fail on YAML/JSON syntax errors. */
7
+ readonly strict?: boolean;
8
+ }
9
+ declare const Linter_base: Context.ServiceClass<Linter, "@shipworthy/openapi-validator/Linter", {
10
+ readonly lintDocument: (source: string | Record<string, unknown>, options?: LintOptions) => Effect.Effect<void, LintError>;
11
+ }>;
12
+ export declare class Linter extends Linter_base {
13
+ static readonly layer: Layer.Layer<Linter, never, never>;
14
+ }
15
+ export {};
package/dist/lint.js ADDED
@@ -0,0 +1,125 @@
1
+ import { Document, Spectral } from "@stoplight/spectral-core";
2
+ import { oas3 } from "@stoplight/spectral-formats";
3
+ // spectral-parsers is CommonJS without statically detectable named exports, so Node needs a default import.
4
+ import spectralParsers from "@stoplight/spectral-parsers";
5
+ import { oas } from "@stoplight/spectral-rulesets";
6
+ import { Array as EffectArray, Config, Console, Context, Effect, Layer, Predicate } from "effect";
7
+ import { LintError } from "./errors.js";
8
+ /** Same TTY and `NO_COLOR` check as `CliOutput.defaultFormatter`. */
9
+ const colorsEnabled = Effect.gen(function* () {
10
+ const noColor = yield* Config.String("NO_COLOR").pipe(Config.withDefault(""), Effect.orDie);
11
+ const current = globalThis.process;
12
+ if (typeof current !== "object" || current === null)
13
+ return false;
14
+ const stdout = current.stdout;
15
+ if (typeof stdout !== "object" || stdout === null)
16
+ return false;
17
+ return stdout.isTTY === true && !noColor;
18
+ });
19
+ function paint(code, enabled) {
20
+ return (text) => (enabled ? `\x1b[${code}m${text}\x1b[0m` : text);
21
+ }
22
+ function spectralRules(strict) {
23
+ const rules = {
24
+ // Lint can inspect partial documents; the parser owns structural validation.
25
+ "oas3-schema": "off",
26
+ };
27
+ if (strict)
28
+ return rules;
29
+ for (const [id, rule] of Object.entries(oas.rules ?? {})) {
30
+ if (id === "operation-operationId-unique")
31
+ continue;
32
+ if (Predicate.isObject(rule) && "severity" in rule && rule.severity === 0) {
33
+ rules[id] = "warn";
34
+ }
35
+ }
36
+ return rules;
37
+ }
38
+ function isExampleRefRead(result) {
39
+ return (result.code === "invalid-ref" &&
40
+ result.message.startsWith("ReadError:") &&
41
+ result.path.includes("examples"));
42
+ }
43
+ function makeSpectral(strict) {
44
+ const spectral = new Spectral();
45
+ spectral.setRuleset({
46
+ extends: [oas],
47
+ formats: [oas3],
48
+ parserOptions: {
49
+ // OpenAPI status codes are YAML numbers (`200:`). Spectral's parser can flag that.
50
+ incompatibleValues: "off",
51
+ },
52
+ rules: spectralRules(strict),
53
+ });
54
+ return spectral;
55
+ }
56
+ function spectralTarget(source, sourceUrl) {
57
+ if (typeof source !== "string")
58
+ return source;
59
+ if (source.trimStart().startsWith("{")) {
60
+ return new Document(source, spectralParsers.Json, sourceUrl);
61
+ }
62
+ return new Document(source, spectralParsers.Yaml, sourceUrl);
63
+ }
64
+ function thrownMessage(cause) {
65
+ return cause instanceof Error ? cause.message : String(cause);
66
+ }
67
+ function pathSegments(path) {
68
+ return path.filter((segment) => Predicate.isString(segment) || Predicate.isNumber(segment));
69
+ }
70
+ export class Linter extends Context.Service()("@shipworthy/openapi-validator/Linter") {
71
+ static layer = Layer.sync(Linter, () => {
72
+ const lenient = makeSpectral(false);
73
+ const strict = makeSpectral(true);
74
+ const lintDocument = Effect.fn("Linter.lintDocument")(function* (source, options = {}) {
75
+ const isStrict = options.strict === true;
76
+ const spectral = isStrict ? strict : lenient;
77
+ const target = yield* Effect.try({
78
+ try: () => spectralTarget(source, options.sourceUrl),
79
+ catch: (cause) => new LintError({ message: thrownMessage(cause), diagnostics: [], cause }),
80
+ });
81
+ if (!oas3(target instanceof Document ? target.data : target, options.sourceUrl ?? null)) {
82
+ const message = "Expected an OpenAPI 3.x document";
83
+ return yield* new LintError({
84
+ message,
85
+ diagnostics: [{ code: "unsupported-format", message, path: [] }],
86
+ });
87
+ }
88
+ const results = yield* Effect.tryPromise({
89
+ try: () => spectral.run(target),
90
+ catch: (cause) => new LintError({
91
+ message: thrownMessage(cause),
92
+ diagnostics: [],
93
+ cause,
94
+ }),
95
+ });
96
+ const useColors = yield* colorsEnabled;
97
+ const red = paint("31", useColors);
98
+ const yellow = paint("33", useColors);
99
+ const failures = yield* Effect.forEach(results, Effect.fnUntraced(function* (result) {
100
+ const location = result.path.length ? ` at #/${result.path.join("/")}` : "";
101
+ const message = `${result.message}${location}`;
102
+ // Spectral's parser rejects merge tags this repo accepts, and example payloads
103
+ // use `$ref` as data. Those diagnostics are not document errors unless strict.
104
+ const blocking = result.severity === 0 &&
105
+ (isStrict || result.code !== "parser") &&
106
+ !isExampleRefRead(result);
107
+ if (blocking) {
108
+ yield* Console.error(red(` ✘ ${message}`));
109
+ return { code: result.code, message, path: pathSegments(result.path) };
110
+ }
111
+ else if (!options.silent) {
112
+ yield* Console.warn(yellow(` ⚠ ${message}`));
113
+ }
114
+ return undefined;
115
+ })).pipe(Effect.map(EffectArray.filter(Predicate.isNotUndefined)));
116
+ if (failures.length > 0) {
117
+ return yield* new LintError({
118
+ message: failures.map((diagnostic) => diagnostic.message).join("\n"),
119
+ diagnostics: failures,
120
+ });
121
+ }
122
+ });
123
+ return Linter.of({ lintDocument });
124
+ });
125
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@shipworthy/openapi-validator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.2",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "dependencies": {
8
+ "@stoplight/spectral-core": "1.24.0",
9
+ "@stoplight/spectral-formats": "1.9.0",
10
+ "@stoplight/spectral-parsers": "1.0.5",
11
+ "@stoplight/spectral-rulesets": "1.23.0",
12
+ "effect": "^4.0.0"
13
+ },
14
+ "description": "Lint an OpenAPI document.",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/ShipWorthyAI/openapi.git",
19
+ "directory": "packages/openapi-validator"
20
+ },
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "scripts": {
30
+ "test": "vitest run",
31
+ "build": "rm -rf dist && tsc -p tsconfig.build.json"
32
+ },
33
+ "devDependencies": {
34
+ "@effect/vitest": "4.0.0",
35
+ "@shipworthy/typescript-config": "0.0.1",
36
+ "@types/node": "^24.10.3",
37
+ "typescript": "^7.0.2",
38
+ "vitest": "5.0.3"
39
+ },
40
+ "engines": {
41
+ "node": ">=22.19.0"
42
+ },
43
+ "main": "./dist/index.js",
44
+ "types": "./dist/index.d.ts",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }