@classytic/repo-core 0.17.0 → 0.19.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/CHANGELOG.md CHANGED
@@ -4,6 +4,53 @@ All notable changes to `@classytic/repo-core` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.19.0] - 2026-07-29
8
+
9
+ ### Added — `coerceFilterDates` + ISO date helpers (`./filter`)
10
+
11
+ - **`coerceFilterDates(filter)`** — walks a record-shape filter (Mongo-dialect or bare bracket-op syntax) and coerces ISO-8601 strings on range operators (`gt`/`gte`/`lt`/`lte` and `$`-prefixed equivalents) to `Date`, recursing through `$and`/`$or`/`$nor`/`$not` logical wrappers. Returns a new object; the input is never mutated. Equality operators are deliberately excluded — a string that happens to look like a date is far more likely a string id than a date equality predicate. Fixes silent empty results on aggregation `$match` stages: MongoDB `$match` (unlike `find`) performs no schema casting, so a string compared to a Date column matches nothing.
12
+ - **`tryCoerceIsoDate(value)`** — coerces one unknown value to a `Date` when it is an unambiguous ISO-8601 string; returns anything else untouched. Safe to apply unconditionally.
13
+ - **`ISO_DATE_PATTERN`** — the single regex source of truth for tight ISO-8601 detection (date-only through millisecond precision + optional timezone). `query-parser/coerce.ts` now imports this instead of keeping a duplicate pattern — one definition means the URL boundary and the compile boundary can never disagree on what "looks like a date".
14
+
15
+ All three are exported from `@classytic/repo-core/filter`. Purely additive.
16
+
17
+ ## [0.18.0] - 2026-07-27
18
+
19
+ ### Added — `definePurgeStep` builder (`./cleanup`) and `resolveTenantField` (`./tenant`)
20
+
21
+ #### `./cleanup` — `definePurgeStep`
22
+
23
+ - **`definePurgeStep(repository, spec)`** — standard builder for the most common
24
+ `CleanupStep` shape: a chunked `purgeByField` over a single scope value.
25
+ Nearly every provider step in a domain kernel is this pattern; the builder owns
26
+ the invariant core so callers only declare what differs:
27
+ - **fail-closed scoping** — missing scope value or unavailable repository is a
28
+ `BLOCKER` (not a silent no-op and never an unscoped purge that would hit every
29
+ tenant); blocked by `SCOPE_REQUIRED:<param>` / `REPOSITORY_UNAVAILABLE:<id>`.
30
+ - **cancellation** — `throwIfCancelled` is called before work starts and the
31
+ `signal` is threaded to the kit so a cancel lands between committed chunks.
32
+ - **honest failure** — a failing purge returns `ok: false` so the recipe composer
33
+ stops (retention §8); thrown errors are caught and reported, never swallowed.
34
+ - **verification** — absence is re-queried after the run via `countDocuments`
35
+ (`spec.verifyFilter` for steps whose match filter differs from the absence proof).
36
+ - **`PurgeStepSpec`** — declaration object: `id`, `resource`, `parameter`, `field`,
37
+ `strategy`, optional `retained`, `warnings`, `batchSize`, `guard(scope, ctx)`,
38
+ `verifyFilter(scope)`, `verifyName`.
39
+ - **`PurgeStepRepository`** — structural subset of `StandardRepo` satisfied by any
40
+ kit repository that implements `purgeByField` + `countDocuments`.
41
+ - **`SCOPE_REQUIRED`** / **`REPOSITORY_UNAVAILABLE`** — blocker-code prefix constants.
42
+
43
+ #### `./tenant` — `resolveTenantField`
44
+
45
+ - **`resolveTenantField(config?)`** — returns the single `tenantField` string a
46
+ resource layer needs for `defineResource({ tenantField })`, or `false` when the
47
+ config disables tenant scoping (`false` / `{ enabled: false }` / `{ strategy: 'none' }`).
48
+ Four spine modules hand-rolled equivalent logic independently, each re-deriving
49
+ `'organizationId'` as the default; one copy omitted the disable branch, making
50
+ that package silently un-configurable. This function is the single definition.
51
+
52
+ Both additions are purely additive — no breaking changes.
53
+
7
54
  ## [0.17.0] - 2026-07-25
8
55
 
9
56
  ### Added — `./cleanup`: framework-free cleanup provider step contract
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Classytic
3
+ Copyright (c) 2026 Classytic LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -239,3 +239,10 @@ See [docs/data-lifecycle.md](./docs/data-lifecycle.md) for the billion-row runbo
239
239
  ## License
240
240
 
241
241
  MIT — see [LICENSE](./LICENSE).
