@opsydyn/elysia-spectral 1.5.1 → 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,12 @@
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
+
3
10
  ## [1.5.1](https://github.com/opsydyn/elysia-spectral/compare/v1.5.0...v1.5.1) (2026-05-12)
4
11
 
5
12
 
package/README.md CHANGED
@@ -58,6 +58,7 @@ Current package scope:
58
58
  - resolver pipeline for advanced ruleset loading
59
59
  - console output
60
60
  - JSON report output
61
+ - self-describing JSON report metadata (`failOn`, `durationMs`, relative artifact paths)
61
62
  - JUnit report output
62
63
  - SARIF report output
63
64
  - OpenAPI snapshot output
@@ -846,6 +847,10 @@ type LintRunResult = {
846
847
  generatedAt: string
847
848
  /** Where the lint run was triggered from. */
848
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
849
854
  summary: {
850
855
  error: number
851
856
  warn: number
@@ -1008,6 +1013,8 @@ Example successful response:
1008
1013
  "ok": true,
1009
1014
  "generatedAt": "2026-04-06T12:00:00.000Z",
1010
1015
  "source": "startup",
1016
+ "failOn": "error",
1017
+ "durationMs": 42,
1011
1018
  "summary": {
1012
1019
  "error": 0,
1013
1020
  "warn": 0,
@@ -1049,6 +1056,8 @@ The current output model has two layers:
1049
1056
 
1050
1057
  The convenience options compile down to built-in sinks so the current API stays simple while the internal output model becomes extensible.
1051
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
+
1052
1061
  ## Explanation
1053
1062
 
1054
1063
  ### Why this package exists
@@ -1096,4 +1105,4 @@ Production-grade linting needs more than a pass/fail boolean. The runtime tracks
1096
1105
 
1097
1106
  ### Project status
1098
1107
 
1099
- The package is published to npm and used in production. Ongoing work — additional rules, deeper Elysia integrations, and dashboard polishis 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,5 +1,5 @@
1
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-4LlfDIZv.mjs";
2
+ import { a as enforceThreshold, i as OpenApiLintThresholdError, n as createOpenApiLintRuntime, o as shouldFail, t as OpenApiLintArtifactWriteError } from "../runtime-PGHAFx-E.mjs";
3
3
  import { n as loadResolvedRuleset, r as loadRuleset, t as defaultRulesetResolvers } from "../load-ruleset-CiikrzWx.mjs";
4
4
  import { t as lintOpenApi } from "../lint-openapi-D76sC7S5.mjs";
5
5
  export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, lintOpenApi, loadResolvedRuleset, loadRuleset, shouldFail };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
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-4LlfDIZv.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
3
  import { t as recommended } from "./recommended-DgrTqq-3.mjs";
4
4
  import { i as server, r as strict, t as presets } from "./presets-CCfU_diN.mjs";
5
5
  import { Elysia } from "elysia";
@@ -411,6 +411,11 @@ const toSarifArtifactUri = (value) => {
411
411
  };
412
412
  //#endregion
413
413
  //#region src/output/sinks.ts
414
+ const relativiseArtifactPath = (artifactPath) => {
415
+ const resolvedPath = path.resolve(process.cwd(), artifactPath);
416
+ const relativePath = path.relative(process.cwd(), resolvedPath);
417
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
418
+ };
414
419
  const createOutputSinks = (options) => {
415
420
  const reporter = resolveReporter(options.logger);
416
421
  const sinks = [];
@@ -421,24 +426,17 @@ const createOutputSinks = (options) => {
421
426
  if (configuredSpecSnapshotPath) sinks.push({
422
427
  name: "spec snapshot",
423
428
  kind: "artifact",
429
+ phase: "pre-finalize",
424
430
  async write(_result, context) {
425
431
  const writtenSpecSnapshotPath = await writeSpecSnapshot(configuredSpecSnapshotPath === true ? await resolveDefaultSpecSnapshotPath() : configuredSpecSnapshotPath, context.spec, options.output?.pretty !== false);
426
432
  reporter.artifact(`OpenAPI lint wrote spec snapshot to ${writtenSpecSnapshotPath}.`);
427
433
  return { specSnapshotPath: writtenSpecSnapshotPath };
428
434
  }
429
435
  });
430
- if (configuredJsonReportPath) sinks.push({
431
- name: "JSON report",
432
- kind: "artifact",
433
- async write(result) {
434
- const writtenJsonReportPath = await writeJsonReport(configuredJsonReportPath, result, options.output?.pretty !== false);
435
- reporter.artifact(`OpenAPI lint wrote JSON report to ${writtenJsonReportPath}.`);
436
- return { jsonReportPath: writtenJsonReportPath };
437
- }
438
- });
439
436
  if (configuredJunitReportPath) sinks.push({
440
437
  name: "JUnit report",
441
438
  kind: "artifact",
439
+ phase: "pre-finalize",
442
440
  async write(result) {
443
441
  const writtenJunitReportPath = await writeJunitReport(configuredJunitReportPath, result);
444
442
  reporter.artifact(`OpenAPI lint wrote JUnit report to ${writtenJunitReportPath}.`);
@@ -449,6 +447,7 @@ const createOutputSinks = (options) => {
449
447
  if (configuredBrunoCollectionPath) sinks.push({
450
448
  name: "Bruno collection",
451
449
  kind: "artifact",
450
+ phase: "pre-finalize",
452
451
  async write(_result, context) {
453
452
  const writtenPath = await writeBrunoCollection(configuredBrunoCollectionPath, context.spec);
454
453
  reporter.artifact(`OpenAPI lint wrote Bruno collection to ${writtenPath}.`);
@@ -458,6 +457,7 @@ const createOutputSinks = (options) => {
458
457
  if (configuredSarifReportPath) sinks.push({
459
458
  name: "SARIF report",
460
459
  kind: "artifact",
460
+ phase: "pre-finalize",
461
461
  async write(result) {
462
462
  const writtenSarifReportPath = await writeSarifReport(configuredSarifReportPath, result, options.output?.pretty !== false);
463
463
  reporter.artifact(`OpenAPI lint wrote SARIF report to ${writtenSarifReportPath}.`);
@@ -467,11 +467,29 @@ const createOutputSinks = (options) => {
467
467
  for (const sink of options.output?.sinks ?? []) sinks.push({
468
468
  name: sink.name,
469
469
  kind: "custom",
470
+ phase: "post-finalize",
470
471
  write: async (result, context) => await Promise.resolve(sink.write(result, context))
471
472
  });
473
+ if (configuredJsonReportPath) sinks.push({
474
+ name: "JSON report",
475
+ kind: "artifact",
476
+ phase: "post-finalize",
477
+ async write(result) {
478
+ const writtenJsonReportPath = await writeJsonReport(configuredJsonReportPath, {
479
+ ...result,
480
+ artifacts: {
481
+ ...result.artifacts ?? {},
482
+ jsonReportPath: relativiseArtifactPath(configuredJsonReportPath)
483
+ }
484
+ }, options.output?.pretty !== false);
485
+ reporter.artifact(`OpenAPI lint wrote JSON report to ${writtenJsonReportPath}.`);
486
+ return { jsonReportPath: writtenJsonReportPath };
487
+ }
488
+ });
472
489
  if (options.output?.console !== false) sinks.push({
473
490
  name: "console",
474
491
  kind: "report",
492
+ phase: "post-finalize",
475
493
  async write(result) {
476
494
  reportToConsole(result, reporter);
477
495
  }
@@ -645,10 +663,12 @@ const createOpenApiLintRuntime = (options = {}) => {
645
663
  result.source = source;
646
664
  result.failOn = options.failOn ?? "error";
647
665
  result.ok = !shouldFail(result, result.failOn);
648
- await writeOutputSinks(result, spec, options, artifactWriteFailureMode);
649
- runtime.latest = result;
666
+ const { preFinalizeSinks, postFinalizeSinks } = partitionOutputSinks(createOutputSinks(options));
667
+ await writeOutputSinks(preFinalizeSinks, result, spec, reporter, artifactWriteFailureMode);
650
668
  finalizeRuntimeRun(runtime, startedAt);
651
669
  result.durationMs = runtime.durationMs;
670
+ await writeOutputSinks(postFinalizeSinks, result, spec, reporter, artifactWriteFailureMode);
671
+ runtime.latest = result;
652
672
  reporter.complete("OpenAPI lint completed.");
653
673
  enforceThreshold(result, options.failOn ?? "error");
654
674
  runtime.status = "passed";
@@ -694,9 +714,7 @@ const handleArtifactWriteFailure = (artifact, error, mode, reporter) => {
694
714
  if (mode === "error") throw wrappedError;
695
715
  reporter.warn(wrappedError.message);
696
716
  };
697
- const writeOutputSinks = async (result, spec, options, artifactWriteFailureMode) => {
698
- const reporter = resolveReporter(options.logger);
699
- const sinks = createOutputSinks(options);
717
+ const writeOutputSinks = async (sinks, result, spec, reporter, artifactWriteFailureMode) => {
700
718
  for (const sink of sinks) try {
701
719
  const artifacts = await sink.write(result, {
702
720
  spec,
@@ -711,6 +729,21 @@ const writeOutputSinks = async (result, spec, options, artifactWriteFailureMode)
711
729
  throw error;
712
730
  }
713
731
  };
732
+ const partitionOutputSinks = (sinks) => {
733
+ const preFinalizeSinks = [];
734
+ const postFinalizeSinks = [];
735
+ for (const sink of sinks) {
736
+ if (sink.phase === "post-finalize") {
737
+ postFinalizeSinks.push(sink);
738
+ continue;
739
+ }
740
+ preFinalizeSinks.push(sink);
741
+ }
742
+ return {
743
+ preFinalizeSinks,
744
+ postFinalizeSinks
745
+ };
746
+ };
714
747
  const relativiseArtifacts = (artifacts) => {
715
748
  const cwd = process.cwd();
716
749
  const result = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsydyn/elysia-spectral",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Thin Elysia plugin that lints generated OpenAPI documents with Spectral.",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "publishConfig": {