@classytic/mongokit 3.28.0 → 3.30.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 (37) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +54 -1
  3. package/TRADEMARK.md +22 -0
  4. package/dist/{create-repository-D4ZU94b3.d.mts → Repository-BXHkUtLk.d.mts} +30 -57
  5. package/dist/{Repository-BFBTAMlA.mjs → Repository-X7U2d0kU.mjs} +417 -47
  6. package/dist/actions/index.mjs +1 -1
  7. package/dist/adapter/index.mjs +1 -1
  8. package/dist/{adapter-CLvUHiTW.mjs → adapter-BitHg-CN.mjs} +1 -1
  9. package/dist/ai/index.d.mts +1 -1
  10. package/dist/better-auth/index.d.mts +13 -0
  11. package/dist/better-auth/index.mjs +76 -6
  12. package/dist/create-repository-CvD4wzuN.d.mts +54 -0
  13. package/dist/{create-repository-2DMwkYVz.mjs → create-repository-DYe5YEie.mjs} +3 -2
  14. package/dist/index.d.mts +275 -41
  15. package/dist/index.mjs +80 -7
  16. package/dist/kernel-conformance/index.d.mts +217 -0
  17. package/dist/kernel-conformance/index.mjs +603 -0
  18. package/dist/method-registry.plugin-DblM7V6j.mjs +112 -0
  19. package/dist/model-registry-ClKtWiHa.mjs +67 -0
  20. package/dist/{mongooseToJsonSchema-CByt1VhQ.mjs → mongooseToJsonSchema-DKbnKduN.mjs} +14 -3
  21. package/dist/{multi-tenant.plugin-DVyVGREb.d.mts → multi-tenant.plugin-2VtSSm1Y.d.mts} +1 -1
  22. package/dist/outbox/index.d.mts +63 -0
  23. package/dist/outbox/index.mjs +239 -0
  24. package/dist/{plugin-options-vFufQZMZ.d.mts → plugin-options-BsFH0sTi.d.mts} +1 -1
  25. package/dist/plugins/index.d.mts +4 -4
  26. package/dist/plugins/index.mjs +3 -2
  27. package/dist/{repository-DCHRWRXw.d.mts → repository-Q16ErTu8.d.mts} +19 -1
  28. package/dist/testkit/index.d.mts +8 -1
  29. package/dist/testkit/index.mjs +1 -1
  30. package/dist/{timestamp.plugin-ennbatTW.mjs → timestamp.plugin-ChFRcLRJ.mjs} +32 -143
  31. package/dist/{to-plain-DkF5px0R.d.mts → to-plain-BuX1RUZV.d.mts} +12 -4
  32. package/dist/{update-1Lq9OMD2.mjs → update-DHdXdOk_.mjs} +43 -30
  33. package/dist/utils/index.d.mts +2 -2
  34. package/dist/utils/index.mjs +1 -1
  35. package/dist/{validation-chain.plugin-Cm0Ttnd8.d.mts → validation-chain.plugin-BSshLJTA.d.mts} +44 -44
  36. package/dist/{validation-chain.plugin-k9joH2qw.mjs → validation-chain.plugin-BxTl_dYR.mjs} +139 -138
  37. package/package.json +28 -15