242
+
243
+
244
+ ## Trademark
245
+
246
+ The code is MIT-licensed. **"Classytic", "arc", and the logos are trademarks of
247
+ Classytic LLC** and are **not** licensed under MIT — see [TRADEMARK.md](TRADEMARK.md).
248
+ Forks must be renamed; the license covers the code, not the brand.
@@ -0,0 +1,76 @@
1
+ import { TenantPurgeOptions, TenantPurgeStrategy } from "../repository/types.mjs";
2
+ import { CleanupStep, CleanupStepContext } from "./types.mjs";
3
+ //#region src/cleanup/define-purge-step.d.ts
4
+ /**
5
+ * The repository surface a purge step needs — a structural subset of
6
+ * `StandardRepo`, so any kit repository satisfies it without an adapter.
7
+ * Both members are optional so an engine that never wired the repository
8
+ * yields a BLOCKER rather than a crash.
9
+ */
10
+ interface PurgeStepRepository {
11
+ purgeByField?(field: string, value: unknown, strategy: TenantPurgeStrategy, options?: TenantPurgeOptions): Promise<{
12
+ processed: number;
13
+ ok: boolean;
14
+ error?: {
15
+ message: string;
16
+ };
17
+ }>;
18
+ countDocuments?(filter: Record<string, unknown>): Promise<number>;
19
+ }
20
+ interface PurgeStepSpec {
21
+ /** Stable machine id, unique within a recipe. */
22
+ id: string;
23
+ /** Business record class for the preview (e.g. `'CRM contacts'`). */
24
+ resource: string;
25
+ /**
26
+ * Run-parameter key carrying the scope value (e.g. `'subjectId'`,
27
+ * `'organizationId'`). Read from `ctx.parameters` at plan AND execute time,
28
+ * so a retry replays the value the operator actually confirmed.
29
+ */
30
+ parameter: string;
31
+ /** Document field matched against the parameter's value. */
32
+ field: string;
33
+ /** What to do with matched rows. */
34
+ strategy: TenantPurgeStrategy;
35
+ /** What survives — surfaced in the preview. */
36
+ retained?: string;
37
+ /** Non-blocking operator warnings. */
38
+ warnings?: readonly string[];
39
+ /** Rows per chunk. Kit default when omitted. */
40
+ batchSize?: number;
41
+ /**
42
+ * Domain blockers beyond the built-in scope/availability checks (e.g.
43
+ * `'OPEN_CHECKOUTS:3'`). A non-empty result is a HARD STOP.
44
+ */
45
+ guard?: (scope: string, ctx: CleanupStepContext) => Promise<readonly string[]>;
46
+ /**
47
+ * Filter proving absence, when it differs from the match filter — e.g. an
48
+ * anonymize keyed on `_id` leaves the row in place, so absence must be
49
+ * proven by querying the redacted identifier instead.
50
+ */
51
+ verifyFilter?: (scope: string) => Record<string, unknown>;
52
+ /** Overrides the generated check name. */
53
+ verifyName?: string;
54
+ }
55
+ /** Prefix for the blocker raised when the run carries no scope value. */
56
+ declare const SCOPE_REQUIRED = "CLEANUP_SCOPE_REQUIRED";
57
+ /** Prefix for the blocker raised when the repository cannot be reached. */
58
+ declare const REPOSITORY_UNAVAILABLE = "CLEANUP_REPOSITORY_UNAVAILABLE";
59
+ /**
60
+ * Build a `CleanupStep` from a repository + declaration.
61
+ *
62
+ * Invariants this owns, so no caller can get them wrong:
63
+ * - **fail-closed scoping** — no scope value, or no repository, is a
64
+ * BLOCKER, never a silent no-op and never an unscoped purge (which would
65
+ * hit every tenant);
66
+ * - **cancellation** — checked before work starts and threaded to the kit so
67
+ * a cancel lands between committed chunks, never mid-write;
68
+ * - **honest failure** — a failing purge returns `ok: false` so the composer
69
+ * stops the recipe (retention §8), and a thrown error is reported, never
70
+ * swallowed;
71
+ * - **verification** — absence is re-queried after the run, because a
72
+ * processed count alone is never proof (§9).
73
+ */
74
+ declare function definePurgeStep(repository: PurgeStepRepository | undefined, spec: PurgeStepSpec): CleanupStep;
75
+ //#endregion
76
+ export { PurgeStepRepository, PurgeStepSpec, REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep };
@@ -0,0 +1,107 @@
1
+ //#region src/cleanup/define-purge-step.ts
2
+ /** Prefix for the blocker raised when the run carries no scope value. */
3
+ const SCOPE_REQUIRED = "CLEANUP_SCOPE_REQUIRED";
4
+ /** Prefix for the blocker raised when the repository cannot be reached. */
5
+ const REPOSITORY_UNAVAILABLE = "CLEANUP_REPOSITORY_UNAVAILABLE";
6
+ function scopeOf(ctx, parameter) {
7
+ const raw = ctx.parameters?.[parameter];
8
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
9
+ }
10
+ /**
11
+ * Build a `CleanupStep` from a repository + declaration.
12
+ *
13
+ * Invariants this owns, so no caller can get them wrong:
14
+ * - **fail-closed scoping** — no scope value, or no repository, is a
15
+ * BLOCKER, never a silent no-op and never an unscoped purge (which would
16
+ * hit every tenant);
17
+ * - **cancellation** — checked before work starts and threaded to the kit so
18
+ * a cancel lands between committed chunks, never mid-write;
19
+ * - **honest failure** — a failing purge returns `ok: false` so the composer
20
+ * stops the recipe (retention §8), and a thrown error is reported, never
21
+ * swallowed;
22
+ * - **verification** — absence is re-queried after the run, because a
23
+ * processed count alone is never proof (§9).
24
+ */
25
+ function definePurgeStep(repository, spec) {
26
+ const destructive = spec.strategy.type !== "skip";
27
+ const verifyName = spec.verifyName ?? `${spec.id}.verified`;
28
+ const filterFor = (scope) => spec.verifyFilter?.(scope) ?? { [spec.field]: scope };
29
+ return {
30
+ id: spec.id,
31
+ resource: spec.resource,
32
+ destructive,
33
+ async estimate(ctx) {
34
+ const scope = scopeOf(ctx, spec.parameter);
35
+ if (!scope) return {
36
+ resource: spec.resource,
37
+ estimated: 0,
38
+ blockers: [`${SCOPE_REQUIRED}:${spec.parameter}`]
39
+ };
40
+ if (!repository?.purgeByField) return {
41
+ resource: spec.resource,
42
+ estimated: 0,
43
+ blockers: [`${REPOSITORY_UNAVAILABLE}:${spec.id}`]
44
+ };
45
+ const estimated = await repository.countDocuments?.({ [spec.field]: scope }) ?? 0;
46
+ const blockers = await spec.guard?.(scope, ctx) ?? [];
47
+ return {
48
+ resource: spec.resource,
49
+ estimated,
50
+ ...spec.retained === void 0 ? {} : { retained: spec.retained },
51
+ ...blockers.length > 0 ? { blockers: [...blockers] } : {},
52
+ ...spec.warnings === void 0 ? {} : { warnings: [...spec.warnings] }
53
+ };
54
+ },
55
+ async execute(ctx) {
56
+ await ctx.throwIfCancelled?.();
57
+ const scope = scopeOf(ctx, spec.parameter);
58
+ if (!scope || !repository?.purgeByField) return {
59
+ resource: spec.resource,
60
+ processed: 0,
61
+ ok: false,
62
+ error: scope ? `repository for '${spec.id}' is unavailable` : `missing run parameter '${spec.parameter}'`
63
+ };
64
+ try {
65
+ const result = await repository.purgeByField(spec.field, scope, spec.strategy, {
66
+ ...spec.batchSize === void 0 ? {} : { batchSize: spec.batchSize },
67
+ ...ctx.signal ? { signal: ctx.signal } : {},
68
+ onProgress: async (event) => {
69
+ await ctx.onProgress?.({
70
+ resource: spec.resource,
71
+ processed: event.processed
72
+ });
73
+ }
74
+ });
75
+ return {
76
+ resource: spec.resource,
77
+ processed: result.processed,
78
+ ok: result.ok,
79
+ ...result.ok ? {} : { error: result.error?.message ?? "purge reported failure" }
80
+ };
81
+ } catch (error) {
82
+ return {
83
+ resource: spec.resource,
84
+ processed: 0,
85
+ ok: false,
86
+ error: error instanceof Error ? error.message : String(error)
87
+ };
88
+ }
89
+ },
90
+ async verify(ctx) {
91
+ const scope = scopeOf(ctx, spec.parameter);
92
+ if (!scope || !repository?.countDocuments) return [{
93
+ name: verifyName,
94
+ ok: false,
95
+ detail: "could not verify — no scope or no count support"
96
+ }];
97
+ const remaining = await repository.countDocuments(filterFor(scope));
98
+ return [{
99
+ name: verifyName,
100
+ ok: remaining === 0,
101
+ detail: remaining === 0 ? `no ${spec.resource} still match the scope` : `${remaining} row(s) still match — cleanup incomplete`
102
+ }];
103
+ }
104
+ };
105
+ }
106
+ //#endregion
107
+ export { REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep };
@@ -1,2 +1,3 @@
1
1
  import { CleanupStep, CleanupStepCheck, CleanupStepContext, CleanupStepEstimate, CleanupStepExecuteContext, CleanupStepLogger, CleanupStepOutcome, CleanupStepProgress } from "./types.mjs";
