@alint-js/core 0.0.19 → 0.0.20

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/README.md CHANGED
@@ -4,39 +4,21 @@ The public SDK and run engine for `alint`.
4
4
 
5
5
  ## What it does
6
6
 
7
- This package provides the core APIs used by configs, plugins, rules, language processors, and agent adapters:
7
+ This package provides the core SDK and run engine APIs used by plugins, rules, language processors, embedding tools, and agent adapters:
8
8
 
9
- - `defineConfig`, `definePlugin`, and `defineRule`
9
+ - `definePlugin` and `defineRule`
10
+ - `runAlint`
10
11
  - rule registry and flat config normalization
11
12
  - source runtime helpers
12
13
  - built-in JavaScript source extraction
13
14
  - model resolution by size and capability
14
15
  - diagnostics and progress payload types
15
- - `runAlint`
16
16
  - framework-neutral agent contracts under `@alint-js/core/agent`
17
+ - tool-call structured output under `@alint-js/core/structured-output`
18
+ - config DSL and types for advanced SDK consumers
17
19
 
18
20
  ## How to use
19
21
 
20
- Define an `alint` config:
21
-
22
- ```ts
23
- import examplePlugin from '@alint-js/plugin-example'
24
-
25
- import { defineConfig } from '@alint-js/core'
26
-
27
- export default defineConfig([
28
- {
29
- files: ['**/*.{js,ts,jsx,tsx}'],
30
- plugins: {
31
- example: examplePlugin,
32
- },
33
- rules: {
34
- 'example/no-redundant-jsdoc': 'warn',
35
- },
36
- },
37
- ])
38
- ```
39
-
40
22
  Write a rule:
41
23
 
42
24
  ```ts
@@ -65,6 +47,39 @@ import { requireAgent } from '@alint-js/core/agent'
65
47
  const agent = requireAgent(ctx)
66
48
  ```
67
49
 
50
+ Ask a model for one validated, typed result with `@alint-js/core/structured-output`. It forces
51
+ the model to call a single reporting tool whose arguments match a valibot schema, validates
52
+ them, and retries with the validation error fed back to the model:
53
+
54
+ ```ts
55
+ import { generateStructured } from '@alint-js/core/structured-output'
56
+ import { array, description, object, pipe } from 'valibot'
57
+
58
+ const responseSchema = pipe(
59
+ object({ findings: array(findingSchema) }),
60
+ description('Report findings for this file.'),
61
+ )
62
+
63
+ const { findings } = await generateStructured({
64
+ createMessages: retryFeedback => [
65
+ { content: prompt, role: 'system' },
66
+ ...(retryFeedback ? [{ content: retryFeedback, role: 'user' as const }] : []),
67
+ { content: numberedSource, role: 'user' },
68
+ ],
69
+ logger: ctx.logger,
70
+ metering: ctx.metering,
71
+ model: await ctx.model(),
72
+ operation: 'my-rule-judge',
73
+ schema: responseSchema,
74
+ })
75
+ ```
76
+
77
+ The reporting tool is named `reportFindings` by default (`toolName` overrides it) and its
78
+ description defaults to the schema's valibot `description(...)`. `toolParametersFromSchema`,
79
+ `formatSourceWithLineNumbers`, and `formatOutputLanguageInstruction` are exported for callers
80
+ that build their own tools or prompts. Use `ctx.agent` instead when the model needs to
81
+ explore with tools before answering, because a forced tool call is a single shot, not a loop.
82
+
68
83
  ## When to use
69
84
 
70
85
  - You are writing an `alint` plugin or rule package.
@@ -74,5 +89,5 @@ const agent = requireAgent(ctx)
74
89
 
75
90
  ## When not to use
76
91
 
77
- - Use `@alint-js/cli` for command-line usage.
78
- - Use `@alint-js/config` for setup TOML and config-file helpers only.
92
+ - Use `@alint-js/cli` for command-line usage and ordinary `alint.config.*` files.
93
+ - Use `@alint-js/config` for setup TOML, config loading, and config-file tooling only.
package/dist/index.mjs CHANGED
@@ -653,7 +653,7 @@ function createBuiltInLanguageRegistry() {
653
653
  }
654
654
  //#endregion
655
655
  //#region package.json
656
- var version = "0.0.19";
656
+ var version = "0.0.20";
657
657
  //#endregion
658
658
  //#region src/dsl/registry.ts