@@ -0,0 +1,603 @@
1
+ import { t as ModelCollisionError } from "../model-registry-ClKtWiHa.mjs";
2
+ import mongoose from "mongoose";
3
+ //#region src/kernel-conformance/transport.ts
4
+ /** Exact match, `*`, or a `prefix.*` / `prefix:*` glob — the kernel bus convention. */
5
+ function matches(pattern, type) {
6
+ if (pattern === "*" || pattern === type) return true;
7
+ if (pattern.endsWith(".*")) return type.startsWith(`${pattern.slice(0, -1)}`);
8
+ if (pattern.endsWith(":*")) return type.startsWith(`${pattern.slice(0, -1)}`);
9
+ return false;
10
+ }
11
+ let eventSeq = 0;
12
+ /** A minimal, valid domain-event envelope (`type` + `meta.id` is all any bus reads). */
13
+ function conformanceEvent(type) {
14
+ eventSeq += 1;
15
+ return {
16
+ type,
17
+ payload: {},
18
+ meta: {
19
+ id: `kernel-conformance-${eventSeq}`,
20
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
21
+ source: "kernel-conformance"
22
+ }
23
+ };
24
+ }
25
+ /** An in-process transport good enough to stand in for a host-owned shared bus. */
26
+ function createConformanceTransport(name = "kernel-conformance-transport") {
27
+ const handlers = /* @__PURE__ */ new Map();
28
+ let closed = false;
29
+ const publish = async (event) => {
30
+ if (closed) return;
31
+ const matched = /* @__PURE__ */ new Set();
32
+ for (const [pattern, set] of handlers) if (matches(pattern, event.type)) for (const h of set) matched.add(h);
33
+ for (const handler of matched) await handler(event);
34
+ };
35
+ return {
36
+ name,
37
+ publish,
38
+ async publishMany(events) {
39
+ const results = /* @__PURE__ */ new Map();
40
+ for (const e of events) {
41
+ await publish(e);
42
+ results.set(String(e.meta?.id ?? e.type), null);
43
+ }
44
+ return results;
45
+ },
46
+ async subscribe(pattern, handler) {
47
+ let set = handlers.get(pattern);
48
+ if (!set) {
49
+ set = /* @__PURE__ */ new Set();
50
+ handlers.set(pattern, set);
51
+ }
52
+ set.add(handler);
53
+ return () => {
54
+ handlers.get(pattern)?.delete(handler);
55
+ };
56
+ },
57
+ async close() {
58
+ closed = true;
59
+ handlers.clear();
60
+ }
61
+ };
62
+ }
63
+ /**
64
+ * Wrap a transport so `close()` calls are counted. The wrapper forwards everything else, so
65
+ * the kernel cannot tell the difference — which is the point: ownership must be decided by
66
+ * "did I create this?", never by inspecting the object.
67
+ */
68
+ function instrumentTransport(inner) {
69
+ let closeCalls = 0;
70
+ return {
71
+ transport: {
72
+ name: inner.name ?? "kernel-conformance-supplied",
73
+ publish: (event) => inner.publish(event),
74
+ ...inner.publishMany ? { publishMany: (events) => inner.publishMany(events) } : {},
75
+ ...inner.subscribe ? { subscribe: (pattern, handler) => inner.subscribe?.(pattern, handler) } : {},
76
+ close: async () => {
77
+ closeCalls += 1;
78
+ await inner.close?.();
79
+ }
80
+ },
81
+ closeCalls: () => closeCalls
82
+ };
83
+ }
84
+ //#endregion
85
+ //#region src/kernel-conformance/types.ts
86
+ /**
87
+ * Every check the suite can run, in execution order. The ids are STABLE — `skip` is typed
88
+ * against this tuple, so a typo is a compile error rather than a silently-ineffective skip
89
+ * (a skip that skips nothing is the same silent-permissiveness bug class the suite exists
90
+ * to catch).
91
+ */
92
+ const KERNEL_CONFORMANCE_CHECKS = [
93
+ "describe-purity",
94
+ "blueprint-model-names",
95
+ "bind-registers-declared-models",
96
+ "bind-connection-scoped",
97
+ "bind-no-index-io",
98
+ "no-autoindex-on-bind",
99
+ "fresh-schema-per-connection",
100
+ "double-bind-collision",
101
+ "transport-not-closed-when-supplied",
102
+ "owned-transport-closed",
103
+ "close-idempotent",
104
+ "no-legacy-surface",
105
+ "maintenance-fails-loud",
106
+ "bind-verifies-requirements",
107
+ "outbox-requires-transactional-save",
108
+ "optional-modules-register-no-models"
109
+ ];
110
+ /**
111
+ * Checks that only run when the kernel supplies the input they need. When the input is
112
+ * absent they are reported as NOT EXERCISED — a visible marker test plus a report entry,
113
+ * never a silent pass.
114
+ */
115
+ const CONDITIONAL_CHECKS = [
116
+ "bind-verifies-requirements",
117
+ "outbox-requires-transactional-save",
118
+ "optional-modules-register-no-models"
119
+ ];
120
+ //#endregion
121
+ //#region src/kernel-conformance/suite.ts
122
+ /**
123
+ * `describeKernelConformance` — the EXECUTABLE form of the Kernel Construction Standard
124
+ * (commerce `STANDARDIZATION-PLAN.md` §11).
125
+ *
126
+ * ## Why this exists
127
+ *
128
+ * ~38 kernels were migrated to Describe → Bind → Verify → Serve → Close and every one of
129
+ * their suites went green. Those suites assert the kernel's OLD behavior still works; almost
130
+ * none assert the STANDARD's invariants. Two real defects survived that green: one kernel
131
+ * swallowed six index-sync errors, another had no transaction gate. Both were reviewer
132
+ * findings, and a reviewer does not scale to 38 packages.
133
+ *
134
+ * So the invariants become tests a consumer RUNS, on the model of arc's port contract suites
135
+ * (`runOutboxStoreContract` et al.): passing the suite IS conformance. Structural conformance
136
+ * (it typechecks, it has a `bind`) is not behavioral conformance.
137
+ *
138
+ * ## Where it lives, and what it may depend on
139
+ *
140
+ * `@classytic/mongokit/kernel-conformance`. Every Mongo-backed kernel already depends on
141
+ * mongokit, and a kernel must NEVER depend on arc — so arc's `testing` subpath, the obvious
142
+ * home, is closed. The suite therefore imports NOTHING beyond mongoose and mongokit's own
143
+ * `ModelCollisionError`: transports, events and engines are structural types.
144
+ *
145
+ * ## Usage
146
+ *
147
+ * ```ts
148
+ * import { describe, it } from 'vitest';
149
+ * import mongoose from 'mongoose';
150
+ * import { describeKernelConformance } from '@classytic/mongokit/kernel-conformance';
151
+ * import * as kernel from '../../src/index.js';
152
+ *
153
+ * describeKernelConformance({
154
+ * name: 'party',
155
+ * runner: { describe, it },
156
+ * blueprint: () => kernel.defineParty({ multiTenant: false, autoIndex: false }),
157
+ * connect: async () => mongoose.createConnection(),
158
+ * bind: (bp, conn, ctx) => bp.bind(conn, { eventTransport: ctx.transport }),
159
+ * expectedModelNames: ['Party'],
160
+ * moduleExports: kernel,
161
+ * skip: [{ check: 'bind-verifies-requirements', reason: '…' }],
162
+ * });
163
+ * ```
164
+ */
165
+ var ConformanceFailure = class extends Error {
166
+ constructor(check, message) {
167
+ super(`[kernel-conformance:${check}] ${message}`);
168
+ this.name = "ConformanceFailure";
169
+ }
170
+ };
171
+ function fail(check, message) {
172
+ throw new ConformanceFailure(check, message);
173
+ }
174
+ function assert(check, condition, message) {
175
+ if (!condition) fail(check, message);
176
+ }
177
+ function sortedSet(values) {
178
+ return [...new Set(values)].sort();
179
+ }
180
+ function diffSets(actual, expected) {
181
+ const a = new Set(actual);
182
+ const e = new Set(expected);
183
+ return {
184
+ missing: [...e].filter((x) => !a.has(x)).sort(),
185
+ extra: [...a].filter((x) => !e.has(x)).sort()
186
+ };
187
+ }
188
+ /** kebab / snake / space → PascalCase, so `supplier-performance` → `SupplierPerformance`. */
189
+ function pascal(name) {
190
+ return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
191
+ }
192
+ /** Yield to the macrotask queue so index I/O SCHEDULED by bind is observed, not missed. */
193
+ function macrotask() {
194
+ return new Promise((resolve) => setTimeout(resolve, 0));
195
+ }
196
+ const INDEX_OPS = [
197
+ "syncIndexes",
198
+ "createIndexes",
199
+ "ensureIndexes",
200
+ "dropIndexes",
201
+ "cleanIndexes"
202
+ ];
203
+ /** Ops that create indexes — mongoose's own `autoIndex` also drives these. */
204
+ const CREATION_OPS = /* @__PURE__ */ new Set(["createIndexes", "ensureIndexes"]);
205
+ function isTransportShaped(value) {
206
+ return typeof value === "object" && value !== null && typeof value.publish === "function";
207
+ }
208
+ function describeKernelConformance(opts) {
209
+ const { runner, name } = opts;
210
+ const P = pascal(name);
211
+ const skipReasons = /* @__PURE__ */ new Map();
212
+ const knownChecks = new Set(KERNEL_CONFORMANCE_CHECKS);
213
+ for (const entry of opts.skip ?? []) {
214
+ if (!knownChecks.has(entry.check)) throw new Error(`[kernel-conformance] unknown check id '${entry.check}' in skip list for '${name}'. Known ids: ${KERNEL_CONFORMANCE_CHECKS.join(", ")}.`);
215
+ if (skipReasons.has(entry.check)) throw new Error(`[kernel-conformance] duplicate skip entry for '${entry.check}' in kernel '${name}'.`);
216
+ if (!entry.reason || entry.reason.trim().length === 0) throw new Error(`[kernel-conformance] skip of '${entry.check}' in kernel '${name}' has no reason. An unexplained skip is indistinguishable from an oversight — state why.`);
217
+ skipReasons.set(entry.check, entry.reason.trim());
218
+ }
219
+ const conditionalInputs = {
220
+ "bind-verifies-requirements": "bindWithUnmetRequirement",
221
+ "outbox-requires-transactional-save": "bindWithNonTransactionalOutbox",
222
+ "optional-modules-register-no-models": "minimalBlueprint"
223
+ };
224
+ const conditionalSupplied = {
225
+ "bind-verifies-requirements": opts.bindWithUnmetRequirement !== void 0,
226
+ "outbox-requires-transactional-save": opts.bindWithNonTransactionalOutbox !== void 0,
227
+ "optional-modules-register-no-models": opts.minimalBlueprint !== void 0
228
+ };
229
+ const statuses = KERNEL_CONFORMANCE_CHECKS.map((check) => {
230
+ const reason = skipReasons.get(check);
231
+ if (reason !== void 0) return {
232
+ check,
233
+ outcome: "skipped",
234
+ reason
235
+ };
236
+ if (CONDITIONAL_CHECKS.includes(check) && !conditionalSupplied[check]) return {
237
+ check,
238
+ outcome: "not-exercised",
239
+ reason: `supply opts.${conditionalInputs[check]} to exercise this check`
240
+ };
241
+ return {
242
+ check,
243
+ outcome: "run"
244
+ };
245
+ });
246
+ const statusOf = new Map(statuses.map((s) => [s.check, s]));
247
+ const report = {
248
+ kernel: name,
249
+ checks: statuses,
250
+ skipped: statuses.filter((s) => s.outcome === "skipped"),
251
+ notExercised: statuses.filter((s) => s.outcome === "not-exercised")
252
+ };
253
+ (opts.onReport ?? defaultReport)(report);
254
+ async function withConnection(fn) {
255
+ const acquired = await opts.connect();
256
+ const handle = "connection" in acquired ? acquired : { connection: acquired };
257
+ try {
258
+ return await fn(handle.connection);
259
+ } finally {
260
+ await handle.teardown?.();
261
+ }
262
+ }
263
+ const bind = (blueprint, connection, transport) => opts.bind(blueprint, connection, transport ? { transport } : {});
264
+ function register(check, title, fn) {
265
+ const status = statusOf.get(check);
266
+ if (status?.outcome === "skipped") {
267
+ runner.it(`[SKIPPED] ${check} — ${status.reason}`, () => {
268
+ assert(check, (status.reason ?? "").length > 0, "a skip must carry a reason");
269
+ });
270
+ return;
271
+ }
272
+ if (status?.outcome === "not-exercised") {
273
+ runner.it(`[NOT EXERCISED] ${check} — ${status.reason}`, () => void 0);
274
+ return;
275
+ }
276
+ runner.it(`${check} — ${title}`, fn);
277
+ }
278
+ runner.describe(`kernel conformance: ${name}`, () => {
279
+ register("describe-purity", "defineX registers no model and performs no I/O", async () => {
280
+ const check = "describe-purity";
281
+ await withConnection(async (connection) => {
282
+ const globalBefore = sortedSet(Object.keys(mongoose.models));
283
+ const connBefore = sortedSet(Object.keys(connection.models));
284
+ const calls = [];
285
+ const realGlobalModel = mongoose.model.bind(mongoose);
286
+ const realConnModel = connection.model.bind(connection);
287
+ const mutableMongoose = mongoose;
288
+ const mutableConn = connection;
289
+ mutableMongoose.model = (...args) => {
290
+ calls.push(`mongoose.model('${String(args[0])}')`);
291
+ return realGlobalModel(...args);
292
+ };
293
+ mutableConn.model = (...args) => {
294
+ calls.push(`connection.model('${String(args[0])}')`);
295
+ return realConnModel(...args);
296
+ };
297
+ let blueprint;
298
+ try {
299
+ blueprint = opts.blueprint();
300
+ } finally {
301
+ mutableMongoose.model = realGlobalModel;
302
+ mutableConn.model = realConnModel;
303
+ }
304
+ assert(check, calls.length === 0, `describe compiled models — it called ${calls.join(", ")}. defineX must only validate + freeze the model NAMES; registration belongs to bind.`);
305
+ const globalAfter = sortedSet(Object.keys(mongoose.models));
306
+ assert(check, globalAfter.join() === globalBefore.join(), `describe mutated the GLOBAL mongoose registry: +[${diffSets(globalAfter, globalBefore).extra.join(", ")}]`);
307
+ const connAfter = sortedSet(Object.keys(connection.models));
308
+ assert(check, connAfter.join() === connBefore.join(), `describe mutated the connection registry: +[${diffSets(connAfter, connBefore).extra.join(", ")}]`);
309
+ const second = opts.blueprint();
310
+ assert(check, sortedSet(second.modelNames).join() === sortedSet(blueprint.modelNames).join(), "two describe calls produced different model sets — describe is not deterministic");
311
+ });
312
+ });
313
+ register("blueprint-model-names", "modelNames is non-empty, frozen and exact", () => {
314
+ const check = "blueprint-model-names";
315
+ const names = opts.blueprint().modelNames;
316
+ assert(check, Array.isArray(names), "blueprint.modelNames must be an array");
317
+ assert(check, names.length > 0, "blueprint.modelNames is empty — a Mongo-backed kernel must DECLARE its models so a collision can be diagnosed before a connection exists");
318
+ assert(check, Object.isFrozen(names), "blueprint.modelNames is not frozen — a caller could mutate the declared model set after describe, and the declaration is the collision-diagnostic contract");
319
+ assert(check, new Set(names).size === names.length, `blueprint.modelNames contains duplicates: [${names.join(", ")}]`);
320
+ if (opts.expectedModelNames) {
321
+ const { missing, extra } = diffSets(names, opts.expectedModelNames);
322
+ assert(check, missing.length === 0 && extra.length === 0, `declared model set drifted from expectedModelNames — missing [${missing.join(", ")}], unexpected [${extra.join(", ")}]`);
323
+ }
324
+ });
325
+ register("bind-registers-declared-models", "bind registers EXACTLY the declared models on the supplied connection", async () => {
326
+ const check = "bind-registers-declared-models";
327
+ await withConnection(async (connection) => {
328
+ const before = new Set(Object.keys(connection.models));
329
+ const blueprint = opts.blueprint();
330
+ await bind(blueprint, connection);
331
+ const { missing, extra } = diffSets(Object.keys(connection.models).filter((n) => !before.has(n)), blueprint.modelNames);
332
+ assert(check, missing.length === 0, `bind DECLARED but did not register [${missing.join(", ")}] — a declared-but-absent model means the collision diagnostic lies about what this kernel owns`);
333
+ assert(check, extra.length === 0, `bind registered UNDECLARED models [${extra.join(", ")}] — every model must appear in blueprint.modelNames or a second kernel can silently collide with it`);
334
+ });
335
+ });
336
+ register("bind-connection-scoped", "bind never touches the global mongoose registry", async () => {
337
+ const check = "bind-connection-scoped";
338
+ await withConnection(async (connection) => {
339
+ const before = sortedSet(Object.keys(mongoose.models));
340
+ await bind(opts.blueprint(), connection);
341
+ const { extra } = diffSets(sortedSet(Object.keys(mongoose.models)), before);
342
+ assert(check, extra.length === 0, `bind registered [${extra.join(", ")}] on the GLOBAL mongoose registry. A connection was supplied, so registration must be connection-local (§5.2) — a global model leaks across every connection in the process, including other tenants/tests`);
343
+ });
344
+ });
345
+ register("bind-no-index-io", "bind synchronizes, creates or drops NO index", async () => {
346
+ const check = "bind-no-index-io";
347
+ await withConnection(async (connection) => {
348
+ const ModelBase = mongoose.Model;
349
+ const originals = /* @__PURE__ */ new Map();
350
+ const explicit = [];
351
+ const autoIndexDriven = [];
352
+ let insideInit = 0;
353
+ const originalInit = ModelBase.init;
354
+ ModelBase.init = function patchedInit(...args) {
355
+ insideInit += 1;
356
+ let settled = false;
357
+ try {
358
+ const result = originalInit.apply(this, args);
359
+ if (result && typeof result.then === "function") {
360
+ settled = true;
361
+ return Promise.resolve(result).finally(() => {
362
+ insideInit -= 1;
363
+ });
364
+ }
365
+ return result;
366
+ } finally {
367
+ if (!settled) insideInit -= 1;
368
+ }
369
+ };
370
+ for (const op of INDEX_OPS) {
371
+ originals.set(op, ModelBase[op]);
372
+ ModelBase[op] = function patched() {
373
+ const label = `${this?.modelName ?? "<model>"}.${op}()`;
374
+ if (insideInit > 0 && CREATION_OPS.has(op)) autoIndexDriven.push(label);
375
+ else explicit.push(label);
376
+ return Promise.resolve([]);
377
+ };
378
+ }
379
+ try {
380
+ const blueprint = opts.blueprint();
381
+ await bind(blueprint, connection);
382
+ await macrotask();
383
+ if (connection.readyState === 1) await Promise.all(blueprint.modelNames.map((n) => connection.models[n]?.$init).filter((p) => Boolean(p)).map((p) => Promise.resolve(p).catch(() => void 0)));
384
+ } finally {
385
+ ModelBase.init = originalInit;
386
+ for (const [op, fn] of originals) ModelBase[op] = fn;
387
+ }
388
+ assert(check, explicit.length === 0, `bind performed index I/O: ${explicit.join(", ")}. §11.11 — binding never synchronizes or drops indexes; index reconciliation is an explicit operational action (engine.syncIndexes()), never a constructor side effect.` + (autoIndexDriven.length > 0 ? ` (${autoIndexDriven.length} further call(s) came from mongoose autoIndex and were not counted.)` : ""));
389
+ assert(check, autoIndexDriven.length === 0, `mongoose autoIndex built indexes during bind: ${autoIndexDriven.join(", ")}. Describe the conformance blueprint with autoIndex:false (§8.1) — a production kernel must not create indexes as a side effect of construction. (See the no-autoindex-on-bind check for the connection-independent form.)`);
390
+ });
391
+ });
392
+ register("no-autoindex-on-bind", "every bound schema disables mongoose autoIndex", async () => {
393
+ const check = "no-autoindex-on-bind";
394
+ await withConnection(async (connection) => {
395
+ const blueprint = opts.blueprint();
396
+ await bind(blueprint, connection);
397
+ const offenders = blueprint.modelNames.filter((n) => {
398
+ const model = connection.models[n];
399
+ return model !== void 0 && model.schema.options.autoIndex !== false;
400
+ });
401
+ assert(check, offenders.length === 0, `these bound schemas leave autoIndex enabled: [${offenders.join(", ")}]. Mongoose DEFAULTS autoIndex to true, so an unset option is not neutral — every bind against a live connection issues createIndex for every declared index, making index reconciliation an accidental constructor effect (§8.1/§8.2). Set autoIndex:false on the schema (or describe the blueprint with it) and reconcile through the explicit engine.syncIndexes() path.`);
402
+ });
403
+ });
404
+ register("fresh-schema-per-connection", "one blueprint binds to two connections with distinct models and schemas", async () => {
405
+ const check = "fresh-schema-per-connection";
406
+ const blueprint = opts.blueprint();
407
+ await withConnection(async (connA) => {
408
+ await withConnection(async (connB) => {
409
+ await bind(blueprint, connA);
410
+ await bind(blueprint, connB);
411
+ for (const modelName of blueprint.modelNames) {
412
+ const a = connA.models[modelName];
413
+ const b = connB.models[modelName];
414
+ assert(check, a !== void 0, `model '${modelName}' missing on connection A`);
415
+ assert(check, b !== void 0, `model '${modelName}' missing on connection B`);
416
+ assert(check, a !== b, `model '${modelName}' is the SAME object on two connections — the blueprint reused a compiled model, so writes route to one connection`);
417
+ assert(check, a?.schema !== b?.schema, `model '${modelName}' SHARES one Schema instance across connections (§11.3). A schema must be a FACTORY: mongoose mutates schemas (plugins, indexes, discriminators), so a shared instance leaks one engine’s config into another`);
418
+ }
419
+ });
420
+ });
421
+ });
422
+ register("double-bind-collision", "a second bind on one connection throws mongokit ModelCollisionError", async () => {
423
+ const check = "double-bind-collision";
424
+ await withConnection(async (connection) => {
425
+ const blueprint = opts.blueprint();
426
+ await bind(blueprint, connection);
427
+ let error;
428
+ try {
429
+ await bind(opts.blueprint(), connection);
430
+ } catch (err) {
431
+ error = err;
432
+ }
433
+ assert(check, error !== void 0, "binding twice on ONE connection did not throw. Silent reuse drops every field and index the second bind would have contributed (§11.2/§11.12) — it must fail loud.");
434
+ const err = error;
435
+ assert(check, error instanceof ModelCollisionError || err?.name === "ModelCollisionError", `double bind threw ${error?.name ?? typeof error} — it must be mongokit's typed ModelCollisionError, not a generic Error and not a kernel-specific collision class (§11.12: one collision error for every kernel).`);
436
+ assert(check, typeof err.modelName === "string" && err.modelName.length > 0, "ModelCollisionError carried no .modelName — the diagnostic must name the model that collided");
437
+ assert(check, blueprint.modelNames.includes(err.modelName), `ModelCollisionError named '${String(err.modelName)}', which is not in the declared model set [${blueprint.modelNames.join(", ")}]`);
438
+ });
439
+ });
440
+ register("transport-not-closed-when-supplied", "close() never closes a host-supplied transport", async () => {
441
+ const check = "transport-not-closed-when-supplied";
442
+ await withConnection(async (connection) => {
443
+ const supplied = instrumentTransport((opts.makeTransport ?? createConformanceTransport)());
444
+ const engine = await bind(opts.blueprint(), connection, supplied.transport);
445
+ assert(check, typeof engine.close === "function", "engine exposes no close()");
446
+ await engine.close?.();
447
+ assert(check, supplied.closeCalls() === 0, `close() closed the SUPPLIED transport (${supplied.closeCalls()} call(s)). A host transport is usually arc’s shared bus — closing it from one kernel silences events for every other module, and nothing errors (§11.7).`);
448
+ let delivered = 0;
449
+ const unsubscribe = await supplied.transport.subscribe?.("*", () => {
450
+ delivered += 1;
451
+ });
452
+ await supplied.transport.publish(conformanceEvent("conformance.after-close"));
453
+ if (typeof unsubscribe === "function") unsubscribe();
454
+ assert(check, delivered === 1, "the supplied transport stopped delivering after engine.close() — it was closed or otherwise disabled by the kernel");
455
+ });
456
+ });
457
+ register("owned-transport-closed", "close() closes the internally-created bus", async () => {
458
+ const check = "owned-transport-closed";
459
+ await withConnection(async (connection) => {
460
+ const engine = await bind(opts.blueprint(), connection);
461
+ const engineLike = engine;
462
+ assert(check, typeof engineLike.close === "function", "engine exposes no close()");
463
+ if (opts.internalTransportClosed) {
464
+ await engineLike.close?.();
465
+ const closed = await opts.internalTransportClosed(engine);
466
+ assert(check, closed === true, "internalTransportClosed() reported the internally-created bus was NOT closed — a bus the kernel created is a resource the kernel must release (§11.7)");
467
+ return;
468
+ }
469
+ const resolved = opts.resolveInternalTransport?.(engine) ?? [
470
+ engineLike.events,
471
+ engineLike.transport,
472
+ engineLike.eventTransport,
473
+ engineLike.bus
474
+ ].find(isTransportShaped);
475
+ assert(check, isTransportShaped(resolved), "could not find the engine’s internal transport. Supply opts.resolveInternalTransport (or opts.internalTransportClosed), or skip this check with a reason — a kernel that creates a bus it never closes leaks it on every module restart.");
476
+ const transport = resolved;
477
+ assert(check, typeof transport.subscribe === "function", "the engine’s transport has no subscribe(), so closure is unobservable. Supply opts.internalTransportClosed.");
478
+ let delivered = 0;
479
+ const handler = (_event) => {
480
+ delivered += 1;
481
+ };
482
+ await transport.subscribe?.("*", handler);
483
+ await transport.publish(conformanceEvent("conformance.before-close"));
484
+ assert(check, delivered === 1, "the internal transport did not deliver BEFORE close — the probe cannot distinguish a closed bus from a broken one. Supply opts.internalTransportClosed.");
485
+ await engineLike.close?.();
486
+ await transport.publish(conformanceEvent("conformance.after-close"));
487
+ assert(check, delivered === 1, "the internally-created bus still delivers after close() — the kernel created it, so the kernel owns it and must close it (§11.7). Subscriptions survive every restart.");
488
+ });
489
+ });
490
+ register("close-idempotent", "close() twice does not throw", async () => {
491
+ const check = "close-idempotent";
492
+ await withConnection(async (connection) => {
493
+ const engine = await bind(opts.blueprint(), connection);
494
+ assert(check, typeof engine.close === "function", "engine exposes no close()");
495
+ await engine.close?.();
496
+ try {
497
+ await engine.close?.();
498
+ } catch (err) {
499
+ fail(check, `the second close() threw (${err?.message}). Arc tears modules down in reverse order and a host may also close explicitly, so close() is called twice routinely (§6.4).`);
500
+ }
501
+ });
502
+ });
503
+ register("no-legacy-surface", "one construction API, one teardown name", async () => {
504
+ const check = "no-legacy-surface";
505
+ assert(check, typeof opts.blueprint().bind === "function", "the blueprint exposes no bind() — defineX(shape).bind(connection, runtime) is the sole construction API (§11.12)");
506
+ await withConnection(async (connection) => {
507
+ const engine = await bind(opts.blueprint(), connection);
508
+ assert(check, typeof engine.close === "function", "engine exposes no close()");
509
+ for (const legacy of ["destroy", "dispose"]) assert(check, engine[legacy] === void 0, `the engine still exposes '${legacy}()'. close() is the ONE teardown name — an alias means two teardown paths that drift (§11.12).`);
510
+ });
511
+ const exportNames = Array.isArray(opts.moduleExports) ? [...opts.moduleExports] : opts.moduleExports ? Object.keys(opts.moduleExports) : void 0;
512
+ assert(check, exportNames !== void 0, "opts.moduleExports was not supplied, so the package surface could not be inspected. Pass the namespace (`import * as kernel from '../../src/index.js'`) or a name list — or skip this check with a reason. A legacy factory nobody looks at is exactly how two construction APIs survive a migration.");
513
+ const exported = new Set(exportNames);
514
+ const construction = opts.constructionExportName ?? `define${P}`;
515
+ assert(check, exported.has(construction), `the package does not export '${construction}'. The sole construction API must be a named export (§11.12); pass opts.constructionExportName if it is named differently.`);
516
+ const present = [
517
+ `create${P}`,
518
+ `create${P}Engine`,
519
+ `ensure${P}Engine`,
520
+ `get${P}Engine`,
521
+ `destroy${P}Engine`,
522
+ `${P}ModelCollisionError`,
523
+ ...opts.forbiddenExportNames ?? []
524
+ ].filter((n) => exported.has(n));
525
+ assert(check, present.length === 0, `the package still exports legacy construction/teardown surface [${present.join(", ")}]. NO LEGACY / NO COMPAT SHIMS: exactly one construction API and one collision error (mongokit’s ModelCollisionError) per kernel (§11.12).`);
526
+ });
527
+ register("maintenance-fails-loud", "engine.syncIndexes() REJECTS when a model index sync fails", async () => {
528
+ const check = "maintenance-fails-loud";
529
+ await withConnection(async (connection) => {
530
+ const blueprint = opts.blueprint();
531
+ const engine = await bind(blueprint, connection);
532
+ assert(check, typeof engine.syncIndexes === "function", "the engine exposes no syncIndexes(). §6.4 requires it — index reconciliation must be an explicit, callable operation. Skip this check with a reason if the kernel genuinely owns no indexes.");
533
+ const sentinel = /* @__PURE__ */ new Error("kernel-conformance: induced index-sync failure");
534
+ let sabotaged = 0;
535
+ for (const modelName of blueprint.modelNames) {
536
+ const model = connection.models[modelName];
537
+ if (!model) continue;
538
+ sabotaged += 1;
539
+ for (const op of INDEX_OPS) model[op] = () => Promise.reject(sentinel);
540
+ }
541
+ assert(check, sabotaged > 0, "no declared model was registered — nothing to sabotage");
542
+ let rejected = false;
543
+ try {
544
+ await engine.syncIndexes?.();
545
+ } catch {
546
+ rejected = true;
547
+ }
548
+ assert(check, rejected, `engine.syncIndexes() RESOLVED while every one of ${sabotaged} model index sync(s) rejected. A swallowed index-sync error means an operator runs the maintenance command, sees success, and ships with the indexes missing — the exact defect this check generalizes. Never catch-and-continue here; aggregate and reject.`);
549
+ });
550
+ });
551
+ register("bind-verifies-requirements", "an unmet runtime requirement fails the bind", async () => {
552
+ const check = "bind-verifies-requirements";
553
+ await withConnection(async (connection) => {
554
+ let threw = false;
555
+ try {
556
+ await opts.bindWithUnmetRequirement?.(connection);
557
+ } catch {
558
+ threw = true;
559
+ }
560
+ assert(check, threw, "bind SUCCEEDED with an unmet runtime requirement (§11.5). A declared capability that is never verified is decoration — it will authorize a route that then 500s.");
561
+ });
562
+ });
563
+ register("outbox-requires-transactional-save", "a non-transactional outbox is REFUSED at bind, naming transactionalSave", async () => {
564
+ const check = "outbox-requires-transactional-save";
565
+ await withConnection(async (connection) => {
566
+ let error;
567
+ let threw = false;
568
+ try {
569
+ await opts.bindWithNonTransactionalOutbox?.(connection);
570
+ } catch (err) {
571
+ threw = true;
572
+ error = err;
573
+ }
574
+ assert(check, threw, "bind ACCEPTED an OutboxStore that does not declare transactionalSave. `OutboxWriteOptions.session` is best-effort BY CONTRACT (see @classytic/primitives/events/outbox), so a store which ignores it persists the event row outside the caller’s transaction: a domain write that later rolls back still emits its event, and a downstream consumer acts on something that never happened — a commission paid on a sale that does not exist, a credential synced for access never granted. Nothing throws, on either side. Verify the flag at the TOP of bind (before the model blueprint binds, so a collision cannot report first and a refused bind leaves the registry untouched):\n if (runtime.outbox && runtime.outbox.transactionalSave !== true) throw …\nIf this kernel’s outbox rows genuinely need not commit with the state change, do not gate it — skip this check and say why.");
575
+ const message = String(error?.message ?? error ?? "");
576
+ assert(check, message.includes("transactionalSave"), `bind refused the non-transactional outbox, but its error never names 'transactionalSave': "${message.slice(0, 300)}". Either the refusal came from something other than the outbox gate — in which case this check is passing for the wrong reason — or the diagnostic points the reader at the wrong layer. Name the unmet capability in the message.`);
577
+ });
578
+ });
579
+ register("optional-modules-register-no-models", "a disabled optional module registers no models", async () => {
580
+ const check = "optional-modules-register-no-models";
581
+ const full = opts.blueprint();
582
+ const minimal = opts.minimalBlueprint?.();
583
+ const { extra } = diffSets(minimal.modelNames, full.modelNames);
584
+ assert(check, extra.length === 0, `the minimal blueprint declares models the full one does not: [${extra.join(", ")}]`);
585
+ assert(check, minimal.modelNames.length < full.modelNames.length, `disabling the optional modules changed nothing (${minimal.modelNames.length} models either way) — a disabled module must register no models, schedules or indexes (§11.10)`);
586
+ await withConnection(async (connection) => {
587
+ const before = new Set(Object.keys(connection.models));
588
+ await bind(minimal, connection);
589
+ const registered = Object.keys(connection.models).filter((n) => !before.has(n));
590
+ const drift = diffSets(registered, minimal.modelNames);
591
+ assert(check, drift.extra.length === 0 && drift.missing.length === 0, `the minimal bind registered [${registered.join(", ")}] but declared [${minimal.modelNames.join(", ")}] — a disabled module still compiled a model`);
592
+ });
593
+ });
594
+ });
595
+ }
596
+ function defaultReport(report) {
597
+ const lines = [`[kernel-conformance] ${report.kernel}: ${report.checks.length} checks (${report.checks.filter((c) => c.outcome === "run").length} run, ${report.skipped.length} skipped, ${report.notExercised.length} not exercised)`];
598
+ for (const s of report.skipped) lines.push(` SKIPPED ${s.check} — ${s.reason}`);
599
+ for (const s of report.notExercised) lines.push(` NOT EXERCISED ${s.check} — ${s.reason}`);
600
+ console.info(lines.join("\n"));
601
+ }
602
+ //#endregion
603
+ export { CONDITIONAL_CHECKS, KERNEL_CONFORMANCE_CHECKS, conformanceEvent, createConformanceTransport, describeKernelConformance, instrumentTransport };