2
- export type { CleanupStep, CleanupStepCheck, CleanupStepContext, CleanupStepEstimate, CleanupStepExecuteContext, CleanupStepLogger, CleanupStepOutcome, CleanupStepProgress };
2
+ import { PurgeStepRepository, PurgeStepSpec, REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep } from "./define-purge-step.mjs";
3
+ export { type CleanupStep, type CleanupStepCheck, type CleanupStepContext, type CleanupStepEstimate, type CleanupStepExecuteContext, type CleanupStepLogger, type CleanupStepOutcome, type CleanupStepProgress, type PurgeStepRepository, type PurgeStepSpec, REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep };
@@ -0,0 +1,2 @@
1
+ import { REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep } from "./define-purge-step.mjs";
2
+ export { REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep };
@@ -0,0 +1,65 @@
1
+ //#region src/filter/coerce-dates.d.ts
2
+ /**
3
+ * ISO-date coercion for record-shape filters — the canonical helper every
4
+ * kit shares.
5
+ *
6
+ * WHY THIS EXISTS. A URL carries strings (`?createdAt[gte]=2026-04-01`), but
7
+ * the stored column is a real date. On a `find`-family call Mongoose casts
8
+ * the query against the schema, so a string silently becomes a `Date` and
9
+ * everything works. **An aggregation `$match` stage gets no such casting** —
10
+ * and BSON type ordering makes `Date` (type 9) and `String` (type 2)
11
+ * non-comparable, so `{ createdAt: { $gte: '2026-04-01' } }` matches
12
+ * NOTHING against a Date field. Silent empty result, no error. The same
13
+ * hazard exists for any kit comparing a typed column to a string literal.
14
+ *
15
+ * Coercion therefore has to happen in the shared layer, before a kit emits
16
+ * its native predicate — which is what this module is for. It is
17
+ * DIALECT-PRESERVING (record in, record out): it never converts to the
18
+ * Filter IR, so a caller that hands over Mongo-dialect syntax gets
19
+ * Mongo-dialect syntax back, just with dates typed correctly. That keeps it
20
+ * usable from `compileFilterToMongo`'s already-built-query passthrough
21
+ * branch, where converting to IR would lose operators the IR doesn't model.
22
+ *
23
+ * Two shapes are handled, because both reach the compile boundary:
24
+ * - **bare shorthand** — `{ gte: '…' }`, what Fastify parses out of arc's
25
+ * bracket-syntax URL params, and
26
+ * - **`$`-prefixed** — `{ $gte: '…' }`, already-built Mongo/policy syntax.
27
+ *
28
+ * Logical wrappers (`$and` / `$or` / `$nor` / `$not`) are recursed — the
29
+ * same operator set {@link policyRecordToFilter} walks, kept deliberately in
30
+ * sync. Without recursion a date range nested under `$and` (exactly what a
31
+ * tenant/policy-scope merge produces when it conjoins a policy filter with a
32
+ * caller filter) is never coerced.
33
+ */
34
+ /**
35
+ * Tight ISO-8601 pattern — date-only through millisecond precision with an
36
+ * optional timezone. Anchored at BOTH ends on purpose: a loose prefix-only
37
+ * match (`/^\d{4}-\d{2}-\d{2}/`) also swallows strings that merely START
38
+ * with something date-shaped (order numbers, slugs, serials), silently
39
+ * rewriting a legitimate string predicate into a Date one.
40
+ *
41
+ * THE single source of truth — `query-parser/coerce.ts` imports it rather
42
+ * than keeping a second copy.
43
+ */
44
+ declare const ISO_DATE_PATTERN: RegExp;
45
+ /**
46
+ * Coerce one value to a `Date` when it is an unambiguous ISO-8601 string.
47
+ * Anything else — including an unparseable date-shaped string — is returned
48
+ * untouched, so this is always safe to apply.
49
+ */
50
+ declare function tryCoerceIsoDate(value: unknown): unknown;
51
+ /**
52
+ * Walk a record-shape filter and coerce ISO-date strings on range operators
53
+ * to `Date`, recursing through logical wrappers. Returns a new object;
54
+ * the input is never mutated. Non-range operators, real nested documents,
55
+ * and already-typed values pass through unchanged.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * coerceFilterDates({ $and: [{ createdAt: { $gte: '2026-04-01' } }] })
60
+ * // → { $and: [{ createdAt: { $gte: Date(2026-04-01) } }] }
61
+ * ```
62
+ */
63
+ declare function coerceFilterDates(filter: Record<string, unknown>): Record<string, unknown>;
64
+ //#endregion
65
+ export { ISO_DATE_PATTERN, coerceFilterDates, tryCoerceIsoDate };
@@ -0,0 +1,125 @@
1
+ //#region src/filter/coerce-dates.ts
2
+ /**
3
+ * ISO-date coercion for record-shape filters — the canonical helper every
4
+ * kit shares.
5
+ *
6
+ * WHY THIS EXISTS. A URL carries strings (`?createdAt[gte]=2026-04-01`), but
7
+ * the stored column is a real date. On a `find`-family call Mongoose casts
8
+ * the query against the schema, so a string silently becomes a `Date` and
9
+ * everything works. **An aggregation `$match` stage gets no such casting** —
10
+ * and BSON type ordering makes `Date` (type 9) and `String` (type 2)
11
+ * non-comparable, so `{ createdAt: { $gte: '2026-04-01' } }` matches
12
+ * NOTHING against a Date field. Silent empty result, no error. The same
13
+ * hazard exists for any kit comparing a typed column to a string literal.
14
+ *
15
+ * Coercion therefore has to happen in the shared layer, before a kit emits
16
+ * its native predicate — which is what this module is for. It is
17
+ * DIALECT-PRESERVING (record in, record out): it never converts to the
18
+ * Filter IR, so a caller that hands over Mongo-dialect syntax gets
19
+ * Mongo-dialect syntax back, just with dates typed correctly. That keeps it
20
+ * usable from `compileFilterToMongo`'s already-built-query passthrough
21
+ * branch, where converting to IR would lose operators the IR doesn't model.
22
+ *
23
+ * Two shapes are handled, because both reach the compile boundary:
24
+ * - **bare shorthand** — `{ gte: '…' }`, what Fastify parses out of arc's
25
+ * bracket-syntax URL params, and
26
+ * - **`$`-prefixed** — `{ $gte: '…' }`, already-built Mongo/policy syntax.
27
+ *
28
+ * Logical wrappers (`$and` / `$or` / `$nor` / `$not`) are recursed — the
29
+ * same operator set {@link policyRecordToFilter} walks, kept deliberately in
30
+ * sync. Without recursion a date range nested under `$and` (exactly what a
31
+ * tenant/policy-scope merge produces when it conjoins a policy filter with a
32
+ * caller filter) is never coerced.
33
+ */
34
+ /**
35
+ * Tight ISO-8601 pattern — date-only through millisecond precision with an
36
+ * optional timezone. Anchored at BOTH ends on purpose: a loose prefix-only
37
+ * match (`/^\d{4}-\d{2}-\d{2}/`) also swallows strings that merely START
38
+ * with something date-shaped (order numbers, slugs, serials), silently
39
+ * rewriting a legitimate string predicate into a Date one.
40
+ *
41
+ * THE single source of truth — `query-parser/coerce.ts` imports it rather
42
+ * than keeping a second copy.
43
+ */
44
+ const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
45
+ /**
46
+ * Coerce one value to a `Date` when it is an unambiguous ISO-8601 string.
47
+ * Anything else — including an unparseable date-shaped string — is returned
48
+ * untouched, so this is always safe to apply.
49
+ */
50
+ function tryCoerceIsoDate(value) {
51
+ if (typeof value !== "string" || !ISO_DATE_PATTERN.test(value)) return value;
52
+ const parsed = new Date(value);
53
+ return Number.isNaN(parsed.getTime()) ? value : parsed;
54
+ }
55
+ /**
56
+ * Range operators whose operand is compared by BSON/SQL type and therefore
57
+ * must be a real date, not a string. Equality is deliberately EXCLUDED:
58
+ * `{ status: '2026-01-01' }` is far more likely a string id than a date, and
59
+ * an `eq` against a Date column is the one case a caller can express exactly
60
+ * by passing a `Date`. Both bare and `$`-prefixed spellings are listed.
61
+ */
62
+ const RANGE_OPS = /* @__PURE__ */ new Set([
63
+ "gt",
64
+ "gte",
65
+ "lt",
66
+ "lte",
67
+ "$gt",
68
+ "$gte",
69
+ "$lt",
70
+ "$lte"
71
+ ]);
72
+ /** Logical operators whose operand is an ARRAY of sub-filters. */
73
+ const LOGICAL_ARRAY_OPS = /* @__PURE__ */ new Set([
74
+ "$and",
75
+ "$or",
76
+ "$nor",
77
+ "and",
78
+ "or",
79
+ "nor"
80
+ ]);
81
+ /** Logical operators whose operand is a SINGLE nested sub-filter. */
82
+ const LOGICAL_OBJECT_OPS = /* @__PURE__ */ new Set(["$not", "not"]);
83
+ function isPlainObject(value) {
84
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
85
+ }
86
+ /**
87
+ * Walk a record-shape filter and coerce ISO-date strings on range operators
88
+ * to `Date`, recursing through logical wrappers. Returns a new object;
89
+ * the input is never mutated. Non-range operators, real nested documents,
90
+ * and already-typed values pass through unchanged.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * coerceFilterDates({ $and: [{ createdAt: { $gte: '2026-04-01' } }] })
95
+ * // → { $and: [{ createdAt: { $gte: Date(2026-04-01) } }] }
96
+ * ```
97
+ */
98
+ function coerceFilterDates(filter) {
99
+ const out = {};
100
+ for (const [key, value] of Object.entries(filter)) {
101
+ if (LOGICAL_ARRAY_OPS.has(key) && Array.isArray(value)) {
102
+ out[key] = value.map((entry) => isPlainObject(entry) ? coerceFilterDates(entry) : entry);
103
+ continue;
104
+ }
105
+ if (LOGICAL_OBJECT_OPS.has(key) && isPlainObject(value)) {
106
+ out[key] = coerceFilterDates(value);
107
+ continue;
108
+ }
109
+ if (isPlainObject(value)) {
110
+ let changed = false;
111
+ const coerced = {};
112
+ for (const [op, operand] of Object.entries(value)) if (RANGE_OPS.has(op)) {
113
+ const next = tryCoerceIsoDate(operand);
114
+ coerced[op] = next;
115
+ if (next !== operand) changed = true;
116
+ } else coerced[op] = operand;
117
+ out[key] = changed ? coerced : value;
118
+ continue;
119
+ }
120
+ out[key] = value;
121
+ }
122
+ return out;
123
+ }
124
+ //#endregion
125
+ export { ISO_DATE_PATTERN, coerceFilterDates, tryCoerceIsoDate };
@@ -1,9 +1,10 @@
1
1
  import { Filter, FilterAnd, FilterEq, FilterExists, FilterFalse, FilterGt, FilterGte, FilterIn, FilterLike, FilterLt, FilterLte, FilterNe, FilterNin, FilterNot, FilterOp, FilterOr, FilterRaw, FilterRegex, FilterTrue } from "./types.mjs";
