@vitest-agent/engine 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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/index.d.ts +2679 -0
  4. package/index.js +63 -0
  5. package/layers/ConfigLive.js +46 -0
  6. package/layers/DataReaderLive.js +1923 -0
  7. package/layers/DataStoreLive.js +1283 -0
  8. package/layers/DetailResolverLive.js +9 -0
  9. package/layers/DiscoveryRegistryLive.js +70 -0
  10. package/layers/EnvironmentDetectorLive.js +36 -0
  11. package/layers/EnvironmentDetectorTest.js +13 -0
  12. package/layers/ExecutorResolverLive.js +9 -0
  13. package/layers/FormatSelectorLive.js +20 -0
  14. package/layers/HistoryTrackerLive.js +49 -0
  15. package/layers/HistoryTrackerTest.js +25 -0
  16. package/layers/LoggerLive.js +70 -0
  17. package/layers/OutputPipelineLive.js +20 -0
  18. package/layers/OutputRendererLive.js +23 -0
  19. package/layers/PathResolutionLive.js +46 -0
  20. package/layers/PerClientSessionMapLive.js +145 -0
  21. package/layers/ProjectDiscoveryLive.js +67 -0
  22. package/layers/ProjectDiscoveryTest.js +12 -0
  23. package/layers/ProjectIdentityLive.js +95 -0
  24. package/layers/RunContextLive.js +82 -0
  25. package/lib/format-triage.js +103 -0
  26. package/lib/format-wrapup.js +67 -0
  27. package/migrations/0001_initial.js +824 -0
  28. package/migrations/0002_test_artifacts.js +30 -0
  29. package/migrations/index.js +27 -0
  30. package/migrations/registry_0001_initial.js +39 -0
  31. package/migrations/session_map_0001_initial.js +38 -0
  32. package/package.json +59 -0
  33. package/platform.js +58 -0
  34. package/programs/end-agent.js +19 -0
  35. package/programs/hook-paths.js +115 -0
  36. package/programs/platform-sidecar.js +37 -0
  37. package/programs/record-session.js +49 -0
  38. package/programs/record-tdd-artifact.js +119 -0
  39. package/programs/record-turn.js +61 -0
  40. package/programs/record-workspace-changes.js +57 -0
  41. package/programs/register-agent.js +85 -0
  42. package/programs/resolve-session-for-recording.js +44 -0
  43. package/programs/session-env.js +85 -0
  44. package/project-dir.js +32 -0
  45. package/services/Config.js +15 -0
  46. package/services/DataReader.js +8 -0
  47. package/services/DataStore.js +15 -0
  48. package/services/DetailResolver.js +8 -0
  49. package/services/DiscoveryRegistry.js +8 -0
  50. package/services/EnvironmentDetector.js +8 -0
  51. package/services/ExecutorResolver.js +8 -0
  52. package/services/FormatSelector.js +8 -0
  53. package/services/HistoryTracker.js +20 -0
  54. package/services/OutputRenderer.js +8 -0
  55. package/services/PerClientSessionMap.js +22 -0
  56. package/services/ProjectDiscovery.js +8 -0
  57. package/services/ProjectIdentity.js +62 -0
  58. package/services/RunContext.js +53 -0
  59. package/services/idempotency.js +58 -0
  60. package/sql/assemblers.js +69 -0
  61. package/testing/layers.js +16 -0
  62. package/testing.d.ts +1101 -0
  63. package/testing.js +243 -0
  64. package/tsdoc-metadata.json +11 -0
  65. package/utils/ensure-migrated.js +47 -0
  66. package/utils/failure-signature.js +34 -0
  67. package/utils/resolve-data-path.js +64 -0
  68. package/utils/resolve-project-key-from-cwd.js +52 -0
  69. package/utils/resolve-workspace-key.js +41 -0
  70. package/version.js +6 -0
