@classytic/repo-core 0.22.0 → 0.23.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.
@@ -23,6 +23,20 @@ type FilterInput = Filter | Record<string, unknown>;
23
23
  * the session through uses `unknown`; kits narrow at the boundary.
24
24
  */
25
25
  type RepositorySession = unknown;
26
+ /**
27
+ * What a transaction callback receives BESIDE the tx-bound repository.
28
+ *
29
+ * `session` is the raw driver handle, exposed so work that lives OUTSIDE the
30
+ * repository can join the same transaction — the canonical consumer is an
31
+ * outbox writer: `outbox.store(event, { session: uow.session })` commits the
32
+ * event row atomically with the business write. Present when the driver has a
33
+ * per-transaction handle (Mongo's ClientSession); connection-bound backends
34
+ * (SQLite) pass an empty handle — their tx-bound repo IS the only join point.
35
+ * Kits MUST pass a handle object (possibly empty), never omit the argument.
36
+ */
37
+ interface TransactionHandle {
38
+ session?: RepositorySession;
39
+ }
26
40
  /**
27
41
  * Read-operation options. The index signature is the escape hatch kits use
28
42
  * for driver-specific flags (`populate`, `select`, `readPreference`,
@@ -72,6 +86,17 @@ interface QueryOptions {
72
86
  interface WriteOptions extends QueryOptions {
73
87
  /** Upsert on update/replace. */
74
88
  upsert?: boolean;
89
+ /**
90
+ * Optimistic-concurrency CAS. When set, the write applies ONLY if the
91
+ * stored version equals `ifVersion`; on mismatch the kit MUST throw
92
+ * `VersionConflictError` (`@classytic/repo-core/errors`) — never return
93
+ * `null`, which means not-found and would invite a blind retry that
94
+ * clobbers the concurrent write. A successful versioned write increments
95
+ * the stored version. Requires the `optimisticConcurrency` capability;
96
+ * kits without it MUST throw on the option rather than ignore it (a
97
+ * silently dropped guard is the defect, not a degraded mode).
98
+ */
99
+ ifVersion?: number;
75
100
  }
76
101
  /**
77
102
  * Options for the optional `findAll` verb.
@@ -1502,6 +1527,15 @@ interface StandardRepo<TDoc> extends MinimalRepo<TDoc> {
1502
1527
  * lives in the kit that knows its driver.
1503
1528
  */
1504
1529
  isDuplicateKeyError?(err: unknown): boolean;
1530
+ /**
1531
+ * Classify an error from a transactional write as a TRANSIENT concurrency
1532
+ * conflict — one the backend expects callers to recover from by re-running
1533
+ * the same work (Mongo `TransientTransactionError` label, PG 40001/40P01,
1534
+ * `SQLITE_BUSY`, Prisma P2034). Consumed by `retryingTransaction`; same
1535
+ * ownership rule as `isDuplicateKeyError` — the kit knows its driver.
1536
+ * Absent = nothing retries (`neverTransient`), the safe default.
1537
+ */
1538
+ isTransientConflictError?(err: unknown): boolean;
1505
1539
  /** Find a single doc by compound filter (used by arc's AccessControl). */
1506
1540
  getOne?(filter: FilterInput, options?: QueryOptions): Promise<TDoc | null>;
1507
1541
  /** Alias many kits expose alongside `getOne`. Arc checks both names. */