2
2
  import { FALSE, TRUE, and, anyOf as in_, between, contains, endsWith, eq, exists, gt, gte, iEq, invert as not, isNotNull, isNull, like, lt, lte, ne, nin, or, raw, regex, startsWith } from "./builders.mjs";
3
+ import { ISO_DATE_PATTERN, coerceFilterDates, tryCoerceIsoDate } from "./coerce-dates.mjs";
3
4
  import { recordToFilter } from "./from-record.mjs";
4
5
  import { isFilter } from "./guard.mjs";
5
6
  import { asPredicate, matchFilter } from "./match.mjs";
6
7
  import { matchesRecordFilter, policyRecordToFilter } from "./match-record.mjs";
7
8
  import { SCOPE_ANY, buildTenantScope, mergeScope } from "./scope.mjs";
8
9
  import { collectFields, mapFilter, walkFilter } from "./walk.mjs";
9
- export { FALSE, type Filter, type FilterAnd, type FilterEq, type FilterExists, type FilterFalse, type FilterGt, type FilterGte, type FilterIn, type FilterLike, type FilterLt, type FilterLte, type FilterNe, type FilterNin, type FilterNot, type FilterOp, type FilterOr, type FilterRaw, type FilterRegex, type FilterTrue, SCOPE_ANY, TRUE, and, in_ as anyOf, asPredicate, between, buildTenantScope, collectFields, contains, endsWith, eq, exists, gt, gte, iEq, in_, not as invert, isFilter, isNotNull, isNull, like, lt, lte, mapFilter, matchFilter, matchesRecordFilter, mergeScope, ne, nin, nin as noneOf, not, or, policyRecordToFilter, raw, recordToFilter, regex, startsWith, walkFilter };
10
+ export { FALSE, type Filter, type FilterAnd, type FilterEq, type FilterExists, type FilterFalse, type FilterGt, type FilterGte, type FilterIn, type FilterLike, type FilterLt, type FilterLte, type FilterNe, type FilterNin, type FilterNot, type FilterOp, type FilterOr, type FilterRaw, type FilterRegex, type FilterTrue, ISO_DATE_PATTERN, SCOPE_ANY, TRUE, and, in_ as anyOf, asPredicate, between, buildTenantScope, coerceFilterDates, collectFields, contains, endsWith, eq, exists, gt, gte, iEq, in_, not as invert, isFilter, isNotNull, isNull, like, lt, lte, mapFilter, matchFilter, matchesRecordFilter, mergeScope, ne, nin, nin as noneOf, not, or, policyRecordToFilter, raw, recordToFilter, regex, startsWith, tryCoerceIsoDate, walkFilter };
@@ -1,8 +1,9 @@
1
1
  import { isFilter } from "./guard.mjs";