659
659
  function buildRuleRegistry(config) {
@@ -0,0 +1,48 @@
1
+ import { W as ResolvedModel, g as RuleContext } from "./types-DQfoxG8J.mjs";
2
+ import { GenericSchema, InferOutput } from "valibot";
3
+ import { JsonSchema } from "@valibot/to-json-schema";
4
+ import { Message } from "@xsai/shared-chat";
5
+
6
+ //#region src/structuredOutput/index.d.ts
7
+ interface GenerateStructuredOptions<Schema extends GenericSchema> {
8
+ /**
9
+ * Builds the chat messages for each attempt. On retries, `retryFeedback`
10
+ * carries a ready-to-send validation-failure message; insert it wherever it
11
+ * fits the conversation, usually right before the final user message.
12
+ */
13
+ createMessages: (retryFeedback?: string) => Message[];
14
+ logger?: RuleContext['logger'];
15
+ /** Maximum number of attempts when validation fails. Defaults to 3. */
16
+ maxAttempts?: number;
17
+ metering?: RuleContext['metering'];
18
+ model: ResolvedModel;
19
+ /** Label recorded in metering metadata and debug logs, e.g. `go-responsibility-boundary-judge`. */
20
+ operation: string;
21
+ /** Milliseconds to wait before the given (1-based) attempt is retried. */
22
+ retryDelay?: (attempt: number) => number;
23
+ schema: Schema;
24
+ temperature?: number;
25
+ /** Shown to the model as the tool description. Defaults to the schema's valibot description. */
26
+ toolDescription?: string;
27
+ toolName?: string;
28
+ }
29
+ declare class InvalidStructuredOutputError extends Error {
30
+ constructor(message: string);
31
+ }
32
+ /** Standard instruction line for localized judge findings; undefined when no language is configured. */
33
+ declare function formatOutputLanguageInstruction(outputLanguage: string | undefined): string | undefined;
34
+ /** Prefixes each line with its 1-based number so schemas can ask for "left-column line numbers". */
35
+ declare function formatSourceWithLineNumbers(source: string): string;
36
+ /**
37
+ * Forces the model to call a single reporting tool and returns the validated
38
+ * tool arguments, so a tool call doubles as structured output. Unlike
39
+ * `response_format`-based structured output (xsai's `generateObject`), a
40
+ * forced tool call works on any provider with function calling, and invalid
41
+ * payloads are retried with validation feedback instead of failing outright.
42
+ */
43
+ declare function generateStructured<Schema extends GenericSchema>(options: GenerateStructuredOptions<Schema>): Promise<InferOutput<Schema>>;
44
+ declare function normalizeToolJsonSchema(schema: JsonSchema): JsonSchema;
45
+ /** Converts a valibot schema into provider-compliant strict tool parameters. */
46
+ declare function toolParametersFromSchema(schema: GenericSchema): JsonSchema;
47
+ //#endregion
48
+ export { GenerateStructuredOptions, InvalidStructuredOutputError, formatOutputLanguageInstruction, formatSourceWithLineNumbers, generateStructured, normalizeToolJsonSchema, toolParametersFromSchema };
@@ -0,0 +1,186 @@
1
+ import { errorMessageFrom } from "@moeru/std/error";
2
+ import { getDescription, parse } from "valibot";
3
+ import { sleep } from "@moeru/std/sleep";
4
+ import { toJsonSchema } from "@valibot/to-json-schema";
5
+ import { generateText } from "@xsai/generate-text";
6
+ import { rawTool } from "@xsai/tool";
7
+ //#region src/structuredOutput/index.ts
8
+ const defaultMaxAttempts = 3;
9
+ const defaultToolName = "reportFindings";
10
+ var InvalidStructuredOutputError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "InvalidStructuredOutputError";
14
+ }
15
+ };
16
+ /** Standard instruction line for localized judge findings; undefined when no language is configured. */
17
+ function formatOutputLanguageInstruction(outputLanguage) {
18
+ return outputLanguage ? `Write all human-readable finding messages and suggestions in this language: ${outputLanguage}.` : void 0;
19
+ }
20
+ /** Prefixes each line with its 1-based number so schemas can ask for "left-column line numbers". */
21
+ function formatSourceWithLineNumbers(source) {
22
+ return source.split("\n").map((line, index) => `${index + 1} | ${line}`).join("\n");
23
+ }
24
+ /**
25
+ * Forces the model to call a single reporting tool and returns the validated
26
+ * tool arguments, so a tool call doubles as structured output. Unlike
27
+ * `response_format`-based structured output (xsai's `generateObject`), a
28
+ * forced tool call works on any provider with function calling, and invalid
29
+ * payloads are retried with validation feedback instead of failing outright.
30
+ */
31
+ async function generateStructured(options) {
32
+ const maxAttempts = options.maxAttempts ?? defaultMaxAttempts;
33
+ const retryDelay = options.retryDelay ?? exponentialRetryDelay;
34
+ const toolName = options.toolName ?? defaultToolName;
35
+ const tool = rawTool({
36
+ description: options.toolDescription ?? getDescription(options.schema),
37
+ execute: (input) => asRecord(input) ?? {},
38
+ name: toolName,
39
+ parameters: toolParametersFromSchema(options.schema),
40
+ strict: true
41
+ });
42
+ let previousError;
43
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
44
+ let response;
45
+ try {
46
+ response = await generateText({
47
+ baseURL: options.model.provider.endpoint,
48
+ headers: options.model.provider.headers,
49
+ messages: options.createMessages(previousError ? retryFeedbackFrom(toolName, previousError) : void 0),
50
+ model: options.model.id,
51
+ parallelToolCalls: false,
52
+ temperature: options.temperature ?? 0,
53
+ toolChoice: {
54
+ function: { name: toolName },
55
+ type: "function"
56
+ },
57
+ tools: [tool]
58
+ });
59
+ } catch (error) {
60
+ previousError = `Tool call failed before validation: ${errorMessageFrom(error) ?? String(error)}`;
61
+ options.logger?.debug(`${options.operation} attempt ${attempt} failed while calling the model: ${previousError}`);
62
+ if (!isRetriableCallError(error) || attempt === maxAttempts) throw error;
63
+ await sleep(retryDelay(attempt));
64
+ continue;
65
+ }
66
+ recordAttemptUsage(options, response);
67
+ const result = parseStructuredResponse(options.schema, toolName, response);
68
+ if (result.ok) return result.value;
69
+ previousError = result.error;
70
+ options.logger?.debug(`${options.operation} attempt ${attempt} returned an invalid structured result: ${previousError}`);
71
+ if (!result.retriable || attempt === maxAttempts) throw new InvalidStructuredOutputError(`Invalid structured model response: ${previousError}`);
72
+ await sleep(retryDelay(attempt));
73
+ }
74
+ throw new InvalidStructuredOutputError("Model did not return a valid structured result");
75
+ }
76
+ function normalizeToolJsonSchema(schema) {
77
+ const normalized = normalizeJsonSchemaDefinition(schema);
78
+ return typeof normalized === "boolean" ? {} : normalized;
79
+ }
80
+ /** Converts a valibot schema into provider-compliant strict tool parameters. */
81
+ function toolParametersFromSchema(schema) {
82
+ return normalizeToolJsonSchema(toJsonSchema(schema));
83
+ }
84
+ function asRecord(value) {
85
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
86
+ }
87
+ function exponentialRetryDelay(attempt) {
88
+ return 500 * 2 ** (attempt - 1);
89
+ }
90
+ function isRetriableCallError(error) {
91
+ if (!(error instanceof Error)) return true;
92
+ return error.name === "InvalidToolCallError" || error.name === "InvalidToolInputError" || error.name === "ToolExecutionError";
93
+ }
94
+ function normalizeJsonSchemaDefinition(schema) {
95
+ if (typeof schema === "boolean") return schema;
96
+ const normalized = { ...schema };
97
+ delete normalized.$schema;
98
+ if (normalized.type === "object") normalized.additionalProperties = false;
99
+ if (normalized.properties) {
100
+ normalized.properties = Object.fromEntries(Object.entries(normalized.properties).map(([key, propertySchema]) => [key, normalizeJsonSchemaDefinition(propertySchema)]));
101
+ normalized.required = Object.keys(normalized.properties);
102
+ }
103
+ if (normalized.items) normalized.items = Array.isArray(normalized.items) ? normalized.items.map((item) => normalizeJsonSchemaDefinition(item)) : normalizeJsonSchemaDefinition(normalized.items);
104
+ if (normalized.$defs) normalized.$defs = normalizeJsonSchemaMap(normalized.$defs);
105
+ if (normalized.definitions) normalized.definitions = normalizeJsonSchemaMap(normalized.definitions);
106
+ for (const key of [
107
+ "allOf",
108
+ "anyOf",
109
+ "oneOf"
110
+ ]) if (normalized[key]) normalized[key] = normalized[key].map((item) => normalizeJsonSchemaDefinition(item));
111
+ if (normalized.not) normalized.not = normalizeJsonSchemaDefinition(normalized.not);
112
+ return normalized;
113
+ }
114
+ function normalizeJsonSchemaMap(map) {
115
+ return Object.fromEntries(Object.entries(map).map(([key, schema]) => [key, normalizeJsonSchemaDefinition(schema)]));
116
+ }
117
+ function normalizeUsage(usage) {
118
+ const record = asRecord(usage);
119
+ if (!record) return;
120
+ const normalized = {
121
+ inputTokens: numberFromRecord(record, "inputTokens") ?? numberFromRecord(record, "input_tokens") ?? numberFromRecord(record, "prompt_tokens"),
122
+ outputTokens: numberFromRecord(record, "outputTokens") ?? numberFromRecord(record, "output_tokens") ?? numberFromRecord(record, "completion_tokens"),
123
+ totalTokens: numberFromRecord(record, "totalTokens") ?? numberFromRecord(record, "total_tokens")
124
+ };
125
+ return Object.values(normalized).some((value) => value !== void 0) ? normalized : void 0;
126
+ }
127
+ function numberFromRecord(record, key) {
128
+ const value = record[key];
129
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
130
+ }
131
+ function parseStructuredResponse(schema, toolName, response) {
132
+ if (response.finishReason === "content_filter") return {
133
+ error: `${toolName} was not returned because the model finished with content_filter`,
134
+ ok: false,
135
+ retriable: false
136
+ };
137
+ if (response.finishReason === "length") return {
138
+ error: `${toolName} was not returned completely because the model finished with length`,
139
+ ok: false,
140
+ retriable: true
141
+ };
142
+ const toolResults = response.toolResults.filter((result) => result.toolName === toolName);
143
+ if (toolResults.length === 0) return {
144
+ error: `Missing ${toolName} tool result; finishReason=${response.finishReason}`,
145
+ ok: false,
146
+ retriable: true
147
+ };
148
+ if (toolResults.length > 1) return {
149
+ error: `Expected one ${toolName} tool result, received ${toolResults.length}`,
150
+ ok: false,
151
+ retriable: true
152
+ };
153
+ try {
154
+ return {
155
+ ok: true,
156
+ value: parse(schema, toolResults[0].result)
157
+ };
158
+ } catch (error) {
159
+ return {
160
+ error: errorMessageFrom(error) ?? String(error),
161
+ ok: false,
162
+ retriable: true
163
+ };
164
+ }
165
+ }
166
+ function recordAttemptUsage(options, response) {
167
+ const usage = normalizeUsage(response.usage);
168
+ if (!options.metering || !usage) return;
169
+ options.metering.recordUsage({
170
+ inputTokens: usage.inputTokens,
171
+ metadata: { operation: options.operation },
172
+ modelId: options.model.id,
173
+ outputTokens: usage.outputTokens,
174
+ providerId: options.model.provider.id,
175
+ totalTokens: usage.totalTokens
176
+ });
177
+ }
178
+ function retryFeedbackFrom(toolName, error) {
179
+ return [
180
+ "Your previous tool call could not be validated.",
181
+ `Validation error: ${error}`,
182
+ `Call ${toolName} again with arguments that exactly match the tool schema.`
183
+ ].join("\n");
184
+ }
185
+ //#endregion
186
+ export { InvalidStructuredOutputError, formatOutputLanguageInstruction, formatSourceWithLineNumbers, generateStructured, normalizeToolJsonSchema, toolParametersFromSchema };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/core",
3
3
  "type": "module",
4
- "version": "0.0.19",
4
+ "version": "0.0.20",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -11,6 +11,10 @@
11
11
  "types": "./dist/agent.d.mts",
12
12
  "default": "./dist/agent.mjs"
13
13
  },
14
+ "./structured-output": {
15
+ "types": "./dist/structured-output.d.mts",
16
+ "default": "./dist/structured-output.mjs"
17
+ },
14
18
  "./package.json": "./package.json"
15
19
  },
16
20
  "files": [
@@ -18,6 +22,10 @@
18
22
  ],
19
23
  "dependencies": {
20
24
  "@moeru/std": "^0.1.0-beta.18",
25
+ "@valibot/to-json-schema": "^1.7.1",
26
+ "@xsai/generate-text": "^0.5.0-beta.6",
27
+ "@xsai/shared-chat": "^0.5.0-beta.6",
28
+ "@xsai/tool": "^0.5.0-beta.6",
21
29
  "es-toolkit": "^1.49.0",
22
30
  "minimatch": "^10.2.5",
23
31
  "oxc-parser": "^0.137.0",