@@ -1810,7 +1844,7 @@ interface StandardRepo<TDoc> extends MinimalRepo<TDoc> {
1810
1844
  * // Either both writes commit or neither does.
1811
1845
  * ```
1812
1846
  */
1813
- withTransaction?<T>(fn: (txRepo: StandardRepo<TDoc>) => Promise<T>, options?: Record<string, unknown>): Promise<T>;
1847
+ withTransaction?<T>(fn: (txRepo: StandardRepo<TDoc>, uow?: TransactionHandle) => Promise<T>, options?: Record<string, unknown>): Promise<T>;
1814
1848
  /**
1815
1849
  * Portable change feed — `for await` over committed mutations:
1816
1850
  *
@@ -1870,4 +1904,4 @@ interface CursorOptions {
1870
1904
  [key: string]: unknown;
1871
1905
  }
1872
1906
  //#endregion
1873
- export { AggCacheOptions, AggDateBucket, AggDateBucketInterval, AggDateBucketUnit, AggExecutionHints, AggMeasure, AggPaginationRequest, AggRequest, AggResult, AggRow, AggTopN, AggTopNTies, BulkCreateResult, BulkWriteOperation, BulkWriteResult, ChangeEvent, ClaimTransition, ClaimVersionTransition, CursorOptions, DeleteManyResult, DeleteOptions, DeleteResult, FilterInput, FindAllOptions, FindOneAndUpdateOptions, InferDoc, KeysetAggPaginationResult, MinimalRepo, PaginationParams, QueryOptions, RepositorySession, StandardRepo, TenantPurgeOptions, TenantPurgeProgress, TenantPurgeResult, TenantPurgeStrategy, TransitionArgs, TransitionMachine, UpdateManyResult, WatchOptions, WriteOptions };
1907
+ export { AggCacheOptions, AggDateBucket, AggDateBucketInterval, AggDateBucketUnit, AggExecutionHints, AggMeasure, AggPaginationRequest, AggRequest, AggResult, AggRow, AggTopN, AggTopNTies, BulkCreateResult, BulkWriteOperation, BulkWriteResult, ChangeEvent, ClaimTransition, ClaimVersionTransition, CursorOptions, DeleteManyResult, DeleteOptions, DeleteResult, FilterInput, FindAllOptions, FindOneAndUpdateOptions, InferDoc, KeysetAggPaginationResult, MinimalRepo, PaginationParams, QueryOptions, RepositorySession, StandardRepo, TenantPurgeOptions, TenantPurgeProgress, TenantPurgeResult, TenantPurgeStrategy, TransactionHandle, TransitionArgs, TransitionMachine, UpdateManyResult, WatchOptions, WriteOptions };
@@ -1,3 +1,4 @@
1
+ import { isVersionConflictError } from "../errors/conflict.mjs";
1
2
  import { and, anyOf as in_, eq, gt, isNull, like, ne, or } from "../filter/builders.mjs";
2
3
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
4
  //#region src/testing/conformance.ts
@@ -1176,6 +1177,14 @@ function runStandardRepoConformance(harness) {
1176
1177
  const rows = await ctx.repo.findAll({ name: "tx-rollback" });
1177
1178
  expect(rows).toHaveLength(0);
1178
1179
  });
1180
+ it("the callback receives a TransactionHandle — the outbox join point", async () => {
1181
+ let handle = "never-called";
1182
+ await ctx.repo.withTransaction(async (_txRepo, uow) => {
1183
+ handle = uow;
1184
+ });
1185
+ expect(typeof handle).toBe("object");
1186
+ expect(handle).not.toBeNull();
1187
+ });
1179
1188
  it("reads inside the txRepo see writes inside the same callback", async () => {
1180
1189
  await ctx.repo.withTransaction(async (txRepo) => {
1181
1190
  const id = idOf(await txRepo.create(harness.makeDoc({
@@ -1186,6 +1195,60 @@ function runStandardRepoConformance(harness) {
1186
1195
  expect(back?.name).toBe("tx-read");
1187
1196
  });
1188
1197
  });
1198
+ it("declares WHO retries a transient conflict — undeclared ownership stacks retry policies", () => {
1199
+ const caps = ctx.repo.capabilities;
1200
+ expect(caps?.transactionRetry, "a repository exposing withTransaction must declare capabilities.transactionRetry: 'managed' (the kit retries internally, callers invoke it once) or 'caller' (single attempt, an outer envelope owns the loop)").toMatch(/^(managed|caller)$/);
1201
+ });
1202
+ it("nestedTransactions matches what a nested call ACTUALLY does", async () => {
1203
+ const caps = ctx.repo.capabilities;
1204
+ let nestedWorked;
1205
+ try {
1206
+ await ctx.repo.withTransaction(async (txRepo) => {
1207
+ await txRepo.withTransaction?.(async () => {});
1208
+ });
1209
+ nestedWorked = true;
1210
+ } catch {
1211
+ nestedWorked = false;
1212
+ }
1213
+ expect(nestedWorked, `capabilities.nestedTransactions is ${String(caps?.nestedTransactions)} but a nested withTransaction ${nestedWorked ? "succeeded" : "threw"} — the descriptor must describe THIS repository, not the underlying driver`).toBe(caps?.nestedTransactions === true);
1214
+ });
1215
+ });
1216
+ describe.skipIf(!harness.features.optimisticConcurrency)("ifVersion CAS", () => {
1217
+ const versionOf = (doc) => Number(doc[harness.versionField ?? "version"]);
1218
+ it("a matching version applies the write and increments the version", async () => {
1219
+ const created = await ctx.repo.create(harness.makeDoc({
1220
+ name: "cas-ok",
1221
+ email: "cas@x.com"
1222
+ }));
1223
+ const id = idOf(created, harness.idField);
1224
+ const v0 = versionOf(created);
1225
+ const updated = await ctx.repo.update(id, { name: "cas-ok-2" }, { ifVersion: v0 });
1226
+ expect(updated?.name).toBe("cas-ok-2");
1227
+ expect(versionOf(updated)).toBe(v0 + 1);
1228
+ });
1229
+ it("a STALE version throws VersionConflictError — never null", async () => {
1230
+ const created = await ctx.repo.create(harness.makeDoc({
1231
+ name: "cas-stale",
1232
+ email: "stale@x.com"
1233
+ }));
1234
+ const id = idOf(created, harness.idField);
1235
+ const v0 = versionOf(created);
1236
+ await ctx.repo.update(id, { name: "cas-stale-2" }, { ifVersion: v0 });
1237
+ let caught;
1238
+ try {
1239
+ await ctx.repo.update(id, { name: "cas-stale-3" }, { ifVersion: v0 });
1240
+ } catch (e) {
1241
+ caught = e;
1242
+ }
1243
+ expect(isVersionConflictError(caught)).toBe(true);
1244
+ const current = await ctx.repo.getById(id);
1245
+ expect(current?.name).toBe("cas-stale-2");
1246
+ });
1247
+ it("not-found stays null — a missing record is NOT a version conflict", async () => {
1248
+ const ghost = harness.missingId ?? "000000000000000000000000";
1249
+ const out = await ctx.repo.update(ghost, { name: "x" }, { ifVersion: 1 });
1250
+ expect(out).toBeNull();
1251
+ });
1189
1252
  });
1190
1253
  describe("purgeByField (tenant cleanup)", () => {
1191
1254
  const seedTwoTenants = async () => {
@@ -97,6 +97,17 @@ interface ConformanceHarness<TDoc extends ConformanceDoc = ConformanceDoc> {
97
97
  idField: string;
98
98
  /** Feature support flags — skipped scenarios show as `skipped` in vitest output. */
99
99
  features: ConformanceFeatures;
100
+ /**
101
+ * Field carrying the optimistic-concurrency version, when the
102
+ * `optimisticConcurrency` capability is declared. Default `'version'`.
103
+ */
104
+ versionField?: string;
105
+ /**
106
+ * A syntactically valid id that matches no record — the ifVersion cases
107
+ * assert not-found stays `null` (never a version conflict). Default is a
108
+ * Mongo-shaped all-zero ObjectId; SQL/string-id kits supply their own.
109
+ */
110
+ missingId?: string;
100
111
  /** Create a fresh, isolated repo + cleanup closure. Called per test. */
101
112
  setup(): Promise<ConformanceContext<TDoc>>;
102
113
  /**
package/package.json CHANGED
@@ -1,184 +1,184 @@
1
1
  {
2
- "name": "@classytic/repo-core",
3
- "version": "0.22.0",
4
- "description": "Driver-agnostic repository primitives: hooks, Filter IR, operations, pagination, cache contract. Foundation for mongokit, sqlitekit, pgkit, and prismakit. Lean by design — no plugins ship here; each kit owns its own.",
5
- "type": "module",
6
- "sideEffects": false,
7
- "files": [
8
- "dist",
9
- "README.md",
10
- "LICENSE",
11
- "CHANGELOG.md"
12
- ],
13
- "engines": {
14
- "node": ">=22"
15
- },
16
- "exports": {
17
- "./hooks": {
18
- "types": "./dist/hooks/index.d.mts",
19
- "default": "./dist/hooks/index.mjs"
20
- },
21
- "./operations": {
22
- "types": "./dist/operations/index.d.mts",
23
- "default": "./dist/operations/index.mjs"
24
- },
25
- "./errors": {
26
- "types": "./dist/errors/index.d.mts",
27
- "default": "./dist/errors/index.mjs"
28
- },
29
- "./pagination": {
30
- "types": "./dist/pagination/index.d.mts",
31
- "default": "./dist/pagination/index.mjs"
32
- },
33
- "./repository": {
34
- "types": "./dist/repository/index.d.mts",
35
- "default": "./dist/repository/index.mjs"
36
- },
37
- "./filter": {
38
- "types": "./dist/filter/index.d.mts",
39
- "default": "./dist/filter/index.mjs"
40
- },
41
- "./update": {
42
- "types": "./dist/update/index.d.mts",
43
- "default": "./dist/update/index.mjs"
44
- },
45
- "./query-parser": {
46
- "types": "./dist/query-parser/index.d.mts",
47
- "default": "./dist/query-parser/index.mjs"
48
- },
49
- "./context": {
50
- "types": "./dist/context/index.d.mts",
51
- "default": "./dist/context/index.mjs"
52
- },
53
- "./cache": {
54
- "types": "./dist/cache/index.d.mts",
55
- "default": "./dist/cache/index.mjs"
56
- },
57
- "./hash": {
58
- "types": "./dist/hash/index.d.mts",
59
- "default": "./dist/hash/index.mjs"
60
- },
61
- "./events": {
62
- "types": "./dist/events/index.d.mts",
63
- "default": "./dist/events/index.mjs"
64
- },
65
- "./schema": {
66
- "types": "./dist/schema/index.d.mts",
67
- "default": "./dist/schema/index.mjs"
68
- },
69
- "./testing": {
70
- "types": "./dist/testing/index.d.mts",
71
- "default": "./dist/testing/index.mjs"
72
- },
73
- "./tenant": {
74
- "types": "./dist/tenant/index.d.mts",
75
- "default": "./dist/tenant/index.mjs"
76
- },
77
- "./lookup": {
78
- "types": "./dist/lookup/index.d.mts",
79
- "default": "./dist/lookup/index.mjs"
80
- },
81
- "./adapter": {
82
- "types": "./dist/adapter/index.d.mts",
83
- "default": "./dist/adapter/index.mjs"
84
- },
85
- "./better-auth": {
86
- "types": "./dist/better-auth/index.d.mts",
87
- "default": "./dist/better-auth/index.mjs"
88
- },
89
- "./aggregate": {
90
- "types": "./dist/aggregate/index.d.mts",
91
- "default": "./dist/aggregate/index.mjs"
92
- },
93
- "./plugins": {
94
- "types": "./dist/plugins/index.d.mts",
95
- "default": "./dist/plugins/index.mjs"
96
- },
97
- "./lock": {
98
- "types": "./dist/lock/index.d.mts",
99
- "default": "./dist/lock/index.mjs"
100
- },
101
- "./usage": {
102
- "types": "./dist/usage/index.d.mts",
103
- "default": "./dist/usage/index.mjs"
104
- },
105
- "./package.json": "./package.json",
106
- "./sync": {
107
- "types": "./dist/sync/index.d.mts",
108
- "default": "./dist/sync/index.mjs"
109
- },
110
- "./cleanup": {
111
- "types": "./dist/cleanup/index.d.mts",
112
- "default": "./dist/cleanup/index.mjs"
113
- }
114
- },
115
- "keywords": [
116
- "repository",
117
- "repository-pattern",
118
- "data-access",
119
- "hooks",
120
- "filter-ir",
121
- "pagination",
122
- "cursor-pagination",
123
- "keyset-pagination",
124
- "plugin-based",
125
- "driver-agnostic",
126
- "typescript",
127
- "esm"
128
- ],
129
- "author": "Classytic <classytic.dev@gmail.com> (https://github.com/classytic)",
130
- "license": "MIT",
131
- "repository": {
132
- "type": "git",
133
- "url": "git+https://github.com/classytic/repo-core.git"
134
- },
135
- "bugs": {
136
- "url": "https://github.com/classytic/repo-core/issues"
137
- },
138
- "homepage": "https://github.com/classytic/repo-core#readme",
139
- "scripts": {
140
- "build": "tsdown",
141
- "dev": "tsdown --watch",
142
- "test": "vitest run --project unit --project integration",
143
- "test:unit": "vitest run --project unit",
144
- "test:integration": "vitest run --project integration",
145
- "test:e2e": "vitest run --project e2e",
146
- "test:all": "vitest run",
147
- "test:watch": "vitest --project unit --project integration",
148
- "bench": "vitest bench --run --project bench",
149
- "test:coverage": "vitest run --coverage",
150
- "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
151
- "lint": "biome check src tests",
152
- "lint:fix": "biome check src tests --write",
153
- "format": "biome format src tests --write",
154
- "check": "biome ci src tests --diagnostic-level=error",
155
- "knip": "knip",
156
- "push": "classytic-push",
157
- "prepublishOnly": "npm run check && npm run build && npm run typecheck && npm test",
158
- "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
159
- "release": "npm run push -- main && npm run release:tag && npm publish",
160
- "publish:dry": "npm publish --dry-run --access public",
161
- "publish:npm": "npm publish --access public"
162
- },
163
- "devDependencies": {
164
- "@arethetypeswrong/cli": "^0.18.2",
165
- "@biomejs/biome": "^2.4.12",
166
- "@classytic/dev-tools": "^0.2.0",
167
- "@types/node": "^22.0.0",
168
- "@vitest/coverage-v8": "^4.1.4",
169
- "fast-check": "^4.7.0",
170
- "knip": "^6.3.0",
171
- "publint": "^0.3.18",
172
- "tsdown": "^0.22.14",
173
- "typescript": "^7.0.2",
174
- "vitest": "^4.1.4"
175
- },
176
- "peerDependencies": {
177
- "vitest": "^3.0.0 || ^4.0.0"
178
- },
179
- "peerDependenciesMeta": {
180
- "vitest": {
181
- "optional": true
182
- }
183
- }
2
+ "name": "@classytic/repo-core",
3
+ "version": "0.23.0",
4
+ "description": "Driver-agnostic repository primitives: hooks, Filter IR, operations, pagination, cache contract. Foundation for mongokit, sqlitekit, pgkit, and prismakit. Lean by design — no plugins ship here; each kit owns its own.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE",
11
+ "CHANGELOG.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=22"
15
+ },
16
+ "exports": {
17
+ "./hooks": {
18
+ "types": "./dist/hooks/index.d.mts",
19
+ "default": "./dist/hooks/index.mjs"
20
+ },
21
+ "./operations": {
22
+ "types": "./dist/operations/index.d.mts",
23
+ "default": "./dist/operations/index.mjs"
24
+ },
25
+ "./errors": {
26
+ "types": "./dist/errors/index.d.mts",
27
+ "default": "./dist/errors/index.mjs"
28
+ },
29
+ "./pagination": {
30
+ "types": "./dist/pagination/index.d.mts",
31
+ "default": "./dist/pagination/index.mjs"
32
+ },
33
+ "./repository": {
34
+ "types": "./dist/repository/index.d.mts",
35
+ "default": "./dist/repository/index.mjs"
36
+ },
37
+ "./filter": {
38
+ "types": "./dist/filter/index.d.mts",
39
+ "default": "./dist/filter/index.mjs"
40
+ },
41
+ "./update": {
42
+ "types": "./dist/update/index.d.mts",
43
+ "default": "./dist/update/index.mjs"
44
+ },
45
+ "./query-parser": {
46
+ "types": "./dist/query-parser/index.d.mts",
47
+ "default": "./dist/query-parser/index.mjs"
48
+ },
49
+ "./context": {
50
+ "types": "./dist/context/index.d.mts",
51
+ "default": "./dist/context/index.mjs"
52
+ },
53
+ "./cache": {
54
+ "types": "./dist/cache/index.d.mts",
55
+ "default": "./dist/cache/index.mjs"
56
+ },
57
+ "./hash": {
58
+ "types": "./dist/hash/index.d.mts",
59
+ "default": "./dist/hash/index.mjs"
60
+ },
61
+ "./events": {
62
+ "types": "./dist/events/index.d.mts",
63
+ "default": "./dist/events/index.mjs"
64
+ },
65
+ "./schema": {
66
+ "types": "./dist/schema/index.d.mts",
67
+ "default": "./dist/schema/index.mjs"
68
+ },
69
+ "./testing": {
70
+ "types": "./dist/testing/index.d.mts",
71
+ "default": "./dist/testing/index.mjs"
72
+ },
73
+ "./tenant": {
74
+ "types": "./dist/tenant/index.d.mts",
75
+ "default": "./dist/tenant/index.mjs"
76
+ },
77
+ "./lookup": {
78
+ "types": "./dist/lookup/index.d.mts",
79
+ "default": "./dist/lookup/index.mjs"
80
+ },
81
+ "./adapter": {
82
+ "types": "./dist/adapter/index.d.mts",
83
+ "default": "./dist/adapter/index.mjs"
84
+ },
85
+ "./better-auth": {
86
+ "types": "./dist/better-auth/index.d.mts",
87
+ "default": "./dist/better-auth/index.mjs"
88
+ },
89
+ "./aggregate": {
90
+ "types": "./dist/aggregate/index.d.mts",
91
+ "default": "./dist/aggregate/index.mjs"
92
+ },
93
+ "./plugins": {
94
+ "types": "./dist/plugins/index.d.mts",
95
+ "default": "./dist/plugins/index.mjs"
96
+ },
97
+ "./lock": {
98
+ "types": "./dist/lock/index.d.mts",
99
+ "default": "./dist/lock/index.mjs"
100
+ },
101
+ "./usage": {
102
+ "types": "./dist/usage/index.d.mts",
103
+ "default": "./dist/usage/index.mjs"
104
+ },
105
+ "./package.json": "./package.json",
106
+ "./sync": {
107
+ "types": "./dist/sync/index.d.mts",
108
+ "default": "./dist/sync/index.mjs"
109
+ },
110
+ "./cleanup": {
111
+ "types": "./dist/cleanup/index.d.mts",
112
+ "default": "./dist/cleanup/index.mjs"
113
+ }
114
+ },
115
+ "keywords": [
116
+ "repository",
117
+ "repository-pattern",
118
+ "data-access",
119
+ "hooks",
120
+ "filter-ir",
121
+ "pagination",
122
+ "cursor-pagination",
123
+ "keyset-pagination",
124
+ "plugin-based",
125
+ "driver-agnostic",
126
+ "typescript",
127
+ "esm"
128
+ ],
129
+ "author": "Classytic <classytic.dev@gmail.com> (https://github.com/classytic)",
130
+ "license": "MIT",
131
+ "repository": {
132
+ "type": "git",
133
+ "url": "git+https://github.com/classytic/repo-core.git"
134
+ },
135
+ "bugs": {
136
+ "url": "https://github.com/classytic/repo-core/issues"
137
+ },
138
+ "homepage": "https://github.com/classytic/repo-core#readme",
139
+ "scripts": {
140
+ "build": "tsdown",
141
+ "dev": "tsdown --watch",
142
+ "test": "vitest run --project unit --project integration",
143
+ "test:unit": "vitest run --project unit",
144
+ "test:integration": "vitest run --project integration",
145
+ "test:e2e": "vitest run --project e2e",
146
+ "test:all": "vitest run",
147
+ "test:watch": "vitest --project unit --project integration",
148
+ "bench": "vitest bench --run --project bench",
149
+ "test:coverage": "vitest run --coverage",
150
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
151
+ "lint": "biome check src tests",
152
+ "lint:fix": "biome check src tests --write",
153
+ "format": "biome format src tests --write",
154
+ "check": "biome ci src tests --diagnostic-level=error",
155
+ "knip": "knip",
156
+ "push": "classytic-push",
157
+ "prepublishOnly": "npm run check && npm run build && npm run typecheck && npm test",
158
+ "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
159
+ "release": "npm run push -- main && npm run release:tag && npm publish",
160
+ "publish:dry": "npm publish --dry-run --access public",
161
+ "publish:npm": "npm publish --access public"
162
+ },
163
+ "devDependencies": {
164
+ "@arethetypeswrong/cli": "^0.18.2",
165
+ "@biomejs/biome": "^2.4.12",
166
+ "@classytic/dev-tools": "^0.2.0",
167
+ "@types/node": "^22.0.0",
168
+ "@vitest/coverage-v8": "^4.1.4",
169
+ "fast-check": "^4.7.0",
170
+ "knip": "^6.3.0",
171
+ "publint": "^0.3.18",
172
+ "tsdown": "^0.22.14",
173
+ "typescript": "^7.0.2",
174
+ "vitest": "^4.1.4"
175
+ },
176
+ "peerDependencies": {
177
+ "vitest": "^3.0.0 || ^4.0.0"
178
+ },
179
+ "peerDependenciesMeta": {
180
+ "vitest": {
181
+ "optional": true
182
+ }
183
+ }
184
184
  }