2
2
  import { collectFields, mapFilter, walkFilter } from "./walk.mjs";
3
3
  import { FALSE, TRUE, and, anyOf as in_, between, contains, endsWith, eq, exists, gt, gte, iEq, invert as not, isNotNull, isNull, like, lt, lte, ne, nin, or, raw, regex, startsWith } from "./builders.mjs";
4
+ import { ISO_DATE_PATTERN, coerceFilterDates, tryCoerceIsoDate } from "./coerce-dates.mjs";
4
5
  import { recordToFilter } from "./from-record.mjs";
5
6
  import { asPredicate, matchFilter } from "./match.mjs";
6
7
  import { matchesRecordFilter, policyRecordToFilter } from "./match-record.mjs";
7
8
  import { SCOPE_ANY, buildTenantScope, mergeScope } from "./scope.mjs";
8
- export { FALSE, SCOPE_ANY, TRUE, and, in_ as anyOf, asPredicate, between, buildTenantScope, collectFields, contains, endsWith, eq, exists, gt, gte, iEq, in_, not as invert, isFilter, isNotNull, isNull, like, lt, lte, mapFilter, matchFilter, matchesRecordFilter, mergeScope, ne, nin, nin as noneOf, not, or, policyRecordToFilter, raw, recordToFilter, regex, startsWith, walkFilter };
9
+ export { FALSE, ISO_DATE_PATTERN, SCOPE_ANY, TRUE, and, in_ as anyOf, asPredicate, between, buildTenantScope, coerceFilterDates, collectFields, contains, endsWith, eq, exists, gt, gte, iEq, in_, not as invert, isFilter, isNotNull, isNull, like, lt, lte, mapFilter, matchFilter, matchesRecordFilter, mergeScope, ne, nin, nin as noneOf, not, or, policyRecordToFilter, raw, recordToFilter, regex, startsWith, tryCoerceIsoDate, walkFilter };
@@ -1,4 +1,11 @@
1
+ import { ISO_DATE_PATTERN } from "../filter/coerce-dates.mjs";
1
2
  //#region src/query-parser/coerce.ts
