@patronage/software-factory 1.0.0-alpha.34 → 1.0.0-alpha.36

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,6 @@
1
+ import { Plugin } from "@oxlint/plugins";
2
+
3
+ //#region src/oxlint-jev/index.d.ts
4
+ declare const plugin: Plugin;
5
+ //#endregion
6
+ export { plugin as default };
@@ -0,0 +1,581 @@
1
+ import { i as buildJevRequests, l as noul, n as spawnJevBridge, o as defaultJevCacheDirectory } from "./bridge-B4Sx95ZO.js";
2
+ import path from "node:path";
3
+ import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
4
+ import { createHash } from "node:crypto";
5
+ //#region src/oxlint-jev/pass.ts
6
+ /** The environment variable that turns spending on. */
7
+ const JEV_MODE_ENV = "PATRONAGE_JEV_MODE";
8
+ /**
9
+ * The mode this run is in.
10
+ *
11
+ * Deliberately an environment variable rather than a rule option. The Oxlint
12
+ * config that carries the questions is a checked-in file: a `mode: "live"` in
13
+ * it would be one merge away from every runner with HQ credentials spending on
14
+ * every invocation, including whichever tool picks the config up next. An
15
+ * environment variable is set per invocation by whoever is paying, and it is
16
+ * bounded by the same credentials — `live` without `HQ_INGEST_URL` and the
17
+ * Cloudflare Access pair buys nothing, it reports that it could not ask. A
18
+ * value that is neither mode is refused rather than rounded to the safe one,
19
+ * because "I set it and nothing happened" is how an operator ends up believing
20
+ * a run asked something it did not.
21
+ */
22
+ const resolveJevMode = (env) => {
23
+ const raw = env[JEV_MODE_ENV]?.trim();
24
+ if (raw === void 0 || raw === "") return "record";
25
+ if (raw === "live" || raw === "record") return raw;
26
+ throw new Error(`jev: ${JEV_MODE_ENV} must be "record" or "live"; got "${raw}"`);
27
+ };
28
+ /** Where recorded request bodies are written. */
29
+ const recordDirectoryFor = (cacheDirectory) => path.join(cacheDirectory, "record");
30
+ const describe = (error) => error instanceof Error ? error.message : String(error);
31
+ /** The filesystem's own short constant (`ENOTDIR`, `EACCES`), never a message. */
32
+ const errnoOf = (error) => error?.code ?? "an unknown error";
33
+ /**
34
+ * Whether a canonical path is the canonical root or sits under it.
35
+ *
36
+ * Compared on canonical paths, so neither side can be a symlink, and with a
37
+ * separator appended so `/repo-secrets` is not read as being inside `/repo`.
38
+ */
39
+ const isInside = (canonicalRoot, canonicalPath) => canonicalPath === canonicalRoot || canonicalPath.startsWith(`${canonicalRoot}${path.sep}`);
40
+ /**
41
+ * The reference files for each question that matched in this file, read whole.
42
+ *
43
+ * Their content rides the request, so it is part of the request hash and
44
+ * therefore part of the cache key: editing a reference re-asks the question
45
+ * rather than returning yesterday's answer about yesterday's file. A reference
46
+ * that cannot be read makes its question's matches unavailable — the question
47
+ * was written to be read against that file, and answering it without the file
48
+ * would be answering a different question.
49
+ *
50
+ * Every reference is canonicalized before it is opened, and one outside the
51
+ * canonical repository root is refused unread. `resolveJevQuestions` rejects an
52
+ * absolute path and a `..` that climbs out, but that check is lexical and
53
+ * `readFileSync` follows symlinks: a repository-relative path whose last
54
+ * component — or any parent directory — is a symlink out of the checkout would
55
+ * otherwise be read and, in `live` mode, sent to HQ. The refusal names the
56
+ * configured reference and never the path it resolved to, which is the value
57
+ * that must not travel.
58
+ */
59
+ const readReferences = (root, questions) => {
60
+ const sharedEntries = [];
61
+ const failed = /* @__PURE__ */ new Map();
62
+ const canonicalRoot = realpathSync(root);
63
+ for (const question of questions) {
64
+ const references = question.references ?? [];
65
+ if (references.length === 0) continue;
66
+ const contentEntries = [];
67
+ for (const reference of references) try {
68
+ const canonical = realpathSync(path.join(root, reference));
69
+ if (!isInside(canonicalRoot, canonical)) {
70
+ failed.set(question.id, `reference ${reference} resolves outside the repository; it was not read`);
71
+ break;
72
+ }
73
+ contentEntries.push([reference, readFileSync(canonical, "utf-8")]);
74
+ } catch (error) {
75
+ failed.set(question.id, `reference ${reference} could not be read: ${error.code ?? describe(error)}`);
76
+ break;
77
+ }
78
+ if (!failed.has(question.id)) sharedEntries.push([question.id, Object.fromEntries(contentEntries)]);
79
+ }
80
+ return {
81
+ failed,
82
+ shared: Object.fromEntries(sharedEntries)
83
+ };
84
+ };
85
+ /**
86
+ * The state one match contributes. A `file` match adds nothing but its target:
87
+ * the file's whole text is already the shared state, and sending it twice
88
+ * would buy the same tokens again.
89
+ */
90
+ const stateFor = (match) => match.question.target === "file" ? { target: "file" } : {
91
+ snippet: match.snippet,
92
+ target: match.question.target
93
+ };
94
+ const toJevFile = (filePath, source, references, matches) => {
95
+ const jevMatches = matches.map((match) => ({
96
+ key: match.key,
97
+ questions: { [match.key]: noul(match.question.question) },
98
+ state: stateFor(match)
99
+ }));
100
+ const shared = { source };
101
+ if (Object.keys(references).length > 0) shared.references = references;
102
+ return {
103
+ matches: jevMatches,
104
+ path: filePath,
105
+ shared
106
+ };
107
+ };
108
+ /**
109
+ * Writes the exact bytes the live mode would send, one file per request, named
110
+ * by the request hash so the same request always lands in the same place.
111
+ * Pretty-printed, because the point of recording is that a reviewer reads it.
112
+ */
113
+ const writeRecords = (directory, file, unavailable) => {
114
+ const recorded = /* @__PURE__ */ new Map();
115
+ const plan = buildJevRequests({ files: [file] });
116
+ for (const entry of plan.unavailable) unavailable.set(entry.key, entry.reason);
117
+ try {
118
+ mkdirSync(directory, { recursive: true });
119
+ } catch (error) {
120
+ for (const match of file.matches) unavailable.set(match.key, `the Jev request could not be recorded: creating ${directory} failed with ${errnoOf(error)}`);
121
+ return recorded;
122
+ }
123
+ for (const request of plan.requests) {
124
+ const target = path.join(directory, `${request.hash}.json`);
125
+ try {
126
+ writeFileSync(target, `${JSON.stringify({
127
+ body: request.body,
128
+ estimatedTokens: request.estimatedTokens,
129
+ matchKeys: request.matchKeys,
130
+ path: request.path,
131
+ requestHash: request.hash
132
+ }, null, 2)}\n`, "utf-8");
133
+ } catch (error) {
134
+ for (const key of request.matchKeys) unavailable.set(key, `the Jev request could not be recorded: writing ${target} failed with ${errnoOf(error)}`);
135
+ continue;
136
+ }
137
+ for (const key of request.matchKeys) recorded.set(key, target);
138
+ }
139
+ return recorded;
140
+ };
141
+ /**
142
+ * Two matches sharing a key is a bug, not a configuration error.
143
+ *
144
+ * The key is an answer name and a dictionary key in the request body, so a
145
+ * collision does not produce a smaller request — it produces a request that
146
+ * silently asks about one of the colliding matches and answers all of them
147
+ * from it. Nothing downstream can detect that, so it is refused here, where
148
+ * both keys are still in hand.
149
+ */
150
+ const assertDistinctKeys = (matches) => {
151
+ const seen = /* @__PURE__ */ new Set();
152
+ for (const match of matches) {
153
+ if (seen.has(match.key)) throw new Error(`jev: two matches share the key ${match.key}. A match key must identify one node; this is a bug in the plugin, not in the configuration.`);
154
+ seen.add(match.key);
155
+ }
156
+ };
157
+ /** Builds the request bodies and writes them; nothing is sent. */
158
+ const recordPass = (cacheDirectory, file, asked) => {
159
+ const recorded = [];
160
+ const unavailable = [];
161
+ const notRecorded = /* @__PURE__ */ new Map();
162
+ const written = writeRecords(recordDirectoryFor(cacheDirectory), file, notRecorded);
163
+ for (const match of asked) {
164
+ const cause = notRecorded.get(match.key);
165
+ if (cause === void 0) recorded.push({
166
+ id: match.question.id,
167
+ loc: match.loc,
168
+ requestPath: written.get(match.key) ?? "",
169
+ textHash: match.question.textHash
170
+ });
171
+ else unavailable.push({
172
+ cause,
173
+ id: match.question.id,
174
+ loc: match.loc
175
+ });
176
+ }
177
+ return {
178
+ findings: [],
179
+ recorded,
180
+ unavailable
181
+ };
182
+ };
183
+ /** Sends the request through the bridge and reads the answers back. */
184
+ const livePass = (bridge, cacheDirectory, file, asked) => {
185
+ const findings = [];
186
+ const unavailable = [];
187
+ const result = bridge({
188
+ cacheDirectory,
189
+ files: [file]
190
+ });
191
+ const modelByKey = /* @__PURE__ */ new Map();
192
+ for (const request of result.requests) for (const key of request.matchKeys) modelByKey.set(key, request.model ?? "unknown");
193
+ for (const match of asked) {
194
+ const answer = result.answersByMatchKey[match.key]?.[match.key];
195
+ if (answer === void 0 || answer.type !== "noul") {
196
+ unavailable.push({
197
+ cause: result.unavailableByMatchKey[match.key] ?? "the Jev run returned no answer and no reason for this match",
198
+ id: match.question.id,
199
+ loc: match.loc
200
+ });
201
+ continue;
202
+ }
203
+ if (answer.noul >= match.question.cutoff) findings.push({
204
+ cutoff: match.question.cutoff,
205
+ id: match.question.id,
206
+ loc: match.loc,
207
+ model: modelByKey.get(match.key) ?? "unknown",
208
+ question: match.question.question,
209
+ score: answer.noul,
210
+ textHash: match.question.textHash
211
+ });
212
+ }
213
+ return {
214
+ findings,
215
+ recorded: [],
216
+ unavailable
217
+ };
218
+ };
219
+ /**
220
+ * Runs one file's matches: reads the references, builds the request, and
221
+ * either records it or sends it.
222
+ */
223
+ const runJevPass = ({ bridge = spawnJevBridge, cacheDirectory = defaultJevCacheDirectory(process.cwd()), filePath, matches, mode, root, source }) => {
224
+ const unavailable = [];
225
+ if (matches.length === 0) return {
226
+ findings: [],
227
+ recorded: [],
228
+ unavailable
229
+ };
230
+ const { failed, shared } = readReferences(root, [...new Map(matches.map((match) => [match.question.id, match.question])).values()]);
231
+ const asked = matches.filter((match) => !failed.has(match.question.id));
232
+ for (const match of matches) {
233
+ const cause = failed.get(match.question.id);
234
+ if (cause !== void 0) unavailable.push({
235
+ cause,
236
+ id: match.question.id,
237
+ loc: match.loc
238
+ });
239
+ }
240
+ if (asked.length === 0) return {
241
+ findings: [],
242
+ recorded: [],
243
+ unavailable
244
+ };
245
+ assertDistinctKeys(asked);
246
+ const file = toJevFile(filePath, source, shared, asked);
247
+ const run = mode === "record" ? recordPass(cacheDirectory, file, asked) : livePass(bridge, cacheDirectory, file, asked);
248
+ return {
249
+ ...run,
250
+ unavailable: [...unavailable, ...run.unavailable]
251
+ };
252
+ };
253
+ //#endregion
254
+ //#region src/oxlint-jev/questions.ts
255
+ /**
256
+ * What a Jev question is, and the one place a caller's configuration becomes
257
+ * one.
258
+ *
259
+ * A question is **configuration, not code** (epic #1234): an id, an AST
260
+ * target, the question text, a cutoff, and optionally a `contains` pattern and
261
+ * a list of repository-relative reference files. There is no hook, no
262
+ * check-as-code contract, and no escape to a callback — a case that code can
263
+ * decide is an ordinary Oxlint rule, not a Jev question.
264
+ *
265
+ * Oxlint validates `context.options` against `JEV_ASK_OPTIONS_SCHEMA` before a
266
+ * rule sees them, so the checks here are only the ones a JSON schema cannot
267
+ * express: that ids are unique, that a `contains` pattern compiles, and that a
268
+ * reference path stays inside the repository. Each of them throws, which
269
+ * Oxlint turns into a plugin error and a non-zero exit — a malformed question
270
+ * set is never quietly a smaller question set.
271
+ */
272
+ /** The AST shapes a question can be asked about. */
273
+ const JEV_TARGETS = [
274
+ "call",
275
+ "file",
276
+ "function",
277
+ "jsx"
278
+ ];
279
+ /**
280
+ * The option schema Oxlint enforces. `additionalProperties: false` is the
281
+ * control that refuses the options this design deliberately does not have — a
282
+ * `mode`, an `exceptions` list, a `ci` behavior, a match cap — instead of
283
+ * ignoring them and leaving a consumer believing they took effect.
284
+ */
285
+ const JEV_ASK_OPTIONS_SCHEMA = {
286
+ additionalProperties: false,
287
+ properties: { questions: {
288
+ items: {
289
+ additionalProperties: false,
290
+ properties: {
291
+ contains: {
292
+ minLength: 1,
293
+ type: "string"
294
+ },
295
+ cutoff: {
296
+ maximum: 1,
297
+ minimum: 0,
298
+ type: "number"
299
+ },
300
+ id: {
301
+ minLength: 1,
302
+ type: "string"
303
+ },
304
+ question: {
305
+ minLength: 1,
306
+ type: "string"
307
+ },
308
+ references: {
309
+ items: {
310
+ minLength: 1,
311
+ type: "string"
312
+ },
313
+ type: "array",
314
+ uniqueItems: true
315
+ },
316
+ target: { enum: [...JEV_TARGETS] }
317
+ },
318
+ required: [
319
+ "id",
320
+ "target",
321
+ "question",
322
+ "cutoff"
323
+ ],
324
+ type: "object"
325
+ },
326
+ minItems: 1,
327
+ type: "array"
328
+ } },
329
+ required: ["questions"],
330
+ type: "object"
331
+ };
332
+ const fail = (detail) => {
333
+ throw new Error(`jev/ask: ${detail}`);
334
+ };
335
+ /** The short hash of a question's text that every finding carries. */
336
+ const questionTextHash = (question) => createHash("sha256").update(question).digest("hex").slice(0, 8);
337
+ /**
338
+ * A reference must name a file inside the repository. An absolute path or one
339
+ * that climbs out of the checkout would make the request depend on the machine
340
+ * that ran the lint, and the request is the cache key — two machines would
341
+ * disagree about what the same configuration asked.
342
+ */
343
+ const checkReference = (id, reference) => {
344
+ if (path.isAbsolute(reference)) fail(`question "${id}" references an absolute path (${reference}); references are repository-relative`);
345
+ const normalized = path.normalize(reference);
346
+ if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) fail(`question "${id}" references a path outside the repository (${reference})`);
347
+ };
348
+ /**
349
+ * Turns the rule's options into the questions this file's pass will ask.
350
+ *
351
+ * Called once per file, so it stays cheap: the only work is compiling
352
+ * `contains` and hashing the question text.
353
+ */
354
+ const resolveJevQuestions = (raw) => {
355
+ if (raw === null || typeof raw !== "object") fail("the rule takes one options object with a `questions` array; configure it as `\"jev/ask\": [\"warn\", { \"questions\": [ … ] }]`");
356
+ const { questions } = raw;
357
+ if (!Array.isArray(questions) || questions.length === 0) fail("`questions` must list at least one question");
358
+ const seen = /* @__PURE__ */ new Set();
359
+ return questions.map((question) => {
360
+ if (seen.has(question.id)) fail(`question id "${question.id}" is used more than once`);
361
+ seen.add(question.id);
362
+ for (const reference of question.references ?? []) checkReference(question.id, reference);
363
+ let pattern;
364
+ if (question.contains !== void 0) try {
365
+ pattern = new RegExp(question.contains, "u");
366
+ } catch {
367
+ fail(`question "${question.id}" has a \`contains\` that is not a valid regular expression: ${question.contains}`);
368
+ }
369
+ return {
370
+ ...question,
371
+ pattern,
372
+ textHash: questionTextHash(question.question)
373
+ };
374
+ });
375
+ };
376
+ //#endregion
377
+ //#region src/oxlint-jev/targets.ts
378
+ const TARGET_NODE_TYPES = {
379
+ call: ["CallExpression"],
380
+ file: ["Program"],
381
+ function: [
382
+ "ArrowFunctionExpression",
383
+ "FunctionDeclaration",
384
+ "FunctionExpression"
385
+ ],
386
+ jsx: ["JSXElement"]
387
+ };
388
+ /** Every node type any target selects, so the visitor is registered once. */
389
+ const ALL_TARGET_NODE_TYPES = [...new Set(JEV_TARGETS.flatMap((target) => TARGET_NODE_TYPES[target]))];
390
+ /** Node type back to the target that selected it. */
391
+ const TARGET_BY_NODE_TYPE = new Map(JEV_TARGETS.flatMap((target) => TARGET_NODE_TYPES[target].map((type) => [type, target])));
392
+ /**
393
+ * The fields in which an anonymous function inherits a name from its parent.
394
+ * `const handler = () => …` reads as "handler", and sending only `() => …`
395
+ * would ask Jev about a snippet with the one identifying word removed.
396
+ */
397
+ const NAMED_PARENTS = {
398
+ MethodDefinition: (parent) => parent.value,
399
+ Property: (parent) => parent.value,
400
+ PropertyDefinition: (parent) => parent.value,
401
+ VariableDeclarator: (parent) => parent.init
402
+ };
403
+ const ANONYMOUS_FUNCTIONS = new Set(["ArrowFunctionExpression", "FunctionExpression"]);
404
+ /**
405
+ * The node whose text is sent for this match: the node itself, unless it is an
406
+ * anonymous function sitting in a field that names it, in which case the
407
+ * named parent is sent instead.
408
+ */
409
+ const snippetNodeFor = (node) => {
410
+ const visited = node;
411
+ const { parent } = visited;
412
+ if (parent === null || parent === void 0) return node;
413
+ const namedField = NAMED_PARENTS[parent.type];
414
+ if (namedField === void 0 || !ANONYMOUS_FUNCTIONS.has(visited.type)) return node;
415
+ return namedField(parent) === node ? parent : node;
416
+ };
417
+ /**
418
+ * The first line of `text`, as a location starting at `line`/`column`. A
419
+ * function or a whole file spans far more than a reviewer wants underlined;
420
+ * the signature line — or the file's first line — is what identifies it.
421
+ */
422
+ const headOf = (text, line, column) => {
423
+ const newline = text.indexOf("\n");
424
+ return {
425
+ end: {
426
+ column: column + (newline === -1 ? text.length : newline),
427
+ line
428
+ },
429
+ start: {
430
+ column,
431
+ line
432
+ }
433
+ };
434
+ };
435
+ /** Where the diagnostic for one match lands. */
436
+ const reportLocationFor = (target, node, ownText) => {
437
+ const visited = node;
438
+ if (target === "file") return headOf(ownText, 1, 0);
439
+ if (target === "function") return headOf(ownText, visited.loc.start.line, visited.loc.start.column);
440
+ if (target === "jsx") return (visited.openingElement ?? visited).loc;
441
+ return visited.loc;
442
+ };
443
+ //#endregion
444
+ //#region src/oxlint-jev/index.ts
445
+ let currentPass = null;
446
+ /** The file `jev/unavailable` is enabled for, set at its `Program` handler. */
447
+ let unavailableArmedFor = null;
448
+ const passFor = (context) => currentPass !== null && currentPass.filename === context.filename ? currentPass : null;
449
+ const resolvePass = (pass, context) => {
450
+ pass.result ??= runJevPass({
451
+ filePath: path.relative(process.cwd(), context.filename),
452
+ matches: pass.matches,
453
+ mode: resolveJevMode(process.env),
454
+ root: process.cwd(),
455
+ source: context.sourceCode.text
456
+ });
457
+ return pass.result;
458
+ };
459
+ /**
460
+ * A match's key: the question's id and the selected node's full source range.
461
+ *
462
+ * The range, not the report location. The key is an answer name and a
463
+ * dictionary key in the request body, so it has to identify one node — and a
464
+ * report location does not. Every `CallExpression` in `a().b().c()` starts at
465
+ * the same line and column, as does every call in `f()()`, so keying on the
466
+ * start collapsed a whole chain onto one entry: the request asked about the
467
+ * innermost call alone and its score was reported on every call in the chain,
468
+ * with nothing saying work had been dropped. Start and end together are unique
469
+ * per node, because two distinct nodes cannot span exactly the same text.
470
+ */
471
+ const matchKeyFor = (id, node) => {
472
+ const [start, end] = node.range;
473
+ return `${id}@${start}-${end}`;
474
+ };
475
+ const askMeta = {
476
+ docs: { description: "Ask Jev a configured question about matched code, through HQ, and report the matches whose probability clears the question's cutoff." },
477
+ messages: {
478
+ finding: "[{{id}} {{hash}}] {{model}} answered {{score}} against a {{cutoff}} cutoff: {{question}}",
479
+ recorded: "[{{id}} {{hash}}] record mode: this would be asked. The request is in {{request}}."
480
+ },
481
+ schema: [JEV_ASK_OPTIONS_SCHEMA],
482
+ type: "problem"
483
+ };
484
+ const unavailableMeta = {
485
+ docs: { description: "Report a match Jev could not answer, naming the cause. Set it to `error`: an incomplete run must exit non-zero." },
486
+ messages: { unavailable: "[{{id}}] Jev could not answer: {{cause}}" },
487
+ schema: [],
488
+ type: "problem"
489
+ };
490
+ const createAsk = (context) => {
491
+ const collect = (nodeType, node) => {
492
+ const pass = currentPass;
493
+ const target = TARGET_BY_NODE_TYPE.get(nodeType);
494
+ if (pass === null || target === void 0) return;
495
+ for (const question of pass.questions) {
496
+ if (question.target !== target) continue;
497
+ const ownText = target === "file" ? context.sourceCode.text : context.sourceCode.getText(node);
498
+ const named = snippetNodeFor(node);
499
+ const snippet = named === node ? ownText : context.sourceCode.getText(named);
500
+ if (question.pattern !== void 0 && !question.pattern.test(snippet)) continue;
501
+ const loc = reportLocationFor(target, node, ownText);
502
+ pass.matches.push({
503
+ key: matchKeyFor(question.id, node),
504
+ loc,
505
+ question,
506
+ snippet
507
+ });
508
+ }
509
+ };
510
+ const visitors = {};
511
+ for (const nodeType of ALL_TARGET_NODE_TYPES) visitors[nodeType] = (node) => collect(nodeType, node);
512
+ visitors.Program = (node) => {
513
+ currentPass = {
514
+ filename: context.filename,
515
+ matches: [],
516
+ questions: resolveJevQuestions(context.options[0]),
517
+ result: void 0
518
+ };
519
+ collect("Program", node);
520
+ };
521
+ visitors["Program:exit"] = () => {
522
+ const pass = passFor(context);
523
+ if (pass === null) return;
524
+ const result = resolvePass(pass, context);
525
+ for (const finding of result.findings) context.report({
526
+ data: {
527
+ cutoff: finding.cutoff.toFixed(2),
528
+ hash: finding.textHash,
529
+ id: finding.id,
530
+ model: finding.model,
531
+ question: finding.question,
532
+ score: finding.score.toFixed(2)
533
+ },
534
+ loc: finding.loc,
535
+ messageId: "finding"
536
+ });
537
+ for (const record of result.recorded) context.report({
538
+ data: {
539
+ hash: record.textHash,
540
+ id: record.id,
541
+ request: record.requestPath
542
+ },
543
+ loc: record.loc,
544
+ messageId: "recorded"
545
+ });
546
+ if (result.unavailable.length > 0 && unavailableArmedFor !== context.filename) throw new Error(`jev/ask: ${result.unavailable.length} match(es) could not be answered and the jev/unavailable rule is not enabled, so nothing would report them. Enable it as an error in the Jev config. First cause: ${result.unavailable[0]?.cause ?? ""}`);
547
+ };
548
+ return visitors;
549
+ };
550
+ const createUnavailable = (context) => ({
551
+ Program: () => {
552
+ unavailableArmedFor = context.filename;
553
+ },
554
+ "Program:exit": () => {
555
+ const pass = passFor(context);
556
+ if (pass === null) return;
557
+ for (const entry of resolvePass(pass, context).unavailable) context.report({
558
+ data: {
559
+ cause: entry.cause,
560
+ id: entry.id
561
+ },
562
+ loc: entry.loc,
563
+ messageId: "unavailable"
564
+ });
565
+ }
566
+ });
567
+ const plugin = {
568
+ meta: { name: "jev" },
569
+ rules: {
570
+ ask: {
571
+ createOnce: createAsk,
572
+ meta: askMeta
573
+ },
574
+ unavailable: {
575
+ createOnce: createUnavailable,
576
+ meta: unavailableMeta
577
+ }
578
+ }
579
+ };
580
+ //#endregion
581
+ export { plugin as default };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,49 @@
1
+ import { a as resolveJevGateway, r as runJevFiles, s as openJevCache, t as allUnavailable } from "./bridge-B4Sx95ZO.js";
2
+ import { readFileSync } from "node:fs";
3
+ //#region src/oxlint-jev/worker.ts
4
+ /**
5
+ * The subprocess behind the synchronous bridge. It reads one
6
+ * `JevBridgeRequest` from stdin, runs it through the shared Jev layer, and
7
+ * prints the `JevRunResult` to stdout as JSON.
8
+ *
9
+ * It prints exactly one thing, always a result, even when it fails. A throw
10
+ * escaping here would reach the plugin as a dead subprocess with no match keys
11
+ * attached, and every match in the file would lose its own reason; instead
12
+ * every throw becomes the same shape as every other Jev failure — each match
13
+ * unavailable, named with its cause.
14
+ */
15
+ const describe = (error) => error instanceof Error ? error.message : String(error);
16
+ const readRequest = () => {
17
+ let parsed;
18
+ try {
19
+ parsed = JSON.parse(readFileSync(0, "utf-8"));
20
+ } catch {
21
+ throw new Error("the Jev worker could not read its request. The input is not reported: it can quote repository source.");
22
+ }
23
+ if (!Array.isArray(parsed.files)) throw new TypeError("the Jev worker was given no files to ask about");
24
+ return parsed;
25
+ };
26
+ const run = async (request) => {
27
+ const gateway = await resolveJevGateway({ env: process.env });
28
+ const cache = gateway.status === "ready" ? openJevCache({
29
+ directory: request.cacheDirectory,
30
+ endpoint: gateway.endpoint
31
+ }) : void 0;
32
+ return await runJevFiles({
33
+ ...cache === void 0 ? {} : { cache },
34
+ files: request.files,
35
+ gateway
36
+ });
37
+ };
38
+ let files = [];
39
+ let result;
40
+ try {
41
+ const request = readRequest();
42
+ ({files} = request);
43
+ result = await run(request);
44
+ } catch (error) {
45
+ result = allUnavailable(files, describe(error));
46
+ }
47
+ process.stdout.write(JSON.stringify(result));
48
+ //#endregion
49
+ export {};