@noctcore/lint-meta-rules 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -81,3 +81,25 @@ they are inert until you point them at your catalogs.
81
81
  | Factory | Category | What it enforces |
82
82
  | --- | --- | --- |
83
83
  | [`createTranslationDeadKeysRule`](./docs/rules/translation-dead-keys.md) | source-text | Every catalog key is reachable: named by a translation call, or spelled by some string in the source. |
84
+
85
+ ### `@noctcore/lint-meta-rules/resolved-config`
86
+
87
+ Checks over the RESOLVED ESLint config. They load ESLint and resolve configs through
88
+ `calculateConfigForFile`, which is async, so they implement the harness's `runAsync`
89
+ (`@noctcore/harness` 0.3.0 or newer) and need the optional peer `eslint`.
90
+
91
+ | Factory | Category | What it enforces |
92
+ | --- | --- | --- |
93
+ | [`createEslintConfigNoWarnRule`](./docs/rules/eslint-config-no-warn.md) | config | No rule RESOLVES to `warn`, including a severity a spread preset injects, which the text scan of `no-warn-severity` cannot see. |
94
+
95
+ ### `@noctcore/lint-meta-rules/prisma`
96
+
97
+ Whole-repo Prisma guardrails that keep `@noctcore/eslint-plugin-prisma`'s inputs honest. They read
98
+ the plugin's method sets, schema parser and registry reconciliation, so the lint rules and these
99
+ checks cannot disagree. The registry parity check resolves an ESLint config (async, `runAsync`) and
100
+ needs the optional peer `eslint`. Neither is in `RULE_FACTORIES`: both need the project's paths.
101
+
102
+ | Factory | Category | What it enforces |
103
+ | --- | --- | --- |
104
+ | [`createTenantModelRegistryParityRule`](./docs/rules/tenant-model-registry-parity.md) | config | Every tenant-bearing schema model is scoped at runtime or exempt with a reason, and the tenant lint rules resolve with exactly that registry. |
105
+ | [`createPrismaMethodSurfaceRule`](./docs/rules/prisma-method-surface.md) | config | The reads and writes the rules police are exactly the generated client's delegate methods, so a Prisma upgrade cannot add an unguarded one. |
@@ -0,0 +1,47 @@
1
+ // src/resolved-config/resolve.ts
2
+ import { realpathSync } from "fs";
3
+ import path from "path";
4
+ import { ESLint } from "eslint";
5
+ function severityOf(entry) {
6
+ const raw = Array.isArray(entry) ? entry[0] : entry;
7
+ if (raw === 1 || raw === "warn") return 1;
8
+ if (raw === 2 || raw === "error") return 2;
9
+ return 0;
10
+ }
11
+ function firstOptionOf(entry) {
12
+ if (!Array.isArray(entry)) return null;
13
+ const options = entry[1];
14
+ return typeof options === "object" && options !== null && !Array.isArray(options) ? options : null;
15
+ }
16
+ async function resolveRules(root, packageDir, probes) {
17
+ let cwd = path.resolve(root, packageDir);
18
+ try {
19
+ cwd = realpathSync(cwd);
20
+ } catch (error) {
21
+ return { ok: false, error: String(error) };
22
+ }
23
+ const eslint = new ESLint({ cwd, errorOnUnmatchedPattern: false });
24
+ const rules = [];
25
+ const ignored = [];
26
+ for (const probe of probes) {
27
+ let config;
28
+ try {
29
+ config = await eslint.calculateConfigForFile(path.join(cwd, probe));
30
+ } catch (error) {
31
+ return { ok: false, error: `resolving "${probe}": ${String(error)}` };
32
+ }
33
+ if (config === void 0) ignored.push(probe);
34
+ else rules.push(config.rules ?? {});
35
+ }
36
+ if (rules.length > 0) return { ok: true, rules };
37
+ return {
38
+ ok: false,
39
+ 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`
40
+ };
41
+ }
42
+
43
+ export {
44
+ severityOf,
45
+ firstOptionOf,
46
+ resolveRules
47
+ };
@@ -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
+ });