3
+ /**
4
+ * Scalar coercion. URLs are strings; filters compare against typed fields.
5
+ * The parser uses `fieldTypes` hints when provided, otherwise a safe
6
+ * heuristic that avoids the classic footguns (string SKUs becoming
7
+ * numbers, numeric-looking strings becoming Dates).
8
+ */
2
9
  const BOOLEAN_STRINGS = /* @__PURE__ */ new Set([
3
10
  "true",
4
11
  "1",
@@ -11,7 +18,7 @@ const FALSEY_STRINGS = /* @__PURE__ */ new Set([
11
18
  "no",
12
19
  "off"
13
20
  ]);
14
- const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
21
+ const ISO_DATE_RE = ISO_DATE_PATTERN;
15
22
  /**
16
23
  * Coerce a single URL value to its field-declared type, or to a best-guess
17
24
  * scalar when no hint exists. Always returns `string`, `number`, `boolean`,
@@ -1,3 +1,3 @@
1
1
  import { ResolvedTenantConfig, TenantConfig, TenantFieldType, TenantStrategy } from "./types.mjs";
2
- import { DEFAULT_TENANT_CONFIG, resolveTenantConfig } from "./resolve.mjs";
3
- export { DEFAULT_TENANT_CONFIG, type ResolvedTenantConfig, type TenantConfig, type TenantFieldType, type TenantStrategy, resolveTenantConfig };
2
+ import { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField } from "./resolve.mjs";
3
+ export { DEFAULT_TENANT_CONFIG, type ResolvedTenantConfig, type TenantConfig, type TenantFieldType, type TenantStrategy, resolveTenantConfig, resolveTenantField };
@@ -1,2 +1,2 @@
1
- import { DEFAULT_TENANT_CONFIG, resolveTenantConfig } from "./resolve.mjs";
2
- export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };
1
+ import { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField } from "./resolve.mjs";
2
+ export { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField };
@@ -12,5 +12,21 @@ import { ResolvedTenantConfig, TenantConfig } from "./types.mjs";
12
12
  type TenantDefaults = { [K in 'strategy' | 'enabled' | 'tenantField' | 'fieldType' | 'ref' | 'contextKey' | 'required']-?: Exclude<TenantConfig[K], undefined>; };
13
13
  declare const DEFAULT_TENANT_CONFIG: TenantDefaults;
14
14
  declare function resolveTenantConfig(config?: TenantConfig | boolean): ResolvedTenantConfig;
15
+ /**
16
+ * The single `tenantField` value a resource layer wants, or `false` when the
17
+ * option disables scoping entirely.
18
+ *
19
+ * Packages composing an arc resource need exactly this shape for
20
+ * `defineResource({ tenantField })`, and four spine modules independently
21
+ * hand-rolled it — each re-deriving `'organizationId'` as the default and
22
+ * unwrapping `{ tenantField }` by hand. One of those copies omitted the
23
+ * disable branch, so that package silently could not be configured
24
+ * company-wide. This wraps {@link resolveTenantConfig} so the default, the
25
+ * disable semantics, and the object-unwrapping have ONE definition.
26
+ *
27
+ * `false` / `{ enabled: false }` / `{ strategy: 'none' }` all mean "no tenant
28
+ * scoping" and all return `false` — callers get one thing to branch on.
29
+ */
30
+ declare function resolveTenantField(config?: TenantConfig | boolean): string | false;
15
31
  //#endregion
16
- export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };
32
+ export { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField };
@@ -51,5 +51,25 @@ function resolveTenantConfig(config) {
51
51
  enabled: cleaned.enabled ?? true
52
52
  };
53
53
  }
54
+ /**
55
+ * The single `tenantField` value a resource layer wants, or `false` when the
56
+ * option disables scoping entirely.
57
+ *
58
+ * Packages composing an arc resource need exactly this shape for
59
+ * `defineResource({ tenantField })`, and four spine modules independently
60
+ * hand-rolled it — each re-deriving `'organizationId'` as the default and
61
+ * unwrapping `{ tenantField }` by hand. One of those copies omitted the
62
+ * disable branch, so that package silently could not be configured
63
+ * company-wide. This wraps {@link resolveTenantConfig} so the default, the
64
+ * disable semantics, and the object-unwrapping have ONE definition.
65
+ *
66
+ * `false` / `{ enabled: false }` / `{ strategy: 'none' }` all mean "no tenant
67
+ * scoping" and all return `false` — callers get one thing to branch on.
68
+ */
69
+ function resolveTenantField(config) {
70
+ const resolved = resolveTenantConfig(config);
71
+ if (!resolved.enabled || resolved.strategy === "none") return false;
72
+ return resolved.tenantField;
73
+ }
54
74
  //#endregion
