@classytic/repo-core 0.30.0 → 0.31.1

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.
@@ -0,0 +1,13 @@
1
+ import { RepoCapabilities } from "../repository/capabilities.mjs";
2
+ import { ReadModelFixture } from "./data-access-conformance.mjs";
3
+ //#region src/testing/read-model-property-conformance.d.ts
4
+ interface ReadModelPropertyHarness {
5
+ name: string;
6
+ features: RepoCapabilities;
7
+ fixture?: () => Promise<ReadModelFixture>;
8
+ /** fast-check runs. Default 15. */
9
+ numRuns?: number;
10
+ }
11
+ declare function runReadModelPropertyConformance(harness: ReadModelPropertyHarness): void;
12
+ //#endregion
13
+ export { ReadModelPropertyHarness, runReadModelPropertyConformance };
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import fc from "fast-check";
3
+ //#region src/testing/read-model-property-conformance.ts
4
+ /**
5
+ * Read-model properties (fast-check), gated by `RepoCapabilities.readModel`.
6
+ *
7
+ * For random increments with random replays: the incremental model equals the source (no drift,
8
+ * a replay never double-counts); a rebuild from the source keeps zero drift; and corrupting one
9
+ * grain yields EXACTLY one drift row, naming that grain. An undeclared capability is a skipped
10
+ * group naming it; a declared one without a fixture fails.
11
+ */
12
+ const eventArb = fc.record({
13
+ period: fc.constantFrom("2026-01", "2026-02", "2026-03"),
14
+ rate: fc.constantFrom(0, 5, 15),
15
+ tax: fc.integer({
16
+ min: 1,
17
+ max: 1e3
18
+ })
19
+ });
20
+ function runReadModelPropertyConformance(harness) {
21
+ const title = `[read-model properties] ${harness.name}`;
22
+ if (harness.features.readModel !== true) {
23
+ describe.skip(`${title} [skipped: capability 'readModel' not declared]`, () => {
24
+ it("skipped", () => void 0);
25
+ });
26
+ return;
27
+ }
28
+ describe(title, () => {
29
+ if (!harness.fixture) {
30
+ it("declares 'readModel' and supplies its fixture", () => {
31
+ throw new Error(`${harness.name} declares 'readModel' but supplies no fixture`);
32
+ });
33
+ return;
34
+ }
35
+ const make = harness.fixture;
36
+ it("increments with replays equal the source; rebuild keeps zero drift; one corruption is one drift row", async () => {
37
+ await fc.assert(fc.asyncProperty(fc.array(eventArb, {
38
+ minLength: 1,
39
+ maxLength: 20
40
+ }), fc.array(fc.nat(), { maxLength: 10 }), fc.nat(), async (events, replayPicks, corruptPick) => {
41
+ const f = await make();
42
+ try {
43
+ const tenant = "t-prop";
44
+ const keyed = events.map((e, i) => ({
45
+ ...e,
46
+ dedupeKey: `e${i}`
47
+ }));
48
+ await f.seedSource(tenant, events);
49
+ const replays = replayPicks.map((p) => keyed[p % keyed.length]).filter((x) => x !== void 0);
50
+ const half = Math.ceil(keyed.length / 2);
51
+ await f.applyIncrements(tenant, keyed.slice(0, half));
52
+ await f.applyIncrements(tenant, [...replays, ...keyed.slice(half)]);
53
+ expect(await f.reconcile(tenant)).toEqual([]);
54
+ await f.rebuild(tenant);
55
+ expect(await f.reconcile(tenant)).toEqual([]);
56
+ const grains = await f.readTotals(tenant);
57
+ const target = grains[corruptPick % grains.length];
58
+ if (!target) throw new Error("no grain to corrupt");
59
+ await f.tamper(tenant, target.period, target.rate);
60
+ const drift = await f.reconcile(tenant);
61
+ expect(drift).toHaveLength(1);
62
+ expect(drift[0]?.key).toMatchObject({
63
+ period: target.period,
64
+ rate: target.rate
65
+ });
66
+ } finally {
67
+ await f.cleanup();
68
+ }
69
+ }), { numRuns: harness.numRuns ?? 15 });
70
+ });
71
+ });
72
+ }
73
+ //#endregion
74
+ export { runReadModelPropertyConformance };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@classytic/repo-core",
3
- "version": "0.30.0",
4
- "description": "Driver-agnostic repository primitives: hooks, Filter IR, operations, pagination, cache contract. Foundation for mongokit, sqlitekit, pgkit, and prismakit. Lean by design \u2014 no plugins ship here; each kit owns its own.",
3
+ "version": "0.31.1",
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
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "files": [
@@ -168,9 +168,17 @@
168
168
  "publish:dry": "npm publish --dry-run --access public",
169
169
  "publish:npm": "npm publish --access public"
170
170
  },
171
+ "peerDependencies": {
172
+ "fast-check": "^4.7.0"
173
+ },
174
+ "peerDependenciesMeta": {
175
+ "fast-check": {
176
+ "optional": true
177
+ }
178
+ },
171
179
  "devDependencies": {
172
180
  "@arethetypeswrong/cli": "^0.18.2",
173
- "@biomejs/biome": "^2.4.12",
181
+ "@biomejs/biome": "^2.5.15",
174
182
  "@classytic/dev-tools": "^0.2.0",
175
183
  "@types/node": "^22.0.0",
176
184
  "@vitest/coverage-v8": "^4.1.4",