package/testing.js ADDED
@@ -0,0 +1,243 @@
1
+ import { DataReader } from "./services/DataReader.js";
2
+ import { DataStore } from "./services/DataStore.js";
3
+ import { PROJECT_MIGRATIONS } from "./migrations/index.js";
4
+ import { DataStoreTestLayer, makeTestLayer } from "./testing/layers.js";
5
+ import { Agent, AgentId, AgentNotFoundError, AgentReport, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, ChatId, CoverageBaselines, CoverageReport, DataStoreError, FileCoverageReport, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, RegistrationConflictError, TddTaskAlreadyEndedError, TddTaskNotFoundError, TrendEntry, TrendRecord } from "@vitest-agent/sdk";
6
+ import { Effect, Layer } from "effect";
7
+
8
+ //#region src/testing/index.ts
9
+ /** @public */
10
+ function empty(filename) {
11
+ return makeTestLayer(filename);
12
+ }
13
+ /** @public */
14
+ function singlePassingRun(filename) {
15
+ const base = makeTestLayer(filename);
16
+ const seed = Effect.gen(function* () {
17
+ const store = yield* DataStore;
18
+ yield* store.writeSettings("hash-preset-spr", {
19
+ vitestVersion: "5.0.0",
20
+ pool: "forks"
21
+ }, {});
22
+ const runId = yield* store.writeRun({
23
+ invocationId: "inv-preset-spr-1",
24
+ project: "default",
25
+ settingsHash: "hash-preset-spr",
26
+ timestamp: "2026-01-01T00:00:00.000Z",
27
+ commitSha: null,
28
+ branch: null,
29
+ reason: "passed",
30
+ duration: 1200,
31
+ total: 3,
32
+ passed: 3,
33
+ failed: 0,
34
+ skipped: 0,
35
+ scoped: false
36
+ });
37
+ const fileId = yield* store.ensureFile("src/math.test.ts");
38
+ const [moduleId] = yield* store.writeModules(runId, [{
39
+ fileId,
40
+ relativeModuleId: "src/math.test.ts",
41
+ state: "passed",
42
+ duration: 1200
43
+ }]);
44
+ yield* store.writeTestCases(moduleId, [
45
+ {
46
+ name: "adds two numbers",
47
+ fullName: "math > adds two numbers",
48
+ state: "passed",
49
+ duration: 400
50
+ },
51
+ {
52
+ name: "subtracts two numbers",
53
+ fullName: "math > subtracts two numbers",
54
+ state: "passed",
55
+ duration: 400
56
+ },
57
+ {
58
+ name: "multiplies two numbers",
59
+ fullName: "math > multiplies two numbers",
60
+ state: "passed",
61
+ duration: 400
62
+ }
63
+ ]);
64
+ });
65
+ return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
66
+ }
67
+ /** @public */
68
+ function withFailures(filename) {
69
+ const base = makeTestLayer(filename);
70
+ const seed = Effect.gen(function* () {
71
+ const store = yield* DataStore;
72
+ yield* store.writeSettings("hash-preset-wf", {
73
+ vitestVersion: "5.0.0",
74
+ pool: "forks"
75
+ }, {});
76
+ const runId = yield* store.writeRun({
77
+ invocationId: "inv-preset-wf-1",
78
+ project: "default",
79
+ settingsHash: "hash-preset-wf",
80
+ timestamp: "2026-01-01T00:00:00.000Z",
81
+ commitSha: null,
82
+ branch: null,
83
+ reason: "failed",
84
+ duration: 1500,
85
+ total: 4,
86
+ passed: 2,
87
+ failed: 2,
88
+ skipped: 0,
89
+ scoped: false
90
+ });
91
+ const fileId = yield* store.ensureFile("src/parser.test.ts");
92
+ const [moduleId] = yield* store.writeModules(runId, [{
93
+ fileId,
94
+ relativeModuleId: "src/parser.test.ts",
95
+ state: "failed",
96
+ duration: 1500
97
+ }]);
98
+ yield* store.writeTestCases(moduleId, [
99
+ {
100
+ name: "parses valid input",
101
+ fullName: "parser > parses valid input",
102
+ state: "passed",
103
+ duration: 300
104
+ },
105
+ {
106
+ name: "parses nested structure",
107
+ fullName: "parser > parses nested structure",
108
+ state: "passed",
109
+ duration: 300
110
+ },
111
+ {
112
+ name: "rejects empty input",
113
+ fullName: "parser > rejects empty input",
114
+ state: "failed",
115
+ duration: 450
116
+ },
117
+ {
118
+ name: "rejects malformed input",
119
+ fullName: "parser > rejects malformed input",
120
+ state: "failed",
121
+ duration: 450
122
+ }
123
+ ]);
124
+ });
125
+ return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
126
+ }
127
+ /** @public */
128
+ function flaky(filename) {
129
+ const base = makeTestLayer(filename);
130
+ const seed = Effect.gen(function* () {
131
+ const store = yield* DataStore;
132
+ yield* store.writeSettings("hash-preset-flaky", {
133
+ vitestVersion: "5.0.0",
134
+ pool: "forks"
135
+ }, {});
136
+ const runId1 = yield* store.writeRun({
137
+ invocationId: "inv-preset-flaky-1",
138
+ project: "default",
139
+ settingsHash: "hash-preset-flaky",
140
+ timestamp: "2026-01-01T00:00:00.000Z",
141
+ commitSha: null,
142
+ branch: null,
143
+ reason: "failed",
144
+ duration: 800,
145
+ total: 2,
146
+ passed: 1,
147
+ failed: 1,
148
+ skipped: 0,
149
+ scoped: false
150
+ });
151
+ const fileId = yield* store.ensureFile("src/async.test.ts");
152
+ const [moduleId1] = yield* store.writeModules(runId1, [{
153
+ fileId,
154
+ relativeModuleId: "src/async.test.ts",
155
+ state: "failed",
156
+ duration: 800
157
+ }]);
158
+ yield* store.writeTestCases(moduleId1, [{
159
+ name: "resolves within timeout",
160
+ fullName: "async > resolves within timeout",
161
+ state: "failed",
162
+ duration: 500
163
+ }, {
164
+ name: "rejects on error",
165
+ fullName: "async > rejects on error",
166
+ state: "passed",
167
+ duration: 300
168
+ }]);
169
+ yield* store.writeHistory("default", "async > resolves within timeout", "src/async.test.ts", runId1, "2025-12-31T23:00:00.000Z", "passed", 500, false, 0, null);
170
+ yield* store.writeHistory("default", "async > resolves within timeout", "src/async.test.ts", runId1, "2026-01-01T00:00:00.000Z", "failed", 500, false, 0, null);
171
+ yield* store.writeHistory("default", "async > rejects on error", "src/async.test.ts", runId1, "2026-01-01T00:00:00.000Z", "passed", 300, false, 0, null);
172
+ const runId2 = yield* store.writeRun({
173
+ invocationId: "inv-preset-flaky-2",
174
+ project: "default",
175
+ settingsHash: "hash-preset-flaky",
176
+ timestamp: "2026-01-01T01:00:00.000Z",
177
+ commitSha: null,
178
+ branch: null,
179
+ reason: "passed",
180
+ duration: 700,
181
+ total: 2,
182
+ passed: 2,
183
+ failed: 0,
184
+ skipped: 0,
185
+ scoped: false
186
+ });
187
+ const [moduleId2] = yield* store.writeModules(runId2, [{
188
+ fileId,
189
+ relativeModuleId: "src/async.test.ts",
190
+ state: "passed",
191
+ duration: 700
192
+ }]);
193
+ yield* store.writeTestCases(moduleId2, [{
194
+ name: "resolves within timeout",
195
+ fullName: "async > resolves within timeout",
196
+ state: "passed",
197
+ duration: 400
198
+ }, {
199
+ name: "rejects on error",
200
+ fullName: "async > rejects on error",
201
+ state: "passed",
202
+ duration: 300
203
+ }]);
204
+ yield* store.writeHistory("default", "async > resolves within timeout", "src/async.test.ts", runId2, "2026-01-01T01:00:00.000Z", "passed", 400, false, 0, null);
205
+ yield* store.writeHistory("default", "async > rejects on error", "src/async.test.ts", runId2, "2026-01-01T01:00:00.000Z", "passed", 300, false, 0, null);
206
+ });
207
+ return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
208
+ }
209
+ /** @public */
210
+ function withTddTask(filename) {
211
+ const base = makeTestLayer(filename);
212
+ const seed = Effect.gen(function* () {
213
+ const store = yield* DataStore;
214
+ const sessionId = yield* store.writeSession({
215
+ chatId: "chat-preset-tdd",
216
+ project: "default",
217
+ cwd: "/workspace",
218
+ agentKind: "main",
219
+ startedAt: "2026-01-01T00:00:00.000Z"
220
+ });
221
+ const tddTaskId = yield* store.writeTddTask({
222
+ sessionId,
223
+ goal: "Implement the parser feature",
224
+ startedAt: "2026-01-01T00:00:00.000Z"
225
+ });
226
+ const goal = yield* store.createGoal({
227
+ tddTaskId,
228
+ goal: "Handle edge cases"
229
+ });
230
+ yield* store.createBehavior({
231
+ goalId: goal.id,
232
+ behavior: "rejects empty input"
233
+ });
234
+ yield* store.createBehavior({
235
+ goalId: goal.id,
236
+ behavior: "rejects null input"
237
+ });
238
+ });
239
+ return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
240
+ }
241
+
242
+ //#endregion
243
+ export { Agent, AgentId, AgentNotFoundError, AgentReport, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, ChatId, CoverageBaselines, CoverageReport, DataReader, DataStore, DataStoreError, DataStoreTestLayer, FileCoverageReport, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, PROJECT_MIGRATIONS, RegistrationConflictError, TddTaskAlreadyEndedError, TddTaskNotFoundError, TrendEntry, TrendRecord, empty, flaky, makeTestLayer, singlePassingRun, withFailures, withTddTask };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.59.1"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,47 @@
1
+ import { LoggerLive } from "../layers/LoggerLive.js";
2
+ import { NodePlatformLayer, makeSqliteStack } from "../platform.js";
3
+ import { Effect, Layer } from "effect";
4
+ import { SqlClient } from "effect/unstable/sql/SqlClient";
5
+
6
+ //#region src/utils/ensure-migrated.ts
7
+ const GLOBAL_KEY = Symbol.for("vitest-agent/migration-promises");
8
+ const getCache = () => {
9
+ const g = globalThis;
10
+ let cache = g[GLOBAL_KEY];
11
+ if (!cache) {
12
+ cache = /* @__PURE__ */ new Map();
13
+ g[GLOBAL_KEY] = cache;
14
+ }
15
+ return cache;
16
+ };
17
+ /**
18
+ * Ensure the SQLite database at `dbPath` is migrated. Runs migrations at
19
+ * most once per dbPath in the current process; concurrent calls share the
20
+ * same in-flight promise. Subsequent calls (after the first resolves) are
21
+ * no-ops.
22
+ * @public
23
+ */
24
+ function ensureMigrated(dbPath, logLevel, logFile) {
25
+ const cache = getCache();
26
+ const cached = cache.get(dbPath);
27
+ if (cached) return cached;
28
+ const { SqliteLayer, MigratorLayer } = makeSqliteStack(dbPath);
29
+ const program = Effect.gen(function* () {
30
+ yield* SqlClient;
31
+ }).pipe(Effect.provide(MigratorLayer), Effect.provide(Layer.merge(SqliteLayer, NodePlatformLayer)), Effect.provide(LoggerLive(logLevel, logFile)));
32
+ const promise = Effect.runPromise(program);
33
+ cache.set(dbPath, promise);
34
+ promise.catch(() => {});
35
+ return promise;
36
+ }
37
+ /**
38
+ * Reset the migration cache. Test-only.
39
+ *
40
+ * @internal
41
+ */
42
+ function _resetMigrationCacheForTesting() {
43
+ getCache().clear();
44
+ }
45
+
46
+ //#endregion
47
+ export { _resetMigrationCacheForTesting, ensureMigrated };
@@ -0,0 +1,34 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ //#region src/utils/failure-signature.ts
4
+ const ASSERTION_REGEX = /(toBe|toEqual|toContain|toMatch|toThrow|toBeNull|toBeUndefined|toBeTruthy|toBeFalsy|toBeGreaterThan|toBeLessThan|toBeInstanceOf|toBeCloseTo|toHaveBeenCalled|toHaveBeenCalledWith|toHaveProperty|toHaveLength)\s*\(([^)]*)\)/;
5
+ const typeTagOf = (literal) => {
6
+ const trimmed = literal.trim();
7
+ if (trimmed === "" || trimmed === ")") return "";
8
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return "<number>";
9
+ if (/^["'`]/.test(trimmed)) return "<string>";
10
+ if (/^(true|false)$/.test(trimmed)) return "<boolean>";
11
+ if (/^null$/.test(trimmed)) return "<null>";
12
+ if (/^undefined$/.test(trimmed)) return "<undefined>";
13
+ if (/^[{[]/.test(trimmed)) return "<object>";
14
+ return "<expr>";
15
+ };
16
+ /** @public */
17
+ const normalizeAssertionShape = (message) => {
18
+ if (typeof message !== "string") return "";
19
+ const match = message.match(ASSERTION_REGEX);
20
+ if (match === null) return null;
21
+ const matcher = match[1];
22
+ const arg = typeTagOf(match[2]);
23
+ return arg === "" ? matcher : `${matcher}(${arg})`;
24
+ };
25
+ /** @public */
26
+ const computeFailureSignature = (input) => {
27
+ const shape = normalizeAssertionShape(input.assertion_message) ?? "<unknown>";
28
+ const lineCoord = input.top_frame_function_boundary_line !== null ? `fb:${input.top_frame_function_boundary_line}` : input.top_frame_raw_line !== void 0 ? `raw:${Math.floor(input.top_frame_raw_line / 10) * 10}` : "raw:?";
29
+ const key = `${input.error_name}|${shape}|${input.top_frame_function_name}|${lineCoord}`;
30
+ return createHash("sha256").update(key).digest("hex").substring(0, 16);
31
+ };
32
+
33
+ //#endregion
34
+ export { computeFailureSignature, normalizeAssertionShape };
@@ -0,0 +1,64 @@
1
+ import { VitestAgentConfigFile } from "../services/Config.js";
2
+ import { resolveProjectKeyFromCwd } from "./resolve-project-key-from-cwd.js";
3
+ import { VitestAgentConfig, normalizeWorkspaceKey } from "@vitest-agent/sdk";
4
+ import { Effect, FileSystem } from "effect";
5
+ import { AppDirs } from "@effected/xdg";
6
+ import { join } from "node:path";
7
+
8
+ //#region src/utils/resolve-data-path.ts
9
+ /**
10
+ * Filename of the SQLite database that stores all reporter data.
11
+ * @public
12
+ */
13
+ const DATABASE_FILENAME = "data.db";
14
+ /**
15
+ * Resolve the absolute path to the SQLite database for `projectDir`.
16
+ *
17
+ * The directory containing the database is ensured to exist before the
18
+ * function returns, so callers can open the database immediately without
19
+ * needing to create parent directories. The SQLite driver creates the file
20
+ * but not its parent.
21
+ *
22
+ * Precedence (highest first):
23
+ *
24
+ * 1. `options.cacheDir` — programmatic override.
25
+ * 2. `cacheDir` from `vitest-agent.config.toml`.
26
+ * 3. `<XDG data>/<normalized projectKey from config>/data.db`.
27
+ * 4. `<XDG data>/<projectKey resolved from cwd's package.json>/data.db`.
28
+ * Source 4 prefers `repository.url` (canonicalized to `host__path`),
29
+ * falling back to the normalized `name`. This matches the sidecar
30
+ * CLI's `_internal register-agent` resolver so the reporter and
31
+ * sidecar always write to the same `data.db`.
32
+ *
33
+ * The XDG data directory is namespaced via `AppDirs` (typically
34
+ * `$XDG_DATA_HOME/vitest-agent`, defaulting to
35
+ * `~/.local/share/vitest-agent`).
36
+ *
37
+ * The path is a function of identity, not filesystem layout. When no
38
+ * `package.json` is reachable from `projectDir`, the cwd basename is used
39
+ * as the final fallback so the function never throws on identity lookup.
40
+ *
41
+ * @param projectDir - Absolute path inside the user's workspace.
42
+ * @param options - Optional programmatic overrides.
43
+ * @public
44
+ */
45
+ const resolveDataPath = (projectDir, options = {}) => Effect.gen(function* () {
46
+ const fs = yield* FileSystem.FileSystem;
47
+ if (options.cacheDir) {
48
+ yield* fs.makeDirectory(options.cacheDir, { recursive: true });
49
+ return join(options.cacheDir, DATABASE_FILENAME);
50
+ }
51
+ const loaded = yield* (yield* VitestAgentConfigFile).loadOrDefault(new VitestAgentConfig({}));
52
+ if (loaded.cacheDir) {
53
+ yield* fs.makeDirectory(loaded.cacheDir, { recursive: true });
54
+ return join(loaded.cacheDir, DATABASE_FILENAME);
55
+ }
56
+ const dataRoot = yield* (yield* AppDirs).ensureData;
57
+ const key = loaded.projectKey ? normalizeWorkspaceKey(loaded.projectKey) : resolveProjectKeyFromCwd(projectDir);
58
+ const dir = join(dataRoot, key);
59
+ yield* fs.makeDirectory(dir, { recursive: true });
60
+ return join(dir, DATABASE_FILENAME);
61
+ });
62
+
63
+ //#endregion
64
+ export { DATABASE_FILENAME, resolveDataPath };
@@ -0,0 +1,52 @@
1
+ import { gitUrlToProjectKey, normalizeWorkspaceKey } from "@vitest-agent/sdk";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+
5
+ //#region src/utils/resolve-project-key-from-cwd.ts
6
+ const PACKAGE_JSON = "package.json";
7
+ const findNearestPackageJson = (startDir) => {
8
+ let dir = startDir;
9
+ while (true) {
10
+ const candidate = join(dir, PACKAGE_JSON);
11
+ if (existsSync(candidate)) return candidate;
12
+ const parent = dirname(dir);
13
+ if (parent === dir) return null;
14
+ dir = parent;
15
+ }
16
+ };
17
+ const readRepositoryUrl = (parsed) => {
18
+ const repo = parsed.repository;
19
+ if (typeof repo === "string" && repo.trim().length > 0) return repo.trim();
20
+ if (repo !== null && typeof repo === "object" && "url" in repo && typeof repo.url === "string") {
21
+ const url = repo.url.trim();
22
+ return url.length > 0 ? url : null;
23
+ }
24
+ return null;
25
+ };
26
+ /**
27
+ * Compute the project key for a workspace by reading `package.json`
28
+ * fields directly. Returns the canonical `host__path` form when a git
29
+ * remote URL is present, otherwise the normalized package name.
30
+ *
31
+ * Always returns a non-empty string — falls back to the cwd basename
32
+ * (or `"anonymous-project"`) so callers don't have to handle the
33
+ * empty case.
34
+ * @public
35
+ */
36
+ const resolveProjectKeyFromCwd = (cwd) => {
37
+ const pkgPath = findNearestPackageJson(cwd);
38
+ if (pkgPath !== null) try {
39
+ const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
40
+ const repoUrl = readRepositoryUrl(parsed);
41
+ if (repoUrl !== null) {
42
+ const key = gitUrlToProjectKey(repoUrl);
43
+ if (key !== null) return key;
44
+ }
45
+ if (typeof parsed.name === "string" && parsed.name.length > 0) return normalizeWorkspaceKey(parsed.name);
46
+ } catch {}
47
+ const basename = cwd.split("/").filter((segment) => segment.length > 0).pop() ?? "anonymous-project";
48
+ return normalizeWorkspaceKey(basename);
49
+ };
50
+
51
+ //#endregion
52
+ export { resolveProjectKeyFromCwd };
@@ -0,0 +1,41 @@
1
+ import { normalizeWorkspaceKey } from "@vitest-agent/sdk";
2
+ import { Effect } from "effect";
3
+ import { WorkspaceDiscovery, WorkspaceRootNotFoundError } from "@effected/workspaces";
4
+
5
+ //#region src/utils/resolve-workspace-key.ts
6
+ /**
7
+ * Resolve the normalized workspace key for the workspace containing
8
+ * `projectDir`.
9
+ *
10
+ * The key is the root `package.json`'s `name`, normalized via
11
+ * `normalizeWorkspaceKey`. This becomes the directory segment under
12
+ * `$XDG_DATA_HOME/vitest-agent/` where the SQLite database lives.
13
+ *
14
+ * Fails with `WorkspaceRootNotFoundError` when no root workspace is
15
+ * discoverable. `WorkspacePackage.name` is enforced non-empty by
16
+ * `@effected/workspaces`'s schema, so a successful root lookup always yields a
17
+ * usable name.
18
+ *
19
+ * Discovery is anchored at `projectDir` per call via `listPackagesIn`, not at
20
+ * the `WorkspaceDiscovery` layer's `cwd`: patterns, member manifests and names
21
+ * are read from beneath `projectDir`'s own root, so one long-lived layer
22
+ * answers correctly for a git worktree, a nested repository, or an unrelated
23
+ * project. Fails with the library's `WorkspaceRootNotFoundError` when
24
+ * `projectDir` sits in no workspace at all; results are memoized per resolved
25
+ * root, so many directories in one workspace share a single discovery.
26
+ *
27
+ * @param projectDir - Absolute path inside the workspace. Typically the
28
+ * reporter's resolved `projectDir` (CLAUDE_PROJECT_DIR or process.cwd()).
29
+ * @public
30
+ */
31
+ const resolveWorkspaceKey = (projectDir) => Effect.gen(function* () {
32
+ const root = (yield* (yield* WorkspaceDiscovery).listPackagesIn(projectDir)).find((pkg) => pkg.isRootWorkspace);
33
+ if (!root) return yield* Effect.fail(new WorkspaceRootNotFoundError({
34
+ searchPath: projectDir,
35
+ markers: []
36
+ }));
37
+ return normalizeWorkspaceKey(root.name);
38
+ });
39
+
40
+ //#endregion
41
+ export { resolveWorkspaceKey };
package/version.js ADDED
@@ -0,0 +1,6 @@
1
+ //#region src/version.ts
2
+ /** Build-time injected package version (see architecture.md D36). @public */
3
+ const CURRENT_ENGINE_VERSION = "0.1.0";
4
+
5
+ //#endregion
6
+ export { CURRENT_ENGINE_VERSION };