@noctcore/lint-meta-rules 0.2.0 → 0.4.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.
@@ -0,0 +1,442 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/prisma.ts
31
+ var prisma_exports = {};
32
+ __export(prisma_exports, {
33
+ createPrismaMethodSurfaceRule: () => createPrismaMethodSurfaceRule,
34
+ createTenantModelRegistryParityRule: () => createTenantModelRegistryParityRule,
35
+ parseDelegateSurfaces: () => parseDelegateSurfaces,
36
+ parseObjectLiteralKeys: () => parseObjectLiteralKeys
37
+ });
38
+ module.exports = __toCommonJS(prisma_exports);
39
+
40
+ // src/prisma/prisma-method-surface.ts
41
+ var import_eslint_plugin_prisma = require("@noctcore/eslint-plugin-prisma");
42
+
43
+ // src/rules/shared.ts
44
+ function globFiles(glob, globs, skipDirs = []) {
45
+ const skip = new Set(skipDirs);
46
+ const found = /* @__PURE__ */ new Set();
47
+ for (const pattern of globs) {
48
+ for (const rel of glob(pattern)) {
49
+ if (!rel.split("/").some((segment) => skip.has(segment))) found.add(rel);
50
+ }
51
+ }
52
+ return [...found].sort();
53
+ }
54
+
55
+ // src/prisma/prisma-method-surface.ts
56
+ var DEFAULT_ID = "prisma-method-surface";
57
+ var DEFAULT_CLIENT_GLOBS = [
58
+ "generated/prisma/models/*.ts",
59
+ "node_modules/.prisma/client/index.d.ts"
60
+ ];
61
+ function blankComments(source) {
62
+ return source.replace(/\/\*[\s\S]*?\*\//gu, (comment) => comment.replace(/[^\n]/gu, " ")).replace(/(^|[^:])\/\/.*$/gmu, "$1");
63
+ }
64
+ function parseDelegateSurfaces(source, file = "") {
65
+ const lines = blankComments(source).split("\n");
66
+ const surfaces = [];
67
+ for (let i = 0; i < lines.length; i += 1) {
68
+ const open = /^(\s*)(?:export\s+)?interface\s+([A-Za-z_]\w*)Delegate\b.*\{\s*$/u.exec(
69
+ lines[i] ?? ""
70
+ );
71
+ if (open?.[2] === void 0) continue;
72
+ const indent = open[1] ?? "";
73
+ let memberIndent = null;
74
+ const methods = /* @__PURE__ */ new Set();
75
+ let j = i + 1;
76
+ for (; j < lines.length; j += 1) {
77
+ const line = lines[j] ?? "";
78
+ if (line.startsWith(`${indent}}`)) break;
79
+ if (line.trim() === "") continue;
80
+ const leading = /^\s*/u.exec(line)?.[0] ?? "";
81
+ memberIndent ??= leading;
82
+ if (leading !== memberIndent) continue;
83
+ const member = /^\s*([A-Za-z_]\w*)\s*</u.exec(line);
84
+ if (member?.[1] !== void 0) methods.add(member[1]);
85
+ }
86
+ surfaces.push({ file, model: open[2], methods: [...methods].sort() });
87
+ i = j;
88
+ }
89
+ return surfaces;
90
+ }
91
+ var sorted = (values) => [...values].sort();
92
+ function createPrismaMethodSurfaceRule(options = {}) {
93
+ const id = options.id ?? DEFAULT_ID;
94
+ const clientGlobs = options.clientGlobs ?? DEFAULT_CLIENT_GLOBS;
95
+ const writeMethods = options.writeMethods ?? import_eslint_plugin_prisma.PRISMA_WRITE_METHODS;
96
+ const readMethods = options.readMethods ?? import_eslint_plugin_prisma.PRISMA_READ_METHODS;
97
+ return {
98
+ id,
99
+ category: "config",
100
+ ciCritical: options.ciCritical ?? true,
101
+ description: "The Prisma reads and writes the rules police partition the generated client's <Model>Delegate method surface exactly, so a Prisma upgrade cannot add an unguarded method.",
102
+ run(ctx) {
103
+ const violations = [];
104
+ const report = (file, message) => {
105
+ violations.push({ file, rule: id, message });
106
+ };
107
+ const listFile = clientGlobs[0] ?? "<clientGlobs>";
108
+ const writes = new Set(writeMethods);
109
+ for (const method of readMethods) {
110
+ if (writes.has(method)) {
111
+ report(listFile, `"${method}" is configured as both a read and a write. Each delegate method belongs to exactly one.`);
112
+ }
113
+ }
114
+ const surfaces = globFiles((pattern) => ctx.glob(pattern), clientGlobs).flatMap(
115
+ (file) => parseDelegateSurfaces(ctx.read(file) ?? "", file)
116
+ ).filter((surface) => surface.methods.length > 0);
117
+ const [reference] = surfaces;
118
+ if (reference === void 0) {
119
+ report(
120
+ listFile,
121
+ `No generated Prisma client found under ${clientGlobs.map((glob) => `"${glob}"`).join(", ")}, so the method surface cannot be checked. Run \`prisma generate\` before lint-meta, or point \`clientGlobs\` at the generator's output.`
122
+ );
123
+ return violations;
124
+ }
125
+ const referenceKey = reference.methods.join(",");
126
+ for (const surface of surfaces) {
127
+ if (surface.methods.join(",") !== referenceKey) {
128
+ report(
129
+ surface.file,
130
+ `${surface.model}Delegate exposes [${surface.methods.join(", ")}], which differs from ${reference.model}Delegate's [${reference.methods.join(", ")}]. The method lists are model-agnostic, so they cannot describe both.`
131
+ );
132
+ }
133
+ }
134
+ const configured = /* @__PURE__ */ new Set([...writeMethods, ...readMethods]);
135
+ const actual = new Set(reference.methods);
136
+ const unguarded = sorted(reference.methods.filter((method) => !configured.has(method)));
137
+ const phantom = sorted([...configured].filter((method) => !actual.has(method)));
138
+ if (unguarded.length > 0) {
139
+ report(
140
+ reference.file,
141
+ `The generated client exposes ${unguarded.map((m) => `"${m}"`).join(", ")}, which is neither a configured read nor a configured write, so every rule guarding Prisma calls by method name misses it. Classify it in \`writeMethods\` or \`readMethods\` (and in the rules that read those lists).`
142
+ );
143
+ }
144
+ if (phantom.length > 0) {
145
+ report(
146
+ reference.file,
147
+ `${phantom.map((m) => `"${m}"`).join(", ")} is configured but the generated client no longer exposes it. Drop it from the lists, or check the client was regenerated for this Prisma version.`
148
+ );
149
+ }
150
+ return violations;
151
+ }
152
+ };
153
+ }
154
+
155
+ // src/prisma/tenant-model-registry-parity.ts
156
+ var import_eslint_plugin_prisma2 = require("@noctcore/eslint-plugin-prisma");
157
+
158
+ // src/resolved-config/resolve.ts
159
+ var import_node_fs = require("fs");
160
+ var import_node_path = __toESM(require("path"), 1);
161
+ var import_eslint = require("eslint");
162
+ function severityOf(entry) {
163
+ const raw = Array.isArray(entry) ? entry[0] : entry;
164
+ if (raw === 1 || raw === "warn") return 1;
165
+ if (raw === 2 || raw === "error") return 2;
166
+ return 0;
167
+ }
168
+ function firstOptionOf(entry) {
169
+ if (!Array.isArray(entry)) return null;
170
+ const options = entry[1];
171
+ return typeof options === "object" && options !== null && !Array.isArray(options) ? options : null;
172
+ }
173
+ async function resolveRules(root, packageDir, probes) {
174
+ let cwd = import_node_path.default.resolve(root, packageDir);
175
+ try {
176
+ cwd = (0, import_node_fs.realpathSync)(cwd);
177
+ } catch (error) {
178
+ return { ok: false, error: String(error) };
179
+ }
180
+ const eslint = new import_eslint.ESLint({ cwd, errorOnUnmatchedPattern: false });
181
+ const rules = [];
182
+ const ignored = [];
183
+ for (const probe of probes) {
184
+ let config;
185
+ try {
186
+ config = await eslint.calculateConfigForFile(import_node_path.default.join(cwd, probe));
187
+ } catch (error) {
188
+ return { ok: false, error: `resolving "${probe}": ${String(error)}` };
189
+ }
190
+ if (config === void 0) ignored.push(probe);
191
+ else rules.push(config.rules ?? {});
192
+ }
193
+ if (rules.length > 0) return { ok: true, rules };
194
+ return {
195
+ ok: false,
196
+ error: probes.length === 0 ? "no probe files are configured" : `every probe (${ignored.map((probe) => `"${probe}"`).join(", ")}) is ignored by the config or matched by no block`
197
+ };
198
+ }
199
+
200
+ // src/prisma/tenant-model-registry-parity.ts
201
+ var DEFAULT_ID2 = "tenant-model-registry-parity";
202
+ var DEFAULT_SCHEMA_PATH = "prisma/schema.prisma";
203
+ var DEFAULT_TENANT_FIELDS = ["tenantId"];
204
+ var DEFAULT_PROBE = "src/__lint_meta_probe__.ts";
205
+ var DEFAULT_MODEL_RULES = [
206
+ "noctcore-prisma/no-cross-tenant-id-in-where",
207
+ "noctcore-prisma/tenant-scoped-tables-require-where",
208
+ "noctcore-prisma/tenant-write-must-carry-tenant-id"
209
+ ];
210
+ var DEFAULT_HAND_SCOPED_RULE = "noctcore-prisma/tenant-scoped-tables-require-where";
211
+ function blankCommentsAndStrings(source) {
212
+ const out = source.split("");
213
+ let i = 0;
214
+ while (i < source.length) {
215
+ const two = source.slice(i, i + 2);
216
+ if (two === "//") {
217
+ while (i < source.length && source[i] !== "\n") out[i++] = " ";
218
+ continue;
219
+ }
220
+ if (two === "/*") {
221
+ const end = source.indexOf("*/", i + 2);
222
+ const stop = end === -1 ? source.length : end + 2;
223
+ for (; i < stop; i += 1) if (source[i] !== "\n") out[i] = " ";
224
+ continue;
225
+ }
226
+ const quote = source[i];
227
+ if (quote === "'" || quote === '"' || quote === "`") {
228
+ i += 1;
229
+ while (i < source.length && source[i] !== quote) {
230
+ if (source[i] === "\\") out[i++] = " ";
231
+ if (i < source.length && source[i] !== "\n") out[i] = " ";
232
+ i += 1;
233
+ }
234
+ i += 1;
235
+ continue;
236
+ }
237
+ i += 1;
238
+ }
239
+ return out.join("");
240
+ }
241
+ var OPENERS = /* @__PURE__ */ new Set(["{", "[", "("]);
242
+ var CLOSERS = /* @__PURE__ */ new Set(["}", "]", ")"]);
243
+ function parseObjectLiteralKeys(source, exportName) {
244
+ const blanked = blankCommentsAndStrings(source);
245
+ const escaped = exportName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
246
+ const declaration = new RegExp(`\\b${escaped}\\s*(?::[^=]+)?=\\s*\\{`, "u").exec(blanked);
247
+ if (declaration === null) return null;
248
+ const open = declaration.index + declaration[0].length - 1;
249
+ const keys = [];
250
+ let depth = 0;
251
+ let token = "";
252
+ for (let i = open; i < blanked.length; i += 1) {
253
+ const char = blanked[i] ?? "";
254
+ if (OPENERS.has(char)) {
255
+ depth += 1;
256
+ token = "";
257
+ } else if (CLOSERS.has(char)) {
258
+ depth -= 1;
259
+ token = "";
260
+ if (depth === 0) return keys;
261
+ } else if (depth === 1 && char === ":") {
262
+ const key = token.trim();
263
+ if (/^[A-Za-z_$][\w$]*$/u.test(key)) keys.push(key);
264
+ token = "";
265
+ } else if (depth === 1 && char === ",") {
266
+ token = "";
267
+ } else if (depth === 1) {
268
+ token += char;
269
+ }
270
+ }
271
+ return null;
272
+ }
273
+ function readSchema(ctx, schemaPath) {
274
+ const base = schemaPath.replace(/\/+$/u, "");
275
+ const files = ctx.glob(`${base}/**/*.prisma`).sort();
276
+ const texts = files.length > 0 ? files.map((file) => ctx.read(file) ?? "") : [ctx.read(base)];
277
+ if (texts.some((text) => text === null)) return null;
278
+ const parsed = (0, import_eslint_plugin_prisma2.parsePrismaSchema)(texts.join("\n"));
279
+ return parsed.models.length > 0 ? parsed : null;
280
+ }
281
+ function fingerprint(models) {
282
+ return Object.keys(models).sort().map((model) => `${model}:${[...models[model] ?? []].sort().join("+")}`).join(",");
283
+ }
284
+ function handScopeMessages(registry) {
285
+ const unscoped = registry.unscopedByDesign;
286
+ const handScoped = registry.handScopedModels ?? {};
287
+ const pending = registry.handScopePending ?? {};
288
+ const messages = [];
289
+ for (const model of Object.keys(unscoped)) {
290
+ if (Object.hasOwn(handScoped, model) || Object.hasOwn(pending, model)) continue;
291
+ messages.push(
292
+ `\`${model}\` is exempt from the tenant-scope extension but has no \`handScopedModels\` entry, so "scoped by hand" is enforced by nothing. List the column(s) every query on it must filter by, or record why it cannot have one in \`handScopePending\`.`
293
+ );
294
+ }
295
+ for (const model of Object.keys(handScoped)) {
296
+ if (!Object.hasOwn(unscoped, model)) {
297
+ messages.push(
298
+ `\`${model}\` has a \`handScopedModels\` entry but is not exempt from the extension, so the static guard polices a model the runtime boundary already scopes. Drop the entry, or add the exemption it belongs to.`
299
+ );
300
+ }
301
+ }
302
+ for (const model of Object.keys(pending)) {
303
+ if (!Object.hasOwn(unscoped, model)) {
304
+ messages.push(
305
+ `\`${model}\` is recorded in \`handScopePending\` but is not exempt from the extension any more. The note is stale: delete it.`
306
+ );
307
+ }
308
+ }
309
+ return messages;
310
+ }
311
+ function createTenantModelRegistryParityRule(options = {}) {
312
+ const id = options.id ?? DEFAULT_ID2;
313
+ const schemaPath = options.schemaPath ?? DEFAULT_SCHEMA_PATH;
314
+ const tenantFields = options.tenantFields ?? DEFAULT_TENANT_FIELDS;
315
+ const registry = options.registry ?? { scopedModels: [], unscopedByDesign: {} };
316
+ const source = options.scopedModelsSource;
317
+ const registryFile = options.registryFile ?? source?.file ?? schemaPath;
318
+ const requireHandScope = options.requireHandScope ?? true;
319
+ const eslint = options.eslint === false ? null : options.eslint ?? {};
320
+ return {
321
+ id,
322
+ category: "config",
323
+ ciCritical: options.ciCritical ?? true,
324
+ description: "Every tenant-bearing Prisma model is scoped by the runtime extension or exempt with a reason, and the tenant lint rules resolve with exactly that registry.",
325
+ async runAsync(ctx) {
326
+ const violations = [];
327
+ const report = (file, message) => {
328
+ violations.push({ file, rule: id, message });
329
+ };
330
+ let scopedModels = registry.scopedModels;
331
+ if (source !== void 0) {
332
+ const text = ctx.read(source.file);
333
+ const keys = text === null ? null : parseObjectLiteralKeys(text, source.exportName);
334
+ if (keys === null || keys.length === 0) {
335
+ report(
336
+ source.file,
337
+ `Could not read the \`${source.exportName}\` object literal in ${source.file}, so the runtime scope map cannot be checked. The rule expects \`const ${source.exportName} = { model: ..., ... }\`; if the declaration moved or changed shape, update \`scopedModelsSource\` rather than dropping the check.`
338
+ );
339
+ return violations;
340
+ }
341
+ scopedModels = keys;
342
+ }
343
+ if (requireHandScope) {
344
+ for (const message of handScopeMessages(registry)) report(registryFile, message);
345
+ }
346
+ const parsed = readSchema(ctx, schemaPath);
347
+ if (parsed === null) {
348
+ report(
349
+ schemaPath,
350
+ `Could not read a Prisma schema with any model at "${schemaPath}", so tenant-bearing models cannot be derived. Point \`schemaPath\` at the schema file or folder rather than dropping the check.`
351
+ );
352
+ } else {
353
+ for (const message of (0, import_eslint_plugin_prisma2.reconcileTenantRegistry)({
354
+ parsed,
355
+ tenantFields,
356
+ scopedModels,
357
+ unscopedByDesign: registry.unscopedByDesign
358
+ }).messages) {
359
+ report(source?.file ?? registryFile, message);
360
+ }
361
+ }
362
+ if (eslint === null) return violations;
363
+ const cwd = eslint.cwd ?? ".";
364
+ const configFile = eslint.configFile ?? cwd;
365
+ const modelsOption = eslint.modelsOption ?? "tenantModels";
366
+ const handScopedOption = eslint.handScopedOption ?? "handScopedModels";
367
+ const handScopedRule = eslint.handScopedRule === void 0 ? DEFAULT_HAND_SCOPED_RULE : eslint.handScopedRule;
368
+ const outcome = await resolveRules(ctx.root, cwd, [eslint.probe ?? DEFAULT_PROBE]);
369
+ if (!outcome.ok) {
370
+ report(
371
+ configFile,
372
+ `Could not resolve the effective ESLint config for "${cwd}", so the tenant rules' registry cannot be checked (if it imports a workspace package, build that first): ${outcome.error}`
373
+ );
374
+ return violations;
375
+ }
376
+ const rules = outcome.rules[0] ?? {};
377
+ const scopedSet = new Set(scopedModels);
378
+ for (const ruleId of eslint.modelRules ?? DEFAULT_MODEL_RULES) {
379
+ const entry = rules[ruleId];
380
+ const raw = firstOptionOf(entry)?.[modelsOption];
381
+ if (severityOf(entry) === 0) {
382
+ report(
383
+ configFile,
384
+ `"${ruleId}" is off (or not configured) for "${cwd}", so nothing statically polices the ${String(scopedModels.length)} tenant-scoped model(s). Enable it with \`${modelsOption}\` set to the scoped models.`
385
+ );
386
+ continue;
387
+ }
388
+ if (!Array.isArray(raw) || !raw.every((model) => typeof model === "string")) {
389
+ report(
390
+ configFile,
391
+ `"${ruleId}" resolves with no \`${modelsOption}\` option for "${cwd}", so it falls back to its default and does not police the ${String(scopedModels.length)} tenant-scoped model(s). Pass the scoped models to it.`
392
+ );
393
+ continue;
394
+ }
395
+ const configured = new Set(raw);
396
+ for (const model of scopedModels) {
397
+ if (!configured.has(model)) {
398
+ report(
399
+ configFile,
400
+ `\`${model}\` is tenant-scoped at runtime but is MISSING from the \`${modelsOption}\` option of "${ruleId}", so no static rule polices it. Add it to the list the config passes.`
401
+ );
402
+ }
403
+ }
404
+ for (const model of configured) {
405
+ if (!scopedSet.has(model)) {
406
+ report(
407
+ configFile,
408
+ `\`${model}\` is in the \`${modelsOption}\` option of "${ruleId}" but is not tenant-scoped at runtime, so lint guards a model the runtime boundary does not isolate. Scope it at runtime, or drop it from the list the config passes.`
409
+ );
410
+ }
411
+ }
412
+ }
413
+ const expectedHandScoped = registry.handScopedModels ?? {};
414
+ if (handScopedRule !== null && Object.keys(expectedHandScoped).length > 0) {
415
+ const raw = firstOptionOf(rules[handScopedRule])?.[handScopedOption];
416
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
417
+ report(
418
+ configFile,
419
+ `"${handScopedRule}" resolves with no \`${handScopedOption}\` option for "${cwd}", so the models the extension does NOT scope (${Object.keys(expectedHandScoped).sort().join(", ")}) are policed by nothing. Pass the registry's hand-scope map to it.`
420
+ );
421
+ } else {
422
+ const resolved = fingerprint(raw);
423
+ const expected = fingerprint(expectedHandScoped);
424
+ if (resolved !== expected) {
425
+ report(
426
+ configFile,
427
+ `The \`${handScopedOption}\` option "${handScopedRule}" resolves to does not match the registry (resolved: ${resolved}; expected: ${expected}). Pass the registry map itself rather than a hand-written copy.`
428
+ );
429
+ }
430
+ }
431
+ }
432
+ return violations;
433
+ }
434
+ };
435
+ }
436
+ // Annotate the CommonJS export names for ESM import in node:
437
+ 0 && (module.exports = {
438
+ createPrismaMethodSurfaceRule,
439
+ createTenantModelRegistryParityRule,
440
+ parseDelegateSurfaces,
441
+ parseObjectLiteralKeys
442
+ });
@@ -0,0 +1,174 @@
1
+ import { IMetaRule } from '@noctcore/harness';
2
+ import { TenantRegistry } from '@noctcore/eslint-plugin-prisma';
3
+ export { TenantRegistry } from '@noctcore/eslint-plugin-prisma';
4
+
5
+ /**
6
+ * Options for {@link createPrismaMethodSurfaceRule}.
7
+ *
8
+ * Every path is relative to the repo root the harness runs in.
9
+ */
10
+ interface PrismaMethodSurfaceOptions {
11
+ /** Rule id, for running more than one instance. Default `prisma-method-surface`. */
12
+ readonly id?: string;
13
+ /**
14
+ * Globs of the GENERATED Prisma client files that declare the
15
+ * `<Model>Delegate` interfaces. Default: the `prisma-client` generator's
16
+ * per-model files under `generated/prisma/models/`, and the
17
+ * `prisma-client-js` generator's `node_modules/.prisma/client/index.d.ts`.
18
+ */
19
+ readonly clientGlobs?: readonly string[];
20
+ /**
21
+ * The methods your rules police as WRITES. Default: `PRISMA_WRITE_METHODS`
22
+ * from `@noctcore/eslint-plugin-prisma`, which is what its rules read.
23
+ */
24
+ readonly writeMethods?: readonly string[];
25
+ /**
26
+ * The methods your rules know to be READS. Default: `PRISMA_READ_METHODS`
27
+ * from `@noctcore/eslint-plugin-prisma`.
28
+ */
29
+ readonly readMethods?: readonly string[];
30
+ /** Whether a violation fails CI. Default `true`. */
31
+ readonly ciCritical?: boolean;
32
+ }
33
+ /** One `<Model>Delegate` interface: where it was read, and its query methods. */
34
+ interface DelegateSurface {
35
+ readonly file: string;
36
+ readonly model: string;
37
+ readonly methods: readonly string[];
38
+ }
39
+ /**
40
+ * Every `<Model>Delegate` interface declared in `source`, with its query methods.
41
+ *
42
+ * A delegate member is a generic method, `name<T ...>(...)`, declared at the
43
+ * interface's member indentation, one per line. That holds for both Prisma
44
+ * generators, so a line scan is enough and needs no TypeScript parse. Anything
45
+ * that is not a generic method (`fields`, the `[K: symbol]` brand) is not a
46
+ * query method and is dropped, as is anything `$`-prefixed.
47
+ */
48
+ declare function parseDelegateSurfaces(source: string, file?: string): DelegateSurface[];
49
+ /**
50
+ * The model-delegate method surface the rules police must be the surface the
51
+ * generated Prisma client actually exposes.
52
+ *
53
+ * Rules that guard Prisma calls by method name read a hand-written list, and a
54
+ * list falls behind the client: Prisma adds a method, nobody edits the list, and
55
+ * the new method is invisible to every fence at once. This reads the GENERATED
56
+ * client's `<Model>Delegate` interfaces and asserts the configured reads and
57
+ * writes partition them exactly, so an upgrade that adds a method turns CI red
58
+ * instead of silently widening an unguarded surface.
59
+ *
60
+ * Fails closed: no generated client is a violation, never a vacuous pass, since
61
+ * a checkout that never ran `prisma generate` is exactly where drift hides.
62
+ */
63
+ declare function createPrismaMethodSurfaceRule(options?: PrismaMethodSurfaceOptions): IMetaRule;
64
+
65
+ /** Where the runtime scope map is declared, when it is read from source. */
66
+ interface ScopedModelsSource {
67
+ /** File declaring the map, e.g. the tenant-scope client extension. */
68
+ readonly file: string;
69
+ /** Name of the `const` whose object-literal KEYS are the scoped delegate accessors. */
70
+ readonly exportName: string;
71
+ }
72
+ /**
73
+ * The STATIC side: the lint config whose tenant rules must receive the registry.
74
+ * Read from the RESOLVED config, so it asserts what the rules actually get.
75
+ */
76
+ interface TenantRegistryEslintOptions {
77
+ /** Directory whose ESLint config is resolved. Default `.`. */
78
+ readonly cwd?: string;
79
+ /** File, relative to `cwd`, the config is resolved for; match your tenant rules' glob. Default `src/__lint_meta_probe__.ts`. */
80
+ readonly probe?: string;
81
+ /**
82
+ * Rules whose `modelsOption` must equal the scoped models. Default the three
83
+ * `noctcore-prisma` tenant rules.
84
+ */
85
+ readonly modelRules?: readonly string[];
86
+ /** The option those rules take the model list in. Default `tenantModels`. */
87
+ readonly modelsOption?: string;
88
+ /**
89
+ * The rule whose `handScopedOption` must equal `registry.handScopedModels`, or
90
+ * `null` to skip. Default `noctcore-prisma/tenant-scoped-tables-require-where`.
91
+ */
92
+ readonly handScopedRule?: string | null;
93
+ /** The option that rule takes the hand-scope map in. Default `handScopedModels`. */
94
+ readonly handScopedOption?: string;
95
+ /** Path reported for config-side violations. Default: `cwd`. */
96
+ readonly configFile?: string;
97
+ }
98
+ /**
99
+ * Options for {@link createTenantModelRegistryParityRule}.
100
+ *
101
+ * Every path is relative to the repo root the harness runs in.
102
+ */
103
+ interface TenantModelRegistryParityOptions {
104
+ /** Rule id, for running more than one instance. Default `tenant-model-registry-parity`. */
105
+ readonly id?: string;
106
+ /**
107
+ * The Prisma schema: a `.prisma` file, or a folder whose `.prisma` files
108
+ * (recursively) are read together. Default `prisma/schema.prisma`.
109
+ */
110
+ readonly schemaPath?: string;
111
+ /**
112
+ * The columns that make a model tenant-bearing: a model carrying ALL of them
113
+ * must be scoped or exempt. Default `['tenantId']`.
114
+ */
115
+ readonly tenantFields?: readonly string[];
116
+ /**
117
+ * The project's registry, injected as data (`TenantRegistry` from
118
+ * `@noctcore/eslint-plugin-prisma`). `scopedModels` is ignored when
119
+ * `scopedModelsSource` is set. Default: empty.
120
+ */
121
+ readonly registry?: TenantRegistry;
122
+ /**
123
+ * Read the scoped models from the runtime source instead of
124
+ * `registry.scopedModels`: the keys of an object literal
125
+ * (`export const SCOPED = { invoice: [...], ... }`). Prefer this when the
126
+ * runtime map is the source of truth, so the check reads it rather than a copy.
127
+ */
128
+ readonly scopedModelsSource?: ScopedModelsSource;
129
+ /**
130
+ * Require every unscoped-by-design model to name its hand-scope columns
131
+ * (`registry.handScopedModels`) or record why it cannot
132
+ * (`registry.handScopePending`). Default `true`.
133
+ */
134
+ readonly requireHandScope?: boolean;
135
+ /** The static side, or `false` to check only the registry against the schema. */
136
+ readonly eslint?: TenantRegistryEslintOptions | false;
137
+ /**
138
+ * Path reported for registry-side violations (the hand-scope maps, and the
139
+ * schema reconciliation when there is no `scopedModelsSource`). Default:
140
+ * `scopedModelsSource.file`, else `schemaPath`.
141
+ */
142
+ readonly registryFile?: string;
143
+ /** Whether a violation fails CI. Default `true`. */
144
+ readonly ciCritical?: boolean;
145
+ }
146
+ /**
147
+ * Top-level keys of the object literal assigned to `exportName`, or `null` when
148
+ * the declaration cannot be located (reported, never read as an empty map).
149
+ *
150
+ * Reads a comment- and string-blanked copy while tracking bracket depth, so a
151
+ * nested object or an array holding a `:` is never mistaken for an entry.
152
+ * Quoted keys (`'invoice': [...]`) are blanked with the strings, so a map must
153
+ * use identifier keys, as a delegate-accessor map does.
154
+ */
155
+ declare function parseObjectLiteralKeys(source: string, exportName: string): string[] | null;
156
+ /**
157
+ * The tenant-model registries must mirror each other AND the Prisma schema.
158
+ *
159
+ * A multi-tenant Prisma project keeps "which models are tenant-scoped?" in two
160
+ * hand-maintained places, the runtime scope map and the model list its tenant
161
+ * lint rules are configured with, and agreement between those two says nothing
162
+ * about a table neither has heard of. So the SCHEMA is the third input: every
163
+ * model carrying the tenant columns must be scoped or explicitly exempt with a
164
+ * reason, and a new tenant-bearing model cannot ship without the guardrails
165
+ * learning about it.
166
+ *
167
+ * The static side is read from the RESOLVED ESLint config, not by parsing the
168
+ * config file: moving the array, spreading a different preset, or handing one
169
+ * rule a hand-written list is all caught. Fails closed on an unreadable schema,
170
+ * runtime map or config. Needs the optional `eslint` peer unless `eslint: false`.
171
+ */
172
+ declare function createTenantModelRegistryParityRule(options?: TenantModelRegistryParityOptions): IMetaRule;
173
+
174
+ export { type DelegateSurface, type PrismaMethodSurfaceOptions, type ScopedModelsSource, type TenantModelRegistryParityOptions, type TenantRegistryEslintOptions, createPrismaMethodSurfaceRule, createTenantModelRegistryParityRule, parseDelegateSurfaces, parseObjectLiteralKeys };