55
- export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };
75
+ export { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField };
@@ -2,6 +2,6 @@ import { AggregateOpsSupport } from "../repository/capabilities.mjs";
2
2
  import { ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness } from "./types.mjs";
3
3
  import { runStandardRepoConformance } from "./conformance.mjs";
4
4
  import { LockConformanceHarness, runLockAdapterConformance } from "./lock-conformance.mjs";
5
- import { UsageConformanceHarness, runUsageStoreContract } from "./usage-conformance.mjs";
6
5
  import { PurgeConformanceContext, PurgeConformanceHarness, runPurgeConformance } from "./purge-conformance.mjs";
6
+ import { UsageConformanceHarness, runUsageStoreContract } from "./usage-conformance.mjs";
7
7
  export { type AggregateOpsSupport, type ConformanceContext, type ConformanceDoc, type ConformanceFeatures, type ConformanceHarness, type LockConformanceHarness, type PurgeConformanceContext, type PurgeConformanceHarness, type UsageConformanceHarness, runLockAdapterConformance, runPurgeConformance, runStandardRepoConformance, runUsageStoreContract };
@@ -1,5 +1,5 @@
1
1
  import { runStandardRepoConformance } from "./conformance.mjs";
2
2
  import { runLockAdapterConformance } from "./lock-conformance.mjs";
3
- import { runUsageStoreContract } from "./usage-conformance.mjs";
4
3
  import { runPurgeConformance } from "./purge-conformance.mjs";
4
+ import { runUsageStoreContract } from "./usage-conformance.mjs";
5
5
  export { runLockAdapterConformance, runPurgeConformance, runStandardRepoConformance, runUsageStoreContract };
