@akshatmittal/invoker 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akshat Mittal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @akshatmittal/invoker
2
+
3
+ A strictly typed TypeScript DSL for matrix-driven regression workflows. Invoker
4
+ expands Tasks into Vitest tests, runs each Task's Cases concurrently, and stores
5
+ validated JSON Output in Vitest metadata for reporters and later analysis.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add -D @akshatmittal/invoker vitest
11
+ ```
12
+
13
+ Invoker supports Node 24 and Vitest 4.1.10 or newer within Vitest 4.
14
+
15
+ ## Define a Workflow
16
+
17
+ ```ts
18
+ // regressions/model/tasks/evaluate-models.ts
19
+ import { defineTask } from "@akshatmittal/invoker";
20
+
21
+ export const evaluateModels = defineTask({
22
+ name: "evaluate-models",
23
+ matrix: {
24
+ model: ["gpt-5", "gpt-5-mini"],
25
+ dataset: ["support", "sales"],
26
+ },
27
+ setup: async ({ cases }) => loadFixtures(cases),
28
+ run: async ({ matrix, setup, vitest }) => {
29
+ vitest.expect(setup.has(matrix.dataset)).toBe(true);
30
+
31
+ return {
32
+ model: matrix.model,
33
+ dataset: matrix.dataset,
34
+ score: await evaluate(matrix, setup),
35
+ };
36
+ },
37
+ teardown: async ({ setup }) => {
38
+ await setup.close();
39
+ },
40
+ });
41
+ ```
42
+
43
+ ```ts
44
+ // regressions/model/index.test.ts
45
+ import { defineWorkflow } from "@akshatmittal/invoker";
46
+ import { evaluateModels } from "./tasks/evaluate-models.js";
47
+
48
+ defineWorkflow({
49
+ name: "model-regressions",
50
+ metadata: {
51
+ commit: process.env.GITHUB_SHA ?? "local",
52
+ baseline: "2026-08-01",
53
+ },
54
+ tasks: [evaluateModels],
55
+ });
56
+ ```
57
+
58
+ Matrix literals determine the exact `matrix` type, `setup` determines the exact
59
+ shared setup type, and the exact JSON return type is retained on the Task.
60
+ Omitting `matrix` creates one Case with `{}`. Setup runs once per Task, Cases
61
+ within that Task run concurrently, and teardown runs once after successful
62
+ setup. Tasks run sequentially in their Workflow.
63
+
64
+ ## Configure Vitest
65
+
66
+ Invoker uses Vitest's built-in reporters. The JSON reporter includes each
67
+ Case's data at `assertionResults[].meta.invoker`:
68
+
69
+ ```ts
70
+ // vitest.config.ts
71
+ import { defineConfig } from "vitest/config";
72
+
73
+ export default defineConfig({
74
+ test: {
75
+ maxConcurrency: 5,
76
+ reporters: ["default", "json", ...(process.env.GITHUB_ACTIONS === "true" ? ["github-actions" as const] : [])],
77
+ outputFile: {
78
+ json: "./artifacts/invoker-results.json",
79
+ },
80
+ },
81
+ });
82
+ ```
83
+
84
+ Run every Workflow or filter to one Task with ordinary Vitest commands:
85
+
86
+ ```sh
87
+ pnpm vitest run
88
+ pnpm vitest run regressions/model/index.test.ts -t evaluate-models
89
+ ```
90
+
91
+ The metadata envelope is stable and JSON-compatible:
92
+
93
+ ```json
94
+ {
95
+ "schema": 1,
96
+ "matrix": { "model": "gpt-5", "dataset": "support" },
97
+ "metadata": { "commit": "abc123" },
98
+ "output": { "model": "gpt-5", "dataset": "support", "score": 0.92 }
99
+ }
100
+ ```
101
+
102
+ `output` is present only after a successful, JSON-valid Task return. Vitest's
103
+ report remains authoritative for status, failures, timing, hierarchy, and
104
+ retries.
105
+
106
+ ## GitHub Actions artifacts
107
+
108
+ Create the output directory before Vitest and upload the report even when the
109
+ run fails:
110
+
111
+ ```yaml
112
+ - run: mkdir -p artifacts && pnpm vitest run
113
+ - if: always()
114
+ uses: actions/upload-artifact@v4
115
+ with:
116
+ name: invoker-results
117
+ path: artifacts/invoker-results.json
118
+ ```
119
+
120
+ The artifact provides per-run retention and can be downloaded later for custom
121
+ queries or reports. Invoker does not upload, index, or persist results itself.
122
+
123
+ ## v1 scope
124
+
125
+ Invoker does not provide a CLI, directory discovery, custom runner, custom
126
+ reporter, configuration helper, Task-level parallelism, matrix include/exclude,
127
+ or hosted result storage. Use Vitest configuration and your CI runner for those
128
+ concerns.
@@ -0,0 +1,79 @@
1
+ import { TestContext } from "vitest";
2
+ //#region src/types.d.ts
3
+ type JsonPrimitive = string | number | boolean | null;
4
+ type JsonValue = JsonPrimitive | readonly JsonValue[] | {
5
+ readonly [key: string]: JsonValue;
6
+ };
7
+ type JsonObject = {
8
+ readonly [key: string]: JsonValue;
9
+ };
10
+ type Matrix = {
11
+ readonly [axis: string]: readonly JsonValue[];
12
+ };
13
+ type CaseCoordinates<M extends Matrix> = { readonly [K in keyof M]: M[K][number]; };
14
+ interface InvokerMeta<Coordinates extends JsonObject, Output extends JsonValue, Metadata extends JsonObject = JsonObject> {
15
+ schema: 1;
16
+ matrix: Coordinates;
17
+ metadata?: Metadata;
18
+ output?: Output;
19
+ }
20
+ interface TaskContext<Coordinates, Setup> {
21
+ readonly matrix: Coordinates;
22
+ readonly setup: Setup;
23
+ readonly vitest: TestContext;
24
+ }
25
+ type Awaitable<Value> = Value | PromiseLike<Value>;
26
+ type SetupContext<M extends Matrix> = {
27
+ readonly cases: readonly CaseCoordinates<M>[];
28
+ };
29
+ type TeardownContext<M extends Matrix, Setup> = SetupContext<M> & {
30
+ readonly setup: Setup;
31
+ };
32
+ //#endregion
33
+ //#region src/task.d.ts
34
+ declare const taskDefinitionBrand: unique symbol;
35
+ interface TaskDefinition<Name extends string = string, M extends Matrix = Matrix, Setup = unknown, Output extends JsonValue = JsonValue> {
36
+ readonly name: Name;
37
+ readonly matrix: M;
38
+ readonly [taskDefinitionBrand]: {
39
+ readonly setup: Setup;
40
+ readonly output: Output;
41
+ };
42
+ readonly setup?: (context: SetupContext<M>) => Awaitable<Setup>;
43
+ readonly run: (context: TaskContext<CaseCoordinates<M>, Setup>) => Awaitable<Output>;
44
+ readonly teardown?: (context: TeardownContext<M, Setup>) => Awaitable<void>;
45
+ }
46
+ type TaskWithSetup<Name extends string, M extends Matrix, Setup, Output extends JsonValue> = {
47
+ readonly name: Name;
48
+ readonly matrix?: M;
49
+ readonly setup: (context: SetupContext<M>) => Awaitable<Setup>;
50
+ readonly run: (context: TaskContext<CaseCoordinates<M>, Setup>) => Awaitable<Output>;
51
+ readonly teardown?: (context: TeardownContext<M, Setup>) => Awaitable<void>;
52
+ };
53
+ type TaskWithoutSetup<Name extends string, M extends Matrix, Output extends JsonValue> = {
54
+ readonly name: Name;
55
+ readonly matrix?: M;
56
+ readonly setup?: never;
57
+ readonly run: (context: TaskContext<CaseCoordinates<M>, undefined>) => Awaitable<Output>;
58
+ readonly teardown?: never;
59
+ };
60
+ declare function defineTask<const Name extends string, const M extends Matrix = Record<never, never>, Setup = unknown, const Output extends JsonValue = JsonValue>(definition: TaskWithSetup<Name, M, Setup, Output>): TaskDefinition<Name, M, Setup, Output>;
61
+ declare function defineTask<const Name extends string, const M extends Matrix = Record<never, never>, const Output extends JsonValue = JsonValue>(definition: TaskWithoutSetup<Name, M, Output>): TaskDefinition<Name, M, undefined, Output>;
62
+ type AnyTaskDefinition = {
63
+ readonly name: string;
64
+ readonly matrix: Matrix;
65
+ readonly [taskDefinitionBrand]: {
66
+ readonly setup: unknown;
67
+ readonly output: JsonValue;
68
+ };
69
+ };
70
+ //#endregion
71
+ //#region src/workflow.d.ts
72
+ type WorkflowDefinition<Tasks extends readonly [AnyTaskDefinition, ...AnyTaskDefinition[]], Metadata extends JsonObject> = {
73
+ readonly name: string;
74
+ readonly metadata?: Metadata;
75
+ readonly tasks: Tasks;
76
+ };
77
+ declare function defineWorkflow<const Tasks extends readonly [AnyTaskDefinition, ...AnyTaskDefinition[]], const Metadata extends JsonObject = JsonObject>(definition: WorkflowDefinition<Tasks, Metadata>): void;
78
+ //#endregion
79
+ export { type CaseCoordinates, type InvokerMeta, type JsonObject, type JsonPrimitive, type JsonValue, type Matrix, type TaskContext, type TaskDefinition, defineTask, defineWorkflow };
package/dist/index.mjs ADDED
@@ -0,0 +1,174 @@
1
+ import { beforeAll, describe, test } from "vitest";
2
+ //#region src/task.ts
3
+ const taskDefinitionBrand = Symbol("invoker.task");
4
+ function defineTask(definition) {
5
+ return {
6
+ ...definition,
7
+ matrix: definition.matrix ?? {},
8
+ [taskDefinitionBrand]: true
9
+ };
10
+ }
11
+ //#endregion
12
+ //#region src/json.ts
13
+ function assertJson(value, owner, path, ancestors = /* @__PURE__ */ new Set()) {
14
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
15
+ if (typeof value === "number") {
16
+ if (!Number.isFinite(value)) fail(owner, path, "expected a finite number");
17
+ return;
18
+ }
19
+ if (typeof value !== "object") fail(owner, path, `expected JSON, received ${typeof value}`);
20
+ if (ancestors.has(value)) fail(owner, path, "cyclic values are not JSON");
21
+ ancestors.add(value);
22
+ if (Array.isArray(value)) for (let index = 0; index < value.length; index += 1) {
23
+ if (!(index in value)) fail(owner, `${path}[${index}]`, "sparse arrays are not JSON");
24
+ assertJson(value[index], owner, `${path}[${index}]`, ancestors);
25
+ }
26
+ else {
27
+ assertPlainObject(value, owner, path);
28
+ if (Object.getOwnPropertySymbols(value).length > 0) fail(owner, path, "JSON objects cannot have symbol keys");
29
+ for (const [key, child] of Object.entries(value)) assertJson(child, owner, `${path}.${key}`, ancestors);
30
+ }
31
+ ancestors.delete(value);
32
+ }
33
+ function assertPlainObject(value, owner, path) {
34
+ if (value === null || typeof value !== "object" || Array.isArray(value)) fail(owner, path, "expected a plain object");
35
+ const prototype = Object.getPrototypeOf(value);
36
+ if (prototype !== Object.prototype && prototype !== null) fail(owner, path, "expected a plain object");
37
+ }
38
+ function assertName(value, owner, path) {
39
+ if (typeof value !== "string" || value.trim() === "") fail(owner, path, "expected a non-empty string");
40
+ }
41
+ function assertOnlyKeys(value, allowed, owner) {
42
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) fail(owner, `.${key}`, "unknown property");
43
+ }
44
+ function canonicalJson(value) {
45
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
46
+ if (value !== null && typeof value === "object") {
47
+ const object = value;
48
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
49
+ }
50
+ return JSON.stringify(value);
51
+ }
52
+ function fail(owner, path, message) {
53
+ throw new TypeError(`${owner}${path}: ${message}`);
54
+ }
55
+ //#endregion
56
+ //#region src/matrix.ts
57
+ function expandMatrix(matrix, owner) {
58
+ if (matrix === void 0) return [{}];
59
+ assertPlainObject(matrix, owner, ".matrix");
60
+ let cases = [{}];
61
+ for (const [axis, values] of Object.entries(matrix)) {
62
+ if (axis.trim() === "") fail(owner, ".matrix", "axis names must not be empty");
63
+ if (!Array.isArray(values)) fail(owner, `.matrix.${axis}`, "expected an array");
64
+ if (values.length === 0) fail(owner, `.matrix.${axis}`, "expected at least one value");
65
+ values.forEach((value, index) => {
66
+ assertJson(value, owner, `.matrix.${axis}[${index}]`);
67
+ });
68
+ cases = cases.flatMap((coordinates) => values.map((value) => ({
69
+ ...coordinates,
70
+ [axis]: value
71
+ })));
72
+ }
73
+ const coordinates = /* @__PURE__ */ new Set();
74
+ for (const value of cases) {
75
+ const key = canonicalJson(value);
76
+ if (coordinates.has(key)) fail(owner, ".matrix", `duplicate coordinate ${JSON.stringify(value)}`);
77
+ coordinates.add(key);
78
+ }
79
+ return cases;
80
+ }
81
+ function caseName(matrix, index) {
82
+ const axes = Object.entries(matrix).map(([axis, value]) => `${axis}=${JSON.stringify(value)}`).join(", ");
83
+ return axes === "" ? `[${index + 1}]` : `[${index + 1}] ${axes}`;
84
+ }
85
+ //#endregion
86
+ //#region src/workflow.ts
87
+ function defineWorkflow(definition) {
88
+ const workflow = prepareWorkflow(definition);
89
+ describe(workflow.name, { concurrent: false }, () => {
90
+ for (const prepared of workflow.tasks) describe(prepared.task.name, { concurrent: false }, () => {
91
+ let setup;
92
+ const setupTask = prepared.task.setup;
93
+ if (setupTask) beforeAll(async () => {
94
+ setup = await setupTask({ cases: prepared.cases });
95
+ const teardownTask = prepared.task.teardown;
96
+ if (teardownTask) return () => teardownTask({
97
+ cases: prepared.cases,
98
+ setup
99
+ });
100
+ });
101
+ for (const [index, matrix] of prepared.cases.entries()) {
102
+ const name = prepared.names[index];
103
+ const invoker = prepared.metadata[index];
104
+ test.concurrent(name, { meta: { invoker } }, async (vitest) => {
105
+ const meta = vitest.task.meta;
106
+ delete meta.invoker.output;
107
+ const output = await prepared.task.run({
108
+ matrix,
109
+ setup,
110
+ vitest
111
+ });
112
+ assertJson(output, `Task ${JSON.stringify(prepared.task.name)}`, ".output");
113
+ meta.invoker.output = output;
114
+ });
115
+ }
116
+ });
117
+ });
118
+ }
119
+ function prepareWorkflow(definition) {
120
+ assertPlainObject(definition, "Workflow", "");
121
+ assertOnlyKeys(definition, [
122
+ "name",
123
+ "metadata",
124
+ "tasks"
125
+ ], "Workflow");
126
+ assertName(definition.name, "Workflow", ".name");
127
+ const metadata = definition.metadata;
128
+ if (metadata !== void 0) {
129
+ assertJson(metadata, `Workflow ${JSON.stringify(definition.name)}`, ".metadata");
130
+ assertPlainObject(metadata, `Workflow ${JSON.stringify(definition.name)}`, ".metadata");
131
+ }
132
+ if (!Array.isArray(definition.tasks) || definition.tasks.length === 0) fail("Workflow", ".tasks", "expected a non-empty Task tuple");
133
+ const names = /* @__PURE__ */ new Set();
134
+ const tasks = definition.tasks.map((value, index) => {
135
+ const owner = `Workflow ${JSON.stringify(definition.name)} Task ${index + 1}`;
136
+ assertPlainObject(value, owner, "");
137
+ const task = value;
138
+ if (task[taskDefinitionBrand] !== true) fail(owner, "", "expected a Task created by defineTask");
139
+ assertOnlyKeys(task, [
140
+ "name",
141
+ "matrix",
142
+ "setup",
143
+ "run",
144
+ "teardown"
145
+ ], owner);
146
+ assertName(task.name, owner, ".name");
147
+ if (names.has(task.name)) fail(owner, ".name", `duplicate Task name ${JSON.stringify(task.name)}`);
148
+ names.add(task.name);
149
+ if (typeof task.run !== "function") fail(owner, ".run", "expected a function");
150
+ if (task.setup !== void 0 && typeof task.setup !== "function") fail(owner, ".setup", "expected a function");
151
+ if (task.teardown !== void 0 && typeof task.teardown !== "function") fail(owner, ".teardown", "expected a function");
152
+ if (task.teardown && !task.setup) fail(owner, ".teardown", "requires setup");
153
+ const cases = expandMatrix(task.matrix, `Task ${JSON.stringify(task.name)}`);
154
+ return {
155
+ task,
156
+ cases,
157
+ names: cases.map(caseName),
158
+ metadata: cases.map((matrix) => metadata === void 0 ? {
159
+ schema: 1,
160
+ matrix
161
+ } : {
162
+ schema: 1,
163
+ matrix,
164
+ metadata
165
+ })
166
+ };
167
+ });
168
+ return {
169
+ name: definition.name,
170
+ tasks
171
+ };
172
+ }
173
+ //#endregion
174
+ export { defineTask, defineWorkflow };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@akshatmittal/invoker",
3
+ "version": "0.1.0",
4
+ "description": "Typed matrix-driven regression workflows for Vitest.",
5
+ "keywords": [
6
+ "matrix",
7
+ "regression",
8
+ "vitest",
9
+ "workflow"
10
+ ],
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/akshatmittal/invoker.git"
15
+ },
16
+ "files": [
17
+ "dist/**",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "type": "module",
22
+ "sideEffects": false,
23
+ "main": "./dist/index.mjs",
24
+ "module": "./dist/index.mjs",
25
+ "types": "./dist/index.d.mts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.mts",
29
+ "import": "./dist/index.mjs"
30
+ }
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^24.13.3",
37
+ "tsdown": "^0.22.14",
38
+ "typescript": "^7.0.2",
39
+ "vitest": "^4.1.10",
40
+ "@workspace/tsconfig": "0.0.0"
41
+ },
42
+ "peerDependencies": {
43
+ "vitest": "^4.1.10"
44
+ },
45
+ "engines": {
46
+ "node": "^24.18.1"
47
+ },
48
+ "scripts": {
49
+ "build": "tsdown",
50
+ "dev": "tsdown --watch",
51
+ "typecheck": "tsc --noEmit"
52
+ }
53
+ }