@jterrazz/intelligence 4.2.0 → 6.0.0

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,629 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ let node_fs = require("node:fs");
6
+ let node_path = require("node:path");
7
+ //#region src/lint/ast.ts
8
+ /**
9
+ * Shared AST helpers for the rule files. Everything here is pure and
10
+ * structural: rules narrow nodes by `type` and read fields defensively, so the
11
+ * layer stays decoupled from oxlint's internal (alpha) typings (mirrors
12
+ * `@jterrazz/test`'s `src/lint/ast.ts`).
13
+ */
14
+ /** Split a path into its non-empty segments (posix or win separators). */
15
+ function segments(path) {
16
+ return path.split(/[/\\]/).filter(Boolean);
17
+ }
18
+ /** The string value of a plain string literal (or a template with no holes). */
19
+ function stringValue(node) {
20
+ if (node === void 0) return;
21
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
22
+ if (node.type === "TemplateLiteral") {
23
+ const expressions = node.expressions;
24
+ const quasis = node.quasis;
25
+ if (expressions?.length === 0 && quasis?.length === 1) return quasis[0].value?.cooked;
26
+ }
27
+ }
28
+ /** The property name of a non-computed member expression, if identifiable. */
29
+ function memberPropertyName(node) {
30
+ if (node.type !== "MemberExpression" || node.computed === true) return;
31
+ const property = node.property;
32
+ return property?.type === "Identifier" ? property.name : void 0;
33
+ }
34
+ //#endregion
35
+ //#region src/lint/agents.ts
36
+ /**
37
+ * Is `filePath` under a folder literally named `agents` (exact path segment,
38
+ * anywhere in the path — P1's broader scope, unlike {@link detectAgentFile}'s
39
+ * narrower `<name>/<name>.ts` shape)?
40
+ */
41
+ function isUnderAgentsFolder(filePath) {
42
+ return segments(filePath).includes("agents");
43
+ }
44
+ /**
45
+ * `agents/<name>/<name>.ts` — the file basename (minus `.ts`) must equal its
46
+ * parent directory name, and an `agents` segment must be a proper ancestor of
47
+ * that parent directory. Returns `undefined` for anything else, including
48
+ * `*.prompt.ts` and `*.test.ts` (never agent files).
49
+ */
50
+ function detectAgentFile(filePath) {
51
+ const parts = segments(filePath);
52
+ const base = parts.at(-1);
53
+ if (base === void 0 || !base.endsWith(".ts")) return;
54
+ if (base.endsWith(".prompt.ts") || base.endsWith(".test.ts")) return;
55
+ const stem = base.slice(0, -3);
56
+ const parentIndex = parts.length - 2;
57
+ if (parts[parentIndex] !== stem) return;
58
+ const agentsIndex = parts.lastIndexOf("agents");
59
+ if (agentsIndex === -1 || agentsIndex >= parentIndex) return;
60
+ return { name: stem };
61
+ }
62
+ /** `<name>.prompt.ts` — any directory; `shared` is true directly under `_shared/`. */
63
+ function detectPromptFile(filePath) {
64
+ const parts = segments(filePath);
65
+ const base = parts.at(-1);
66
+ if (base === void 0 || !base.endsWith(".prompt.ts")) return;
67
+ const stem = base.slice(0, -10);
68
+ return {
69
+ dir: (0, node_path.dirname)(filePath),
70
+ name: stem,
71
+ shared: parts.at(-2) === "_shared"
72
+ };
73
+ }
74
+ //#endregion
75
+ //#region src/lint/manifest.ts
76
+ /**
77
+ * The rule manifest — the single source of truth for the mechanized
78
+ * agent/prompt conventions catalogue (mirrors `@jterrazz/test`'s
79
+ * `src/lint/manifest.ts` docs-as-code inversion: the normative text lives
80
+ * NEXT TO the rule it documents, attached as `meta.docs`, instead of drifting
81
+ * apart in a hand-maintained doc).
82
+ *
83
+ * The README's "Agent & prompt conventions" section is the human-facing
84
+ * explanation of *why* this shape exists; this manifest is the machine-facing
85
+ * "what exactly is checked" — `plugin.test.ts` asserts every shipped rule
86
+ * carries its entry and that no entry is orphaned.
87
+ */
88
+ const RULE_DOCS = {
89
+ "g1-agent-class-shape": {
90
+ id: "G1",
91
+ convention: "An agent file (`agents/<name>/<name>.ts`) exports exactly one class; that class has a `static readonly SCHEMA` member, a `run` method, and a constructor whose first parameter is named `model`.",
92
+ rationale: "A fixed shape makes every agent class predictable to read and to wire up from a DI container without re-deriving its contract each time."
93
+ },
94
+ "m1-model-resolution-in-container": {
95
+ id: "M1",
96
+ convention: "Calls to `createIntelligence(...)`, `createGatewayProvider(...)`, `createOpenRouterProvider(...)`, and `.model('…')` on the value they produce, are only allowed in a file whose path contains `/di/` or whose name ends in `container.ts`.",
97
+ rationale: "Model resolution is composition-root work — scattering it lets a provider/model choice drift outside the one place meant to own it."
98
+ },
99
+ "m2w-no-hardcoded-model-id": {
100
+ id: "M2",
101
+ convention: "A string literal that looks like a model id (`claude-…`, `openai/gpt-…`, …) outside config/test/fixture/spec files is a warning — model ids belong in configuration.",
102
+ rationale: "A model id inlined in application code can only change by a code deploy; configuration lets it change without one."
103
+ },
104
+ "p1-prose-in-prompt-files": {
105
+ id: "P1",
106
+ convention: "In any file under an `agents/` folder that is not a `*.prompt.ts` (nor a `*.test.ts`), a template literal spanning 3+ lines that reads as natural-language prose (a line with 4+ space-separated words, or a markdown heading) is an error — move it to the sibling `*.prompt.ts`.",
107
+ rationale: "The agent class only shapes data; prose that leaks into it hides the actual prompt contract and makes the two impossible to review independently."
108
+ },
109
+ "p2-prompt-file-exports": {
110
+ id: "P2",
111
+ convention: "A `*.prompt.ts` file exports only const arrow functions returning a string, plus types/interfaces — no default export, no class, no non-function const.",
112
+ rationale: "A closed export surface keeps a prompt file a pure builder module — anything else (state, a class, a default export) would invite prompt logic to grow side effects."
113
+ },
114
+ "p3-agent-prompt-sibling": {
115
+ id: "P3",
116
+ convention: "An agent file (`agents/<name>/<name>.ts`) imports its prompt from `./<name>.prompt.js`; a `<name>.prompt.ts` file outside `_shared/` has a sibling `<name>.ts` in the same directory.",
117
+ rationale: "The two-way link is what makes the pairing mechanical instead of a naming convention nobody enforces — an orphaned prompt file, or an agent that silently doesn't use its prompt, is almost always a mistake."
118
+ }
119
+ };
120
+ //#endregion
121
+ //#region src/lint/rules/g1-agent-class-shape.ts
122
+ /** Unwrap `private readonly model: T` (TSParameterProperty) / defaults to the bare identifier. */
123
+ function parameterName(param) {
124
+ if (param === void 0) return;
125
+ if (param.type === "Identifier") return param.name;
126
+ if (param.type === "TSParameterProperty") return parameterName(param.parameter);
127
+ if (param.type === "AssignmentPattern") return parameterName(param.left);
128
+ }
129
+ function classBody(classNode) {
130
+ return classNode.body?.body ?? [];
131
+ }
132
+ /**
133
+ * CONVENTIONS G1 — an agent file's class shape: exactly one exported class,
134
+ * carrying a `static readonly SCHEMA` member, a `run` method, and a
135
+ * constructor whose first parameter is `model` (the `LanguageModel` the
136
+ * class's `run()` passes straight to `generateText`/`streamText`).
137
+ */
138
+ const g1AgentClassShape = {
139
+ create(context) {
140
+ const file = context.physicalFilename;
141
+ if (detectAgentFile(file) === void 0) return {};
142
+ const exportedClasses = [];
143
+ return {
144
+ ExportDefaultDeclaration(node) {
145
+ const declaration = node.declaration;
146
+ if (declaration?.type === "ClassDeclaration" || declaration?.type === "ClassExpression") exportedClasses.push(declaration);
147
+ },
148
+ ExportNamedDeclaration(node) {
149
+ const declaration = node.declaration;
150
+ if (declaration?.type === "ClassDeclaration") exportedClasses.push(declaration);
151
+ },
152
+ "Program:exit"(node) {
153
+ if (exportedClasses.length === 0) {
154
+ context.report({
155
+ messageId: "noExportedClass",
156
+ node
157
+ });
158
+ return;
159
+ }
160
+ if (exportedClasses.length > 1) for (const extra of exportedClasses.slice(1)) context.report({
161
+ messageId: "multipleExportedClasses",
162
+ node: extra
163
+ });
164
+ const target = exportedClasses[0];
165
+ const members = classBody(target);
166
+ if (!members.some((member) => {
167
+ if (member.type !== "PropertyDefinition" || member.static !== true) return false;
168
+ const key = member.key;
169
+ return key?.type === "Identifier" && key.name === "SCHEMA";
170
+ })) context.report({
171
+ messageId: "missingSchema",
172
+ node: target
173
+ });
174
+ if (!members.some((member) => {
175
+ if (member.type !== "MethodDefinition" && member.type !== "PropertyDefinition") return false;
176
+ const key = member.key;
177
+ return key?.type === "Identifier" && key.name === "run";
178
+ })) context.report({
179
+ messageId: "missingRun",
180
+ node: target
181
+ });
182
+ const constructor = members.find((member) => member.type === "MethodDefinition" && (member.kind === "constructor" || member.key?.name === "constructor"));
183
+ if (constructor === void 0) {
184
+ context.report({
185
+ messageId: "missingConstructorModel",
186
+ node: target
187
+ });
188
+ return;
189
+ }
190
+ const firstParam = (constructor.value?.params)?.[0];
191
+ if (parameterName(firstParam) !== "model") context.report({
192
+ messageId: "missingConstructorModel",
193
+ node: firstParam ?? constructor
194
+ });
195
+ }
196
+ };
197
+ },
198
+ meta: {
199
+ docs: RULE_DOCS["g1-agent-class-shape"],
200
+ messages: {
201
+ missingConstructorModel: "Agent class constructor's first parameter must be named `model` (G1).",
202
+ missingRun: "Agent class is missing a `run` method (G1).",
203
+ missingSchema: "Agent class is missing a `static readonly SCHEMA` member (G1).",
204
+ multipleExportedClasses: "Agent file exports more than one class — exactly one (G1).",
205
+ noExportedClass: "Agent file exports no class — exactly one is required (G1)."
206
+ },
207
+ type: "problem"
208
+ }
209
+ };
210
+ //#endregion
211
+ //#region src/lint/rules/m1-model-resolution-in-container.ts
212
+ /** The composition-root factories `@jterrazz/intelligence` exposes. */
213
+ const FACTORY_NAMES = /* @__PURE__ */ new Set([
214
+ "createGatewayProvider",
215
+ "createIntelligence",
216
+ "createOpenRouterProvider"
217
+ ]);
218
+ /** Is `file` a DI/composition-root file? */
219
+ function isContainerFile(file) {
220
+ return file.includes("/di/") || file.endsWith("container.ts");
221
+ }
222
+ /**
223
+ * CONVENTIONS M1 — model resolution is composition-root work. `createIntelligence`
224
+ * and the two provider factories, plus `.model('…')` calls on whatever they
225
+ * produce, are only allowed in a file under `di/` or named `*container.ts`.
226
+ *
227
+ * Detection is best effort by design: a `.model(<string literal>)` call is
228
+ * flagged wherever it appears in a file that imports `@jterrazz/intelligence`,
229
+ * WITHOUT verifying the receiver is actually the `Intelligence` instance —
230
+ * static analysis cannot reliably trace that binding across parameter
231
+ * passing/destructuring (see the container.ts example in the README, where
232
+ * `Intelligence` arrives as an injected parameter, not a local `const`). A
233
+ * project with an unrelated `.model()` method on some other object, imported
234
+ * from the same file as `@jterrazz/intelligence`, would false-positive here —
235
+ * an accepted, documented limitation of this rule.
236
+ */
237
+ const m1ModelResolutionInContainer = {
238
+ create(context) {
239
+ const file = context.physicalFilename;
240
+ const allowed = isContainerFile(file);
241
+ let importsIntelligence = false;
242
+ const modelCalls = [];
243
+ const factoryCalls = [];
244
+ return {
245
+ CallExpression(node) {
246
+ const callee = node.callee;
247
+ if (callee === void 0) return;
248
+ if (callee.type === "Identifier" && FACTORY_NAMES.has(callee.name)) {
249
+ factoryCalls.push({
250
+ name: callee.name,
251
+ node
252
+ });
253
+ return;
254
+ }
255
+ if (callee.type === "MemberExpression" && memberPropertyName(callee) === "model") {
256
+ const args = node.arguments ?? [];
257
+ if (args.length === 1 && stringValue(args[0]) !== void 0) modelCalls.push(node);
258
+ }
259
+ },
260
+ ImportDeclaration(node) {
261
+ if (stringValue(node.source) === "@jterrazz/intelligence") importsIntelligence = true;
262
+ },
263
+ "Program:exit"() {
264
+ if (allowed) return;
265
+ for (const { name, node } of factoryCalls) context.report({
266
+ data: { name },
267
+ messageId: "factoryOutsideContainer",
268
+ node
269
+ });
270
+ if (importsIntelligence) for (const node of modelCalls) context.report({
271
+ messageId: "modelCallOutsideContainer",
272
+ node
273
+ });
274
+ }
275
+ };
276
+ },
277
+ meta: {
278
+ docs: RULE_DOCS["m1-model-resolution-in-container"],
279
+ messages: {
280
+ factoryOutsideContainer: "{{name}}() must only be called from a DI/container file (path containing \"/di/\" or ending in \"container.ts\") (M1).",
281
+ modelCallOutsideContainer: ".model('…') resolution must only happen in a DI/container file (M1)."
282
+ },
283
+ type: "problem"
284
+ }
285
+ };
286
+ //#endregion
287
+ //#region src/lint/rules/m2w-no-hardcoded-model-id.ts
288
+ /** `claude-…`, `gpt-4o`, `o1-preview`, `grok-2`, `deepseek-v3`, … */
289
+ const BARE_MODEL_ID = /^(?<family>claude|deepseek|gemini|gpt|grok|llama|mistral|o[0-9])[-0-9a-z.]/iu;
290
+ /** `openai/gpt-4o`, `anthropic/claude-3-5-sonnet`, … (provider-prefixed form). */
291
+ const PREFIXED_MODEL_ID = /^[a-z0-9-]+\/(?<family>claude|deepseek|gemini|gpt|grok|llama|mistral)/iu;
292
+ /** Config/test/fixture/spec files are exempt — that's exactly where a model id belongs. */
293
+ function isExemptFile(file) {
294
+ const parts = segments(file);
295
+ const base = parts.at(-1) ?? "";
296
+ if (/\.(?:test|spec)\.[cm]?tsx?$/u.test(base)) return true;
297
+ if (parts.includes("fixtures") || parts.includes("specs") || parts.includes("__fixtures__")) return true;
298
+ return parts.includes("config") || /\.config\.[cm]?tsx?$/u.test(base);
299
+ }
300
+ function looksLikeModelId(value) {
301
+ return BARE_MODEL_ID.test(value) || PREFIXED_MODEL_ID.test(value);
302
+ }
303
+ /**
304
+ * CONVENTIONS M2 (warning) — a string literal shaped like a model id belongs
305
+ * in configuration, not inlined in application code. Config/test/fixture/spec
306
+ * files are exempt by design (oxlint only ever visits `.ts`/`.tsx` sources —
307
+ * a `.yml`/`.json` config value is never in its reach regardless).
308
+ */
309
+ const m2wNoHardcodedModelId = {
310
+ create(context) {
311
+ const file = context.physicalFilename;
312
+ if (isExemptFile(file)) return {};
313
+ return { Literal(node) {
314
+ if (typeof node.value !== "string") return;
315
+ if (looksLikeModelId(node.value)) context.report({
316
+ data: { value: node.value },
317
+ messageId: "hardcodedModelId",
318
+ node
319
+ });
320
+ } };
321
+ },
322
+ meta: {
323
+ docs: RULE_DOCS["m2w-no-hardcoded-model-id"],
324
+ messages: { hardcodedModelId: "String \"{{value}}\" looks like a model id — model ids belong in configuration (M2)." },
325
+ type: "suggestion"
326
+ }
327
+ };
328
+ //#endregion
329
+ //#region src/lint/rules/p1-prose-in-prompt-files.ts
330
+ /** A markdown heading — `#`, `##`, or `###` followed by a space. */
331
+ const MARKDOWN_HEADING = /^#{1,3}\s/;
332
+ /** A line reading as prose: 4+ tokens separated by whitespace. */
333
+ const MIN_PROSE_WORDS = 4;
334
+ /** Does `text` (one physical line) look like natural-language prose? */
335
+ function looksLikeProseLine(line) {
336
+ const trimmed = line.trim();
337
+ if (trimmed.length === 0) return false;
338
+ if (MARKDOWN_HEADING.test(trimmed)) return true;
339
+ return trimmed.split(/\s+/u).filter(Boolean).length >= MIN_PROSE_WORDS;
340
+ }
341
+ /** Number of physical lines a source span covers, from a `\n` count. */
342
+ function lineSpan(text) {
343
+ return text.split("\n").length;
344
+ }
345
+ /**
346
+ * CONVENTIONS P1 — the flagship rule: no multi-line natural-language literal
347
+ * outside a `*.prompt.ts` file. A template literal is flagged when its full
348
+ * source span reaches 3+ lines AND at least one of its own static quasis
349
+ * (never the interpolated expressions) reads as prose — a line with 4+
350
+ * space-separated words, or a markdown heading. Single-line literals, JSON-ish
351
+ * multi-line literals, and pure data interpolation stay under the threshold
352
+ * and pass.
353
+ */
354
+ const p1ProseInPromptFiles = {
355
+ create(context) {
356
+ const file = context.physicalFilename;
357
+ if (!isUnderAgentsFolder(file)) return {};
358
+ const base = file.split(/[/\\]/).pop() ?? "";
359
+ if (base.endsWith(".prompt.ts") || base.endsWith(".test.ts")) return {};
360
+ const target = `${base.replace(/\.ts$/, "")}.prompt.ts`;
361
+ return { TemplateLiteral(node) {
362
+ const start = node.start ?? node.range?.[0];
363
+ const end = node.end ?? node.range?.[1];
364
+ if (typeof start !== "number" || typeof end !== "number") return;
365
+ if (lineSpan(context.sourceCode.text.slice(start, end)) < 3) return;
366
+ if ((node.quasis ?? []).some((quasi) => {
367
+ return (quasi.value?.raw ?? "").split("\n").some(looksLikeProseLine);
368
+ })) context.report({
369
+ data: { target },
370
+ messageId: "moveProse",
371
+ node
372
+ });
373
+ } };
374
+ },
375
+ meta: {
376
+ docs: RULE_DOCS["p1-prose-in-prompt-files"],
377
+ messages: { moveProse: "Multi-line natural-language template literal outside a *.prompt.ts file — move prompt prose to {{target}} (P1)." },
378
+ type: "problem"
379
+ }
380
+ };
381
+ //#endregion
382
+ //#region src/lint/rules/p2-prompt-file-exports.ts
383
+ /** Declaration types that are types, not values — always allowed. */
384
+ const TYPE_DECLARATIONS = /* @__PURE__ */ new Set(["TSInterfaceDeclaration", "TSTypeAliasDeclaration"]);
385
+ /**
386
+ * Best-effort "does this expression plausibly evaluate to a string?" check.
387
+ * Permissive by design (P2 is a shape gate, not a type checker): a delegated
388
+ * call (`return sharedSection();`), a bare identifier, member access, string
389
+ * concatenation, and `? :` / `||` narrowing all pass. Only expressions that
390
+ * are CLEARLY the wrong shape — an object/array literal, a non-string literal,
391
+ * a nested function — are rejected.
392
+ */
393
+ function looksLikeStringExpression(node) {
394
+ if (node === void 0) return false;
395
+ switch (node.type) {
396
+ case "BinaryExpression": return node.operator === "+";
397
+ case "CallExpression":
398
+ case "Identifier":
399
+ case "MemberExpression":
400
+ case "TemplateLiteral": return true;
401
+ case "ConditionalExpression": return looksLikeStringExpression(node.consequent) && looksLikeStringExpression(node.alternate);
402
+ case "Literal": return typeof node.value === "string";
403
+ case "LogicalExpression": return looksLikeStringExpression(node.right);
404
+ default: return false;
405
+ }
406
+ }
407
+ /**
408
+ * The expressions `fn` itself returns — the concise-arrow expression body, or
409
+ * every top-level `return`'s argument in a block body (never descending into a
410
+ * nested function's own returns). Each is a REAL AST node (unlike a synthetic
411
+ * `ReturnStatement` wrapper), so it always carries a valid position to report on.
412
+ */
413
+ function ownReturnExpressions(fn) {
414
+ const body = fn.body;
415
+ if (body === void 0) return [];
416
+ if (body.type !== "BlockStatement") return [body];
417
+ const expressions = [];
418
+ const visit = (node) => {
419
+ if (node === void 0) return;
420
+ if (node.type === "ReturnStatement") {
421
+ const argument = node.argument;
422
+ if (argument !== void 0) expressions.push(argument);
423
+ return;
424
+ }
425
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration") return;
426
+ for (const key of Object.keys(node)) {
427
+ if (key === "parent") continue;
428
+ const value = node[key];
429
+ if (Array.isArray(value)) {
430
+ for (const item of value) if (isNode(item)) visit(item);
431
+ } else if (isNode(value)) visit(value);
432
+ }
433
+ };
434
+ visit(body);
435
+ return expressions;
436
+ }
437
+ function isNode(value) {
438
+ return typeof value === "object" && value !== null && typeof value.type === "string";
439
+ }
440
+ /**
441
+ * CONVENTIONS P2 — a `*.prompt.ts` file's export surface is closed: const
442
+ * arrow functions returning a string (the builders), and types/interfaces.
443
+ * No default export, no class, no non-function const, no plain `function`
444
+ * declaration (the convention is arrow consts specifically — see the
445
+ * README's "Agent & prompt conventions" section).
446
+ */
447
+ const p2PromptFileExports = {
448
+ create(context) {
449
+ if (!context.physicalFilename.endsWith(".prompt.ts")) return {};
450
+ return { Program(node) {
451
+ for (const statement of node.body ?? []) {
452
+ if (statement.type === "ExportDefaultDeclaration") {
453
+ context.report({
454
+ messageId: "defaultExport",
455
+ node: statement
456
+ });
457
+ continue;
458
+ }
459
+ if (statement.type !== "ExportNamedDeclaration") continue;
460
+ const declaration = statement.declaration;
461
+ if (declaration === void 0) continue;
462
+ if (TYPE_DECLARATIONS.has(declaration.type)) continue;
463
+ if (declaration.type === "ClassDeclaration") {
464
+ context.report({
465
+ messageId: "classExport",
466
+ node: statement
467
+ });
468
+ continue;
469
+ }
470
+ if (declaration.type !== "VariableDeclaration") {
471
+ const declId = declaration.id;
472
+ const declName = declId?.type === "Identifier" ? declId.name : "?";
473
+ context.report({
474
+ data: { name: declName },
475
+ messageId: "nonFunctionExport",
476
+ node: statement
477
+ });
478
+ continue;
479
+ }
480
+ for (const declarator of declaration.declarations ?? []) {
481
+ const id = declarator.id;
482
+ const name = id?.type === "Identifier" ? id.name : "?";
483
+ const init = declarator.init;
484
+ if (init?.type !== "ArrowFunctionExpression") {
485
+ context.report({
486
+ data: { name },
487
+ messageId: "nonFunctionExport",
488
+ node: declarator
489
+ });
490
+ continue;
491
+ }
492
+ const returnExpressions = ownReturnExpressions(init);
493
+ const badReturn = returnExpressions.length === 0 ? init : returnExpressions.find((expression) => !looksLikeStringExpression(expression));
494
+ if (badReturn !== void 0) context.report({
495
+ data: { name },
496
+ messageId: "nonStringReturn",
497
+ node: badReturn
498
+ });
499
+ }
500
+ }
501
+ } };
502
+ },
503
+ meta: {
504
+ docs: RULE_DOCS["p2-prompt-file-exports"],
505
+ messages: {
506
+ classExport: "Prompt file exports a class — a *.prompt.ts file exports only const string-builder functions and types (P2).",
507
+ defaultExport: "Prompt file has a default export — a *.prompt.ts file exports only const string-builder functions and types (P2).",
508
+ nonFunctionExport: "Prompt file export \"{{name}}\" is not a const arrow function — a *.prompt.ts file exports only const string-builder functions and types (P2).",
509
+ nonStringReturn: "Prompt file export \"{{name}}\" does not appear to return a string (P2)."
510
+ },
511
+ type: "problem"
512
+ }
513
+ };
514
+ //#endregion
515
+ //#region src/lint/fs.ts
516
+ /**
517
+ * Filesystem probe for the one fs-anchored rule (P3 — the sibling check).
518
+ * Mirrors `@jterrazz/test`'s `src/lint/fs-cache.ts` in spirit, minus the
519
+ * memoization: P3 does at most one `existsSync` per visited file, so a cache
520
+ * would add complexity without a measurable payoff at this plugin's size.
521
+ */
522
+ function fileExists(path) {
523
+ try {
524
+ return (0, node_fs.existsSync)(path);
525
+ } catch {
526
+ return false;
527
+ }
528
+ }
529
+ //#endregion
530
+ //#region src/lint/plugin.ts
531
+ /**
532
+ * The `@jterrazz/intelligence` oxlint plugin — formalizes the agent/prompt
533
+ * folder convention documented in the README's "Agent & prompt conventions"
534
+ * section as statically-checkable rules, mirroring `@jterrazz/test`'s
535
+ * `src/lint/plugin.ts` (same composable-fragment architecture, same
536
+ * `RuleTester` test layer, same manifest/docs-as-code pattern).
537
+ *
538
+ * Registered in a consumer's `oxlint.config.ts` via
539
+ * `jsPlugins: ['@jterrazz/intelligence/oxlint']` and referenced as
540
+ * `intelligence/<rule>` in the `rules` map — or enabled wholesale via the
541
+ * {@link intelligence} composable fragment:
542
+ *
543
+ * import { compose, node } from '@jterrazz/typescript/oxlint';
544
+ * import { intelligence } from '@jterrazz/intelligence/oxlint';
545
+ * export default compose(node, intelligence);
546
+ *
547
+ * Bundled by tsdown (`dist/oxlint.js`); rules import nothing from this
548
+ * package's AI SDK runtime (only pure structural helpers: `ast.ts`, `fs.ts`,
549
+ * `agents.ts`), so the bundle stays free of the `ai`/`@ai-sdk/*` dependency
550
+ * graph the main entry pulls in.
551
+ */
552
+ const plugin = {
553
+ meta: { name: "intelligence" },
554
+ rules: {
555
+ "g1-agent-class-shape": g1AgentClassShape,
556
+ "m1-model-resolution-in-container": m1ModelResolutionInContainer,
557
+ "m2w-no-hardcoded-model-id": m2wNoHardcodedModelId,
558
+ "p1-prose-in-prompt-files": p1ProseInPromptFiles,
559
+ "p2-prompt-file-exports": p2PromptFileExports,
560
+ "p3-agent-prompt-sibling": {
561
+ create(context) {
562
+ const file = context.physicalFilename;
563
+ const agent = detectAgentFile(file);
564
+ const prompt = agent === void 0 ? detectPromptFile(file) : void 0;
565
+ if (agent === void 0 && prompt === void 0) return {};
566
+ return { Program(node) {
567
+ if (agent !== void 0) {
568
+ const expected = `./${agent.name}.prompt.js`;
569
+ if (!(node.body ?? []).some((statement) => statement.type === "ImportDeclaration" && stringValue(statement.source) === expected)) context.report({
570
+ data: { expected },
571
+ messageId: "missingPromptImport",
572
+ node
573
+ });
574
+ return;
575
+ }
576
+ if (prompt !== void 0 && !prompt.shared) {
577
+ if (!fileExists((0, node_path.join)(prompt.dir, `${prompt.name}.ts`))) context.report({
578
+ data: { name: prompt.name },
579
+ messageId: "missingAgentSibling",
580
+ node
581
+ });
582
+ }
583
+ } };
584
+ },
585
+ meta: {
586
+ docs: RULE_DOCS["p3-agent-prompt-sibling"],
587
+ messages: {
588
+ missingAgentSibling: "Prompt file \"{{name}}.prompt.ts\" has no sibling agent file \"{{name}}.ts\" in the same directory (P3).",
589
+ missingPromptImport: "Agent file must import its prompt from \"{{expected}}\" (P3)."
590
+ },
591
+ type: "problem"
592
+ }
593
+ }
594
+ }
595
+ };
596
+ /**
597
+ * The full catalogue at its intended severities — spread into an oxlint
598
+ * `rules` map to enable everything in one line:
599
+ *
600
+ * rules: { ...recommendedRules }
601
+ *
602
+ * Hard conventions are errors; `m2w-*` (the model-id heuristic) is a warning.
603
+ */
604
+ const recommendedRules = Object.fromEntries(Object.keys(plugin.rules).map((rule) => [`intelligence/${rule}`, /^\w+w-/.test(rule) ? "warn" : "error"]));
605
+ /**
606
+ * The composable fragment — wire the plugin and enable the whole catalogue.
607
+ * Designed to be composed with a base preset (e.g. `@jterrazz/typescript/oxlint`):
608
+ *
609
+ * import { compose, node } from '@jterrazz/typescript/oxlint';
610
+ * import { intelligence } from '@jterrazz/intelligence/oxlint';
611
+ * export default compose(node, intelligence);
612
+ *
613
+ * `jsPlugins` registers the tool-facing entry, `rules` is {@link recommendedRules}.
614
+ * `overrides` ships empty (no per-glob relaxation is needed today — every rule
615
+ * gates itself by file path/name internally) but is kept on the fragment's
616
+ * shape for parity with `@jterrazz/test`'s `testing` fragment and so a future
617
+ * relaxation has somewhere to go without a breaking shape change.
618
+ */
619
+ const intelligence = {
620
+ jsPlugins: ["@jterrazz/intelligence/oxlint"],
621
+ overrides: [],
622
+ rules: recommendedRules
623
+ };
624
+ //#endregion
625
+ exports.default = plugin;
626
+ exports.intelligence = intelligence;
627
+ exports.recommendedRules = recommendedRules;
628
+
629
+ //# sourceMappingURL=oxlint.cjs.map