@thymian/plugin-spectral 0.2.0-canary.20260810-95e9b0c

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.
@@ -0,0 +1,19 @@
1
+ import type { LintExecution, Logger, RuleDescriptor, ThymianFormat } from '@thymian/core';
2
+ import type { SpectralResult } from './spectral-types.js';
3
+ export interface ConvertedResults {
4
+ executions: LintExecution[];
5
+ rules: RuleDescriptor[];
6
+ }
7
+ /**
8
+ * Maps Spectral results to failed lint executions plus one RuleDescriptor per
9
+ * distinct code. Every Spectral result is a problem, so every execution is
10
+ * `failed` with the finding's own severity on the status (per-finding
11
+ * fidelity regardless of rule-level severity resolution); the descriptor
12
+ * carries the highest severity seen for its code and links back to Spectral's
13
+ * docs via `documentationUrl` when present.
14
+ */
15
+ export declare function convertResults(results: SpectralResult[], options: {
16
+ logger: Logger;
17
+ format?: ThymianFormat;
18
+ }): ConvertedResults;
19
+ //# sourceMappingURL=convert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,MAAM,EACN,cAAc,EAEd,aAAa,EACd,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA+B1D,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,cAAc,EAAE,EACzB,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,aAAa,CAAA;CAAE,GAClD,gBAAgB,CAqClB"}
@@ -0,0 +1,67 @@
1
+ import { mapLocation } from './map-location.js';
2
+ /** DiagnosticSeverity → Thymian severity; exact 1:1 for 0/1/2/3. */
3
+ const SEVERITY_BY_NUMBER = {
4
+ 0: 'error',
5
+ 1: 'warn',
6
+ 2: 'info',
7
+ 3: 'hint',
8
+ };
9
+ /** Higher number = more severe, for RuleDescriptor severity aggregation. */
10
+ const SEVERITY_RANK = {
11
+ hint: 0,
12
+ info: 1,
13
+ warn: 2,
14
+ error: 3,
15
+ };
16
+ function mapSeverity(severity, logger) {
17
+ const mapped = SEVERITY_BY_NUMBER[severity];
18
+ if (mapped === undefined) {
19
+ logger.warn(`Unknown Spectral severity ${severity.toString()} — converting conservatively as 'error'.`);
20
+ return 'error';
21
+ }
22
+ return mapped;
23
+ }
24
+ /**
25
+ * Maps Spectral results to failed lint executions plus one RuleDescriptor per
26
+ * distinct code. Every Spectral result is a problem, so every execution is
27
+ * `failed` with the finding's own severity on the status (per-finding
28
+ * fidelity regardless of rule-level severity resolution); the descriptor
29
+ * carries the highest severity seen for its code and links back to Spectral's
30
+ * docs via `documentationUrl` when present.
31
+ */
32
+ export function convertResults(results, options) {
33
+ const executions = [];
34
+ const rulesById = new Map();
35
+ for (const result of results) {
36
+ const ruleId = `spectral/${String(result.code)}`;
37
+ const severity = mapSeverity(result.severity, options.logger);
38
+ executions.push({
39
+ kind: 'lint',
40
+ ruleId,
41
+ status: { kind: 'failed', reason: result.message, severity },
42
+ location: mapLocation(result, options.format),
43
+ findings: [],
44
+ });
45
+ const existing = rulesById.get(ruleId);
46
+ if (!existing) {
47
+ rulesById.set(ruleId, {
48
+ id: ruleId,
49
+ severity,
50
+ summary: { text: result.message },
51
+ ...(result.documentationUrl
52
+ ? { helpUri: result.documentationUrl }
53
+ : {}),
54
+ });
55
+ }
56
+ else {
57
+ if (SEVERITY_RANK[severity] > SEVERITY_RANK[existing.severity]) {
58
+ existing.severity = severity;
59
+ }
60
+ if (!existing.helpUri && result.documentationUrl) {
61
+ existing.helpUri = result.documentationUrl;
62
+ }
63
+ }
64
+ }
65
+ return { executions, rules: [...rulesById.values()] };
66
+ }
67
+ //# sourceMappingURL=convert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert.js","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,oEAAoE;AACpE,MAAM,kBAAkB,GAA6B;IACnD,CAAC,EAAE,OAAO;IACV,CAAC,EAAE,MAAM;IACT,CAAC,EAAE,MAAM;IACT,CAAC,EAAE,MAAM;CACV,CAAC;AAEF,4EAA4E;AAC5E,MAAM,aAAa,GAA6B;IAC9C,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,SAAS,WAAW,CAAC,QAAgB,EAAE,MAAc;IACnD,MAAM,MAAM,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAE5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,CACT,6BAA6B,QAAQ,CAAC,QAAQ,EAAE,0CAA0C,CAC3F,CAAC;QACF,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAOD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAyB,EACzB,OAAmD;IAEnD,MAAM,UAAU,GAAoB,EAAE,CAAC;IACvC,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEpD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,YAAY,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAE9D,UAAU,CAAC,IAAI,CAAC;YACd,IAAI,EAAE,MAAM;YACZ,MAAM;YACN,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE;YAC5D,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC;YAC7C,QAAQ,EAAE,EAAE;SACb,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE;gBACpB,EAAE,EAAE,MAAM;gBACV,QAAQ;gBACR,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE;gBACjC,GAAG,CAAC,MAAM,CAAC,gBAAgB;oBACzB,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,gBAAgB,EAAE;oBACtC,CAAC,CAAC,EAAE,CAAC;aACR,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,IAAI,aAAa,CAAC,QAAQ,CAAC,GAAG,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC/D,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC;YAC/B,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,OAAO,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;gBACjD,QAAQ,CAAC,OAAO,GAAG,MAAM,CAAC,gBAAgB,CAAC;YAC7C,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { ThymianPlugin } from '@thymian/core';
2
+ export type { SpectralResult } from './spectral-types.js';
3
+ /**
4
+ * `@thymian/plugin-spectral` converts pre-generated Spectral JSON output
5
+ * (`spectral lint -f json`) into canonical Thymian lint runs. It never
6
+ * executes Spectral — the input is the report file. See
7
+ * {@link SpectralResult} for the supported input format.
8
+ *
9
+ * The plugin extends `thymian report convert` purely by listening on the
10
+ * core-owned `core.report.convert` collect action (ADR-0016/0017): it claims
11
+ * `spectral`-typed report inputs and replies one lint `ToolRun` per claimed
12
+ * input, tagged with that input's identity. The converted report kind is
13
+ * always `lint`.
14
+ */
15
+ export declare function createSpectralPlugin(pluginName?: string): ThymianPlugin;
16
+ export declare const spectralPlugin: ThymianPlugin;
17
+ export default spectralPlugin;
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAwB,aAAa,EAAE,MAAM,eAAe,CAAC;AAMzE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,SAA6B,GACtC,aAAa,CAgEf;AAED,eAAO,MAAM,cAAc,eAAyB,CAAC;AAErD,eAAe,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ import { createToolRun, ThymianFormat } from '@thymian/core';
2
+ import { convertResults } from './convert.js';
3
+ import { loadSpectralReport } from './load-spectral-report.js';
4
+ /**
5
+ * `@thymian/plugin-spectral` converts pre-generated Spectral JSON output
6
+ * (`spectral lint -f json`) into canonical Thymian lint runs. It never
7
+ * executes Spectral — the input is the report file. See
8
+ * {@link SpectralResult} for the supported input format.
9
+ *
10
+ * The plugin extends `thymian report convert` purely by listening on the
11
+ * core-owned `core.report.convert` collect action (ADR-0016/0017): it claims
12
+ * `spectral`-typed report inputs and replies one lint `ToolRun` per claimed
13
+ * input, tagged with that input's identity. The converted report kind is
14
+ * always `lint`.
15
+ */
16
+ export function createSpectralPlugin(pluginName = '@thymian/plugin-spectral') {
17
+ return {
18
+ name: pluginName,
19
+ version: '0.x',
20
+ actions: {
21
+ listensOn: ['core.report.convert'],
22
+ },
23
+ async plugin(emitter, logger, options) {
24
+ emitter.onAction('core.report.convert', async (input, ctx) => {
25
+ const spectralInputs = input.inputs.filter((reportInput) => reportInput.type === 'spectral');
26
+ // Always reply, even with nothing claimed — the collect strategy
27
+ // waits for every registered listener (14.1 listener contract).
28
+ if (spectralInputs.length === 0) {
29
+ logger.info('No spectral report inputs found, nothing to convert.');
30
+ ctx.reply([]);
31
+ return;
32
+ }
33
+ const format = input.format
34
+ ? ThymianFormat.import(input.format)
35
+ : undefined;
36
+ const fragments = [];
37
+ for (const reportInput of spectralInputs) {
38
+ const location = String(reportInput.location);
39
+ const inputLabel = `${reportInput.type}:${location}`;
40
+ logger.info(`Converting Spectral report: ${location}`);
41
+ // Failures propagate as thrown ThymianBaseErrors — the intended
42
+ // tool/runtime error semantics; never a silently dropped input.
43
+ const results = await loadSpectralReport(location, inputLabel, options.cwd);
44
+ const { executions, rules } = convertResults(results, {
45
+ logger,
46
+ format,
47
+ });
48
+ fragments.push({
49
+ // Tag with the stringified input identity — core derives claim
50
+ // coverage by exact type + String(location) match.
51
+ input: { type: reportInput.type, location },
52
+ run: createToolRun({
53
+ tool: { name: pluginName },
54
+ runType: 'lint',
55
+ executions,
56
+ rules: rules.length > 0 ? rules : undefined,
57
+ thymianFormatVersion: input.format?.attributes.hash,
58
+ }),
59
+ });
60
+ }
61
+ ctx.reply(fragments);
62
+ });
63
+ },
64
+ };
65
+ }
66
+ export const spectralPlugin = createSpectralPlugin();
67
+ export default spectralPlugin;
68
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE7D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAI/D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAAU,GAAG,0BAA0B;IAEvC,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,KAAK;QACd,OAAO,EAAE;YACP,SAAS,EAAE,CAAC,qBAAqB,CAAC;SACnC;QACD,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO;YACnC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;gBAC3D,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CACxC,CAAC,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,KAAK,UAAU,CACjD,CAAC;gBAEF,iEAAiE;gBACjE,gEAAgE;gBAChE,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAChC,MAAM,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC;oBACpE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;oBACd,OAAO;gBACT,CAAC;gBAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM;oBACzB,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;oBACpC,CAAC,CAAC,SAAS,CAAC;gBAEd,MAAM,SAAS,GAA2B,EAAE,CAAC;gBAE7C,KAAK,MAAM,WAAW,IAAI,cAAc,EAAE,CAAC;oBACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;oBAC9C,MAAM,UAAU,GAAG,GAAG,WAAW,CAAC,IAAI,IAAI,QAAQ,EAAE,CAAC;oBAErD,MAAM,CAAC,IAAI,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;oBAEvD,gEAAgE;oBAChE,gEAAgE;oBAChE,MAAM,OAAO,GAAG,MAAM,kBAAkB,CACtC,QAAQ,EACR,UAAU,EACV,OAAO,CAAC,GAAG,CACZ,CAAC;oBAEF,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC,OAAO,EAAE;wBACpD,MAAM;wBACN,MAAM;qBACP,CAAC,CAAC;oBAEH,SAAS,CAAC,IAAI,CAAC;wBACb,+DAA+D;wBAC/D,mDAAmD;wBACnD,KAAK,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE;wBAC3C,GAAG,EAAE,aAAa,CAAC;4BACjB,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;4BAC1B,OAAO,EAAE,MAAM;4BACf,UAAU;4BACV,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;4BAC3C,oBAAoB,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI;yBACpD,CAAC;qBACH,CAAC,CAAC;gBACL,CAAC;gBAED,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YACvB,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,oBAAoB,EAAE,CAAC;AAErD,eAAe,cAAc,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { SpectralResult } from './spectral-types.js';
2
+ /**
3
+ * Reads a single Spectral JSON report (`spectral lint -f json` output),
4
+ * validates its structure, and returns the parsed results.
5
+ *
6
+ * Unknown extra fields on entries are tolerated (forward-compatible).
7
+ *
8
+ * @param inputLabel the offending input's identity (`type:location`), used in
9
+ * every error message so failures trace back to the CLI input (AC 5).
10
+ * @throws {ThymianBaseError} if the file is unreadable, not valid JSON, not a
11
+ * result array, or an entry misses the required
12
+ * `code`/`message`/`severity`/`path`/`range` shape.
13
+ */
14
+ export declare function loadSpectralReport(location: string, inputLabel: string, cwd: string): Promise<SpectralResult[]>;
15
+ //# sourceMappingURL=load-spectral-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-spectral-report.d.ts","sourceRoot":"","sources":["../src/load-spectral-report.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA8C1D;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,EAAE,CAAC,CAsC3B"}
@@ -0,0 +1,75 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { isAbsolute, join } from 'node:path';
3
+ import { ThymianBaseError } from '@thymian/core';
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ function isNonNegativeInteger(value) {
8
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
9
+ }
10
+ function isPosition(value) {
11
+ return (isRecord(value) &&
12
+ isNonNegativeInteger(value['line']) &&
13
+ isNonNegativeInteger(value['character']));
14
+ }
15
+ function isStringWhenPresent(value) {
16
+ return value === undefined || typeof value === 'string';
17
+ }
18
+ // Validates the full declared SpectralResult contract — the `as
19
+ // SpectralResult[]` cast below vouches for exactly what is checked here, so
20
+ // every required field the mapping code dereferences must be covered.
21
+ function isSpectralResult(value) {
22
+ if (!isRecord(value)) {
23
+ return false;
24
+ }
25
+ const range = value['range'];
26
+ const code = value['code'];
27
+ return ((typeof code === 'string' || typeof code === 'number') &&
28
+ typeof value['message'] === 'string' &&
29
+ typeof value['severity'] === 'number' &&
30
+ Array.isArray(value['path']) &&
31
+ isRecord(range) &&
32
+ isPosition(range['start']) &&
33
+ isPosition(range['end']) &&
34
+ isStringWhenPresent(value['source']) &&
35
+ isStringWhenPresent(value['documentationUrl']));
36
+ }
37
+ /**
38
+ * Reads a single Spectral JSON report (`spectral lint -f json` output),
39
+ * validates its structure, and returns the parsed results.
40
+ *
41
+ * Unknown extra fields on entries are tolerated (forward-compatible).
42
+ *
43
+ * @param inputLabel the offending input's identity (`type:location`), used in
44
+ * every error message so failures trace back to the CLI input (AC 5).
45
+ * @throws {ThymianBaseError} if the file is unreadable, not valid JSON, not a
46
+ * result array, or an entry misses the required
47
+ * `code`/`message`/`severity`/`path`/`range` shape.
48
+ */
49
+ export async function loadSpectralReport(location, inputLabel, cwd) {
50
+ const filePath = isAbsolute(location) ? location : join(cwd, location);
51
+ let content;
52
+ try {
53
+ content = await readFile(filePath, 'utf-8');
54
+ }
55
+ catch (err) {
56
+ throw new ThymianBaseError(`Failed to read Spectral report "${inputLabel}" (resolved to ${filePath}).`, { cause: err instanceof Error ? err : undefined });
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(content);
61
+ }
62
+ catch (err) {
63
+ throw new ThymianBaseError(`Failed to parse Spectral report "${inputLabel}" as JSON.`, { cause: err instanceof Error ? err : undefined });
64
+ }
65
+ if (!Array.isArray(parsed)) {
66
+ throw new ThymianBaseError(`Unsupported Spectral report "${inputLabel}": expected the JSON array produced by \`spectral lint -f json\`.`);
67
+ }
68
+ for (const [index, entry] of parsed.entries()) {
69
+ if (!isSpectralResult(entry)) {
70
+ throw new ThymianBaseError(`Unsupported Spectral report "${inputLabel}": result ${index.toString()} does not match the \`spectral lint -f json\` result shape (code/message/severity/path/range).`);
71
+ }
72
+ }
73
+ return parsed;
74
+ }
75
+ //# sourceMappingURL=load-spectral-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-spectral-report.js","sourceRoot":"","sources":["../src/load-spectral-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { Location, ThymianFormat } from '@thymian/core';
2
+ import type { SpectralResult } from './spectral-types.js';
3
+ /**
4
+ * Best-effort mapping of a Spectral finding onto the loaded format (AC 4):
5
+ *
6
+ * 1. With a format and a finding `source`: the graph node from the same source
7
+ * file whose `sourceLocation.position` is nearest at-or-before the finding.
8
+ * Positions are 1-based (openapi loc-mappers); Spectral ranges are 0-based,
9
+ * so the finding line/column are shifted by +1 before comparing.
10
+ * 2. Otherwise, a `file` location from `source` + the finding range,
11
+ * **1-based** line/column (0-based Spectral values shifted by +1).
12
+ * 3. Without a `source`, a `custom` location from the joined `path` segments.
13
+ *
14
+ * The fallback chain is the contract — full resolution is genuinely
15
+ * best-effort, never required.
16
+ */
17
+ export declare function mapLocation(result: SpectralResult, format: ThymianFormat | undefined): Location;
18
+ //# sourceMappingURL=map-location.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-location.d.ts","sourceRoot":"","sources":["../src/map-location.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAuB1D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,QAAQ,CAuDV"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Does the finding's `source` refer to the node's source file? Spectral emits
3
+ * OS-native absolute paths while format nodes carry the path the spec was
4
+ * loaded from — backslashes are normalized so Windows sources match too, then
5
+ * paths match exactly or when one is a `/`-suffix of the other.
6
+ */
7
+ function sourceMatches(source, nodePath) {
8
+ const normalizedSource = source.replaceAll('\\', '/');
9
+ const normalizedNodePath = nodePath.replaceAll('\\', '/');
10
+ if (!normalizedSource || !normalizedNodePath) {
11
+ return false;
12
+ }
13
+ return (normalizedSource === normalizedNodePath ||
14
+ normalizedSource.endsWith(`/${normalizedNodePath}`) ||
15
+ normalizedNodePath.endsWith(`/${normalizedSource}`));
16
+ }
17
+ /**
18
+ * Best-effort mapping of a Spectral finding onto the loaded format (AC 4):
19
+ *
20
+ * 1. With a format and a finding `source`: the graph node from the same source
21
+ * file whose `sourceLocation.position` is nearest at-or-before the finding.
22
+ * Positions are 1-based (openapi loc-mappers); Spectral ranges are 0-based,
23
+ * so the finding line/column are shifted by +1 before comparing.
24
+ * 2. Otherwise, a `file` location from `source` + the finding range,
25
+ * **1-based** line/column (0-based Spectral values shifted by +1).
26
+ * 3. Without a `source`, a `custom` location from the joined `path` segments.
27
+ *
28
+ * The fallback chain is the contract — full resolution is genuinely
29
+ * best-effort, never required.
30
+ */
31
+ export function mapLocation(result, format) {
32
+ const line = result.range.start.line + 1;
33
+ const column = result.range.start.character + 1;
34
+ if (format && result.source) {
35
+ const source = result.source;
36
+ let bestId;
37
+ let bestLine = 0;
38
+ let bestColumn = 0;
39
+ format.graph.forEachNode((id, node) => {
40
+ const { sourceLocation } = node;
41
+ // URI-sourced nodes cannot match a Spectral file source — skip them.
42
+ if (!sourceLocation?.position ||
43
+ !('path' in sourceLocation) ||
44
+ !sourceMatches(source, sourceLocation.path)) {
45
+ return;
46
+ }
47
+ const { line: nodeLine, column: nodeColumn } = sourceLocation.position;
48
+ const atOrBefore = nodeLine < line || (nodeLine === line && nodeColumn <= column);
49
+ const nearerThanBest = nodeLine > bestLine ||
50
+ (nodeLine === bestLine && nodeColumn > bestColumn);
51
+ if (atOrBefore && (bestId === undefined || nearerThanBest)) {
52
+ bestId = id;
53
+ bestLine = nodeLine;
54
+ bestColumn = nodeColumn;
55
+ }
56
+ });
57
+ if (bestId !== undefined) {
58
+ return {
59
+ type: 'thymianFormat',
60
+ elementType: 'node',
61
+ elementId: bestId,
62
+ pointer: '',
63
+ };
64
+ }
65
+ }
66
+ if (result.source) {
67
+ return { type: 'file', path: result.source, line, column };
68
+ }
69
+ return {
70
+ type: 'custom',
71
+ value: result.path.length > 0 ? result.path.join('.') : 'unknown',
72
+ };
73
+ }
74
+ //# sourceMappingURL=map-location.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-location.js","sourceRoot":"","sources":["../src/map-location.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,SAAS,aAAa,CAAC,MAAc,EAAE,QAAgB;IACrD,MAAM,gBAAgB,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACtD,MAAM,kBAAkB,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAE1D,IAAI,CAAC,gBAAgB,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC7C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,CACL,gBAAgB,KAAK,kBAAkB;QACvC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,kBAAkB,EAAE,CAAC;QACnD,kBAAkB,CAAC,QAAQ,CAAC,IAAI,gBAAgB,EAAE,CAAC,CACpD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,WAAW,CACzB,MAAsB,EACtB,MAAiC;IAEjC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;IAEhD,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC7B,IAAI,MAA0B,CAAC;QAC/B,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,IAAI,UAAU,GAAG,CAAC,CAAC;QAEnB,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE;YACpC,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC;YAEhC,qEAAqE;YACrE,IACE,CAAC,cAAc,EAAE,QAAQ;gBACzB,CAAC,CAAC,MAAM,IAAI,cAAc,CAAC;gBAC3B,CAAC,aAAa,CAAC,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,EAC3C,CAAC;gBACD,OAAO;YACT,CAAC;YAED,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,cAAc,CAAC,QAAQ,CAAC;YAEvE,MAAM,UAAU,GACd,QAAQ,GAAG,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,IAAI,UAAU,IAAI,MAAM,CAAC,CAAC;YACjE,MAAM,cAAc,GAClB,QAAQ,GAAG,QAAQ;gBACnB,CAAC,QAAQ,KAAK,QAAQ,IAAI,UAAU,GAAG,UAAU,CAAC,CAAC;YAErD,IAAI,UAAU,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,cAAc,CAAC,EAAE,CAAC;gBAC3D,MAAM,GAAG,EAAE,CAAC;gBACZ,QAAQ,GAAG,QAAQ,CAAC;gBACpB,UAAU,GAAG,UAAU,CAAC;YAC1B,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO;gBACL,IAAI,EAAE,eAAe;gBACrB,WAAW,EAAE,MAAM;gBACnB,SAAS,EAAE,MAAM;gBACjB,OAAO,EAAE,EAAE;aACZ,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC7D,CAAC;IAED,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;KAClE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The supported Spectral input: the JSON array produced by
3
+ * `spectral lint -f json` (`@stoplight/spectral-cli`).
4
+ *
5
+ * Field set as emitted by Spectral's `json` formatter: `code`, `path`,
6
+ * `message`, `severity`, `range`, `source`, plus `documentationUrl` only when
7
+ * the rule declares one. Verified against real Spectral 6 output.
8
+ *
9
+ * - `range` positions are **0-based** (lines and characters).
10
+ * - `severity` numbers are DiagnosticSeverity: 0=error, 1=warning,
11
+ * 2=information, 3=hint.
12
+ * - Unknown extra fields are tolerated (forward-compatible); unknown severity
13
+ * numbers convert conservatively to `error` with a warning — findings are
14
+ * never dropped.
15
+ */
16
+ export interface SpectralResult {
17
+ /** Rule identifier; Spectral allows numeric codes — stringified on mapping. */
18
+ code: string | number;
19
+ message: string;
20
+ /** DiagnosticSeverity: 0=error, 1=warning, 2=information, 3=hint. */
21
+ severity: number;
22
+ /** JSON path segments of the offending document location. */
23
+ path: (string | number)[];
24
+ /** 0-based document range of the finding. */
25
+ range: {
26
+ start: {
27
+ line: number;
28
+ character: number;
29
+ };
30
+ end: {
31
+ line: number;
32
+ character: number;
33
+ };
34
+ };
35
+ /** Source file the finding refers to, when Spectral knows it. */
36
+ source?: string;
37
+ /** Rule documentation link, present only when the rule declares one. */
38
+ documentationUrl?: string;
39
+ }
40
+ //# sourceMappingURL=spectral-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spectral-types.d.ts","sourceRoot":"","sources":["../src/spectral-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,KAAK,EAAE;QACL,KAAK,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;QAC3C,GAAG,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;KAC1C,CAAC;IACF,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=spectral-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spectral-types.js","sourceRoot":"","sources":["../src/spectral-types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@thymian/plugin-spectral",
3
+ "version": "0.2.0-canary.20260810-95e9b0c",
4
+ "description": "Thymian plugin that converts pre-generated Spectral JSON output into canonical Thymian lint reports",
5
+ "license": "AGPL-3.0-only",
6
+ "bugs": {
7
+ "url": "https://github.com/thymianofficial/thymian/issues"
8
+ },
9
+ "homepage": "https://thymian.dev",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/thymianofficial/thymian.git",
13
+ "directory": "packages/plugin-spectral"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "keywords": [
19
+ "thymian",
20
+ "spectral",
21
+ "report-converter",
22
+ "lint",
23
+ "api-conformance"
24
+ ],
25
+ "main": "./dist/index.js",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "type": "module",
29
+ "exports": {
30
+ "./package.json": "./package.json",
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "!**/*.tsbuildinfo"
40
+ ],
41
+ "dependencies": {
42
+ "@thymian/core": "0.2.0-canary.20260810-95e9b0c",
43
+ "tslib": "^2.3.0"
44
+ },
45
+ "devDependencies": {
46
+ "@thymian/core-testing": "0.2.0-canary.20260810-95e9b0c"
47
+ }
48
+ }