package/package.json CHANGED
@@ -1,180 +1,180 @@
1
1
  {
2
- "name": "@classytic/repo-core",
3
- "version": "0.17.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
- "./events": {
58
- "types": "./dist/events/index.d.mts",
59
- "default": "./dist/events/index.mjs"
60
- },
61
- "./schema": {
62
- "types": "./dist/schema/index.d.mts",
63
- "default": "./dist/schema/index.mjs"
64
- },
65
- "./testing": {
66
- "types": "./dist/testing/index.d.mts",
67
- "default": "./dist/testing/index.mjs"
68
- },
69
- "./tenant": {
70
- "types": "./dist/tenant/index.d.mts",
71
- "default": "./dist/tenant/index.mjs"
72
- },
73
- "./lookup": {
74
- "types": "./dist/lookup/index.d.mts",
75
- "default": "./dist/lookup/index.mjs"
76
- },
77
- "./adapter": {
78
- "types": "./dist/adapter/index.d.mts",
79
- "default": "./dist/adapter/index.mjs"
80
- },
81
- "./better-auth": {
82
- "types": "./dist/better-auth/index.d.mts",
83
- "default": "./dist/better-auth/index.mjs"
84
- },
85
- "./aggregate": {
86
- "types": "./dist/aggregate/index.d.mts",
87
- "default": "./dist/aggregate/index.mjs"
88
- },
89
- "./plugins": {
90
- "types": "./dist/plugins/index.d.mts",
91
- "default": "./dist/plugins/index.mjs"
92
- },
93
- "./lock": {
94
- "types": "./dist/lock/index.d.mts",
95
- "default": "./dist/lock/index.mjs"
96
- },
97
- "./usage": {
98
- "types": "./dist/usage/index.d.mts",
99
- "default": "./dist/usage/index.mjs"
100
- },
101
- "./package.json": "./package.json",
102
- "./sync": {
103
- "types": "./dist/sync/index.d.mts",
104
- "default": "./dist/sync/index.mjs"
105
- },
106
- "./cleanup": {
107
- "types": "./dist/cleanup/index.d.mts",
108
- "default": "./dist/cleanup/index.mjs"
109
- }
110
- },
111
- "keywords": [
112
- "repository",
113
- "repository-pattern",
114
- "data-access",
115
- "hooks",
116
- "filter-ir",
117
- "pagination",
118
- "cursor-pagination",
119
- "keyset-pagination",
120
- "plugin-based",
121
- "driver-agnostic",
122
- "typescript",
123
- "esm"
124
- ],
125
- "author": "Classytic <classytic.dev@gmail.com> (https://github.com/classytic)",
126
- "license": "MIT",
127
- "repository": {
128
- "type": "git",
129
- "url": "git+https://github.com/classytic/repo-core.git"
130
- },
131
- "bugs": {
132
- "url": "https://github.com/classytic/repo-core/issues"
133
- },
134
- "homepage": "https://github.com/classytic/repo-core#readme",
135
- "scripts": {
136
- "build": "tsdown",
137
- "dev": "tsdown --watch",
138
- "test": "vitest run --project unit --project integration",
139
- "test:unit": "vitest run --project unit",
140
- "test:integration": "vitest run --project integration",
141
- "test:e2e": "vitest run --project e2e",
142
- "test:all": "vitest run",
143
- "test:watch": "vitest --project unit --project integration",
144
- "bench": "vitest bench --run --project bench",
145
- "test:coverage": "vitest run --coverage",
146
- "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
147
- "lint": "biome check src tests",
148
- "lint:fix": "biome check src tests --write",
149
- "format": "biome format src tests --write",
150
- "check": "biome ci src tests --diagnostic-level=error",
151
- "knip": "knip",
152
- "push": "classytic-push",
153
- "prepublishOnly": "npm run check && npm run build && npm run typecheck && npm test",
154
- "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
155
- "release": "npm run push -- main && npm run release:tag && npm publish",
156
- "publish:dry": "npm publish --dry-run --access public",
157
- "publish:npm": "npm publish --access public"
158
- },
159
- "devDependencies": {
160
- "@arethetypeswrong/cli": "^0.18.2",
161
- "@biomejs/biome": "^2.4.12",
162
- "@classytic/dev-tools": "^0.2.0",
163
- "@types/node": "^22.0.0",
164
- "@vitest/coverage-v8": "^4.1.4",
165
- "fast-check": "^4.7.0",
166
- "knip": "^6.3.0",
167
- "publint": "^0.3.18",
168
- "tsdown": "^0.22.5",
169
- "typescript": "^7.0.2",
170
- "vitest": "^4.1.4"
171
- },
172
- "peerDependencies": {
173
- "vitest": "^3.0.0 || ^4.0.0"
174
- },
175
- "peerDependenciesMeta": {
176
- "vitest": {
177
- "optional": true
178
- }
179
- }
2
+ "name": "@classytic/repo-core",
3
+ "version": "0.19.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
+ "./events": {
58
+ "types": "./dist/events/index.d.mts",
59
+ "default": "./dist/events/index.mjs"
60
+ },
61
+ "./schema": {
62
+ "types": "./dist/schema/index.d.mts",
63
+ "default": "./dist/schema/index.mjs"
64
+ },
65
+ "./testing": {
66
+ "types": "./dist/testing/index.d.mts",
67
+ "default": "./dist/testing/index.mjs"
68
+ },
69
+ "./tenant": {
70
+ "types": "./dist/tenant/index.d.mts",
71
+ "default": "./dist/tenant/index.mjs"
72
+ },
73
+ "./lookup": {
74
+ "types": "./dist/lookup/index.d.mts",
75
+ "default": "./dist/lookup/index.mjs"
76
+ },
77
+ "./adapter": {
78
+ "types": "./dist/adapter/index.d.mts",
79
+ "default": "./dist/adapter/index.mjs"
80
+ },
81
+ "./better-auth": {
82
+ "types": "./dist/better-auth/index.d.mts",
83
+ "default": "./dist/better-auth/index.mjs"
84
+ },
85
+ "./aggregate": {
86
+ "types": "./dist/aggregate/index.d.mts",
87
+ "default": "./dist/aggregate/index.mjs"
88
+ },
89
+ "./plugins": {
90
+ "types": "./dist/plugins/index.d.mts",
91
+ "default": "./dist/plugins/index.mjs"
92
+ },
93
+ "./lock": {
94
+ "types": "./dist/lock/index.d.mts",
95
+ "default": "./dist/lock/index.mjs"
96
+ },
97
+ "./usage": {
98
+ "types": "./dist/usage/index.d.mts",
99
+ "default": "./dist/usage/index.mjs"
100
+ },
101
+ "./package.json": "./package.json",
102
+ "./sync": {
103
+ "types": "./dist/sync/index.d.mts",
104
+ "default": "./dist/sync/index.mjs"
105
+ },
106
+ "./cleanup": {
107
+ "types": "./dist/cleanup/index.d.mts",
108
+ "default": "./dist/cleanup/index.mjs"
109
+ }
110
+ },
111
+ "keywords": [
112
+ "repository",
113
+ "repository-pattern",
114
+ "data-access",
115
+ "hooks",
116
+ "filter-ir",
117
+ "pagination",
118
+ "cursor-pagination",
119
+ "keyset-pagination",
120
+ "plugin-based",
121
+ "driver-agnostic",
122
+ "typescript",
123
+ "esm"
124
+ ],
125
+ "author": "Classytic <classytic.dev@gmail.com> (https://github.com/classytic)",
126
+ "license": "MIT",
127
+ "repository": {
128
+ "type": "git",
129
+ "url": "git+https://github.com/classytic/repo-core.git"
130
+ },
131
+ "bugs": {
132
+ "url": "https://github.com/classytic/repo-core/issues"
133
+ },
134
+ "homepage": "https://github.com/classytic/repo-core#readme",
135
+ "scripts": {
136
+ "build": "tsdown",
137
+ "dev": "tsdown --watch",
138
+ "test": "vitest run --project unit --project integration",
139
+ "test:unit": "vitest run --project unit",
140
+ "test:integration": "vitest run --project integration",
141
+ "test:e2e": "vitest run --project e2e",
142
+ "test:all": "vitest run",
143
+ "test:watch": "vitest --project unit --project integration",
144
+ "bench": "vitest bench --run --project bench",
145
+ "test:coverage": "vitest run --coverage",
146
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
147
+ "lint": "biome check src tests",
148
+ "lint:fix": "biome check src tests --write",
149
+ "format": "biome format src tests --write",
150
+ "check": "biome ci src tests --diagnostic-level=error",
151
+ "knip": "knip",
152
+ "push": "classytic-push",
153
+ "prepublishOnly": "npm run check && npm run build && npm run typecheck && npm test",
154
+ "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
155
+ "release": "npm run push -- main && npm run release:tag && npm publish",
156
+ "publish:dry": "npm publish --dry-run --access public",
157
+ "publish:npm": "npm publish --access public"
158
+ },
159
+ "devDependencies": {
160
+ "@arethetypeswrong/cli": "^0.18.2",
161
+ "@biomejs/biome": "^2.4.12",
162
+ "@classytic/dev-tools": "^0.2.0",
163
+ "@types/node": "^22.0.0",
164
+ "@vitest/coverage-v8": "^4.1.4",
165
+ "fast-check": "^4.7.0",
166
+ "knip": "^6.3.0",
167
+ "publint": "^0.3.18",
168
+ "tsdown": "^0.22.5",
169
+ "typescript": "^7.0.2",
170
+ "vitest": "^4.1.4"
171
+ },
172
+ "peerDependencies": {
173
+ "vitest": "^3.0.0 || ^4.0.0"
174
+ },
175
+ "peerDependenciesMeta": {
176
+ "vitest": {
177
+ "optional": true
178
+ }
179
+ }
180
180
  }