@geonosis/oxlint-plugin-biological-architecture 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3916 @@
1
+ import {
2
+ PRESETS
3
+ } from "./chunk-74VWOEPL.js";
4
+
5
+ // src/rules/atom-no-deps.ts
6
+ var FORBIDDEN_PATTERNS = [
7
+ /molecules\//,
8
+ /compounds\//,
9
+ /organelles\//,
10
+ /cells\//,
11
+ /tissues\//,
12
+ /stores\//,
13
+ /domains/,
14
+ /\/app\//
15
+ ];
16
+ var atomNoDeps = {
17
+ create(context) {
18
+ const filename = context.filename;
19
+ const isAtom = /\/atoms\//.test(filename);
20
+ if (!isAtom) return {};
21
+ const fromModule = (node) => {
22
+ if (!node.source) return;
23
+ const source = node.source.value;
24
+ for (const pattern of FORBIDDEN_PATTERNS) {
25
+ if (pattern.test(source)) {
26
+ context.report({
27
+ data: { source },
28
+ messageId: "forbidden",
29
+ node
30
+ });
31
+ break;
32
+ }
33
+ }
34
+ };
35
+ return {
36
+ ExportAllDeclaration: fromModule,
37
+ ExportNamedDeclaration: fromModule,
38
+ ImportDeclaration: fromModule
39
+ };
40
+ },
41
+ fixShape: `Raw \`<button>\`, \`<input>\`, or a \`<div>\` with border + background in levels 2-6 belongs in an atom.
42
+ An atom imports nothing above itself \u2014 no molecule, compound, organelle, cell, tissue, store or domain,
43
+ and that includes \`export \u2026 from\`. If an atom needs a shared constant, the constant moves down to the atom
44
+ tier or into a plain \`lib/\` module, never the other way round.`,
45
+ meta: {
46
+ docs: {
47
+ description: "Atoms cannot import from molecules, compounds, organelles, cells, stores, or domains"
48
+ },
49
+ messages: {
50
+ forbidden: "Atoms cannot import from {{source}}. Atoms are the lowest level and must not depend on higher-level components."
51
+ },
52
+ schema: [],
53
+ type: "problem"
54
+ }
55
+ };
56
+ var atom_no_deps_default = atomNoDeps;
57
+
58
+ // src/rules/cell-must-be-stateful.ts
59
+ import * as fs from "fs";
60
+ var STATE_SIGNATURE = /\buse[A-Z]\w*\s*\(|from\s+['"][^'"]*\/organelles(?:\/|['"])|from\s+['"][^'"]*\/stores?(?:\/|['"])|createContext\s*\(/;
61
+ var SIBLING_SUFFIXES = [
62
+ ".tsx",
63
+ ".parts.tsx",
64
+ ".sections.tsx",
65
+ ".layouts.tsx",
66
+ ".icons.tsx",
67
+ ".sidebar.tsx",
68
+ ".summary.tsx",
69
+ ".form.tsx",
70
+ ".data.tsx",
71
+ ".steps.tsx"
72
+ ];
73
+ var PRIVATE_OR_NON_ENTRY = /\.(parts|sections|layouts|types|icons|sidebar|summary|form|data|steps|stories|test|spec)\.tsx$/;
74
+ function isReexportOnly(file) {
75
+ try {
76
+ const src = fs.readFileSync(file, "utf8").replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/\/\/.*$/gm, "");
77
+ const hasReexport = /\bexport\b[^;]*\bfrom\b\s*['"]/.test(src);
78
+ const hasComponent = /\bfunction\b|=>|\breturn\b|<[A-Za-z]/.test(src);
79
+ return hasReexport && !hasComponent;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+ function familyHasState(mainFile) {
85
+ const stem = mainFile.replace(/\.tsx$/, "");
86
+ for (const suffix of SIBLING_SUFFIXES) {
87
+ const file = stem + suffix;
88
+ try {
89
+ if (fs.existsSync(file) && STATE_SIGNATURE.test(fs.readFileSync(file, "utf8"))) {
90
+ return true;
91
+ }
92
+ } catch {
93
+ }
94
+ }
95
+ return false;
96
+ }
97
+ var cellMustBeStateful = {
98
+ create(context) {
99
+ const normalized = context.filename.replaceAll(/\\/g, "/");
100
+ const base = normalized.split("/").pop() || "";
101
+ const isCellEntrypoint = /(?:^|\/)cells\//.test(normalized) && base.endsWith(".tsx") && base !== "index.tsx" && !PRIVATE_OR_NON_ENTRY.test(base);
102
+ if (!isCellEntrypoint) return {};
103
+ if (isReexportOnly(context.filename)) return {};
104
+ let programNode = null;
105
+ return {
106
+ Program(node) {
107
+ programNode = node;
108
+ },
109
+ "Program:exit"() {
110
+ if (!familyHasState(normalized)) {
111
+ context.report({
112
+ data: { file: base },
113
+ messageId: "shouldBeCompound",
114
+ node: programNode
115
+ });
116
+ }
117
+ }
118
+ };
119
+ },
120
+ fixShape: `A cell OWNS state: a \`use*()\` call, an organelle import, or a store read, somewhere in its family
121
+ (\`x.tsx\` + \`x.parts.tsx\` + \`x.sections.tsx\` + \u2026). A file in \`cells/\` that is props-in / JSX-out with
122
+ no state anywhere in that family is inert chemistry \u2014 move it to \`compounds/\`. If it should be
123
+ stateful, put the store read or the organelle in it rather than taking the data as a prop.`,
124
+ meta: {
125
+ docs: {
126
+ description: "A cell must own state (a hook, a state-bearing organelle, or a store) somewhere in its family. A cell whose whole family is stateless props-in/JSX-out is a compound \u2014 move it to compounds/."
127
+ },
128
+ messages: {
129
+ shouldBeCompound: 'Cell "{{file}}" holds no state (no hooks, no organelle, no store) anywhere in its family \u2014 it is a stateless display and belongs in compounds/, not cells/.'
130
+ },
131
+ schema: [],
132
+ type: "problem"
133
+ }
134
+ };
135
+ var cell_must_be_stateful_default = cellMustBeStateful;
136
+
137
+ // src/rules/cell-must-not-compose-cell.ts
138
+ import * as path from "path";
139
+ function cellStem(filePath) {
140
+ const base = filePath.split("/").pop() || filePath;
141
+ return base.replace(/\.[jt]sx?$/, "").replace(
142
+ /\.(parts|sections|summary|layouts|icons|types|hooks|utils|context|store|sidebar|data|config)$/i,
143
+ ""
144
+ );
145
+ }
146
+ var cellMustNotComposeCell = {
147
+ create(context) {
148
+ const filename = context.filename;
149
+ const normalized = filename.replaceAll(/\\/g, "/");
150
+ if (!/(?:^|\/)cells\//.test(normalized)) return {};
151
+ const currentFile = normalized.split("/").pop() || normalized;
152
+ const currentDir = path.posix.dirname(normalized);
153
+ const importerStem = cellStem(normalized);
154
+ function report(importedPath, node) {
155
+ if (cellStem(importedPath) === importerStem) return;
156
+ context.report({
157
+ data: { currentFile, importedFile: importedPath.split("/").pop() || importedPath },
158
+ messageId: "forbidden",
159
+ node
160
+ });
161
+ }
162
+ const fromModule = (node) => {
163
+ if (!node.source) return;
164
+ if (node.importKind === "type") return;
165
+ const source = node.source.value;
166
+ if (/(?:^|\/)cells\//.test(source)) {
167
+ report(source, node);
168
+ return;
169
+ }
170
+ if (source.startsWith("./") || source.startsWith("../")) {
171
+ const resolved = path.posix.normalize(path.posix.join(currentDir, source));
172
+ if (/(?:^|\/)cells\//.test(resolved)) {
173
+ report(resolved, node);
174
+ }
175
+ }
176
+ };
177
+ return {
178
+ ExportAllDeclaration: fromModule,
179
+ ExportNamedDeclaration: fromModule,
180
+ ImportDeclaration: fromModule
181
+ };
182
+ },
183
+ fixShape: `A cell never composes another cell \u2014 not same-feature, not \`shared/\`, no exception. If you want to,
184
+ one of the two is misclassified: arrangement goes up to a \`tissues/\` file, a stateless display goes
185
+ down to \`compounds/\`, a store adapter goes sideways to \`organelles/\`. Private siblings of the SAME
186
+ component (\`invoice-editor.parts.tsx\` from \`invoice-editor.tsx\`) are one cell and are fine.`,
187
+ meta: {
188
+ docs: {
189
+ description: "Cells must not import from any other cell anywhere in the repo. No same-feature exception, no shared/document/charts exception. Cells compose only atoms, molecules, compounds, and organelles. Type-only imports are allowed."
190
+ },
191
+ messages: {
192
+ forbidden: 'Cell "{{currentFile}}" imports from another cell "{{importedFile}}". Cells never compose other cells. Decompose: if this is arrangement, move the parent to tissues/. If the import is a stateless display, move the child to compounds/. If the child is a store adapter, move it to organelles/.'
193
+ },
194
+ schema: [],
195
+ type: "problem"
196
+ }
197
+ };
198
+ var cell_must_not_compose_cell_default = cellMustNotComposeCell;
199
+
200
+ // src/rules/cell-no-tissues.ts
201
+ var HIGHER_LEVEL_PATTERNS = [
202
+ /(?:features\/[^/]+\/)?tissues\//,
203
+ /(?:features\/[^/]+\/)?organs\//,
204
+ /(?:^|\/)layouts\//,
205
+ /apps\/web\/app\//
206
+ ];
207
+ var cellNoTissues = {
208
+ create(context) {
209
+ const filename = context.filename;
210
+ const normalized = filename.replaceAll(/\\/g, "/");
211
+ const isCell = /(?:features\/[^/]+\/)?cells\//.test(normalized);
212
+ if (!isCell) return {};
213
+ const shortName = normalized.split("/").pop() || normalized;
214
+ const fromModule = (node) => {
215
+ if (!node.source) return;
216
+ if (node.importKind === "type") return;
217
+ const source = node.source.value;
218
+ for (const pattern of HIGHER_LEVEL_PATTERNS) {
219
+ if (pattern.test(source)) {
220
+ context.report({
221
+ data: { file: shortName, source },
222
+ messageId: "forbidden",
223
+ node
224
+ });
225
+ return;
226
+ }
227
+ }
228
+ };
229
+ return {
230
+ ExportAllDeclaration: fromModule,
231
+ ExportNamedDeclaration: fromModule,
232
+ ImportDeclaration: fromModule
233
+ };
234
+ },
235
+ fixShape: `A cell cannot import a tissue, an organ, a layout or an app route \u2014 that is the direction
236
+ inverted. If a cell needs a layout around it, take the layout as \`children\` and let a tissue arrange
237
+ both. Type-only imports are exempt; \`export \u2026 from\` is not.`,
238
+ meta: {
239
+ docs: {
240
+ description: "Cells cannot import tissues, organs, or app route files. Direction is wrong: tissues compose cells (not the reverse), and organs compose tissues and cells."
241
+ },
242
+ messages: {
243
+ forbidden: 'Cell "{{file}}" cannot import higher-level component "{{source}}". Direction is wrong: tissues/organs compose cells, not the reverse. If this cell needs to render a layout-like wrapper, the wrapper should be a compound, or this cell should be promoted.'
244
+ },
245
+ schema: [],
246
+ type: "problem"
247
+ }
248
+ };
249
+ var cell_no_tissues_default = cellNoTissues;
250
+
251
+ // src/rules/cells-folder-index-is-barrel.ts
252
+ var BARREL_FILE_PATTERN = /\/cells\/(?:index\.ts|dynamic\.tsx?)$/;
253
+ function describeStatement(stmt) {
254
+ switch (stmt.type) {
255
+ case "ClassDeclaration":
256
+ return "a class declaration";
257
+ case "ExportDefaultDeclaration":
258
+ return "a default export declaration";
259
+ case "ExportNamedDeclaration":
260
+ return "a named export declaration (not a re-export)";
261
+ case "ExpressionStatement":
262
+ return "a top-level expression statement";
263
+ case "FunctionDeclaration":
264
+ return "a function declaration";
265
+ case "ImportDeclaration":
266
+ return "a side-effect import";
267
+ case "VariableDeclaration":
268
+ return "a variable declaration";
269
+ default:
270
+ return `a ${stmt.type} statement`;
271
+ }
272
+ }
273
+ var cellsFolderIndexIsBarrel = {
274
+ create(context) {
275
+ const filename = context.filename;
276
+ const normalized = filename.replaceAll(/\\/g, "/");
277
+ if (!BARREL_FILE_PATTERN.test(normalized)) return {};
278
+ const shortName = normalized.split("/").pop() || normalized;
279
+ return {
280
+ Program(node) {
281
+ for (const stmt of node.body) {
282
+ if (stmt.type === "ExportAllDeclaration") continue;
283
+ if (stmt.type === "ExportNamedDeclaration" && stmt.source) continue;
284
+ if (stmt.type === "ImportDeclaration" && stmt.importKind === "type") continue;
285
+ context.report({
286
+ data: { file: shortName, kind: describeStatement(stmt) },
287
+ messageId: "notBarrel",
288
+ node: stmt
289
+ });
290
+ }
291
+ }
292
+ };
293
+ },
294
+ fixShape: `\`cells/index.ts\` and \`cells/dynamic.ts\` are barrels: \`export \u2026 from\` lines and nothing else. No
295
+ function, class or variable declaration, no side-effect import. Logic that wants to live in the
296
+ barrel belongs in a cell file the barrel re-exports.`,
297
+ meta: {
298
+ docs: {
299
+ description: "Barrel files under cells/ (index.ts, dynamic.ts, dynamic.tsx) must contain ONLY re-export statements. Component definitions belong in their own file."
300
+ },
301
+ messages: {
302
+ notBarrel: 'Barrel file "{{file}}" contains {{kind}}. Barrel files under cells/ must be pure re-export manifests (only `export * from` or `export { X } from`). Component definitions belong in their own file.'
303
+ },
304
+ schema: [],
305
+ type: "problem"
306
+ }
307
+ };
308
+ var cells_folder_index_is_barrel_default = cellsFolderIndexIsBarrel;
309
+
310
+ // src/rules/compound-must-be-stateless.ts
311
+ var reactForbiddenHooks = /* @__PURE__ */ new Set([
312
+ "useCallback",
313
+ "useEffect",
314
+ "useImperativeHandle",
315
+ "useLayoutEffect",
316
+ "useMemo",
317
+ "useReducer",
318
+ "useRef",
319
+ "useState"
320
+ ]);
321
+ var COMPOUND_FILE_PATTERN = /(?:(?:features\/[^/]+\/)?compounds\/|packages\/ui\/src\/compounds\/)/;
322
+ var compoundMustBeStateless = {
323
+ create(context) {
324
+ const filename = context.filename;
325
+ const normalized = filename.replaceAll(/\\/g, "/");
326
+ const isCompound = COMPOUND_FILE_PATTERN.test(normalized);
327
+ if (!isCompound) return {};
328
+ const shortName = normalized.split("/").pop() || normalized;
329
+ return {
330
+ CallExpression(node) {
331
+ if (node.callee.type !== "Identifier") return;
332
+ const name = node.callee.name;
333
+ if (!name || !/^use[A-Z]/.test(name)) return;
334
+ context.report({
335
+ data: { file: shortName, hook: name },
336
+ messageId: "forbiddenHookCall",
337
+ node
338
+ });
339
+ },
340
+ ImportDeclaration(node) {
341
+ if (node.importKind === "type") return;
342
+ const source = node.source.value;
343
+ if (source === "react") {
344
+ const specifiers = node.specifiers ?? [];
345
+ for (const spec of specifiers) {
346
+ if (spec.type !== "ImportSpecifier") continue;
347
+ const importedName = spec.imported?.name;
348
+ if (!importedName) continue;
349
+ if (reactForbiddenHooks.has(importedName)) {
350
+ context.report({
351
+ data: { file: shortName, hook: importedName },
352
+ messageId: "forbiddenReactHook",
353
+ node
354
+ });
355
+ }
356
+ }
357
+ return;
358
+ }
359
+ if (source.includes("/providers/") || source.startsWith("@/providers/")) {
360
+ context.report({
361
+ data: { file: shortName, source },
362
+ messageId: "forbiddenContextImport",
363
+ node
364
+ });
365
+ }
366
+ }
367
+ };
368
+ },
369
+ fixShape: `A compound is inert: no \`useState\`, no \`useEffect\`, no \`useMemo\`, no store read, no provider import \u2014
370
+ no hook call at all. It takes what it renders as props. If it needs state, the caller is a cell or an
371
+ organelle and holds it; if the file itself needs state, it is not a compound.`,
372
+ meta: {
373
+ docs: {
374
+ description: "Compounds are pure stateless props-in/JSX-out components. They cannot: (1) import React hooks from react, (2) call ANY hook (use* functions), (3) import from providers/ or contexts/. If you need state, this is a cell or organelle."
375
+ },
376
+ messages: {
377
+ forbiddenContextImport: 'Compound "{{file}}" imports from "{{source}}". Compounds must not use context/providers \u2014 receive data via props instead.',
378
+ forbiddenHookCall: 'Compound "{{file}}" calls hook "{{hook}}". Compounds are stateless \u2014 no hooks allowed. Move to cells/ or organelles/.',
379
+ forbiddenReactHook: 'Compound "{{file}}" imports "{{hook}}" from react. Compounds are stateless \u2014 move to cells/ or organelles/.'
380
+ },
381
+ schema: [],
382
+ type: "problem"
383
+ }
384
+ };
385
+ var compound_must_be_stateless_default = compoundMustBeStateless;
386
+
387
+ // src/rules/compound-no-stores.ts
388
+ var FORBIDDEN_PATTERNS2 = [
389
+ /stores\//,
390
+ /organelles\//,
391
+ /cells\//,
392
+ /tissues\//,
393
+ /layouts\//,
394
+ /\/app\//
395
+ ];
396
+ var compoundNoStores = {
397
+ create(context) {
398
+ const filename = context.filename;
399
+ if (/__tests__\//.test(filename)) return {};
400
+ const isCompound = /\/compounds\//.test(filename);
401
+ if (!isCompound) return {};
402
+ const fromModule = (node) => {
403
+ if (!node.source) return;
404
+ if (node.importKind === "type") return;
405
+ const source = node.source.value;
406
+ for (const pattern of FORBIDDEN_PATTERNS2) {
407
+ if (pattern.test(source)) {
408
+ context.report({
409
+ data: { source },
410
+ messageId: "forbidden",
411
+ node
412
+ });
413
+ break;
414
+ }
415
+ }
416
+ };
417
+ return {
418
+ ExportAllDeclaration: fromModule,
419
+ ExportNamedDeclaration: fromModule,
420
+ ImportDeclaration: fromModule
421
+ };
422
+ },
423
+ fixShape: `A compound may import atoms, molecules and other compounds. Stores, organelles, cells, tissues,
424
+ layouts and app routes are all above it. Take the data as a prop from the cell that reads the store.
425
+ Type-only imports are exempt; \`export \u2026 from\` is not.`,
426
+ meta: {
427
+ docs: {
428
+ description: "Compounds may only import atoms, molecules, and other compounds. They cannot import organelles, cells, tissues, stores, or app routes \u2014 direction rule: a parent may only compose children at levels <= its own."
429
+ },
430
+ messages: {
431
+ forbidden: "Compounds cannot import from {{source}}. Direction violation \u2014 compounds are level 3 and can only depend on atoms, molecules, other compounds, and shared libs/domains/types."
432
+ },
433
+ schema: [],
434
+ type: "problem"
435
+ }
436
+ };
437
+ var compound_no_stores_default = compoundNoStores;
438
+
439
+ // src/rules/constants-in-constants-file.ts
440
+ var exemptFiles = [/\/constants\.ts$/, /\/__tests__\//, /\/scripts\//, /\/testing\//, /\.d\.ts$/];
441
+ var screamingSnake = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/;
442
+ var constantsInConstantsFile = {
443
+ create(context) {
444
+ const normalized = context.filename.replace(/\\/g, "/");
445
+ if (exemptFiles.some((file) => file.test(normalized))) return {};
446
+ return {
447
+ ExportNamedDeclaration(node) {
448
+ const declaration = node.declaration;
449
+ if (!declaration || declaration.type !== "VariableDeclaration") return;
450
+ for (const declarator of declaration.declarations ?? []) {
451
+ const name = declarator.id.type === "Identifier" ? declarator.id.name : void 0;
452
+ if (name !== void 0 && name.length >= 3 && screamingSnake.test(name)) {
453
+ context.report({ messageId: "outsideConstantsFile", node, data: { name } });
454
+ }
455
+ }
456
+ }
457
+ };
458
+ },
459
+ fixShape: `A \`SCREAMING_SNAKE\` export is a fact, and a fact lives in a \`constants.ts\` beside its owner \u2014 not
460
+ scattered across the module that happens to use it first. Move the declaration, import it back.
461
+ Non-exported module locals are unaffected.`,
462
+ meta: {
463
+ docs: {
464
+ description: "A SCREAMING_SNAKE export is a constant, and constants live in the constants.ts beside their owner."
465
+ },
466
+ messages: {
467
+ outsideConstantsFile: "{{name}} is a constant declared outside constants.ts. Move it to the constants.ts of its domain, feature lib or package."
468
+ },
469
+ schema: [],
470
+ type: "problem"
471
+ }
472
+ };
473
+ var constants_in_constants_file_default = constantsInConstantsFile;
474
+
475
+ // src/rules/dialect-through-the-seam.ts
476
+ var seamPattern = /\/packages\/db\/(src\/(schema\/|dialect\.ts$|client\.ts$|cell\/drivers\/)|drizzle[^/]*\.config\.ts$)/;
477
+ var testPattern = /\/(__tests__|testing)\//;
478
+ var dialectModules = [
479
+ "drizzle-orm/pg-core",
480
+ "drizzle-orm/sqlite-core",
481
+ "drizzle-orm/d1",
482
+ "drizzle-orm/postgres-js",
483
+ "drizzle-orm/node-postgres",
484
+ "drizzle-orm/pglite",
485
+ "drizzle-orm/neon-http",
486
+ "drizzle-orm/neon-serverless",
487
+ "postgres",
488
+ "pg"
489
+ ];
490
+ var postgresWords = /\b(to_tsquery|ts_rank|set_config|current_setting|to_char|to_timestamp|regexp_replace|explain|information_schema|set local)\b/i;
491
+ var isDialectModule = (source) => typeof source === "string" && dialectModules.some((module) => source === module || source.startsWith(`${module}/`));
492
+ var dialectThroughTheSeam = {
493
+ create(context) {
494
+ const normalized = context.filename.replace(/\\/g, "/");
495
+ const atTheSeam = seamPattern.test(normalized);
496
+ const inTests = testPattern.test(normalized);
497
+ const checkImport = (source, node) => {
498
+ if (atTheSeam || !isDialectModule(source)) return;
499
+ context.report({ data: { source }, messageId: "dialectOutsideSeam", node });
500
+ };
501
+ const fromModule = (node) => {
502
+ if (!node.source) return;
503
+ checkImport(node.source?.value, node);
504
+ };
505
+ const checkText = (text, node) => {
506
+ if (atTheSeam || inTests) return;
507
+ const word = postgresWords.exec(text)?.[1];
508
+ if (word === void 0) return;
509
+ context.report({ data: { word }, messageId: "postgresOutsideDialect", node });
510
+ };
511
+ return {
512
+ ExportAllDeclaration: fromModule,
513
+ ExportNamedDeclaration: fromModule,
514
+ ImportDeclaration: fromModule,
515
+ ImportExpression(node) {
516
+ if (node.source?.type !== "Literal") return;
517
+ checkImport(node.source.value, node);
518
+ },
519
+ TaggedTemplateExpression(node) {
520
+ if (node.tag?.type !== "Identifier" || node.tag.name !== "sql") return;
521
+ checkText((node.quasi?.quasis ?? []).map((part) => part.value?.raw ?? "").join(" "), node);
522
+ },
523
+ CallExpression(node) {
524
+ const callee = node.callee;
525
+ if (callee?.type !== "MemberExpression") return;
526
+ if (callee.object?.name !== "sql" || callee.property?.name !== "raw") return;
527
+ const [first] = node.arguments ?? [];
528
+ if (first?.type !== "Literal" || typeof first.value !== "string") return;
529
+ checkText(first.value, node);
530
+ }
531
+ };
532
+ },
533
+ fixShape: `Dialect-typed names (\`drizzle-orm/pg-core\`, \`drizzle-orm/sqlite-core\`, \`drizzle-orm/d1\`, \u2026) are
534
+ imported only at the seam: the schema, \`dialect.ts\`, \`client.ts\`, the drivers, and drizzle-kit's
535
+ configs. Everywhere else, use the dialect-free aliases the seam exports (\`Database\`, \`AnyColumn\`,
536
+ \`rowsOf\`) so the same query compiles against either engine.`,
537
+ meta: {
538
+ docs: {
539
+ description: "The dialect and the driver are named at one seam in packages/db (the schema, dialect.ts, client.ts, cell/drivers, the drizzle-kit configs); nothing else imports a dialect or driver module, and nothing outside the dialect module spells a Postgres function."
540
+ },
541
+ messages: {
542
+ dialectOutsideSeam: 'This file imports "{{source}}". A dialect or driver module is named only at the seam (packages/db/src/schema, dialect.ts, client.ts, cell/drivers) \u2014 use the neutral types and the cell port from @during/db instead.',
543
+ postgresOutsideDialect: '"{{word}}" is Postgres, not SQL. Name the fragment in packages/db/src/dialect.ts and call it from here, so the query reads as the question it asks.'
544
+ },
545
+ schema: [],
546
+ type: "problem"
547
+ }
548
+ };
549
+ var dialect_through_the_seam_default = dialectThroughTheSeam;
550
+
551
+ // src/rules/lib/literal-call.ts
552
+ var literalNamedBy = (node, callee) => {
553
+ if (node.callee.type !== "Identifier" || node.callee.name !== callee) return null;
554
+ const [name] = node.arguments ?? [];
555
+ return name?.type === "Literal" && typeof name.value === "string" ? name.value : null;
556
+ };
557
+
558
+ // src/rules/document-sagas-are-generic.ts
559
+ var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
560
+ var documentSagasAreGeneric = {
561
+ create(context) {
562
+ const prefixes = context.options?.[0]?.perKindPrefixes ?? [];
563
+ if (prefixes.length === 0) return {};
564
+ if (!/\/packages\/workflows\/src\//.test(context.filename.replaceAll("\\", "/"))) return {};
565
+ const perKindPrefix = new RegExp(`^(?:${prefixes.map(escapeForRegex).join("|")})\\.`);
566
+ return {
567
+ CallExpression(node) {
568
+ const name = literalNamedBy(node, "saga");
569
+ if (name === null || !perKindPrefix.test(name)) return;
570
+ context.report({ data: { name }, messageId: "perKindSaga", node });
571
+ }
572
+ };
573
+ },
574
+ fixShape: `A saga named for one document kind (\`invoice.issue\`, \`quote.accept\`) is the hand-written fork the
575
+ generic base entity replaced. Mint it from the generic factory \u2014 \`documentSagas(entity, effects)\` \u2014
576
+ and pass the kind as data. Configure the kind vocabulary through the rule's \`perKindPrefixes\` option.`,
577
+ meta: {
578
+ docs: {
579
+ description: "Document sagas are minted, not written: `documentSagas(entity, effects)` gives every entity that extends Document its create/save/setStatus/remove/send/convert/duplicate/share from one body. A saga literally named for one entity is a fork."
580
+ },
581
+ messages: {
582
+ perKindSaga: '"{{name}}" is a hand-written saga for one entity. Every entity that extends Document gets its sagas from `documentSagas(entity, effects)`; put what this entity decides in its `Document.extend` definition or in its `DocumentEffects`.'
583
+ },
584
+ schema: [
585
+ {
586
+ additionalProperties: false,
587
+ properties: {
588
+ perKindPrefixes: { items: { type: "string" }, type: "array" }
589
+ },
590
+ type: "object"
591
+ }
592
+ ],
593
+ type: "problem"
594
+ }
595
+ };
596
+ var document_sagas_are_generic_default = documentSagasAreGeneric;
597
+
598
+ // src/rules/documents-share-one-table.ts
599
+ var TABLE_CALLEES = ["sqliteTable", "pgTable"];
600
+ var documentsShareOneTable = {
601
+ create(context) {
602
+ const perKindTables = new Set(context.options?.[0]?.perKindTables ?? []);
603
+ if (perKindTables.size === 0) return {};
604
+ if (!/\/packages\/db\/src\/schema\//.test(context.filename.replaceAll("\\", "/"))) return {};
605
+ return {
606
+ CallExpression(node) {
607
+ for (const callee of TABLE_CALLEES) {
608
+ const table = literalNamedBy(node, callee);
609
+ if (table === null || !perKindTables.has(table)) continue;
610
+ context.report({ data: { table }, messageId: "perKindTable", node });
611
+ return;
612
+ }
613
+ }
614
+ };
615
+ },
616
+ fixShape: `Every document kind is a row of the one \`documents\` table with a \`kind\` column, not a table of its
617
+ own. A \`pgTable('invoices', \u2026)\` beside a \`pgTable('quotes', \u2026)\` is the second storage the base entity
618
+ retired: write the row, not the table. The forbidden table names are the rule's \`perKindTables\`
619
+ option.`,
620
+ meta: {
621
+ docs: {
622
+ description: "Documents share ONE table: `documents` holds the head every kind derives and the body its kind declares. A table named for one kind of document is a second storage."
623
+ },
624
+ messages: {
625
+ perKindTable: '"{{table}}" is a table for one kind of document. Every kind is a row of `documents` \u2014 add the kind to `documentEntities` and let its `headOf` fill the columns.'
626
+ },
627
+ schema: [
628
+ {
629
+ additionalProperties: false,
630
+ properties: {
631
+ perKindTables: { items: { type: "string" }, type: "array" }
632
+ },
633
+ type: "object"
634
+ }
635
+ ],
636
+ type: "problem"
637
+ }
638
+ };
639
+ var documents_share_one_table_default = documentsShareOneTable;
640
+
641
+ // src/rules/effect-hook-naming.ts
642
+ var HOOK_FILE_PATTERN = /(?:features\/[^/]+\/)?organelles\/.*\.ts$/;
643
+ var effectHookNaming = {
644
+ create(context) {
645
+ const normalized = context.filename.replaceAll(/\\/g, "/");
646
+ if (!HOOK_FILE_PATTERN.test(normalized)) return {};
647
+ const file = normalized.split("/").pop() || "";
648
+ if (file.endsWith("-effect.ts")) return {};
649
+ let usesEffect = false;
650
+ let effectHookName = "";
651
+ let programNode = null;
652
+ const exportedFunctions = [];
653
+ return {
654
+ CallExpression(node) {
655
+ if (node.callee.type !== "Identifier") return;
656
+ if (node.callee.name === "useEffect" || node.callee.name === "useLayoutEffect") {
657
+ usesEffect = true;
658
+ effectHookName = node.callee.name;
659
+ }
660
+ },
661
+ "ExportNamedDeclaration > FunctionDeclaration"(node) {
662
+ if (node.id?.name) {
663
+ exportedFunctions.push({ name: node.id.name, node });
664
+ }
665
+ },
666
+ Program(node) {
667
+ programNode = node;
668
+ },
669
+ "Program:exit"() {
670
+ if (!usesEffect) return;
671
+ const baseName = file.replace(/\.ts$/, "");
672
+ const suggestedFile = baseName + "-effect.ts";
673
+ context.report({
674
+ data: { file, hook: effectHookName, suggested: suggestedFile },
675
+ messageId: "filenameMissingEffect",
676
+ node: programNode
677
+ });
678
+ for (const fn of exportedFunctions) {
679
+ if (!fn.name.endsWith("Effect")) {
680
+ context.report({
681
+ data: {
682
+ hook: effectHookName,
683
+ name: fn.name,
684
+ suggested: fn.name + "Effect"
685
+ },
686
+ messageId: "functionMissingEffect",
687
+ node: fn.node
688
+ });
689
+ }
690
+ }
691
+ }
692
+ };
693
+ },
694
+ fixShape: `A hook in \`organelles/\` that calls \`useEffect\` says so in its name: the file is \`use-<thing>-effect.ts\`
695
+ and the exported function is \`use<Thing>Effect\`. A hook that only derives needs neither. Rename the
696
+ file and the export together \u2014 half the pair still fires.`,
697
+ meta: {
698
+ docs: {
699
+ description: 'Hook files that use useEffect/useLayoutEffect must have "-effect" suffix in the filename and "Effect" suffix in the exported function name. This makes side-effectful hooks immediately identifiable.'
700
+ },
701
+ messages: {
702
+ filenameMissingEffect: 'File uses {{hook}} but filename "{{file}}" does not end with "-effect.ts". Rename to "{{suggested}}".',
703
+ functionMissingEffect: 'Function "{{name}}" uses {{hook}} but does not end with "Effect". Rename to "{{suggested}}".'
704
+ },
705
+ schema: [],
706
+ type: "problem"
707
+ }
708
+ };
709
+ var effect_hook_naming_default = effectHookNaming;
710
+
711
+ // src/rules/max-comment-density.ts
712
+ var exemptFiles2 = [
713
+ /\/__tests__\//,
714
+ /\.config\.(ts|mjs|js)$/,
715
+ /\.d\.ts$/,
716
+ /\/scripts\//,
717
+ /\/testing\//
718
+ ];
719
+ var commentCeilingPercent = 10;
720
+ var commentBlockCeilingLines = 3;
721
+ var commentLinesAlwaysAllowed = 3;
722
+ var directive = /^\s*(eslint|oxlint|biome|prettier|@ts-|c8 |v8 |istanbul )/;
723
+ var commentsOf = (sourceCode) => sourceCode.getAllComments?.() ?? sourceCode.ast?.comments ?? [];
724
+ var lineCount = (sourceCode) => sourceCode.lines?.length ?? (sourceCode.text ?? "").split("\n").length;
725
+ var maxCommentDensity = {
726
+ create(context) {
727
+ const normalized = context.filename.replace(/\\/g, "/");
728
+ if (exemptFiles2.some((file) => file.test(normalized))) return {};
729
+ return {
730
+ Program(node) {
731
+ const comments = commentsOf(context.sourceCode).filter((c) => !directive.test(c.value));
732
+ const total = lineCount(context.sourceCode);
733
+ if (total === 0) return;
734
+ const commented = /* @__PURE__ */ new Set();
735
+ for (const comment of comments) {
736
+ const lines = comment.loc.end.line - comment.loc.start.line + 1;
737
+ if (lines > commentBlockCeilingLines) {
738
+ context.report({
739
+ messageId: "blockTooLong",
740
+ node,
741
+ loc: comment.loc,
742
+ data: { lines: String(lines), max: String(commentBlockCeilingLines) }
743
+ });
744
+ }
745
+ for (let line = comment.loc.start.line; line <= comment.loc.end.line; line += 1) {
746
+ commented.add(line);
747
+ }
748
+ }
749
+ const ratio = Math.round(commented.size / total * 100);
750
+ if (commented.size > commentLinesAlwaysAllowed && ratio > commentCeilingPercent) {
751
+ context.report({
752
+ messageId: "aboveCeiling",
753
+ node,
754
+ data: { ratio: String(ratio), max: String(commentCeilingPercent) }
755
+ });
756
+ }
757
+ }
758
+ };
759
+ },
760
+ fixShape: `Comments are at most 10 % of a file's lines and no block runs past 3. A 60-line file gets six comment
761
+ lines: budget them before writing. A comment says a non-obvious WHY \u2014 never what the code does, never
762
+ a law citation, never the story of how it came to be. At the ceiling, delete one to add one.`,
763
+ meta: {
764
+ docs: {
765
+ description: "A file carries at most 10 % comment lines and no comment block longer than three lines. Names carry the meaning; a comment is a one-line non-obvious WHY."
766
+ },
767
+ messages: {
768
+ aboveCeiling: "{{ratio}}% of this file is comments (ceiling {{max}}%). Delete narration, law citations and stories; rename until the code says it.",
769
+ blockTooLong: "A {{lines}}-line comment block (ceiling {{max}}). State the non-obvious WHY in one line or move the explanation into a name."
770
+ },
771
+ schema: [],
772
+ type: "problem"
773
+ }
774
+ };
775
+ var max_comment_density_default = maxCommentDensity;
776
+
777
+ // src/rules/molecule-atoms-only.ts
778
+ var FORBIDDEN_PATTERNS3 = [
779
+ /components\/ui\//,
780
+ /compounds\//,
781
+ /organelles\//,
782
+ /cells\//,
783
+ /tissues\//,
784
+ /stores\//,
785
+ /domains/,
786
+ /\/app\//
787
+ ];
788
+ var moleculeAtomsOnly = {
789
+ create(context) {
790
+ const filename = context.filename;
791
+ const isMolecule = /\/molecules\//.test(filename);
792
+ if (!isMolecule) return {};
793
+ const fromModule = (node) => {
794
+ if (!node.source) return;
795
+ const source = node.source.value;
796
+ for (const pattern of FORBIDDEN_PATTERNS3) {
797
+ if (pattern.test(source)) {
798
+ context.report({
799
+ data: { source },
800
+ messageId: "forbidden",
801
+ node
802
+ });
803
+ break;
804
+ }
805
+ }
806
+ };
807
+ return {
808
+ ExportAllDeclaration: fromModule,
809
+ ExportNamedDeclaration: fromModule,
810
+ ImportDeclaration: fromModule
811
+ };
812
+ },
813
+ fixShape: `A molecule combines atoms and other molecules and nothing else \u2014 no compound, organelle, cell,
814
+ tissue, store or domain, and no state-bearing \`components/ui\` primitive (Calendar, Popover, Carousel).
815
+ Composing a stateful primitive makes the file a cell. \`export \u2026 from\` is checked like an import.`,
816
+ meta: {
817
+ docs: {
818
+ description: "Molecules are inert combinations of atoms. They must not import compounds, organelles, cells, tissues, stores, domains, or the raw state-bearing components/ui primitives \u2014 composing a stateful primitive makes the file a cell."
819
+ },
820
+ messages: {
821
+ forbidden: "Molecules cannot import from {{source}}. Molecules are inert atom-combinations \u2014 state-bearing primitives (components/ui) and higher tiers belong in a cell."
822
+ },
823
+ schema: [],
824
+ type: "problem"
825
+ }
826
+ };
827
+ var molecule_atoms_only_default = moleculeAtomsOnly;
828
+
829
+ // src/rules/molecule-must-compose.ts
830
+ import fs2 from "fs";
831
+ function isReexportOnly2(file) {
832
+ try {
833
+ const src = fs2.readFileSync(file, "utf8").replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/\/\/.*$/gm, "");
834
+ const hasReexport = /\bexport\b[^;]*\bfrom\b\s*['"]/.test(src);
835
+ const hasComponent = /\bfunction\b|=>|\breturn\b|<[A-Za-z]/.test(src);
836
+ return hasReexport && !hasComponent;
837
+ } catch {
838
+ return false;
839
+ }
840
+ }
841
+ function isCompositionSource(src) {
842
+ if (/\/(atoms|molecules)(\/|$)/.test(src)) return true;
843
+ if (/^\.\/[^/]+$/.test(src)) return true;
844
+ if (src.startsWith("@radix-ui/") || src === "radix-ui" || src.startsWith("radix-ui/")) return true;
845
+ if (src.startsWith("@react-email/")) return true;
846
+ return false;
847
+ }
848
+ var moleculeMustCompose = {
849
+ create(context) {
850
+ const filename = context.filename;
851
+ const isMolecule = /\/molecules\/(?!index\.)[^/]+\.tsx?$/.test(filename);
852
+ if (!isMolecule) return {};
853
+ if (isReexportOnly2(filename)) return {};
854
+ const bioImportLocals = /* @__PURE__ */ new Set();
855
+ const renderedTagNames = /* @__PURE__ */ new Set();
856
+ let programNode = null;
857
+ return {
858
+ ImportDeclaration(node) {
859
+ if (node.importKind === "type") return;
860
+ const source = node.source.value;
861
+ if (!isCompositionSource(source)) return;
862
+ for (const spec of node.specifiers ?? []) {
863
+ const localName = spec.local?.name;
864
+ if (localName) bioImportLocals.add(localName);
865
+ }
866
+ },
867
+ JSXOpeningElement(node) {
868
+ const name = node.name;
869
+ if (!name) return;
870
+ if (name.type === "JSXIdentifier") {
871
+ const id = name;
872
+ if (id.name) renderedTagNames.add(id.name);
873
+ } else if (name.type === "JSXMemberExpression") {
874
+ const member = name;
875
+ const root = member.object?.name;
876
+ if (root) renderedTagNames.add(root);
877
+ }
878
+ },
879
+ "Program:exit"(node) {
880
+ programNode = node;
881
+ const composesByRender = [...bioImportLocals].some(
882
+ (localName) => renderedTagNames.has(localName)
883
+ );
884
+ if (!composesByRender) {
885
+ const shortName = filename.split("/").pop() || filename;
886
+ context.report({
887
+ data: { filename: shortName },
888
+ messageId: "missingComposition",
889
+ node: programNode
890
+ });
891
+ }
892
+ }
893
+ };
894
+ },
895
+ fixShape: `A molecule must RENDER at least one of the atoms or molecules it imports. An import that never
896
+ appears as a JSX tag is lint-theater: it satisfies a dependency check while composing nothing. If the
897
+ file renders only native HTML, it is an atom; if it renders nothing, delete it. Radix and react-email
898
+ primitives count as composition; a pure \`export \u2026 from\` shim is not a molecule at all.`,
899
+ meta: {
900
+ docs: {
901
+ description: "Molecules MUST RENDER at least one atom or another molecule (or Radix primitive). A molecule is BY DEFINITION a combination \u2014 importing an atom without rendering it is FAKE composition (lint-theater). This rule checks the JSX render tree, not just the import list. That is lint-theater."
902
+ },
903
+ messages: {
904
+ missingComposition: 'Molecule "{{filename}}" does not RENDER any atom, molecule, or Radix primitive. Molecules are BY DEFINITION combinations \u2014 they must actually render at least one ../atoms/, ../molecules/, or @radix-ui/ element. Importing without rendering is FAKE composition. Fix: actually render the imported atom in JSX, OR demote this file to an atom if it only renders a single native HTML element, OR delete it.'
905
+ },
906
+ schema: [],
907
+ type: "problem"
908
+ }
909
+ };
910
+ var molecule_must_compose_default = moleculeMustCompose;
911
+
912
+ // src/rules/next-route-segment-is-thin-delegate.ts
913
+ var DELEGATE_BASENAMES = /* @__PURE__ */ new Set(["layout.tsx", "page.tsx", "template.tsx"]);
914
+ var FALLBACK_BASENAMES = /* @__PURE__ */ new Set([
915
+ "default.tsx",
916
+ "error.tsx",
917
+ "global-error.tsx",
918
+ "loading.tsx",
919
+ "not-found.tsx"
920
+ ]);
921
+ var SKIP_KEYS = /* @__PURE__ */ new Set(["parent", "loc", "range", "scope"]);
922
+ function collectReturnStatements(body) {
923
+ const out = [];
924
+ const visited = /* @__PURE__ */ new WeakSet();
925
+ function walk(node) {
926
+ if (!node || typeof node !== "object") return;
927
+ if (visited.has(node)) return;
928
+ visited.add(node);
929
+ const n = node;
930
+ if (node !== body && (n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression")) {
931
+ return;
932
+ }
933
+ if (n.type === "ReturnStatement") {
934
+ out.push(node);
935
+ }
936
+ for (const key of Object.keys(n)) {
937
+ if (SKIP_KEYS.has(key)) continue;
938
+ const v = n[key];
939
+ if (Array.isArray(v)) {
940
+ for (const item of v) walk(item);
941
+ } else if (v && typeof v === "object") {
942
+ walk(v);
943
+ }
944
+ }
945
+ }
946
+ walk(body);
947
+ return out;
948
+ }
949
+ function tagOf(el) {
950
+ const name = el.openingElement?.name;
951
+ if (!name) return null;
952
+ if (name.type === "JSXIdentifier") return name.name ?? null;
953
+ return null;
954
+ }
955
+ function describeReturn(arg) {
956
+ if (!arg || typeof arg !== "object") return "empty";
957
+ const a = arg;
958
+ if (a.type === "Literal" && a.value === null) return "null";
959
+ if (a.type === "JSXElement") {
960
+ const t = tagOf(arg);
961
+ return t ? `<${t}/>` : "JSXElement";
962
+ }
963
+ if (a.type === "JSXFragment") return "fragment";
964
+ return a.type ?? "unknown";
965
+ }
966
+ function bioFolderOf(source) {
967
+ const m = source.match(/\/(atoms|molecules|compounds|organelles|cells|tissues|lib|stores)\//);
968
+ return m?.[1] ?? "unknown";
969
+ }
970
+ function isTissueSource(source) {
971
+ if (!source) return false;
972
+ return /\/tissues\//.test(source);
973
+ }
974
+ var nextRouteSegmentIsThinDelegate = {
975
+ create(context) {
976
+ const normalized = context.filename.replaceAll(/\\/g, "/");
977
+ if (/__tests__\//.test(normalized)) return {};
978
+ if (!/(?:^|\/)app\//.test(normalized)) return {};
979
+ const basename = normalized.split("/").pop() ?? "";
980
+ if (!DELEGATE_BASENAMES.has(basename)) return {};
981
+ if (FALLBACK_BASENAMES.has(basename)) return {};
982
+ const importSources = /* @__PURE__ */ new Map();
983
+ function isImportedPascalCase(tag) {
984
+ if (!tag) return false;
985
+ if (!/^[A-Z]/.test(tag)) return false;
986
+ return importSources.has(tag);
987
+ }
988
+ function reportNonTissueDelegate(tag, ret) {
989
+ const source = importSources.get(tag) ?? "";
990
+ context.report({
991
+ data: {
992
+ bio: bioFolderOf(source),
993
+ file: basename,
994
+ source,
995
+ tag
996
+ },
997
+ messageId: "delegateMustBeTissue",
998
+ node: ret
999
+ });
1000
+ }
1001
+ function checkReturn(ret) {
1002
+ const arg = ret.argument;
1003
+ if (!arg || typeof arg !== "object") return;
1004
+ const a = arg;
1005
+ if (a.type === "Literal" && a.value === null) return;
1006
+ if (a.type === "JSXElement") {
1007
+ const tag = tagOf(arg);
1008
+ if (!isImportedPascalCase(tag)) {
1009
+ context.report({
1010
+ data: { file: basename, found: describeReturn(arg) },
1011
+ messageId: "mustBeThinDelegate",
1012
+ node: ret
1013
+ });
1014
+ return;
1015
+ }
1016
+ const source = importSources.get(tag);
1017
+ if (!isTissueSource(source)) {
1018
+ reportNonTissueDelegate(tag, ret);
1019
+ }
1020
+ return;
1021
+ }
1022
+ if (a.type === "JSXFragment") {
1023
+ const kids = arg.children ?? [];
1024
+ const meaningful = kids.filter((c) => {
1025
+ if (c.type === "JSXText") return (c.value ?? "").trim().length > 0;
1026
+ return true;
1027
+ });
1028
+ if (meaningful.length !== 1) {
1029
+ context.report({
1030
+ data: { count: String(meaningful.length), file: basename },
1031
+ messageId: "fragmentMustBeSingleDelegate",
1032
+ node: ret
1033
+ });
1034
+ return;
1035
+ }
1036
+ const child = meaningful[0];
1037
+ if (child === void 0 || child.type !== "JSXElement") {
1038
+ context.report({
1039
+ data: { file: basename, found: "non-element-child" },
1040
+ messageId: "mustBeThinDelegate",
1041
+ node: ret
1042
+ });
1043
+ return;
1044
+ }
1045
+ const tagName = child.openingElement?.name?.type === "JSXIdentifier" ? child.openingElement.name.name ?? null : null;
1046
+ if (!isImportedPascalCase(tagName)) {
1047
+ context.report({
1048
+ data: { file: basename, found: `fragment><${tagName ?? "?"}/></fragment` },
1049
+ messageId: "mustBeThinDelegate",
1050
+ node: ret
1051
+ });
1052
+ return;
1053
+ }
1054
+ const source = importSources.get(tagName);
1055
+ if (!isTissueSource(source)) {
1056
+ reportNonTissueDelegate(tagName, ret);
1057
+ }
1058
+ return;
1059
+ }
1060
+ context.report({
1061
+ data: { file: basename, found: describeReturn(arg) },
1062
+ messageId: "mustBeThinDelegate",
1063
+ node: ret
1064
+ });
1065
+ }
1066
+ return {
1067
+ ExportDefaultDeclaration(node) {
1068
+ const decl = node.declaration;
1069
+ if (!decl || decl.type !== "FunctionDeclaration") return;
1070
+ const returns = collectReturnStatements(decl.body);
1071
+ for (const ret of returns) checkReturn(ret);
1072
+ },
1073
+ ImportDeclaration(node) {
1074
+ const source = node.source?.value ?? "";
1075
+ for (const spec of node.specifiers ?? []) {
1076
+ const local = spec.local?.name;
1077
+ if (local) importSources.set(local, source);
1078
+ }
1079
+ }
1080
+ };
1081
+ },
1082
+ fixShape: `A route segment (\`page.tsx\`, \`layout.tsx\`, \`template.tsx\`) returns exactly one imported component, or
1083
+ null. No inline JSX, no native HTML wrapper, no helper function, no local component. The delegate it
1084
+ returns is a tissue \u2014 a route that renders a cell directly wraps it in a tissue first. Fallback files
1085
+ (\`default\`, \`not-found\`, \`loading\`, \`error\`, \`global-error\`) are exempt.`,
1086
+ meta: {
1087
+ docs: {
1088
+ description: "Next.js route segment files (page/layout/template.tsx) must be thin delegates: return exactly one imported component or null. No inline JSX, no native HTML wrapping, no helpers. Route fallback files are exempt."
1089
+ },
1090
+ messages: {
1091
+ delegateMustBeTissue: "Route segment '{{file}}' delegates to '<{{tag}}/>' imported from '{{source}}' (a {{bio}}). Route segments compose TISSUES only \u2014 import {{tag}} from features/*/tissues/** or wrap it in a new tissue.",
1092
+ fragmentMustBeSingleDelegate: "Route segment '{{file}}' returns a fragment with {{count}} elements. Must be a single imported tissue (wrap the fragment contents in a tissue).",
1093
+ mustBeThinDelegate: "Route segment '{{file}}' must return a single imported tissue or null \u2014 found {{found}}. Move the implementation into a tissue and delegate."
1094
+ },
1095
+ schema: [],
1096
+ type: "problem"
1097
+ }
1098
+ };
1099
+ var next_route_segment_is_thin_delegate_default = nextRouteSegmentIsThinDelegate;
1100
+
1101
+ // src/rules/no-brand-names.ts
1102
+ import * as fs3 from "fs";
1103
+ var EXEMPT_PATH = /\/examples\//;
1104
+ var escapeForRegex2 = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
1105
+ var noBrandNames = {
1106
+ create(context) {
1107
+ const { allowedSubstrings = [], brands = [] } = context.options?.[0] ?? {};
1108
+ if (brands.length === 0) return {};
1109
+ const filename = context.filename.replaceAll(/\\/g, "/");
1110
+ if (EXEMPT_PATH.test(filename)) return {};
1111
+ const brandPattern = new RegExp(`\\b(?:${brands.map(escapeForRegex2).join("|")})\\b`, "i");
1112
+ const allowedPatterns = allowedSubstrings.map(
1113
+ (allowed) => new RegExp(escapeForRegex2(allowed), "gi")
1114
+ );
1115
+ let programNode = null;
1116
+ return {
1117
+ Program(node) {
1118
+ programNode = node;
1119
+ },
1120
+ "Program:exit"() {
1121
+ let content = context.sourceCode?.text ?? "";
1122
+ if (content === "") {
1123
+ try {
1124
+ content = fs3.readFileSync(filename, "utf8");
1125
+ } catch {
1126
+ return;
1127
+ }
1128
+ }
1129
+ const lines = content.split("\n");
1130
+ for (const [index, line] of lines.entries()) {
1131
+ const stripped = allowedPatterns.reduce(
1132
+ (text, pattern) => text.replaceAll(pattern, ""),
1133
+ line
1134
+ );
1135
+ const match = stripped.match(brandPattern);
1136
+ if (match) {
1137
+ context.report({
1138
+ data: { brand: match[0], line: String(index + 1) },
1139
+ messageId: "brandLeak",
1140
+ node: programNode
1141
+ });
1142
+ return;
1143
+ }
1144
+ }
1145
+ }
1146
+ };
1147
+ },
1148
+ fixShape: `The kit is brand-agnostic: no brand identity in its source \u2014 not in identifiers, strings, copy or
1149
+ comments. Brand names live in the demo/example layer. Configure the rule's \`brands\` list (it does
1150
+ nothing with an empty list) and put real demo-asset hosts in \`allowedSubstrings\` so a CDN URL never
1151
+ trips it.`,
1152
+ meta: {
1153
+ docs: {
1154
+ description: "No brand identity in a brand-agnostic kit. Brand names (in code, copy, or comments) belong in the examples/demo layer, not the kit source. Configure the brand list and any allowed demo-asset substrings through the rule options; with no brands the rule does nothing."
1155
+ },
1156
+ messages: {
1157
+ brandLeak: 'Brand name "{{brand}}" leaked into the kit at line {{line}}. The kit is brand-agnostic \u2014 move brand identity to the examples/demo layer.'
1158
+ },
1159
+ schema: [
1160
+ {
1161
+ additionalProperties: false,
1162
+ properties: {
1163
+ allowedSubstrings: { items: { type: "string" }, type: "array" },
1164
+ brands: { items: { type: "string" }, type: "array" }
1165
+ },
1166
+ type: "object"
1167
+ }
1168
+ ],
1169
+ type: "problem"
1170
+ }
1171
+ };
1172
+ var no_brand_names_default = noBrandNames;
1173
+
1174
+ // src/rules/no-card-shaped-div.ts
1175
+ var biologicalHierarchyPattern = /\/(molecules|compounds|organelles|cells|layouts)\//;
1176
+ var MODIFIER_PREFIX = "(?:(?:dark|print|sm|md|lg|xl|2xl|first|last|odd|even):)?";
1177
+ var BORDER_PATTERN = new RegExp(`(?:^|\\s)${MODIFIER_PREFIX}border(?:\\s|$|-[1-9]\\d*(?:\\s|$))`);
1178
+ var BG_PATTERN = new RegExp(
1179
+ `(?:^|\\s)${MODIFIER_PREFIX}bg-(?:white|black|container|page|neutral-\\d+|gray-\\d+|zinc-\\d+|stone-\\d+|slate-\\d+)`
1180
+ );
1181
+ function extractClassNameString(node) {
1182
+ if (!node) return null;
1183
+ if (node.type === "Literal") {
1184
+ const literal = node;
1185
+ return typeof literal.value === "string" ? literal.value : null;
1186
+ }
1187
+ if (node.type === "JSXExpressionContainer") {
1188
+ return extractClassNameString(node.expression);
1189
+ }
1190
+ if (node.type === "TemplateLiteral") {
1191
+ const tl = node;
1192
+ return tl.quasis.map((q) => q.value?.raw ?? "").join(" ");
1193
+ }
1194
+ if (node.type === "BinaryExpression") {
1195
+ const bin = node;
1196
+ if (bin.operator !== "+") return null;
1197
+ const left = extractClassNameString(bin.left) ?? "";
1198
+ const right = extractClassNameString(bin.right) ?? "";
1199
+ const combined = (left + " " + right).trim();
1200
+ return combined.length > 0 ? combined : null;
1201
+ }
1202
+ return null;
1203
+ }
1204
+ function isCardLikeClassName(className) {
1205
+ return BORDER_PATTERN.test(className) && BG_PATTERN.test(className);
1206
+ }
1207
+ var noCardShapedDiv = {
1208
+ create(context) {
1209
+ const filename = context.filename;
1210
+ if (!biologicalHierarchyPattern.test(filename)) return {};
1211
+ return {
1212
+ JSXOpeningElement(node) {
1213
+ if (node?.name?.type !== "JSXIdentifier" || node.name.name !== "div") return;
1214
+ const classAttr = node.attributes?.find(
1215
+ (a) => a?.type === "JSXAttribute" && a?.name?.type === "JSXIdentifier" && a?.name?.name === "className"
1216
+ );
1217
+ if (!classAttr) return;
1218
+ const cls = extractClassNameString(classAttr.value ?? null);
1219
+ if (!cls) return;
1220
+ if (isCardLikeClassName(cls)) {
1221
+ context.report({ messageId: "cardLike", node });
1222
+ }
1223
+ }
1224
+ };
1225
+ },
1226
+ fixShape: `A raw \`<div>\` whose className carries both a border and a background is a card someone re-drew. Use
1227
+ the \`Card\` atom. If the shape is genuinely not a card, drop one of the two classes or move the
1228
+ styling into an atom that owns it.`,
1229
+ meta: {
1230
+ docs: {
1231
+ description: "Forbid raw <div> styled as a card (border + background combination). Use the `Card` atom instead so consumers compose the design system rather than recreating it."
1232
+ },
1233
+ messages: {
1234
+ cardLike: "This <div> is styled as a card (className contains both a border and background class). Use the `Card` atom instead. The Card atom centralizes card styling so consumers compose the design system instead of recreating it."
1235
+ },
1236
+ schema: [],
1237
+ type: "problem"
1238
+ }
1239
+ };
1240
+ var no_card_shaped_div_default = noCardShapedDiv;
1241
+
1242
+ // src/rules/no-cross-feature-stores.ts
1243
+ var BIO_FILE_PATTERN = /features\/([^/]+)\/(cells|organelles)\/.*\.(tsx?|ts)$/;
1244
+ var STORE_IMPORT_PATTERN = /(?:@\/)?features\/([^/]+)\/stores\//;
1245
+ var SHARED_FEATURE = "shared";
1246
+ var noCrossFeatureStores = {
1247
+ create(context) {
1248
+ const normalized = context.filename.replaceAll(/\\/g, "/");
1249
+ const fileMatch = normalized.match(BIO_FILE_PATTERN);
1250
+ if (!fileMatch) return {};
1251
+ const ownFeature = fileMatch[1] ?? "";
1252
+ const file = normalized.split("/").pop() || "unknown";
1253
+ const fromModule = (node) => {
1254
+ if (!node.source) return;
1255
+ if (node.importKind === "type") return;
1256
+ const source = node.source.value;
1257
+ const storeMatch = source.match(STORE_IMPORT_PATTERN);
1258
+ if (!storeMatch) return;
1259
+ const importedFeature = storeMatch[1];
1260
+ if (importedFeature === void 0) return;
1261
+ if (importedFeature === ownFeature) return;
1262
+ if (importedFeature === SHARED_FEATURE) return;
1263
+ context.report({
1264
+ data: {
1265
+ file,
1266
+ importedFeature,
1267
+ ownFeature,
1268
+ source
1269
+ },
1270
+ messageId: "crossFeatureStore",
1271
+ node
1272
+ });
1273
+ };
1274
+ return {
1275
+ ExportAllDeclaration: fromModule,
1276
+ ExportNamedDeclaration: fromModule,
1277
+ ImportDeclaration: fromModule
1278
+ };
1279
+ },
1280
+ fixShape: `A cell or organelle in \`features/X/\` reads only \`features/X/stores/*\` and \`features/shared/stores/*\`.
1281
+ Another feature's store is a hidden coupling: let the data flow through the parent tissue as props
1282
+ instead. Type-only imports are exempt; \`export \u2026 from\` is not.`,
1283
+ meta: {
1284
+ docs: {
1285
+ description: "Cells and organelles must not read stores from other features. A cell/organelle in features/X/ may read only from features/X/stores/ or features/shared/stores/. Cross-feature data flows through the parent tissue/organ as props \u2014 never via direct store import."
1286
+ },
1287
+ messages: {
1288
+ crossFeatureStore: `"{{file}}" in feature "{{ownFeature}}" imports from another feature's store: "{{source}}" (feature: "{{importedFeature}}"). Cross-feature data must flow through the parent as props. Only features/{{ownFeature}}/stores/* and features/shared/stores/* are allowed here.`
1289
+ },
1290
+ schema: [],
1291
+ type: "problem"
1292
+ }
1293
+ };
1294
+ var no_cross_feature_stores_default = noCrossFeatureStores;
1295
+
1296
+ // src/rules/no-d1-transaction.ts
1297
+ var transactionMethod = "transaction";
1298
+ var databaseHandleNames = /* @__PURE__ */ new Set(["db", "database", "drizzle"]);
1299
+ var databaseProperties = /* @__PURE__ */ new Set(["db", "database"]);
1300
+ function methodName(callee) {
1301
+ if (callee.type !== "MemberExpression") return null;
1302
+ const property = callee.property;
1303
+ if (!property) return null;
1304
+ if (callee.computed !== true)
1305
+ return property.type === "Identifier" ? property.name ?? null : null;
1306
+ return property.type === "Literal" && typeof property.value === "string" ? property.value : null;
1307
+ }
1308
+ function receiverName(object) {
1309
+ if (!object) return null;
1310
+ if (object.type === "ThisExpression") return "this";
1311
+ if (object.type === "Identifier") return object.name ?? null;
1312
+ if (object.type !== "MemberExpression" || object.computed === true) return null;
1313
+ const property = object.property?.type === "Identifier" ? object.property.name : void 0;
1314
+ if (property === void 0) return null;
1315
+ const owner = receiverName(object.object);
1316
+ return owner === null ? property : `${owner}.${property}`;
1317
+ }
1318
+ function isDatabaseReceiver(name) {
1319
+ if (name === null) return false;
1320
+ const last = name.slice(name.lastIndexOf(".") + 1);
1321
+ return databaseHandleNames.has(name.toLowerCase()) || databaseProperties.has(last.toLowerCase());
1322
+ }
1323
+ function takesACallback(callArguments) {
1324
+ const [first] = callArguments;
1325
+ return first?.type === "ArrowFunctionExpression" || first?.type === "FunctionExpression";
1326
+ }
1327
+ var tenantPlane = /\/packages\/db\/(src|testing)\//;
1328
+ var controlPlane = /\/packages\/db\/src\/(identity|placement|inbox-address)\.ts$|\/schema\/control\//;
1329
+ var noD1Transaction = {
1330
+ create(context) {
1331
+ const filename = context.filename.replaceAll("\\", "/");
1332
+ if (tenantPlane.test(filename) && !controlPlane.test(filename)) return {};
1333
+ return {
1334
+ CallExpression(node) {
1335
+ const callee = node.callee;
1336
+ if (!callee) return;
1337
+ if (methodName(callee) !== transactionMethod) return;
1338
+ const receiver = receiverName(callee.object);
1339
+ if (!isDatabaseReceiver(receiver) && !takesACallback(node.arguments ?? [])) return;
1340
+ context.report({
1341
+ data: { receiver: receiver ?? "the database" },
1342
+ messageId: "d1HasNoTransactions",
1343
+ node
1344
+ });
1345
+ }
1346
+ };
1347
+ },
1348
+ fixShape: `\`db.transaction()\` type-checks and then throws at runtime on D1. One step writes at most one atomic
1349
+ \`db.batch([...])\`; anything larger is two steps with a compensation between them. Reach for the
1350
+ saga, not the transaction.`,
1351
+ meta: {
1352
+ docs: {
1353
+ description: "D1 has no transactions. `db.transaction()` type-checks and then throws at runtime (drizzle issue #2463). The tenant plane in packages/db is Postgres and owns its transactions (plan 037); everywhere else one step is one call into it."
1354
+ },
1355
+ messages: {
1356
+ d1HasNoTransactions: "`{{receiver}}.transaction()` throws at runtime on D1 (drizzle #2463). One step writes at most one atomic `db.batch([...])`; consistency across steps is the workflow's compensation chain."
1357
+ },
1358
+ schema: [],
1359
+ type: "problem"
1360
+ }
1361
+ };
1362
+ var no_d1_transaction_default = noD1Transaction;
1363
+
1364
+ // src/rules/no-duplicate-jsx-patterns.ts
1365
+ var COMPONENT_FILE_PATTERN = /(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
1366
+ var MIN_CLASS_LENGTH = 30;
1367
+ var MIN_DUPLICATES = 3;
1368
+ var ITERATORS = /* @__PURE__ */ new Set(["map", "flatMap", "forEach", "filter", "reduce"]);
1369
+ function isInsideIteratedRender(node) {
1370
+ let cur = node.parent;
1371
+ while (cur) {
1372
+ if (cur.type === "ArrayExpression") return true;
1373
+ if (cur.type === "ArrowFunctionExpression" || cur.type === "FunctionExpression") {
1374
+ const call = cur.parent;
1375
+ if (call && call.type === "CallExpression" && call.callee?.type === "MemberExpression" && call.callee.property?.name && ITERATORS.has(call.callee.property.name)) {
1376
+ return true;
1377
+ }
1378
+ }
1379
+ cur = cur.parent;
1380
+ }
1381
+ return false;
1382
+ }
1383
+ function record(map, key, node) {
1384
+ const line = node.loc?.start.line ?? -1;
1385
+ let entry = map.get(key);
1386
+ if (!entry) {
1387
+ entry = { lines: /* @__PURE__ */ new Set(), nodes: [] };
1388
+ map.set(key, entry);
1389
+ }
1390
+ if (entry.lines.has(line)) return;
1391
+ entry.lines.add(line);
1392
+ entry.nodes.push(node);
1393
+ }
1394
+ var noDuplicateJsxPatterns = {
1395
+ create(context) {
1396
+ const normalized = context.filename.replaceAll(/\\/g, "/");
1397
+ if (!COMPONENT_FILE_PATTERN.test(normalized)) return {};
1398
+ const classNamesByComponent = /* @__PURE__ */ new Map();
1399
+ const allClassNames = /* @__PURE__ */ new Map();
1400
+ return {
1401
+ JSXOpeningElement(node) {
1402
+ if (node.name.type !== "JSXIdentifier") return;
1403
+ const componentName = node.name.name;
1404
+ if (!componentName) return;
1405
+ if (isInsideIteratedRender(node)) return;
1406
+ const classNameAttr = node.attributes.find(
1407
+ (attr) => attr.type === "JSXAttribute" && attr.name?.name === "className" && attr.value?.type === "Literal" && typeof attr.value.value === "string"
1408
+ );
1409
+ if (!classNameAttr || !classNameAttr.value?.value) return;
1410
+ const className = classNameAttr.value.value;
1411
+ if (className.length < MIN_CLASS_LENGTH) return;
1412
+ record(classNamesByComponent, `${componentName}::${className}`, node);
1413
+ record(allClassNames, className, node);
1414
+ },
1415
+ "Program:exit"() {
1416
+ const reportedClassNames = /* @__PURE__ */ new Set();
1417
+ for (const [key, entry] of classNamesByComponent) {
1418
+ if (entry.nodes.length < MIN_DUPLICATES) continue;
1419
+ const [componentName = "", className = ""] = key.split("::");
1420
+ reportedClassNames.add(className);
1421
+ context.report({
1422
+ data: { component: componentName, count: String(entry.nodes.length) },
1423
+ messageId: "duplicateComponentPattern",
1424
+ node: entry.nodes[1]
1425
+ });
1426
+ }
1427
+ for (const [className, entry] of allClassNames) {
1428
+ if (entry.nodes.length < MIN_DUPLICATES) continue;
1429
+ if (reportedClassNames.has(className)) continue;
1430
+ context.report({
1431
+ data: { count: String(entry.nodes.length), preview: className.slice(0, 50) },
1432
+ messageId: "duplicateClassName",
1433
+ node: entry.nodes[1]
1434
+ });
1435
+ }
1436
+ }
1437
+ };
1438
+ },
1439
+ fixShape: `The same JSX shape written three times is a compound waiting to be named. Extract it and call it
1440
+ three times, or drive it from an array with \`.map()\`. Three near-identical blocks that differ only in
1441
+ their text are one component with a prop.`,
1442
+ meta: {
1443
+ docs: {
1444
+ description: "Detect a hand-duplicated JSX className pattern (same long className repeated 3+ times) that signals a missed compound extraction. Ignores JSX authored once inside an iterator callback (.map/.forEach/etc.) or array literal, and symmetric pairs."
1445
+ },
1446
+ messages: {
1447
+ duplicateClassName: 'className "{{preview}}..." appears {{count}} times. Extract the repeated pattern into a compound or add a variant to the atom.',
1448
+ duplicateComponentPattern: '<{{component}} className="..."> appears {{count}} times with the same className. Extract a reusable compound that encapsulates this styling.'
1449
+ },
1450
+ schema: [],
1451
+ type: "problem"
1452
+ }
1453
+ };
1454
+ var no_duplicate_jsx_patterns_default = noDuplicateJsxPatterns;
1455
+
1456
+ // src/rules/no-hook-in-component-disguise.ts
1457
+ var EXPLICIT_SIDE_EFFECT_HOOKS = /* @__PURE__ */ new Set([
1458
+ "useEffect",
1459
+ "useInsertionEffect",
1460
+ "useLayoutEffect",
1461
+ "useRef"
1462
+ ]);
1463
+ var CUSTOM_EFFECT_HOOK_PATTERN = /^use[A-Z][A-Za-z0-9]*Effect$/;
1464
+ var STORE_HOOK_PATTERN = /^use[A-Z][A-Za-z0-9]*Store$/;
1465
+ function isSideEffectHook(name) {
1466
+ if (EXPLICIT_SIDE_EFFECT_HOOKS.has(name)) return true;
1467
+ if (CUSTOM_EFFECT_HOOK_PATTERN.test(name)) return true;
1468
+ if (STORE_HOOK_PATTERN.test(name)) return true;
1469
+ return false;
1470
+ }
1471
+ var ROUTE_FALLBACK_BASENAMES = /* @__PURE__ */ new Set([
1472
+ "default.tsx",
1473
+ "error.tsx",
1474
+ "global-error.tsx",
1475
+ "loading.tsx",
1476
+ "not-found.tsx"
1477
+ ]);
1478
+ var R3F_PACKAGES = /* @__PURE__ */ new Set(["@react-three/drei", "@react-three/fiber"]);
1479
+ function isPascalCase(name) {
1480
+ return /^[A-Z]/.test(name);
1481
+ }
1482
+ function isNullLiteral(n) {
1483
+ if (!n) return true;
1484
+ return n.type === "Literal" && n.value === null;
1485
+ }
1486
+ function isChildrenIdentifier(n) {
1487
+ return Boolean(n && n.type === "Identifier" && n.name === "children");
1488
+ }
1489
+ function isIdentityFragment(n) {
1490
+ if (!n) return false;
1491
+ if (n.type !== "JSXFragment") return false;
1492
+ const children = n.children ?? [];
1493
+ const meaningful = children.filter((c) => {
1494
+ if (c.type === "JSXText") {
1495
+ const text = c.value ?? c.name ?? "";
1496
+ return text.trim().length > 0;
1497
+ }
1498
+ return true;
1499
+ });
1500
+ if (meaningful.length === 0) return true;
1501
+ if (meaningful.length === 1) {
1502
+ const c = meaningful[0];
1503
+ if (c?.type === "JSXExpressionContainer" && c.expression?.type === "Identifier" && c.expression.name === "children") {
1504
+ return true;
1505
+ }
1506
+ }
1507
+ return false;
1508
+ }
1509
+ function classifyReturnShape(arg) {
1510
+ if (isNullLiteral(arg)) return "identity";
1511
+ if (isChildrenIdentifier(arg)) return "identity";
1512
+ if (isIdentityFragment(arg)) return "identity";
1513
+ return "real";
1514
+ }
1515
+ function suggestedHookName(componentName) {
1516
+ const trimmed = componentName.replace(/(Provider|Sync|Listener|Hydration)$/, "");
1517
+ const kebab = trimmed.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
1518
+ return kebab || "effect";
1519
+ }
1520
+ var noHookInComponentDisguise = {
1521
+ create(context) {
1522
+ const normalized = context.filename.replaceAll(/\\/g, "/");
1523
+ if (/__tests__\//.test(normalized)) return {};
1524
+ if (!normalized.endsWith(".tsx")) return {};
1525
+ const basename = normalized.split("/").pop() ?? "";
1526
+ if (ROUTE_FALLBACK_BASENAMES.has(basename)) return {};
1527
+ let fileUsesR3F = false;
1528
+ const scopeStack = [];
1529
+ let pendingName = null;
1530
+ function currentScope() {
1531
+ return scopeStack.at(-1) ?? null;
1532
+ }
1533
+ function enterFunction(name, node) {
1534
+ scopeStack.push({
1535
+ calledHooks: /* @__PURE__ */ new Set(),
1536
+ hasIdentityReturn: false,
1537
+ hasNonIdentityReturn: false,
1538
+ name,
1539
+ node
1540
+ });
1541
+ }
1542
+ function exitFunction() {
1543
+ const scope = scopeStack.pop();
1544
+ if (!scope) return;
1545
+ if (fileUsesR3F) return;
1546
+ if (!isPascalCase(scope.name)) return;
1547
+ if (scope.hasNonIdentityReturn) return;
1548
+ if (!scope.hasIdentityReturn) return;
1549
+ if (basename === "page.tsx" && scope.calledHooks.size === 0) return;
1550
+ const hasHooks = scope.calledHooks.size > 0;
1551
+ context.report({
1552
+ data: {
1553
+ hooks: hasHooks ? [...scope.calledHooks].toSorted((a, b) => a.localeCompare(b)).join(", ") : "",
1554
+ name: scope.name,
1555
+ suggested: suggestedHookName(scope.name)
1556
+ },
1557
+ messageId: hasHooks ? "hookInDisguise" : "passthroughComponent",
1558
+ node: scope.node
1559
+ });
1560
+ }
1561
+ return {
1562
+ ArrowFunctionExpression(node) {
1563
+ const name = pendingName ?? "<anonymous>";
1564
+ pendingName = null;
1565
+ enterFunction(name, node);
1566
+ if (node.body && node.body.type !== "BlockStatement") {
1567
+ const shape = classifyReturnShape(node.body);
1568
+ const scope = currentScope();
1569
+ if (scope) {
1570
+ if (shape === "identity") scope.hasIdentityReturn = true;
1571
+ else scope.hasNonIdentityReturn = true;
1572
+ }
1573
+ }
1574
+ },
1575
+ "ArrowFunctionExpression:exit"() {
1576
+ exitFunction();
1577
+ },
1578
+ CallExpression(node) {
1579
+ if (node.callee.type !== "Identifier") return;
1580
+ const scope = currentScope();
1581
+ if (!scope) return;
1582
+ const name = node.callee.name;
1583
+ if (!name) return;
1584
+ if (isSideEffectHook(name)) {
1585
+ scope.calledHooks.add(name);
1586
+ }
1587
+ },
1588
+ FunctionDeclaration(node) {
1589
+ const name = node.id?.name ?? pendingName ?? "<anonymous>";
1590
+ pendingName = null;
1591
+ enterFunction(name, node);
1592
+ },
1593
+ "FunctionDeclaration:exit"() {
1594
+ exitFunction();
1595
+ },
1596
+ FunctionExpression(node) {
1597
+ const name = node.id?.name ?? pendingName ?? "<anonymous>";
1598
+ pendingName = null;
1599
+ enterFunction(name, node);
1600
+ },
1601
+ "FunctionExpression:exit"() {
1602
+ exitFunction();
1603
+ },
1604
+ ImportDeclaration(node) {
1605
+ if (R3F_PACKAGES.has(node.source.value)) {
1606
+ fileUsesR3F = true;
1607
+ }
1608
+ },
1609
+ ReturnStatement(node) {
1610
+ const scope = currentScope();
1611
+ if (!scope) return;
1612
+ const shape = classifyReturnShape(node.argument);
1613
+ if (shape === "identity") scope.hasIdentityReturn = true;
1614
+ else scope.hasNonIdentityReturn = true;
1615
+ },
1616
+ VariableDeclarator(node) {
1617
+ if (node.id?.type === "Identifier" && node.init && (node.init.type === "ArrowFunctionExpression" || node.init.type === "FunctionExpression")) {
1618
+ pendingName = node.id.name ?? null;
1619
+ }
1620
+ }
1621
+ };
1622
+ },
1623
+ fixShape: `A component that calls hooks, renders nothing, and exists only to run an effect is a hook wearing
1624
+ \`.tsx\`. Move it to \`organelles/use-<thing>.ts\`, export it as \`use<Thing>\`, and let the cell call it.
1625
+ A component returns UI; a hook returns state.`,
1626
+ meta: {
1627
+ docs: {
1628
+ description: "Forbid components whose body calls side-effect hooks (useEffect/useLayoutEffect/useInsertionEffect/useRef) but whose every return is identity (null, bare `children`, `<></>`, `<>{children}</>`). Such a component renders no real UI \u2014 it's a hook masquerading as a component. Move to organelles/use-*-effect.ts and call the hook from a real component. Exempt: route fallback files (default/not-found/loading/error/global-error.tsx) and files importing from @react-three/fiber or @react-three/drei. That is lint-theater."
1629
+ },
1630
+ messages: {
1631
+ hookInDisguise: "Component '{{name}}' has side-effect hooks ({{hooks}}) but renders only identity/null. It's a hook in disguise \u2014 move to organelles/use-{{suggested}}-effect.ts and call the hook from a real component.",
1632
+ passthroughComponent: "Component '{{name}}' renders only identity/null with no effects. It serves no architectural purpose. If this is a metadata-only layout, move `metadata`/`generateMetadata` to the sibling page.tsx and delete this file. If it's structural, give it real content."
1633
+ },
1634
+ schema: [],
1635
+ type: "problem"
1636
+ }
1637
+ };
1638
+ var no_hook_in_component_disguise_default = noHookInComponentDisguise;
1639
+
1640
+ // src/rules/no-inert-hidden-jsx.ts
1641
+ var PURELY_PRESENTATIONAL_ATTRS = /* @__PURE__ */ new Set([
1642
+ "className",
1643
+ "data-testid",
1644
+ "aria-hidden",
1645
+ "role",
1646
+ "key"
1647
+ ]);
1648
+ function getAttrName(attr) {
1649
+ if (attr.type !== "JSXAttribute") return null;
1650
+ if (attr.name?.type !== "JSXIdentifier") return null;
1651
+ return attr.name.name ?? null;
1652
+ }
1653
+ var noInertHiddenJsx = {
1654
+ create(context) {
1655
+ const normalized = context.filename.replaceAll(/\\/g, "/");
1656
+ if (/__tests__\//.test(normalized)) return {};
1657
+ return {
1658
+ JSXOpeningElement(node) {
1659
+ if (node.name?.type !== "JSXIdentifier") return;
1660
+ const componentName = node.name.name;
1661
+ if (!componentName) return;
1662
+ const attributes = node.attributes ?? [];
1663
+ const classNameAttr = attributes.find((a) => getAttrName(a) === "className");
1664
+ if (!classNameAttr || !classNameAttr.value) return;
1665
+ if (classNameAttr.value.type !== "Literal") return;
1666
+ const raw = classNameAttr.value.value;
1667
+ if (typeof raw !== "string") return;
1668
+ if (raw.trim() !== "hidden") return;
1669
+ const hasFunctionalAttr = attributes.some((attr) => {
1670
+ if (attr.type === "JSXSpreadAttribute") return true;
1671
+ const name = getAttrName(attr);
1672
+ if (!name) return false;
1673
+ return !PURELY_PRESENTATIONAL_ATTRS.has(name);
1674
+ });
1675
+ if (hasFunctionalAttr) return;
1676
+ context.report({
1677
+ data: { component: componentName },
1678
+ messageId: "inertHiddenRender",
1679
+ node
1680
+ });
1681
+ }
1682
+ };
1683
+ },
1684
+ fixShape: `JSX rendered behind \`hidden\`, \`display:none\` or a dead branch, purely so a compose rule sees a tag,
1685
+ is lint-theater. Render it for real or delete it. A gate satisfied by markup nobody can see was not
1686
+ satisfied.`,
1687
+ meta: {
1688
+ docs: {
1689
+ description: 'Forbid JSX elements whose only className is "hidden" AND which have no functional attributes (no event handlers, no id, no href, no type, etc.). Such elements render invisible, non-interactive DOM nodes \u2014 the signature of lint-satisfaction imports. Hidden-but-functional elements (file inputs behind labels, skip links, keyboard triggers) are allowed because they have functional attributes.'
1690
+ },
1691
+ messages: {
1692
+ inertHiddenRender: '<{{component}} className="hidden" /> renders an inert, permanently-invisible, non-interactive DOM node \u2014 the signature of lint-theater. Return null or remove the element. If the element is functional but visually hidden (e.g. file input behind a label), add the functional attribute (onChange, onClick, href, id, type, name, etc.) and the rule will recognize it as a legitimate hidden-but-functional element.'
1693
+ },
1694
+ schema: [],
1695
+ type: "problem"
1696
+ }
1697
+ };
1698
+ var no_inert_hidden_jsx_default = noInertHiddenJsx;
1699
+
1700
+ // src/rules/no-inline-data-in-jsx.ts
1701
+ var COMPONENT_FILE_PATTERN2 = /(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
1702
+ var MIN_OBJECTS = 3;
1703
+ function countObjects(elements) {
1704
+ if (!elements) return 0;
1705
+ return elements.filter((el) => el.type === "ObjectExpression").length;
1706
+ }
1707
+ var noInlineDataInJsx = {
1708
+ create(context) {
1709
+ const normalized = context.filename.replaceAll(/\\/g, "/");
1710
+ if (!COMPONENT_FILE_PATTERN2.test(normalized)) return {};
1711
+ const feature = normalized.match(/features\/([^/]+)\//)?.[1] || "";
1712
+ return {
1713
+ // Catch: options={[{ ... }, { ... }, { ... }]}
1714
+ JSXAttribute(node) {
1715
+ if (!node.value || node.value.type !== "JSXExpressionContainer") return;
1716
+ const expr = node.value.expression;
1717
+ if (!expr || expr.type !== "ArrayExpression") return;
1718
+ const objectCount = countObjects(expr.elements);
1719
+ if (objectCount < MIN_OBJECTS) return;
1720
+ const propName = node.name?.name || "unknown";
1721
+ const componentName = node.parent?.name?.type === "JSXIdentifier" ? node.parent.name.name || "unknown" : "unknown";
1722
+ context.report({
1723
+ data: {
1724
+ component: componentName,
1725
+ count: String(objectCount),
1726
+ feature,
1727
+ prop: propName
1728
+ },
1729
+ messageId: "inlineDataArray",
1730
+ node
1731
+ });
1732
+ },
1733
+ // Catch: {[{ ... }, { ... }, { ... }].map(...)} inside JSX
1734
+ JSXExpressionContainer(node) {
1735
+ if (!node.expression || node.expression.type !== "CallExpression") return;
1736
+ const callee = node.expression.callee;
1737
+ if (!callee || callee.type !== "MemberExpression") return;
1738
+ if (callee.property?.name !== "map") return;
1739
+ let arrayNode = callee.object;
1740
+ if (arrayNode && arrayNode.type === "TSAsExpression") {
1741
+ arrayNode = arrayNode.expression;
1742
+ }
1743
+ if (arrayNode && arrayNode.type === "SequenceExpression") {
1744
+ return;
1745
+ }
1746
+ if (!arrayNode || arrayNode.type !== "ArrayExpression") return;
1747
+ const objectCount = countObjects(arrayNode.elements);
1748
+ if (objectCount < MIN_OBJECTS) return;
1749
+ context.report({
1750
+ data: {
1751
+ count: String(objectCount),
1752
+ feature
1753
+ },
1754
+ messageId: "inlineDataInJsx",
1755
+ node
1756
+ });
1757
+ }
1758
+ };
1759
+ },
1760
+ fixShape: `An array of three or more objects written inline in JSX is data, not markup. Move it to a \`lib/\`
1761
+ module beside the feature, name it, and import it. The component maps over the import; the fact
1762
+ lives in one place.`,
1763
+ meta: {
1764
+ docs: {
1765
+ description: "Forbid inline data arrays (3+ object literals) in JSX. Catches both prop values ({options={[...]}}) and inline expression containers ({[...].map()}). Configuration data must live in lib/, not inline in component JSX."
1766
+ },
1767
+ messages: {
1768
+ inlineDataArray: 'Inline array with {{count}} objects passed to prop "{{prop}}" on <{{component}}>. Extract this data to features/{{feature}}/lib/.',
1769
+ inlineDataInJsx: "Inline array with {{count}} objects in JSX expression. Extract this data to features/{{feature}}/lib/ \u2014 component files are for rendering, not data definitions."
1770
+ },
1771
+ schema: [],
1772
+ type: "problem"
1773
+ }
1774
+ };
1775
+ var no_inline_data_in_jsx_default = noInlineDataInJsx;
1776
+
1777
+ // src/rules/no-invalid-feature-folders.ts
1778
+ var VALID_FOLDERS = /* @__PURE__ */ new Set([
1779
+ "atoms",
1780
+ "cells",
1781
+ "compounds",
1782
+ "lib",
1783
+ "molecules",
1784
+ "organelles",
1785
+ "stores",
1786
+ "tissues"
1787
+ ]);
1788
+ var FEATURE_PATH_PATTERN = /features\/([^/]+)\/([^/]+)\//;
1789
+ var noInvalidFeatureFolders = {
1790
+ create(context) {
1791
+ const normalized = context.filename.replaceAll(/\\/g, "/");
1792
+ const match = normalized.match(FEATURE_PATH_PATTERN);
1793
+ if (!match) return {};
1794
+ const feature = match[1] ?? "";
1795
+ const folder = match[2] ?? "";
1796
+ if (!VALID_FOLDERS.has(folder)) {
1797
+ return {
1798
+ Program(node) {
1799
+ context.report({ data: { feature, folder }, messageId: "invalid", node });
1800
+ }
1801
+ };
1802
+ }
1803
+ if (folder === "lib") {
1804
+ const fileName = normalized.split("/").pop() || "";
1805
+ if (fileName.startsWith("use-") || /^use[A-Z]/.test(fileName)) {
1806
+ return {
1807
+ Program(node) {
1808
+ context.report({
1809
+ data: { feature, file: fileName },
1810
+ messageId: "hookInLib",
1811
+ node
1812
+ });
1813
+ }
1814
+ };
1815
+ }
1816
+ }
1817
+ return {};
1818
+ },
1819
+ fixShape: `A feature folder holds only the tiers and their two neighbours: \`atoms\`, \`molecules\`, \`compounds\`,
1820
+ \`organelles\`, \`cells\`, \`tissues\`, \`stores\`, \`lib\`. Anything else (\`components\`, \`utils\`, \`helpers\`,
1821
+ \`types\`) is an unclassified pile \u2014 put the file in the tier that describes what it does, and pure
1822
+ logic in \`lib/\`.`,
1823
+ meta: {
1824
+ docs: {
1825
+ description: 'Files inside features/*/ must be in a valid biological hierarchy folder. Hooks (use-*) must be in organelles/, not lib/. Folders like "dynamic", "utils", "helpers", "components", "hooks" are forbidden.'
1826
+ },
1827
+ messages: {
1828
+ hookInLib: 'Hook file "{{file}}" is in features/{{feature}}/lib/ but hooks must be in features/{{feature}}/organelles/. lib/ is for pure logic only \u2014 no hooks, no state.',
1829
+ invalid: 'File is in "features/{{feature}}/{{folder}}/" which is not a valid biological hierarchy folder. Valid folders: compounds, organelles, cells, tissues, stores, lib.'
1830
+ },
1831
+ schema: [],
1832
+ type: "problem"
1833
+ }
1834
+ };
1835
+ var no_invalid_feature_folders_default = noInvalidFeatureFolders;
1836
+
1837
+ // src/rules/no-logic-in-component-files.ts
1838
+ var COMPONENT_FILE_PATTERN3 = /(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
1839
+ var PRIVATE_OR_NON_ENTRYPOINT = /(\.(parts|sections|summary|layouts|icons|types|hooks|utils|context|store|sidebar|data|config|stories|spec|test)\.tsx$|\/index\.tsx$)/;
1840
+ function isReactComponent(name) {
1841
+ return /^[A-Z][a-zA-Z0-9]*$/.test(name) && !/^[A-Z0-9_]+$/.test(name);
1842
+ }
1843
+ function jsxRootIsComponent(node) {
1844
+ if (!node || node.type !== "JSXElement") return false;
1845
+ const name = node.openingElement?.name;
1846
+ if (!name) return false;
1847
+ if (name.type === "JSXMemberExpression") return true;
1848
+ return name.type === "JSXIdentifier" && /^[A-Z]/.test(name.name || "");
1849
+ }
1850
+ function isThinWrapper(decl) {
1851
+ if (!decl) return false;
1852
+ try {
1853
+ let body = decl.body;
1854
+ if (!body && decl.declarations?.[0]?.init) {
1855
+ body = decl.declarations[0].init.body;
1856
+ }
1857
+ if (!body || Array.isArray(body)) return false;
1858
+ if (body.type === "JSXElement") return jsxRootIsComponent(body);
1859
+ if (body.type === "BlockStatement" && Array.isArray(body.body)) {
1860
+ const stmts = body.body;
1861
+ if (stmts.length !== 1) return false;
1862
+ const only = stmts[0];
1863
+ if (only?.type !== "ReturnStatement") return false;
1864
+ return jsxRootIsComponent(only.argument);
1865
+ }
1866
+ } catch {
1867
+ return false;
1868
+ }
1869
+ return false;
1870
+ }
1871
+ var noLogicInComponentFiles = {
1872
+ create(context) {
1873
+ const normalized = context.filename.replaceAll(/\\/g, "/");
1874
+ if (!COMPONENT_FILE_PATTERN3.test(normalized)) return {};
1875
+ if (PRIVATE_OR_NON_ENTRYPOINT.test(normalized)) return {};
1876
+ const exportedComponents = [];
1877
+ let programNode = null;
1878
+ return {
1879
+ ExportNamedDeclaration(node) {
1880
+ const decl = node.declaration;
1881
+ if (!decl) return;
1882
+ if (decl.type === "FunctionDeclaration" && decl.id?.name && isReactComponent(decl.id.name) && !isThinWrapper(decl)) {
1883
+ exportedComponents.push(decl.id.name);
1884
+ }
1885
+ if (decl.type === "VariableDeclaration" && decl.declarations) {
1886
+ for (const d of decl.declarations) {
1887
+ const initType = d.init?.type;
1888
+ const isComponentInit = initType === "ArrowFunctionExpression" || initType === "FunctionExpression" || initType === "CallExpression";
1889
+ if (d.id?.name && isReactComponent(d.id.name) && isComponentInit && !isThinWrapper(decl)) {
1890
+ exportedComponents.push(d.id.name);
1891
+ }
1892
+ }
1893
+ }
1894
+ },
1895
+ Program(node) {
1896
+ programNode = node;
1897
+ },
1898
+ "Program:exit"() {
1899
+ if (exportedComponents.length > 1) {
1900
+ context.report({
1901
+ data: {
1902
+ count: String(exportedComponents.length),
1903
+ names: exportedComponents.join(", ")
1904
+ },
1905
+ messageId: "multipleExportedComponents",
1906
+ node: programNode
1907
+ });
1908
+ }
1909
+ }
1910
+ };
1911
+ },
1912
+ fixShape: `A public component file exports ONE component. Several distinct components in one \`.tsx\` is a
1913
+ god-file: split them into sibling files, or into private \`.parts.tsx\` / \`.sections.tsx\` siblings of
1914
+ the same component. Thin preset wrappers over one imported component do not count against you.`,
1915
+ meta: {
1916
+ docs: {
1917
+ description: "A public component file (`<name>.tsx` in a bio tier) should export ONE component. Multiple exported components in a single public file is a god-file \u2014 split into separate files. Private sibling splits (.parts/.sections/.layouts/.types), co-located types/helpers/hooks, config constants, and thin preset wrappers are accepted and ignored."
1918
+ },
1919
+ messages: {
1920
+ multipleExportedComponents: "File exports {{count}} components ({{names}}). A public component file should export one component \u2014 split the extras into their own files (or a private .parts sibling if they are internal)."
1921
+ },
1922
+ schema: [],
1923
+ type: "problem"
1924
+ }
1925
+ };
1926
+ var no_logic_in_component_files_default = noLogicInComponentFiles;
1927
+
1928
+ // src/rules/no-orm-outside-db.ts
1929
+ var ormPackage = "drizzle-orm";
1930
+ var dataLayerPattern = /\/packages\/db\//;
1931
+ function isOrmModule(source) {
1932
+ if (typeof source !== "string") return false;
1933
+ return source === ormPackage || source.startsWith(`${ormPackage}/`);
1934
+ }
1935
+ var noOrmOutsideDb = {
1936
+ create(context) {
1937
+ const normalized = context.filename.replace(/\\/g, "/");
1938
+ const contained = dataLayerPattern.test(normalized);
1939
+ const check = (source, node) => {
1940
+ if (contained) return;
1941
+ if (!isOrmModule(source)) return;
1942
+ context.report({ data: { source }, messageId: "ormOutsideDb", node });
1943
+ };
1944
+ const fromModule = (node) => {
1945
+ if (!node.source) return;
1946
+ check(node.source?.value, node);
1947
+ };
1948
+ return {
1949
+ ExportAllDeclaration: fromModule,
1950
+ ExportNamedDeclaration: fromModule,
1951
+ ImportDeclaration: fromModule,
1952
+ // `await import('drizzle-orm')` reaches the ORM exactly as a static import does; only a
1953
+ // module named by something other than a literal is beyond deciding.
1954
+ ImportExpression(node) {
1955
+ if (node.source?.type !== "Literal") return;
1956
+ check(node.source.value, node);
1957
+ }
1958
+ };
1959
+ },
1960
+ fixShape: `\`drizzle-orm\` is imported only inside the data-layer package \u2014 statically or via \`await import()\`.
1961
+ Everywhere else, reach the data through a query the data layer exports. The ORM is a storage detail
1962
+ below the seam, and a caller that names it has crossed it.`,
1963
+ meta: {
1964
+ docs: {
1965
+ description: "drizzle-orm may only be imported inside packages/db. The ORM is a storage detail below the domain, and containing it caps the cost of ever replacing it at one package's internals. Type-only imports count: a type borrowed from the ORM is a line that a replacement would still have to rewrite."
1966
+ },
1967
+ messages: {
1968
+ ormOutsideDb: 'This file imports "{{source}}". drizzle-orm lives only in packages/db \u2014 reach the data through a query exported from @during/db instead, or add the query there.'
1969
+ },
1970
+ schema: [],
1971
+ type: "problem"
1972
+ }
1973
+ };
1974
+ var no_orm_outside_db_default = noOrmOutsideDb;
1975
+
1976
+ // src/rules/no-raw-html-atoms.ts
1977
+ var noRawHtmlAtoms = {
1978
+ create(context) {
1979
+ const restrictions = context.options[0];
1980
+ if (!restrictions || restrictions.length === 0) return {};
1981
+ const elementMap = /* @__PURE__ */ new Map();
1982
+ for (const r of restrictions) {
1983
+ elementMap.set(r.element, r);
1984
+ }
1985
+ return {
1986
+ JSXOpeningElement(node) {
1987
+ if (node?.name?.type !== "JSXIdentifier") return;
1988
+ const elementName = node.name.name;
1989
+ if (!elementName) return;
1990
+ const restriction = elementMap.get(elementName);
1991
+ if (!restriction) return;
1992
+ context.report({
1993
+ data: {
1994
+ atom: restriction.atom,
1995
+ element: restriction.element,
1996
+ importPath: restriction.importPath
1997
+ },
1998
+ messageId: "forbidden",
1999
+ node
2000
+ });
2001
+ }
2002
+ };
2003
+ },
2004
+ fixShape: `A native element that already has an atom wrapper (\`<button>\`, \`<input>\`, \`<td>\`, \`<header>\`, \u2026) is
2005
+ written as the atom in levels 2-6. Import the atom named in the rule's options and use it. Configure
2006
+ the element\u2192atom map per repo; with no map the rule does nothing.`,
2007
+ meta: {
2008
+ docs: {
2009
+ description: "Forbid raw native HTML elements when an atom wrapper exists. Use the atom the options name instead."
2010
+ },
2011
+ messages: {
2012
+ forbidden: 'Use <{{atom}}> from "{{importPath}}" instead of raw <{{element}}>. Native HTML elements that have an atom wrapper must not be used directly in molecules/compounds/cells/tissues - use the atom to preserve consistent styling and behavior.'
2013
+ },
2014
+ schema: [
2015
+ {
2016
+ items: {
2017
+ properties: {
2018
+ atom: { type: "string" },
2019
+ element: { type: "string" },
2020
+ importPath: { type: "string" }
2021
+ },
2022
+ required: ["element", "atom", "importPath"],
2023
+ type: "object"
2024
+ },
2025
+ type: "array"
2026
+ }
2027
+ ],
2028
+ type: "problem"
2029
+ }
2030
+ };
2031
+ var no_raw_html_atoms_default = noRawHtmlAtoms;
2032
+
2033
+ // src/rules/no-raw-sql-outside-allowed.ts
2034
+ var allowedFiles = [
2035
+ /\/packages\/db\/src\/ops\/backup\.ts$/,
2036
+ /\/packages\/db\/src\/dialect\.ts$/,
2037
+ /\/packages\/db\/src\/cell\/app-role\.ts$/,
2038
+ /\/packages\/db\/src\/queries\/(reads|writes)\/search\.ts$/,
2039
+ /\/__tests__\//,
2040
+ /\/scripts\//,
2041
+ /\/testing\//,
2042
+ /\/seeds\//,
2043
+ /\/migrations\//
2044
+ ];
2045
+ var statement = /^\s*(select|insert|update|delete|create|drop|alter|pragma)\b/i;
2046
+ var isClientPrepare = (callee) => callee.type === "MemberExpression" && callee.property?.type === "Identifier" && callee.property.name === "prepare" && callee.object?.type === "MemberExpression" && callee.object.property?.type === "Identifier" && callee.object.property.name === "$client";
2047
+ var noRawSqlOutsideAllowed = {
2048
+ create(context) {
2049
+ const normalized = context.filename.replace(/\\/g, "/");
2050
+ if (allowedFiles.some((file) => file.test(normalized))) return {};
2051
+ return {
2052
+ CallExpression(node) {
2053
+ if (isClientPrepare(node.callee)) context.report({ messageId: "rawSql", node });
2054
+ },
2055
+ TaggedTemplateExpression(node) {
2056
+ if (node.tag.type !== "Identifier" || node.tag.name !== "sql") return;
2057
+ const first = node.quasi.quasis[0]?.value.cooked ?? node.quasi.quasis[0]?.value.raw ?? "";
2058
+ if (statement.test(first)) context.report({ messageId: "rawSql", node });
2059
+ }
2060
+ };
2061
+ },
2062
+ fixShape: `Queries are builders on the schema tables; a raw statement spells the table and column names a second
2063
+ time and drifts from the first. Keep a tagged \`sql\` template for fragments the builder cannot express, inside the
2064
+ few files the rule allows (backup, dialect, search, migrations).`,
2065
+ meta: {
2066
+ docs: {
2067
+ description: "Queries are drizzle builders; a raw statement spells table and column names a second time. Raw SQL is allowed only where drizzle has no vocabulary: FTS5 and dynamic backup snapshots."
2068
+ },
2069
+ messages: {
2070
+ rawSql: "Raw SQL statement outside the allowed files. Use the drizzle builders on the schema tables; keep sql`` for fragments (count(*), coalesce)."
2071
+ },
2072
+ schema: [],
2073
+ type: "problem"
2074
+ }
2075
+ };
2076
+ var no_raw_sql_outside_allowed_default = noRawSqlOutsideAllowed;
2077
+
2078
+ // src/rules/no-react-namespace.ts
2079
+ var noReactNamespace = {
2080
+ create(context) {
2081
+ return {
2082
+ MemberExpression(node) {
2083
+ if (node.object.type !== "Identifier" || node.object.name !== "React") return;
2084
+ if (node.property.type !== "Identifier" || !node.property.name) return;
2085
+ context.report({
2086
+ data: { member: node.property.name },
2087
+ messageId: "noReactNamespace",
2088
+ node
2089
+ });
2090
+ },
2091
+ TSQualifiedName(node) {
2092
+ if (node.left.type !== "Identifier" || node.left.name !== "React") return;
2093
+ if (!node.right.name) return;
2094
+ context.report({
2095
+ data: { member: node.right.name },
2096
+ messageId: "noReactNamespace",
2097
+ node
2098
+ });
2099
+ }
2100
+ };
2101
+ },
2102
+ fixShape: `\`React.useState\`, \`React.FC\`, \`React.ReactNode\` \u2014 use named imports instead: \`import { useState }\`,
2103
+ \`import type { ReactNode }\`. The namespace form defeats tree-shaking and hides what a file actually
2104
+ depends on.`,
2105
+ meta: {
2106
+ docs: {
2107
+ description: 'Forbid React.* namespace access (React.ReactNode, React.MouseEvent, etc.). Use explicit named imports instead: import type { ReactNode, MouseEvent } from "react". This keeps imports explicit and tree-shakeable.'
2108
+ },
2109
+ messages: {
2110
+ noReactNamespace: `Use "import type { {{member}} } from 'react'" instead of "React.{{member}}". Explicit named imports are clearer and tree-shakeable.`
2111
+ },
2112
+ schema: [],
2113
+ type: "problem"
2114
+ }
2115
+ };
2116
+ var no_react_namespace_default = noReactNamespace;
2117
+
2118
+ // src/rules/no-renamed-html-props.ts
2119
+ var forbiddenPropMap = {
2120
+ ariaAtomic: "aria-atomic",
2121
+ ariaBusy: "aria-busy",
2122
+ ariaChecked: "aria-checked",
2123
+ ariaControls: "aria-controls",
2124
+ ariaCurrent: "aria-current",
2125
+ ariaDescribedby: "aria-describedby",
2126
+ ariaDisabled: "aria-disabled",
2127
+ ariaExpanded: "aria-expanded",
2128
+ ariaHaspopup: "aria-haspopup",
2129
+ ariaHidden: "aria-hidden",
2130
+ ariaInvalid: "aria-invalid",
2131
+ ariaLabel: "aria-label",
2132
+ ariaLabelledby: "aria-labelledby",
2133
+ ariaLive: "aria-live",
2134
+ ariaPressed: "aria-pressed",
2135
+ ariaReadonly: "aria-readonly",
2136
+ ariaRequired: "aria-required",
2137
+ ariaSelected: "aria-selected",
2138
+ dataTestid: "data-testid",
2139
+ dataTestId: "data-testid",
2140
+ testId: "data-testid"
2141
+ };
2142
+ var biologicalHierarchyPattern2 = /\/(molecules|compounds|organelles|cells|layouts)\//;
2143
+ var noRenamedHtmlProps = {
2144
+ create(context) {
2145
+ const filename = context.filename;
2146
+ if (!biologicalHierarchyPattern2.test(filename)) return {};
2147
+ const checkProperty = (member) => {
2148
+ if (member.type !== "TSPropertySignature" && member.type !== "PropertyDefinition") return;
2149
+ const key = member.key;
2150
+ if (!key || key.type !== "Identifier") return;
2151
+ const name = key.name;
2152
+ if (!name) return;
2153
+ const standard = forbiddenPropMap[name];
2154
+ if (!standard) return;
2155
+ context.report({
2156
+ data: { forbidden: name, standard },
2157
+ messageId: "renamed",
2158
+ node: member
2159
+ });
2160
+ };
2161
+ return {
2162
+ TSInterfaceBody(node) {
2163
+ for (const member of node.body) {
2164
+ checkProperty(member);
2165
+ }
2166
+ },
2167
+ TSTypeLiteral(node) {
2168
+ for (const member of node.members) {
2169
+ checkProperty(member);
2170
+ }
2171
+ }
2172
+ };
2173
+ },
2174
+ fixShape: `A prop that HTML already names keeps HTML's name: \`aria-label\`, \`data-testid\`, \`id\`, \`name\`, \`form\`.
2175
+ Never \`ariaLabel\`, \`testId\` or \`dataTestid\` in an interface. Extend \`ComponentProps<'element'>\` (or
2176
+ the Radix primitive's props) and inherit them instead of re-declaring.`,
2177
+ meta: {
2178
+ docs: {
2179
+ description: "Molecules/compounds/cells/layouts must use HTML-native prop names via ComponentProps spreading instead of renaming them (Radix/shadcn convention)"
2180
+ },
2181
+ messages: {
2182
+ renamed: 'Prop "{{forbidden}}" is a renamed HTML attribute. Use the standard HTML attribute name "{{standard}}" via prop spreading. Extend ComponentProps<Element> or Radix primitive props to inherit HTML attributes instead of renaming them.'
2183
+ },
2184
+ schema: [],
2185
+ type: "problem"
2186
+ }
2187
+ };
2188
+ var no_renamed_html_props_default = noRenamedHtmlProps;
2189
+
2190
+ // src/rules/no-render-prop-reader.ts
2191
+ var noRenderPropReader = {
2192
+ create(context) {
2193
+ return {
2194
+ JSXOpeningElement(node) {
2195
+ if (node.name?.type !== "JSXIdentifier") return;
2196
+ const componentName = node.name.name;
2197
+ if (!componentName) return;
2198
+ const viewProp = node.attributes?.find(
2199
+ (a) => a.type === "JSXAttribute" && a.name?.type === "JSXIdentifier" && a.name.name === "View" && a.value?.type === "JSXExpressionContainer"
2200
+ );
2201
+ if (viewProp) {
2202
+ context.report({
2203
+ data: { component: componentName },
2204
+ messageId: "renderPropReader",
2205
+ node
2206
+ });
2207
+ }
2208
+ },
2209
+ // Catch definition side: function Foo({ View }: { View: React.ComponentType<...> })
2210
+ TSPropertySignature(node) {
2211
+ if (node.key?.name !== "View") return;
2212
+ const ta = node.typeAnnotation?.typeAnnotation;
2213
+ if (!ta?.typeName) return;
2214
+ const tn = ta.typeName;
2215
+ const isComponentType = tn.type === "TSQualifiedName" && tn.left?.name === "React" && tn.right?.name === "ComponentType" || tn.type === "Identifier" && tn.name === "ComponentType";
2216
+ if (!isComponentType) return;
2217
+ const filename = context.filename.replaceAll(/\\/g, "/");
2218
+ const componentName = filename.split("/").pop()?.replace(".tsx", "") || "unknown";
2219
+ context.report({
2220
+ data: { component: componentName },
2221
+ messageId: "renderPropDefinition",
2222
+ node
2223
+ });
2224
+ }
2225
+ };
2226
+ },
2227
+ fixShape: `\`<Reader View={\u2026} />\`, \`<Reader Wrapper={\u2026} NotFound={\u2026} />\` \u2014 a polymorphic render-prop bridge is
2228
+ not an organelle: it holds no state and renders no output of its own. Reclassify one of the two ends.
2229
+ The state goes in an organelle that renders its own JSX; the arrangement goes in a tissue.`,
2230
+ meta: {
2231
+ docs: {
2232
+ description: "Forbid the View={} render-prop reader pattern. Organelles should read from stores directly and render their own output \u2014 not act as thin bridges that pass data via render props. A real organelle holds state and renders its own output."
2233
+ },
2234
+ messages: {
2235
+ renderPropDefinition: '"{{component}}" accepts a View prop typed as React.ComponentType. This is the render-prop Reader anti-pattern. The organelle should read from the store directly and render its own JSX.',
2236
+ renderPropReader: "<{{component}}> uses a View={{}} render prop. This is the polymorphic render-prop bridge anti-pattern. The organelle should render its own JSX with store data, or the parent should read from the store directly via useShallow."
2237
+ },
2238
+ schema: [],
2239
+ type: "problem"
2240
+ }
2241
+ };
2242
+ var no_render_prop_reader_default = noRenderPropReader;
2243
+
2244
+ // src/rules/no-trivial-wrapper-component.ts
2245
+ var ROUTE_BASENAMES = /* @__PURE__ */ new Set([
2246
+ "default.tsx",
2247
+ "error.tsx",
2248
+ "global-error.tsx",
2249
+ "layout.tsx",
2250
+ "loading.tsx",
2251
+ "not-found.tsx",
2252
+ "page.tsx",
2253
+ "template.tsx"
2254
+ ]);
2255
+ var R3F_PACKAGES2 = /* @__PURE__ */ new Set(["@react-three/drei", "@react-three/fiber"]);
2256
+ var WALK_SKIP_KEYS = /* @__PURE__ */ new Set(["parent", "loc", "range", "scope"]);
2257
+ function functionHasHookCall(body) {
2258
+ let found = false;
2259
+ const visited = /* @__PURE__ */ new WeakSet();
2260
+ function walk(node) {
2261
+ if (found) return;
2262
+ if (!node || typeof node !== "object") return;
2263
+ if (visited.has(node)) return;
2264
+ visited.add(node);
2265
+ const n = node;
2266
+ if (n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression") {
2267
+ if (node !== body) return;
2268
+ }
2269
+ if (n.type === "CallExpression") {
2270
+ const callee = n.callee;
2271
+ if (callee?.type === "Identifier" && callee.name && /^use[A-Z]/.test(callee.name)) {
2272
+ found = true;
2273
+ return;
2274
+ }
2275
+ }
2276
+ for (const key of Object.keys(n)) {
2277
+ if (WALK_SKIP_KEYS.has(key)) continue;
2278
+ const v = n[key];
2279
+ if (Array.isArray(v)) {
2280
+ for (const item of v) walk(item);
2281
+ } else if (v && typeof v === "object") {
2282
+ walk(v);
2283
+ }
2284
+ }
2285
+ }
2286
+ walk(body);
2287
+ return found;
2288
+ }
2289
+ function hasMeaningfulChildren(el) {
2290
+ const kids = el.children ?? [];
2291
+ for (const c of kids) {
2292
+ if (c.type === "JSXText") {
2293
+ if ((c.value ?? "").trim().length > 0) return true;
2294
+ continue;
2295
+ }
2296
+ return true;
2297
+ }
2298
+ return false;
2299
+ }
2300
+ function hasDynamicAttr(el) {
2301
+ const attrs = el.openingElement?.attributes ?? [];
2302
+ for (const a of attrs) {
2303
+ if (a.type === "JSXSpreadAttribute") return true;
2304
+ if (a.type !== "JSXAttribute") continue;
2305
+ const v = a.value;
2306
+ if (!v) continue;
2307
+ if (v.type === "Literal") continue;
2308
+ return true;
2309
+ }
2310
+ return false;
2311
+ }
2312
+ function checkFunction(fnName, fn, importedLocals, importSources, isBridgeFromTissue, report) {
2313
+ if (!/^[A-Z]/.test(fnName)) return;
2314
+ const body = fn.body;
2315
+ if (!body || body.type !== "BlockStatement") return;
2316
+ const stmts = body.body ?? [];
2317
+ if (stmts.length !== 1) return;
2318
+ const only = stmts[0];
2319
+ if (!only || only.type !== "ReturnStatement") return;
2320
+ const arg = only.argument;
2321
+ if (!arg || arg.type !== "JSXElement") return;
2322
+ const tagNode = arg.openingElement?.name;
2323
+ if (tagNode?.type !== "JSXIdentifier") return;
2324
+ const tag = tagNode.name ?? "";
2325
+ if (!/^[A-Z]/.test(tag)) return;
2326
+ if (!importedLocals.has(tag)) return;
2327
+ if (hasMeaningfulChildren(arg)) return;
2328
+ if (hasDynamicAttr(arg)) return;
2329
+ if (functionHasHookCall(body)) return;
2330
+ if (isBridgeFromTissue) {
2331
+ const source = importSources.get(tag) ?? "";
2332
+ const wrapsNonTissueBio = /\/(cells|compounds|organelles|molecules|atoms)\//.test(source);
2333
+ if (wrapsNonTissueBio) return;
2334
+ }
2335
+ report({
2336
+ data: { inner: tag, name: fnName },
2337
+ messageId: "trivialWrapper",
2338
+ node: fn
2339
+ });
2340
+ }
2341
+ var noTrivialWrapperComponent = {
2342
+ create(context) {
2343
+ const normalized = context.filename.replaceAll(/\\/g, "/");
2344
+ if (/__tests__\//.test(normalized)) return {};
2345
+ const basename = normalized.split("/").pop() ?? "";
2346
+ if (/(?:^|\/)app\//.test(normalized) && ROUTE_BASENAMES.has(basename)) return {};
2347
+ const isBridgeFromTissue = /(?:features\/[^/]+\/)?tissues\//.test(normalized);
2348
+ let fileUsesR3F = false;
2349
+ const importedLocals = /* @__PURE__ */ new Set();
2350
+ const importSources = /* @__PURE__ */ new Map();
2351
+ return {
2352
+ ExportDefaultDeclaration(node) {
2353
+ if (fileUsesR3F) return;
2354
+ const decl = node.declaration;
2355
+ if (!decl || decl.type !== "FunctionDeclaration") return;
2356
+ const name = decl.id?.name ?? "";
2357
+ if (!name) return;
2358
+ checkFunction(name, decl, importedLocals, importSources, isBridgeFromTissue, context.report);
2359
+ },
2360
+ ExportNamedDeclaration(node) {
2361
+ if (fileUsesR3F) return;
2362
+ const decl = node.declaration;
2363
+ if (!decl || decl.type !== "FunctionDeclaration") return;
2364
+ const name = decl.id?.name ?? "";
2365
+ if (!name) return;
2366
+ checkFunction(name, decl, importedLocals, importSources, isBridgeFromTissue, context.report);
2367
+ },
2368
+ ImportDeclaration(node) {
2369
+ if (R3F_PACKAGES2.has(node.source.value)) {
2370
+ fileUsesR3F = true;
2371
+ }
2372
+ for (const spec of node.specifiers ?? []) {
2373
+ const local = spec.local?.name;
2374
+ if (local) {
2375
+ importedLocals.add(local);
2376
+ importSources.set(local, node.source.value);
2377
+ }
2378
+ }
2379
+ }
2380
+ };
2381
+ },
2382
+ fixShape: `A component whose whole body is \`return <Imported/>\` \u2014 no children, no dynamic attribute, no hook \u2014
2383
+ is a rename, not a layer. Delete it and use the imported component directly, or give it the prop that
2384
+ justifies its existence. Route files are exempt: thin delegation is their purpose.`,
2385
+ meta: {
2386
+ docs: {
2387
+ description: "Flag components whose body is only `return <ImportedComponent/>` with no children, no dynamic attrs, and no hooks. Such components are trivial wrappers \u2014 pointless renames that add a layer without value. If you need the shape, inline or delete. Route files are exempt (thin delegation is their purpose)."
2388
+ },
2389
+ messages: {
2390
+ trivialWrapper: "Component '{{name}}' is a trivial wrapper \u2014 body is only `return <{{inner}}/>` with no children, no dynamic attrs, no hooks. This is lint-theater: it renames {{inner}} for no reason. Delete the wrapper and call {{inner}} directly, OR give the component real work (wrap in structure, compose multiple elements, add hooks, or pass through children)."
2391
+ },
2392
+ schema: [],
2393
+ type: "problem"
2394
+ }
2395
+ };
2396
+ var no_trivial_wrapper_component_default = noTrivialWrapperComponent;
2397
+
2398
+ // src/rules/no-ts-in-bio-folders.ts
2399
+ var BIO_FOLDER_TS_PATTERN = /(?:features\/([^/]+)\/)?(atoms|molecules|compounds|cells|tissues|gutenberg|emails|organs)\/[^/]+\.ts$/;
2400
+ var noTsInBioFolders = {
2401
+ create(context) {
2402
+ const normalized = context.filename.replaceAll(/\\/g, "/");
2403
+ const match = normalized.match(BIO_FOLDER_TS_PATTERN);
2404
+ if (!match) return {};
2405
+ const file = normalized.split("/").pop() || "";
2406
+ if (file === "index.ts" || file.endsWith(".types.ts")) return {};
2407
+ const folder = match[2] ?? "";
2408
+ return {
2409
+ Program(node) {
2410
+ context.report({
2411
+ data: { file, folder },
2412
+ messageId: "tsInBioFolder",
2413
+ node
2414
+ });
2415
+ }
2416
+ };
2417
+ },
2418
+ fixShape: `Component folders hold \`.tsx\`. A \`.ts\` file in \`atoms/\`, \`molecules/\`, \`compounds/\`, \`cells/\` or
2419
+ \`tissues/\` is logic that wandered in \u2014 move it to \`lib/\`, a hook to \`organelles/\`, a store to
2420
+ \`stores/\`. The barrel (\`index.ts\`) and a co-located \`<component>.types.ts\` are the two exceptions,
2421
+ and \`organelles/\` may hold \`use-*.ts\` hooks by design.`,
2422
+ meta: {
2423
+ docs: {
2424
+ description: "Pure .ts files must not live in biological component folders (atoms/, molecules/, compounds/, cells/, tissues/, gutenberg/, emails/). Component folders are for .tsx files only. Pure logic and config go in lib/; hooks go in organelles/; stores go in stores/. Exception: co-located `<component>.types.ts` files are allowed."
2425
+ },
2426
+ messages: {
2427
+ tsInBioFolder: '"{{file}}" is a .ts file in {{folder}}/. Only .tsx components (and co-located *.types.ts) belong here. Move pure logic to lib/, hooks to organelles/, stores to stores/.'
2428
+ },
2429
+ schema: [],
2430
+ type: "problem"
2431
+ }
2432
+ };
2433
+ var no_ts_in_bio_folders_default = noTsInBioFolders;
2434
+
2435
+ // src/rules/no-type-definitions-in-components.ts
2436
+ var COMPONENT_FILE_PATTERN4 = /(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
2437
+ var noTypeDefinitionsInComponents = {
2438
+ create(context) {
2439
+ const normalized = context.filename.replaceAll(/\\/g, "/");
2440
+ if (!COMPONENT_FILE_PATTERN4.test(normalized)) return {};
2441
+ const feature = normalized.match(/features\/([^/]+)\//)?.[1] || "";
2442
+ return {
2443
+ TSInterfaceDeclaration(node) {
2444
+ const name = node.id?.name || "unknown";
2445
+ context.report({
2446
+ data: { feature, name },
2447
+ messageId: "interfaceInComponent",
2448
+ node
2449
+ });
2450
+ },
2451
+ TSTypeAliasDeclaration(node) {
2452
+ const name = node.id?.name || "unknown";
2453
+ context.report({
2454
+ data: { feature, name },
2455
+ messageId: "typeInComponent",
2456
+ node
2457
+ });
2458
+ }
2459
+ };
2460
+ },
2461
+ fixShape: `Props are typed inline in the component's signature. No \`interface FooProps\`, no \`type\` declaration
2462
+ in a \`.tsx\`. If a shape is shared, it goes in \`lib/types.ts\` or a co-located \`<component>.types.ts\`
2463
+ and is imported.`,
2464
+ meta: {
2465
+ docs: {
2466
+ description: "Component .tsx files must not contain interface or type declarations. Props must be typed inline in the function signature. Shared types go in features/*/lib/types.ts."
2467
+ },
2468
+ messages: {
2469
+ interfaceInComponent: 'Interface "{{name}}" defined in a component file. Inline the props in the function signature: ({ prop }: { prop: Type }) or move shared types to features/{{feature}}/lib/types.ts',
2470
+ typeInComponent: 'Type alias "{{name}}" defined in a component file. Inline the type or move to features/{{feature}}/lib/types.ts'
2471
+ },
2472
+ schema: [],
2473
+ type: "problem"
2474
+ }
2475
+ };
2476
+ var no_type_definitions_in_components_default = noTypeDefinitionsInComponents;
2477
+
2478
+ // src/rules/no-void-port.ts
2479
+ var voidPromise = (annotation) => {
2480
+ if (annotation.type !== "TSTypeReference" || annotation.typeName?.name !== "Promise") {
2481
+ return false;
2482
+ }
2483
+ const [argument] = (annotation.typeArguments ?? annotation.typeParameters)?.params ?? [];
2484
+ return argument?.type === "TSVoidKeyword";
2485
+ };
2486
+ var noVoidPort = {
2487
+ create(context) {
2488
+ return {
2489
+ ExportNamedDeclaration(node) {
2490
+ const alias = node.declaration;
2491
+ if (alias?.type !== "TSTypeAliasDeclaration" || alias.id === void 0) return;
2492
+ const fn = alias.typeAnnotation;
2493
+ if (fn?.type !== "TSFunctionType") return;
2494
+ const returned = fn.returnType?.typeAnnotation;
2495
+ if (returned === void 0 || !voidPromise(returned)) return;
2496
+ context.report({ data: { name: alias.id.name }, messageId: "voidPort", node });
2497
+ }
2498
+ };
2499
+ },
2500
+ fixShape: `An exported function type that can fail returns \`Result<void, TaggedError>\`, never \`Promise<void>\` \u2014
2501
+ \`void\` leaves the caller no way to know it failed. A module-private callback whose failure channel is
2502
+ the engine that catches it is not a port and is unaffected.`,
2503
+ meta: {
2504
+ docs: {
2505
+ description: "An exported function type alias returning Promise<void> is a port with no answer: whoever calls it cannot know it failed. Ports return Result<void, Error> from better-result. A module-private callback is not a port."
2506
+ },
2507
+ messages: {
2508
+ voidPort: '"{{name}}" returns Promise<void>, so the caller cannot know it failed. Return Result<void, YourTaggedError> from better-result and let the caller decide.'
2509
+ },
2510
+ schema: [],
2511
+ type: "problem"
2512
+ }
2513
+ };
2514
+ var no_void_port_default = noVoidPort;
2515
+
2516
+ // src/rules/organelle-dependency.ts
2517
+ var DEFAULT_SHARED_FEATURES = ["shared"];
2518
+ var organelleDependency = {
2519
+ create(context) {
2520
+ const sharedFeatures = new Set(context.options?.[0]?.sharedFeatures ?? DEFAULT_SHARED_FEATURES);
2521
+ const filename = context.filename.replaceAll(/\\/g, "/");
2522
+ const organelleMatch = filename.match(/(?:features\/([^/]+)\/)?organelles\//);
2523
+ if (!organelleMatch) return {};
2524
+ const currentFeature = organelleMatch[1];
2525
+ const shortName = filename.split("/").pop() || filename;
2526
+ return {
2527
+ ImportDeclaration(node) {
2528
+ if (node.importKind === "type") return;
2529
+ const source = node.source.value;
2530
+ if (/\/cells\//.test(source)) {
2531
+ context.report({
2532
+ data: { file: shortName, source },
2533
+ messageId: "forbidden",
2534
+ node
2535
+ });
2536
+ return;
2537
+ }
2538
+ if (/\/tissues\//.test(source) || /\/organs\//.test(source) || /(?:^|\/)layouts\//.test(source) || /apps\/web\/app\//.test(source)) {
2539
+ context.report({
2540
+ data: { file: shortName, source },
2541
+ messageId: "forbidden",
2542
+ node
2543
+ });
2544
+ return;
2545
+ }
2546
+ const crossFeatureOrganelleMatch = source.match(/features\/([^/]+)\/organelles\//);
2547
+ if (crossFeatureOrganelleMatch) {
2548
+ const importedFeature = crossFeatureOrganelleMatch[1] ?? "";
2549
+ if (importedFeature !== currentFeature && !sharedFeatures.has(importedFeature)) {
2550
+ context.report({
2551
+ data: { file: shortName, source },
2552
+ messageId: "forbidden",
2553
+ node
2554
+ });
2555
+ }
2556
+ }
2557
+ }
2558
+ };
2559
+ },
2560
+ fixShape: `An organelle may reach atoms, molecules, compounds, stores, domains and other organelles. Cells,
2561
+ tissues, organs and layouts are above it \u2014 a cell contains the organelle, never the reverse.
2562
+ Cross-feature organelle imports are allowed only from the features named in the rule's
2563
+ \`sharedFeatures\` option (default: \`shared\`); anything else flows through the cell as props.`,
2564
+ meta: {
2565
+ docs: {
2566
+ description: "Organelles may depend on atoms, molecules, compounds, stores, domains, and OTHER ORGANELLES (sub-organelles: nucleolus in nucleus, thylakoid in chloroplast). Organelles cannot depend on cells, tissues, organs, or layouts - direction is wrong: cells contain organelles, not the reverse. Cross-feature organelle imports are allowed only from shared/document/charts (shared infra) or same feature."
2567
+ },
2568
+ messages: {
2569
+ forbidden: 'Organelle "{{file}}" cannot import from "{{source}}". Organelles may depend on atoms, molecules, compounds, stores, domains, and sub-organelles (same feature or shared/document/charts).'
2570
+ },
2571
+ schema: [
2572
+ {
2573
+ additionalProperties: false,
2574
+ properties: {
2575
+ sharedFeatures: { items: { type: "string" }, type: "array" }
2576
+ },
2577
+ type: "object"
2578
+ }
2579
+ ],
2580
+ type: "problem"
2581
+ }
2582
+ };
2583
+ var organelle_dependency_default = organelleDependency;
2584
+
2585
+ // src/rules/organelle-single-source.ts
2586
+ var ORGANELLE_FILE_PATTERN = /(?:features\/[^/]+\/)?organelles\//;
2587
+ var ZUSTAND_STORE_PATTERN = /^use\w+Store$/;
2588
+ var FORM_SOURCES = /* @__PURE__ */ new Set(["react-hook-form"]);
2589
+ var FORM_CONTEXT_IMPORT_PATTERN = /FormContext$/;
2590
+ var organelleSingleSource = {
2591
+ create(context) {
2592
+ const normalized = context.filename.replaceAll(/\\/g, "/");
2593
+ if (!ORGANELLE_FILE_PATTERN.test(normalized)) return {};
2594
+ const file = normalized.split("/").pop()?.replace(/\.tsx?$/, "") || "unknown";
2595
+ const storeHooks = /* @__PURE__ */ new Map();
2596
+ let formNode = null;
2597
+ let formSource = "";
2598
+ let programNode = null;
2599
+ const fromModule = (node) => {
2600
+ if (!node.source) return;
2601
+ if (node.importKind === "type") return;
2602
+ const source = node.source.value;
2603
+ if (FORM_SOURCES.has(source) && !formNode) {
2604
+ formNode = node;
2605
+ formSource = source;
2606
+ return;
2607
+ }
2608
+ if (source.includes("/providers/")) {
2609
+ const specs = node.specifiers ?? [];
2610
+ for (const spec of specs) {
2611
+ const localName = spec.local?.name || "";
2612
+ if (FORM_CONTEXT_IMPORT_PATTERN.test(localName) && !formNode) {
2613
+ formNode = node;
2614
+ formSource = localName;
2615
+ }
2616
+ }
2617
+ }
2618
+ };
2619
+ return {
2620
+ CallExpression(node) {
2621
+ if (node.callee.type !== "Identifier") return;
2622
+ const name = node.callee.name;
2623
+ if (!name) return;
2624
+ if (ZUSTAND_STORE_PATTERN.test(name) && !storeHooks.has(name)) {
2625
+ storeHooks.set(name, node);
2626
+ }
2627
+ },
2628
+ ExportAllDeclaration: fromModule,
2629
+ ExportNamedDeclaration: fromModule,
2630
+ ImportDeclaration: fromModule,
2631
+ Program(node) {
2632
+ programNode = node;
2633
+ },
2634
+ "Program:exit"() {
2635
+ const storeNames = [...storeHooks.keys()];
2636
+ const isTsx = normalized.endsWith(".tsx");
2637
+ if (storeNames.length > 0 && formNode) {
2638
+ context.report({
2639
+ data: {
2640
+ file,
2641
+ formSource,
2642
+ zustandSource: storeNames.join(", ")
2643
+ },
2644
+ messageId: isTsx ? "mixedStoreAndFormComponent" : "mixedStoreAndFormHook",
2645
+ node: programNode
2646
+ });
2647
+ }
2648
+ if (storeNames.length > 1) {
2649
+ context.report({
2650
+ data: {
2651
+ count: String(storeNames.length),
2652
+ file,
2653
+ stores: storeNames.join(", ")
2654
+ },
2655
+ messageId: isTsx ? "multipleStoresComponent" : "multipleStoresHook",
2656
+ node: programNode
2657
+ });
2658
+ }
2659
+ }
2660
+ };
2661
+ },
2662
+ fixShape: `One organelle, one state source: at most ONE Zustand store, and never Zustand and react-hook-form in
2663
+ the same file. Mixing them means DECOMPOSE, not promote \u2014 split into one focused organelle per
2664
+ source. The CELL is the only level where several sources meet; it derives and passes props down.`,
2665
+ meta: {
2666
+ docs: {
2667
+ description: "Organelles must have a single state source. (1) An organelle must not mix Zustand and react-hook-form \u2014 the cell coordinates across sources. (2) An organelle must read from at most ONE Zustand store \u2014 if it needs multiple stores, the cell should derive and pass the data down."
2668
+ },
2669
+ messages: {
2670
+ mixedStoreAndFormComponent: 'Organelle "{{file}}" mixes Zustand ({{zustandSource}}) and form state ({{formSource}}). DECOMPOSE: split into a store organelle + a form organelle, each with ONE source. Cell composes both and passes derived data as props.',
2671
+ mixedStoreAndFormHook: 'Hook "{{file}}" mixes Zustand ({{zustandSource}}) and form state ({{formSource}}). SPLIT into separate hooks \u2014 one per source. Cell calls both.',
2672
+ multipleStoresComponent: 'Organelle "{{file}}" reads from {{count}} stores ({{stores}}). DECOMPOSE: split into {{count}} focused organelles, each reading ONE store. Cell composes them and passes shared/derived data as props to compounds.',
2673
+ multipleStoresHook: 'Hook "{{file}}" reads from {{count}} stores ({{stores}}). SPLIT into {{count}} hooks \u2014 one per store. Cell calls all of them.'
2674
+ },
2675
+ schema: [],
2676
+ type: "problem"
2677
+ }
2678
+ };
2679
+ var organelle_single_source_default = organelleSingleSource;
2680
+
2681
+ // src/rules/queries-require-org-scope.ts
2682
+ var QUERY_FILE_PATTERN = /\/db\/src\/queries\/.*\.ts$/;
2683
+ function writesOrgId(annotation) {
2684
+ if (!annotation) return false;
2685
+ if (annotation.type === "TSIntersectionType")
2686
+ return (annotation.types ?? []).some((member) => writesOrgId(member));
2687
+ if (annotation.type !== "TSTypeLiteral") return false;
2688
+ return (annotation.members ?? []).some(
2689
+ (member) => member.type === "TSPropertySignature" && member.key?.name === "orgId"
2690
+ );
2691
+ }
2692
+ function declaresOrgId(param) {
2693
+ return writesOrgId(param.typeAnnotation?.typeAnnotation);
2694
+ }
2695
+ var queriesRequireOrgScope = {
2696
+ create(context) {
2697
+ const normalized = context.filename.replace(/\\/g, "/");
2698
+ if (!QUERY_FILE_PATTERN.test(normalized)) return {};
2699
+ const check = (name, params, node) => {
2700
+ if (params.some(declaresOrgId)) return;
2701
+ context.report({ data: { name }, messageId: "missingOrgScope", node });
2702
+ };
2703
+ return {
2704
+ "ExportNamedDeclaration > FunctionDeclaration"(node) {
2705
+ if (!node.id?.name) return;
2706
+ check(node.id.name, node.params ?? [], node);
2707
+ },
2708
+ "ExportNamedDeclaration > VariableDeclaration"(node) {
2709
+ for (const declarator of node.declarations ?? []) {
2710
+ const init = declarator.init;
2711
+ if (!init) continue;
2712
+ if (init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression")
2713
+ continue;
2714
+ if (declarator.id?.type !== "Identifier" || !declarator.id.name) continue;
2715
+ check(declarator.id.name, init.params ?? [], node);
2716
+ }
2717
+ }
2718
+ };
2719
+ },
2720
+ fixShape: `Every exported query takes \`(db, params)\` where \`params\` WRITES \`orgId: string\` inline in the
2721
+ signature \u2014 a type alias says nothing checkable. An intersection is fine as long as one member spells
2722
+ \`orgId\`. A query that cannot name the organisation cannot be scoped to it.`,
2723
+ meta: {
2724
+ docs: {
2725
+ description: "Every exported query in packages/db/src/queries/ must take orgId in an inline params type literal. The predicate belongs to the query \u2014 the row-level policies are the wall behind it \u2014 and org scoping is enforced by the query signature, and a params type hidden behind an alias cannot be checked, so the literal must be written in the signature."
2726
+ },
2727
+ messages: {
2728
+ missingOrgScope: 'Query "{{name}}" does not declare orgId. Give it a (db, params) signature whose params type literal includes "orgId: string" \u2014 every read and write is org-scoped (plan 026 D3).'
2729
+ },
2730
+ schema: [],
2731
+ type: "problem"
2732
+ }
2733
+ };
2734
+ var queries_require_org_scope_default = queriesRequireOrgScope;
2735
+
2736
+ // src/rules/queue-loop-is-the-library.ts
2737
+ var queueMessageMethods = /* @__PURE__ */ new Set(["ack", "retry", "ackAll", "retryAll"]);
2738
+ var queueLoopIsTheLibrary = {
2739
+ create(context) {
2740
+ if (/node_modules|\/__tests__\//.test(context.filename)) return {};
2741
+ return {
2742
+ CallExpression(node) {
2743
+ const property = node.callee.type === "MemberExpression" ? node.callee.property : void 0;
2744
+ const name = property?.type === "Identifier" ? property.name : void 0;
2745
+ if (name === void 0 || !queueMessageMethods.has(name)) return;
2746
+ context.report({ data: { method: name }, messageId: "handRolledLoop", node });
2747
+ }
2748
+ };
2749
+ },
2750
+ fixShape: `Application code never calls a queue message's \`ack()\`, \`retry()\`, \`ackAll()\` or \`retryAll()\`. The
2751
+ consumer loop, the dedupe and the retry policy belong to the library \u2014 hand it the batch. Your code
2752
+ decides what an event MEANS and throws when it cannot.`,
2753
+ meta: {
2754
+ docs: {
2755
+ description: "Application code never calls a queue message's ack/retry: sagaflow's handleQueue owns the consumer loop, the dedupe (`seen`) and the retry policy. A consumer decides what an event means in `onEvent` and throws when it cannot."
2756
+ },
2757
+ messages: {
2758
+ handRolledLoop: `".{{method}}()" is the queue loop, and the loop is sagaflow's: hand the batch to handleQueue({ seen, onEvent }) and throw from onEvent when a message cannot be handled.`
2759
+ },
2760
+ schema: [],
2761
+ type: "problem"
2762
+ }
2763
+ };
2764
+ var queue_loop_is_the_library_default = queueLoopIsTheLibrary;
2765
+
2766
+ // src/rules/ssot-no-inline-facts.ts
2767
+ var factOwners = [
2768
+ /\/constants\.ts$/,
2769
+ /\/packages\/domains\//,
2770
+ /\/packages\/client\/src\//,
2771
+ /\/lib\/env\.ts$/,
2772
+ /\/lib\/routes\.ts$/,
2773
+ /\/lib\/selectors\.ts$/,
2774
+ /\/src\/paths\.ts$/,
2775
+ /\/rest\/paths\.ts$/,
2776
+ /\/packages\/db\/src\/schema\//,
2777
+ /\/__tests__\//,
2778
+ /\/scripts\//,
2779
+ /\/testing\//,
2780
+ /\/test\//,
2781
+ /\/seeds\//,
2782
+ /\/migrations\//,
2783
+ /\.config\.(ts|mjs|js)$/
2784
+ ];
2785
+ var libraryVocabulary = /* @__PURE__ */ new Set([
2786
+ "use client",
2787
+ "use server",
2788
+ "content-type",
2789
+ "content-length",
2790
+ "content-disposition",
2791
+ "authorization",
2792
+ "cookie",
2793
+ "set-cookie",
2794
+ "cache-control",
2795
+ "user-agent",
2796
+ "application/json",
2797
+ "application/octet-stream",
2798
+ "application/pdf",
2799
+ "application/ld+json",
2800
+ "text/plain",
2801
+ "text/csv",
2802
+ "text/html",
2803
+ "BAD_REQUEST",
2804
+ "UNAUTHORIZED",
2805
+ "FORBIDDEN",
2806
+ "NOT_FOUND",
2807
+ "CONFLICT",
2808
+ "PRECONDITION_FAILED",
2809
+ "TOO_MANY_REQUESTS",
2810
+ "INTERNAL_SERVER_ERROR",
2811
+ "NOT_IMPLEMENTED",
2812
+ "TIMEOUT",
2813
+ "CLIENT_CLOSED_REQUEST",
2814
+ "PAYLOAD_TOO_LARGE",
2815
+ "UNPROCESSABLE_CONTENT",
2816
+ "METHOD_NOT_SUPPORTED",
2817
+ "UNSUPPORTED_MEDIA_TYPE",
2818
+ "BAD_GATEWAY",
2819
+ "SERVICE_UNAVAILABLE",
2820
+ "GATEWAY_TIMEOUT",
2821
+ "PARSE_ERROR",
2822
+ "https://schema.org",
2823
+ "summary_large_image",
2824
+ "https://www.w3.org/2000/svg",
2825
+ "http://www.w3.org/2000/svg"
2826
+ ]);
2827
+ var styleAttributes = /* @__PURE__ */ new Set([
2828
+ "className",
2829
+ "class",
2830
+ "data-testid",
2831
+ "id",
2832
+ "key",
2833
+ "htmlFor",
2834
+ "for",
2835
+ "name",
2836
+ "transform",
2837
+ "style",
2838
+ "placeholder",
2839
+ "autoComplete"
2840
+ ]);
2841
+ var styleCallees = /* @__PURE__ */ new Set(["cn", "clsx", "twMerge", "cva", "tv"]);
2842
+ var declarationCallees = /* @__PURE__ */ new Set([
2843
+ "declare",
2844
+ "saga",
2845
+ "step",
2846
+ "action",
2847
+ "defineEvent",
2848
+ "defineWorkflow",
2849
+ "createStep",
2850
+ "sqliteTable",
2851
+ "text",
2852
+ "integer",
2853
+ "real",
2854
+ "blob",
2855
+ "index",
2856
+ "uniqueIndex",
2857
+ "primaryKey",
2858
+ "foreignKey",
2859
+ "check",
2860
+ "literal",
2861
+ "enum"
2862
+ ]);
2863
+ var formFieldCallees = /* @__PURE__ */ new Set([
2864
+ "register",
2865
+ "watch",
2866
+ "useWatch",
2867
+ "setValue",
2868
+ "getValues",
2869
+ "getFieldState",
2870
+ "trigger",
2871
+ "resetField",
2872
+ "setError",
2873
+ "clearErrors",
2874
+ "setFocus",
2875
+ "useController",
2876
+ "useFieldArray"
2877
+ ]);
2878
+ var isFactShaped = (literal) => {
2879
+ if (literal.length < 4 || /\s/.test(literal)) return false;
2880
+ if (libraryVocabulary.has(literal)) return false;
2881
+ if (/^(text|application|image|audio|video|multipart|font)\//.test(literal)) return false;
2882
+ if (literal.startsWith("/")) return true;
2883
+ if (literal.includes("://")) return true;
2884
+ if (/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/.test(literal)) return true;
2885
+ if (/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/.test(literal)) return true;
2886
+ return literal.length >= 16 && !/^[a-z0-9:\-[\]/.%#!]+$/.test(literal);
2887
+ };
2888
+ var nameOf = (node) => {
2889
+ if (node === void 0) return void 0;
2890
+ if (typeof node === "string") return node;
2891
+ if (node.type === "Identifier" || node.type === "JSXIdentifier") return node.name;
2892
+ if (node.type === "Literal") return String(node.value);
2893
+ return void 0;
2894
+ };
2895
+ var isSpelledAsStyleOrModule = (node) => {
2896
+ let current = node.parent;
2897
+ let child = node;
2898
+ while (current && typeof current.type === "string" && current.type !== "Program") {
2899
+ if (current.type === "JSXAttribute") return styleAttributes.has(nameOf(current.name) ?? "");
2900
+ if (current.type === "Property" && current.key === child) return true;
2901
+ if (current.type === "Property" && styleAttributes.has(nameOf(current.key) ?? "")) return true;
2902
+ if (current.type === "CallExpression") {
2903
+ const callee = current.callee;
2904
+ const calleeName = callee?.type === "MemberExpression" ? nameOf(callee.property) : nameOf(callee);
2905
+ if (styleCallees.has(calleeName ?? "") || formFieldCallees.has(calleeName ?? "")) return true;
2906
+ const firstArgument = current.arguments?.[0];
2907
+ if (declarationCallees.has(calleeName ?? "") && firstArgument === child) return true;
2908
+ }
2909
+ if (current.type === "ImportExpression" || current.type.startsWith("TS")) return true;
2910
+ if ((current.type === "ImportDeclaration" || current.type === "ExportNamedDeclaration" || current.type === "ExportAllDeclaration") && current.source === child) {
2911
+ return true;
2912
+ }
2913
+ child = current;
2914
+ current = current.parent;
2915
+ }
2916
+ return false;
2917
+ };
2918
+ var ssotNoInlineFacts = {
2919
+ create(context) {
2920
+ const normalized = context.filename.replace(/\\/g, "/");
2921
+ if (factOwners.some((owner) => owner.test(normalized))) return {};
2922
+ const check = (literal, node) => {
2923
+ if (!isFactShaped(literal)) return;
2924
+ if (isSpelledAsStyleOrModule(node)) return;
2925
+ context.report({ data: { literal }, messageId: "inlineFact", node });
2926
+ };
2927
+ return {
2928
+ Literal(node) {
2929
+ if (typeof node.value === "string") check(node.value, node);
2930
+ },
2931
+ TemplateLiteral(node) {
2932
+ if ((node.expressions?.length ?? 0) > 0) return;
2933
+ check(node.quasis?.[0]?.value?.cooked ?? "", node);
2934
+ }
2935
+ };
2936
+ },
2937
+ fixShape: `A route, a URL, a code, a dotted key: a fact is declared once, in the module that owns it, and
2938
+ imported. An inline literal used as configuration is a second copy waiting to drift \u2014 name it and put
2939
+ it where its owner lives.`,
2940
+ meta: {
2941
+ docs: {
2942
+ description: "A fact \u2014 a route, a URL, a CODE, a dotted.name, a long key \u2014 is declared once, in its owner module (the owning module, lib/routes.ts, lib/env.ts, @during/client), and imported everywhere else. Spelling it inline makes a second source."
2943
+ },
2944
+ messages: {
2945
+ inlineFact: '"{{literal}}" is a fact spelled inline. Declare it once in its owner module and import it.'
2946
+ },
2947
+ schema: [],
2948
+ type: "problem"
2949
+ }
2950
+ };
2951
+ var ssot_no_inline_facts_default = ssotNoInlineFacts;
2952
+
2953
+ // src/rules/ssot-no-process-env.ts
2954
+ var envDoors = [
2955
+ /\/lib\/env\.ts$/,
2956
+ /\/next\.config\.(ts|mjs|js)$/,
2957
+ /\.config\.(ts|mjs|js)$/,
2958
+ /\/scripts\//,
2959
+ /\/__tests__\//,
2960
+ /\/testing\//,
2961
+ /\/test\//,
2962
+ /\/seeds\//
2963
+ ];
2964
+ var isProcessEnv = (node) => node.object.type === "Identifier" && node.object.name === "process" && node.property.type === "Identifier" && node.property.name === "env";
2965
+ var ssotNoProcessEnv = {
2966
+ create(context) {
2967
+ const normalized = context.filename.replace(/\\/g, "/");
2968
+ if (envDoors.some((door) => door.test(normalized))) return {};
2969
+ return {
2970
+ MemberExpression(node) {
2971
+ if (isProcessEnv(node)) context.report({ messageId: "envOutsideDoor", node });
2972
+ }
2973
+ };
2974
+ },
2975
+ fixShape: `\`process.env\` is read at one door per app \u2014 the env module, the framework config, scripts and tests.
2976
+ Everywhere else, import the typed value from that door. A second reader is a second place the name
2977
+ can be misspelled and a second default nobody agreed to.`,
2978
+ meta: {
2979
+ docs: {
2980
+ description: "A host reads its environment in exactly one place (lib/env.ts, parsed once); every other module imports the parsed `env`. Reading process.env elsewhere makes a second source of configuration."
2981
+ },
2982
+ messages: {
2983
+ envOutsideDoor: "process.env is read outside the env door. Add the variable to lib/env.ts and import `env` from there."
2984
+ },
2985
+ schema: [],
2986
+ type: "problem"
2987
+ }
2988
+ };
2989
+ var ssot_no_process_env_default = ssotNoProcessEnv;
2990
+
2991
+ // src/rules/step-opens-its-own-cell.ts
2992
+ var STEP = "step";
2993
+ var readable = (pattern) => pattern.replaceAll("\\", "").replace(/\$$/, "");
2994
+ var stepOpensItsOwnCell = {
2995
+ create(context) {
2996
+ const { engine, within, wrapper } = context.options?.[0] ?? {};
2997
+ if (engine === void 0 || within === void 0) return {};
2998
+ const filename = context.filename.replaceAll("\\", "/");
2999
+ if (!new RegExp(within).test(filename)) return {};
3000
+ if (wrapper !== void 0 && new RegExp(wrapper).test(filename)) return {};
3001
+ const file = filename.split("/").pop() ?? filename;
3002
+ return {
3003
+ ImportDeclaration(node) {
3004
+ if (node.importKind === "type") return;
3005
+ if (node.source.value !== engine) return;
3006
+ for (const specifier of node.specifiers ?? []) {
3007
+ if (specifier.type !== "ImportSpecifier") continue;
3008
+ if (specifier.importKind === "type") continue;
3009
+ if (specifier.imported?.name !== STEP) continue;
3010
+ context.report({
3011
+ data: {
3012
+ engine,
3013
+ file,
3014
+ wrapper: wrapper === void 0 ? "the repo's own step wrapper" : readable(wrapper)
3015
+ },
3016
+ messageId: "engineStep",
3017
+ node
3018
+ });
3019
+ }
3020
+ }
3021
+ };
3022
+ },
3023
+ fixShape: `Inside the guarded workflow tree, import \`step\` from the repo's own wrapper, never from the engine
3024
+ package. A durable run hibernates and retries between steps, so each step body must open the
3025
+ connection it uses rather than share one opened once per run \u2014 the wrapper does that, the engine's
3026
+ \`step\` does not. The wrapper file is the one place allowed to reach the engine. Configure
3027
+ \`engine\`, \`within\` and \`wrapper\`; unconfigured, the rule does nothing.`,
3028
+ meta: {
3029
+ docs: {
3030
+ description: "A durable saga takes its `step` from the repo's own wrapper, which opens a connection inside each step body \u2014 a durable run hibernates and retries, so a handle shared across steps is dead by the time a later step uses it. The engine module, the guarded tree and the wrapper file are options; with none the rule does nothing."
3031
+ },
3032
+ messages: {
3033
+ engineStep: '"{{file}}" imports `step` from `{{engine}}`. Import it from {{wrapper}} instead: a durable run spans hibernation and retries, so the connection a step uses has to be opened inside that step rather than shared across steps. The wrapper opens it; the engine\'s `step` does not.'
3034
+ },
3035
+ schema: [
3036
+ {
3037
+ additionalProperties: false,
3038
+ properties: {
3039
+ engine: { type: "string" },
3040
+ within: { type: "string" },
3041
+ wrapper: { type: "string" }
3042
+ },
3043
+ type: "object"
3044
+ }
3045
+ ],
3046
+ type: "problem"
3047
+ }
3048
+ };
3049
+ var step_opens_its_own_cell_default = stepOpensItsOwnCell;
3050
+
3051
+ // src/rules/store-route-scopes-tenant-data.ts
3052
+ var STORE_ROUTE_PATTERN = /\/api\/store\/.*\/route\.tsx?$/;
3053
+ var TENANT_ENTITIES = /* @__PURE__ */ new Set([
3054
+ "cart",
3055
+ "claim",
3056
+ "company",
3057
+ "customer",
3058
+ "employee",
3059
+ "order",
3060
+ "planned_order",
3061
+ "production_order",
3062
+ "quote"
3063
+ ]);
3064
+ var SCOPE_MARKERS = /auth_context|actor_id|customer_id|company_id|\bcustomer\b\s*:/;
3065
+ var storeRouteScopesTenantData = {
3066
+ create(context) {
3067
+ const normalized = context.filename.replaceAll(/\\/g, "/");
3068
+ if (!STORE_ROUTE_PATTERN.test(normalized)) return {};
3069
+ const shortName = normalized.split("/").slice(-3).join("/");
3070
+ return {
3071
+ Program(node) {
3072
+ const text = context.sourceCode?.text ?? "";
3073
+ if (text === "" || SCOPE_MARKERS.test(text)) return;
3074
+ for (const entity of TENANT_ENTITIES) {
3075
+ const reads = new RegExp(`entity:\\s*['"\`]${entity}['"\`]`).test(text);
3076
+ if (reads) {
3077
+ context.report({
3078
+ data: { entity, file: shortName },
3079
+ messageId: "unscoped",
3080
+ node
3081
+ });
3082
+ return;
3083
+ }
3084
+ }
3085
+ }
3086
+ };
3087
+ },
3088
+ fixShape: `A customer-facing store route that reads a customer-owned entity (cart, order, quote, company, \u2026)
3089
+ narrows the read to the authenticated caller \u2014 \`auth_context\`, \`actor_id\`, \`customer_id\` or
3090
+ \`company_id\` must appear in the handler. Without it a publishable key returns every tenant's rows.
3091
+ Global catalog entities are deliberately exempt; if the data is genuinely not customer data, it
3092
+ belongs on the admin surface.`,
3093
+ meta: {
3094
+ docs: {
3095
+ description: "A /store/** route that reads a customer-owned entity must narrow the read to the authenticated caller. Without it the handler returns every tenant\u2019s rows to anyone holding a publishable key. Global catalog entities are exempt."
3096
+ },
3097
+ messages: {
3098
+ unscoped: 'Store route "{{file}}" reads the customer-owned entity "{{entity}}" without any caller scoping (no auth_context / actor_id / customer_id / company_id in the file). Narrow the read to the authenticated caller, or move it to /admin if it is genuinely not customer data.'
3099
+ },
3100
+ schema: [],
3101
+ type: "problem"
3102
+ }
3103
+ };
3104
+ var store_route_scopes_tenant_data_default = storeRouteScopesTenantData;
3105
+
3106
+ // src/rules/tables-declare-their-plane.ts
3107
+ var tableCallees = ["sqliteTable", "pgTable"];
3108
+ var tablesDeclareTheirPlane = {
3109
+ create(context) {
3110
+ const filename = context.filename.replaceAll("\\", "/");
3111
+ if (!/\/packages\/db\/src\/schema\//.test(filename)) return {};
3112
+ if (/\/schema\/(control|tenant)\//.test(filename)) return {};
3113
+ return {
3114
+ CallExpression(node) {
3115
+ for (const callee of tableCallees) {
3116
+ const table = literalNamedBy(node, callee);
3117
+ if (table !== null) context.report({ data: { table }, messageId: "undeclared", node });
3118
+ }
3119
+ }
3120
+ };
3121
+ },
3122
+ fixShape: `Every table is declared under \`schema/control/\` (who is who, read at the edge) or \`schema/tenant/\`
3123
+ (an organisation's rows, placed in a cell). A table declared loose in \`schema/\` has not decided which
3124
+ it is, and nothing downstream can place or isolate it. Move the file.`,
3125
+ meta: {
3126
+ docs: {
3127
+ description: "Every table lives under schema/control/ (the control plane) or schema/tenant/ (an organisation's rows, placed in a cell). A table declared elsewhere has not chosen its plane (plan 037)."
3128
+ },
3129
+ messages: {
3130
+ undeclared: '"{{table}}" is declared outside schema/control/ and schema/tenant/. Move it: control-plane tables are read by user, key or address at the edge; tenant tables carry org_id and live in a cell (plan 037).'
3131
+ },
3132
+ schema: [],
3133
+ type: "problem"
3134
+ }
3135
+ };
3136
+ var tables_declare_their_plane_default = tablesDeclareTheirPlane;
3137
+
3138
+ // src/rules/tenant-tables-carry-org-id.ts
3139
+ var tableCallees2 = /* @__PURE__ */ new Set(["sqliteTable", "pgTable"]);
3140
+ var carriesOrgId = (columns) => columns?.type === "ObjectExpression" && (columns.properties ?? []).some(
3141
+ (property) => property.type === "Property" && property.key?.name === "orgId"
3142
+ );
3143
+ var tenantTablesCarryOrgId = {
3144
+ create(context) {
3145
+ if (!/\/packages\/db\/src\/schema\/tenant\//.test(context.filename.replaceAll("\\", "/"))) {
3146
+ return {};
3147
+ }
3148
+ return {
3149
+ CallExpression(node) {
3150
+ if (node.callee.type !== "Identifier" || !tableCallees2.has(node.callee.name ?? "")) return;
3151
+ const [name, columns] = node.arguments ?? [];
3152
+ const table = name && "value" in name && typeof name.value === "string" ? name.value : "this table";
3153
+ if (carriesOrgId(columns)) return;
3154
+ context.report({ data: { table }, messageId: "noOrgId", node });
3155
+ }
3156
+ };
3157
+ },
3158
+ fixShape: `A tenant-plane table declares an \`orgId\` column. Without it the row cannot be scoped, placed or
3159
+ isolated, and it is really a control-plane table in the wrong folder. Add the column, or move the
3160
+ table to \`schema/control/\`.`,
3161
+ meta: {
3162
+ docs: {
3163
+ description: "Every table under schema/tenant/ has an `orgId` column: the organisation is the partition key of a cell and the subject of its isolation policy (plan 037)."
3164
+ },
3165
+ messages: {
3166
+ noOrgId: '"{{table}}" is a tenant table without an `orgId` column. Add `orgId: orgIdColumn()` \u2014 or move it to schema/control/ if it is not one organisation\'s (plan 037).'
3167
+ },
3168
+ schema: [],
3169
+ type: "problem"
3170
+ }
3171
+ };
3172
+ var tenant_tables_carry_org_id_default = tenantTablesCarryOrgId;
3173
+
3174
+ // src/rules/time-through-the-door.ts
3175
+ var timeDoor = /\/packages\/utils\/src\/time\.ts$/;
3176
+ var exemptFiles3 = [timeDoor, /\/__tests__\//, /\/scripts\//, /\/testing\//, /\/seeds\//];
3177
+ var dateStatics = /* @__PURE__ */ new Set(["parse", "UTC"]);
3178
+ var dateInstanceMethods = /* @__PURE__ */ new Set([
3179
+ "toISOString",
3180
+ "toLocaleDateString",
3181
+ "toDateString",
3182
+ "getFullYear",
3183
+ "getMonth",
3184
+ "getDate",
3185
+ "getDay",
3186
+ "getHours",
3187
+ "getUTCFullYear",
3188
+ "getUTCMonth",
3189
+ "getUTCDate",
3190
+ "setFullYear",
3191
+ "setMonth",
3192
+ "setDate"
3193
+ ]);
3194
+ var memberName = (callee) => ({
3195
+ object: callee.object?.type === "Identifier" ? callee.object.name : void 0,
3196
+ property: callee.property?.type === "Identifier" ? callee.property.name : void 0
3197
+ });
3198
+ var timeThroughTheDoor = {
3199
+ create(context) {
3200
+ const normalized = context.filename.replace(/\\/g, "/");
3201
+ if (exemptFiles3.some((file) => file.test(normalized))) return {};
3202
+ return {
3203
+ ImportDeclaration(node) {
3204
+ if (node.source.value === "dayjs" || node.source.value.startsWith("dayjs/")) {
3205
+ context.report({ messageId: "dayjsOutsideDoor", node });
3206
+ }
3207
+ },
3208
+ NewExpression(node) {
3209
+ if (node.callee.type === "Identifier" && node.callee.name === "Date") {
3210
+ context.report({ messageId: "outsideTimeDoor", node, data: { what: "new Date" } });
3211
+ }
3212
+ },
3213
+ CallExpression(node) {
3214
+ if (node.callee.type !== "MemberExpression") return;
3215
+ const { object, property } = memberName(node.callee);
3216
+ if (property === void 0) return;
3217
+ if (object === "Date" && property === "now") return;
3218
+ if (object === "Date" && dateStatics.has(property) || dateInstanceMethods.has(property)) {
3219
+ context.report({
3220
+ messageId: "outsideTimeDoor",
3221
+ node,
3222
+ data: { what: `${object ?? "\u2026"}.${property}` }
3223
+ });
3224
+ }
3225
+ }
3226
+ };
3227
+ },
3228
+ fixShape: `\`new Date(...)\`, \`Date.parse\`, \`toISOString\`, \`toLocaleDateString\`, \`dayjs\` \u2014 every time operation
3229
+ goes through the one time module, which owns the plugins, the zone and the formats. Import
3230
+ \`isoDate\` / \`today\` / \`addDays\` from it. \`Date.now()\` is allowed anywhere.`,
3231
+ meta: {
3232
+ docs: {
3233
+ description: "Time is handled by dayjs behind the time module (`utils/time`); nothing else constructs, parses or formats a Date. Date.now() is the one call allowed everywhere."
3234
+ },
3235
+ messages: {
3236
+ outsideTimeDoor: "{{what}} outside the time door. Import the operation from the time module (`utils/time`) (isoDate, today, addDays, addMonths, \u2026) or add it there.",
3237
+ dayjsOutsideDoor: "dayjs is imported outside the time module (`utils/time`). The door owns the plugins and formats; import from it."
3238
+ },
3239
+ schema: [],
3240
+ type: "problem"
3241
+ }
3242
+ };
3243
+ var time_through_the_door_default = timeThroughTheDoor;
3244
+
3245
+ // src/rules/tissue-must-compose.ts
3246
+ import fs4 from "fs";
3247
+ var COMPOSITION_PATTERNS = [
3248
+ // Tier segment with a trailing slash OR at the end of the specifier, so both
3249
+ // relative (`../cells/x`, `../../feature/cells/x`) and self-subpath feature
3250
+ // barrels (`#ui/cart/cells`, `#email/hero/compounds`) are recognized.
3251
+ /\/(atoms|molecules|compounds|cells|tissues|organelles)(\/|$)/,
3252
+ // react-email primitives are the email surface's compositional units — an
3253
+ // email template that arranges <Section>/<Row>/<Container> IS composing.
3254
+ /^@react-email\//,
3255
+ // Flat-kit sibling splits: a tissue arranges its UI across private siblings
3256
+ // (`import { X } from './site-header.parts'`). Importing + rendering those is
3257
+ // real composition, not lint-theater.
3258
+ /\.(parts|sections|layouts)$/
3259
+ ];
3260
+ function isReexportOnly3(file) {
3261
+ try {
3262
+ const src = fs4.readFileSync(file, "utf8").replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/\/\/.*$/gm, "");
3263
+ const hasReexport = /\bexport\b[^;]*\bfrom\b\s*['"]/.test(src);
3264
+ const hasComponent = /\bfunction\b|=>|\breturn\b|<[A-Za-z]/.test(src);
3265
+ return hasReexport && !hasComponent;
3266
+ } catch {
3267
+ return false;
3268
+ }
3269
+ }
3270
+ var APP_ROUTE_MUST_COMPOSE = /* @__PURE__ */ new Set(["page.tsx", "layout.tsx", "template.tsx"]);
3271
+ var APP_ROUTE_FALLBACKS = /* @__PURE__ */ new Set([
3272
+ "default.tsx",
3273
+ "not-found.tsx",
3274
+ "loading.tsx",
3275
+ "error.tsx",
3276
+ "global-error.tsx"
3277
+ ]);
3278
+ var METADATA_EXEMPT_BASENAMES = /* @__PURE__ */ new Set(["layout.tsx", "template.tsx"]);
3279
+ var METADATA_EXPORT_NAMES = /* @__PURE__ */ new Set([
3280
+ "metadata",
3281
+ "viewport",
3282
+ "generateMetadata",
3283
+ "generateViewport"
3284
+ ]);
3285
+ var tissueMustCompose = {
3286
+ create(context) {
3287
+ const filename = context.filename;
3288
+ const normalized = filename.replaceAll(/\\/g, "/");
3289
+ const isTissueDir = /(?:features\/[^/]+\/)?tissues\//.test(normalized);
3290
+ const basename = normalized.split("/").pop() ?? "";
3291
+ const isTissue = isTissueDir && basename.endsWith(".tsx") && basename !== "index.tsx" && !/\.(parts|sections|summary|layouts|icons|types|hooks|utils|context|store|sidebar|data|config|stories|spec|test)\.tsx$/.test(
3292
+ basename
3293
+ );
3294
+ const isAppRouteFile = /\/app\//.test(normalized) && APP_ROUTE_MUST_COMPOSE.has(basename);
3295
+ const isAppFallback = /\/app\//.test(normalized) && APP_ROUTE_FALLBACKS.has(basename);
3296
+ if (isAppFallback) return {};
3297
+ if (!isTissue && !isAppRouteFile) return {};
3298
+ if (isReexportOnly3(filename)) return {};
3299
+ const bioImportLocals = /* @__PURE__ */ new Set();
3300
+ const renderedTagNames = /* @__PURE__ */ new Set();
3301
+ let hasMetadataExport = false;
3302
+ let programNode = null;
3303
+ return {
3304
+ ExportNamedDeclaration(node) {
3305
+ const decl = node.declaration;
3306
+ if (!decl) return;
3307
+ if (decl.type === "VariableDeclaration") {
3308
+ for (const d of decl.declarations ?? []) {
3309
+ const name = d.id?.name;
3310
+ if (name && METADATA_EXPORT_NAMES.has(name)) {
3311
+ hasMetadataExport = true;
3312
+ }
3313
+ }
3314
+ } else if (decl.type === "FunctionDeclaration") {
3315
+ const name = decl.id?.name;
3316
+ if (name && METADATA_EXPORT_NAMES.has(name)) {
3317
+ hasMetadataExport = true;
3318
+ }
3319
+ }
3320
+ },
3321
+ ImportDeclaration(node) {
3322
+ if (node.importKind === "type") return;
3323
+ const source = node.source.value;
3324
+ if (!COMPOSITION_PATTERNS.some((pattern) => pattern.test(source))) return;
3325
+ for (const spec of node.specifiers ?? []) {
3326
+ const localName = spec.local?.name;
3327
+ if (localName) bioImportLocals.add(localName);
3328
+ }
3329
+ },
3330
+ JSXOpeningElement(node) {
3331
+ const name = node.name;
3332
+ if (!name) return;
3333
+ if (name.type === "JSXIdentifier") {
3334
+ const id = name;
3335
+ if (id.name) renderedTagNames.add(id.name);
3336
+ } else if (name.type === "JSXMemberExpression") {
3337
+ const member = name;
3338
+ const root = member.object?.name;
3339
+ if (root) renderedTagNames.add(root);
3340
+ }
3341
+ },
3342
+ "Program:exit"(node) {
3343
+ programNode = node;
3344
+ if (hasMetadataExport && METADATA_EXEMPT_BASENAMES.has(basename)) {
3345
+ return;
3346
+ }
3347
+ const isTrulyEmpty = bioImportLocals.size === 0 && renderedTagNames.size === 0 && !hasMetadataExport;
3348
+ if (basename === "page.tsx" && isTrulyEmpty) {
3349
+ return;
3350
+ }
3351
+ const composesByRender = [...bioImportLocals].some(
3352
+ (localName) => renderedTagNames.has(localName)
3353
+ );
3354
+ if (!composesByRender) {
3355
+ const shortName = normalized.split("/").pop() || normalized;
3356
+ const reason = bioImportLocals.size === 0 ? "noBioImports" : "bioImportedButNotRendered";
3357
+ context.report({
3358
+ data: { file: shortName, reason },
3359
+ messageId: "missingComposition",
3360
+ node: programNode
3361
+ });
3362
+ }
3363
+ }
3364
+ };
3365
+ },
3366
+ fixShape: `A tissue must RENDER something it imported from the tiers below \u2014 a cell, a compound, a molecule, an
3367
+ atom or another tissue. A tissue that only passes \`children\` through, or returns null, arranges
3368
+ nothing: that is lint-theater. If there is nothing to arrange, the file should not exist.`,
3369
+ meta: {
3370
+ docs: {
3371
+ description: "Tissues MUST RENDER at least one cell, compound, molecule, atom, or other tissue. Tissues arrange bio elements into functional units \u2014 a tissue that imports bio symbols but never renders them is performing FAKE composition (lint-theater). This rule checks the JSX render tree, not just the import list, to close the fake-compose bypass. That is lint-theater."
3372
+ },
3373
+ messages: {
3374
+ missingComposition: 'Tissue/route "{{file}}" does not RENDER any cell/compound/molecule/atom/tissue. Tissues and Next.js route UI files (page/layout/template) MUST arrange bio elements \u2014 passthrough wrappers like `<>{children}</>`, `<div>{children}</div>`, or `return null` are lint-theater. Fix: compose real bio in JSX, OR delete the file if it exists only for metadata (move metadata to a child page.tsx), OR reclassify it. Next.js fallback files (default/not-found/loading/error/global-error) are automatically exempt.'
3375
+ },
3376
+ schema: [],
3377
+ type: "problem"
3378
+ }
3379
+ };
3380
+ var tissue_must_compose_default = tissueMustCompose;
3381
+
3382
+ // src/rules/tissue-no-data-props.ts
3383
+ var TISSUE_FILE_PATTERN = /(?:features\/[^/]+\/)?tissues\//;
3384
+ var APP_LAYOUT_OR_TEMPLATE = /* @__PURE__ */ new Set(["layout.tsx", "template.tsx"]);
3385
+ var ROUTE_FALLBACK_BASENAMES2 = /* @__PURE__ */ new Set([
3386
+ "default.tsx",
3387
+ "error.tsx",
3388
+ "global-error.tsx",
3389
+ "loading.tsx",
3390
+ "not-found.tsx"
3391
+ ]);
3392
+ var ALWAYS_ALLOWED_PROP_NAMES = /* @__PURE__ */ new Set(["children", "className", "params", "searchParams"]);
3393
+ var REACT_NODE_TYPE_NAMES = /* @__PURE__ */ new Set([
3394
+ "ComponentType",
3395
+ "Element",
3396
+ "JSX",
3397
+ "PropsWithChildren",
3398
+ "ReactElement",
3399
+ "ReactNode"
3400
+ ]);
3401
+ var isKindVocabulary = (name) => /(Kind|EntityName)$/.test(name);
3402
+ function isAllowedTypeRefName(name) {
3403
+ if (REACT_NODE_TYPE_NAMES.has(name)) return true;
3404
+ if (isKindVocabulary(name)) return true;
3405
+ if (name.endsWith("Icon")) return true;
3406
+ if (name.endsWith("IconComponent")) return true;
3407
+ if (name.endsWith("IconType")) return true;
3408
+ return false;
3409
+ }
3410
+ function describeTypeAnnotation(t) {
3411
+ if (!t || !t.type) return "unknown";
3412
+ switch (t.type) {
3413
+ case "TSStringKeyword":
3414
+ return "string";
3415
+ case "TSNumberKeyword":
3416
+ return "number";
3417
+ case "TSBooleanKeyword":
3418
+ return "boolean";
3419
+ case "TSArrayType":
3420
+ return `Array<${describeTypeAnnotation(t.elementType)}>`;
3421
+ case "TSTypeReference":
3422
+ return t.typeName?.name ?? "(unknown-ref)";
3423
+ case "TSUnionType":
3424
+ return (t.types ?? []).map(describeTypeAnnotation).join(" | ");
3425
+ default:
3426
+ return t.type;
3427
+ }
3428
+ }
3429
+ function isLiteralStringUnion(t) {
3430
+ if (t.type !== "TSUnionType") return false;
3431
+ const types = t.types ?? [];
3432
+ if (types.length === 0) return false;
3433
+ return types.every((x) => x.type === "TSLiteralType" && typeof x.literal?.value === "string");
3434
+ }
3435
+ var narrowsAKindVocabulary = (t) => {
3436
+ const name = t.typeName?.name ?? "";
3437
+ if (name !== "Exclude" && name !== "Extract") return false;
3438
+ const [subject] = (t.typeArguments ?? t.typeParameters)?.params ?? [];
3439
+ return subject?.type === "TSTypeReference" && isKindVocabulary(subject.typeName?.name ?? "");
3440
+ };
3441
+ function isAllowedType(t) {
3442
+ if (!t || !t.type) return true;
3443
+ switch (t.type) {
3444
+ case "TSTypeReference":
3445
+ return isAllowedTypeRefName(t.typeName?.name ?? "") || narrowsAKindVocabulary(t);
3446
+ case "TSUnionType":
3447
+ return isLiteralStringUnion(t);
3448
+ // TSTypeLiteral (inline object shape) — allowed because nested shapes
3449
+ // Are typically ReactNode records; if it turns out to contain data
3450
+ // Primitives, the outer check will miss them (v1 limitation).
3451
+ case "TSTypeLiteral":
3452
+ return true;
3453
+ // Primitives and arrays fall through to forbidden.
3454
+ case "TSStringKeyword":
3455
+ case "TSNumberKeyword":
3456
+ case "TSBooleanKeyword":
3457
+ case "TSArrayType":
3458
+ return false;
3459
+ default:
3460
+ return true;
3461
+ }
3462
+ }
3463
+ var tissueNoDataProps = {
3464
+ create(context) {
3465
+ const normalized = context.filename.replaceAll(/\\/g, "/");
3466
+ if (/__tests__\//.test(normalized)) return {};
3467
+ const basename = normalized.split("/").pop() ?? "";
3468
+ const isFeatureTissue = TISSUE_FILE_PATTERN.test(normalized);
3469
+ const isAppTissue = /(?:^|\/)app\//.test(normalized) && APP_LAYOUT_OR_TEMPLATE.has(basename);
3470
+ if (!isFeatureTissue && !isAppTissue) return {};
3471
+ if (ROUTE_FALLBACK_BASENAMES2.has(basename)) return {};
3472
+ function checkFunction2(node) {
3473
+ const firstParam = node.params?.[0];
3474
+ if (!firstParam) return;
3475
+ const typeLiteral = firstParam.typeAnnotation?.typeAnnotation;
3476
+ if (!typeLiteral || typeLiteral.type !== "TSTypeLiteral") return;
3477
+ const members = typeLiteral.members ?? [];
3478
+ for (const member of members) {
3479
+ if (member.key?.type !== "Identifier") continue;
3480
+ const propName = member.key.name ?? "";
3481
+ if (!propName) continue;
3482
+ if (ALWAYS_ALLOWED_PROP_NAMES.has(propName)) continue;
3483
+ const t = member.typeAnnotation?.typeAnnotation;
3484
+ if (!t) continue;
3485
+ if (isAllowedType(t)) continue;
3486
+ context.report({
3487
+ data: { prop: propName, type: describeTypeAnnotation(t) },
3488
+ messageId: "dataProp",
3489
+ node: member
3490
+ });
3491
+ }
3492
+ }
3493
+ function checkDeclaration(decl) {
3494
+ if (!decl || typeof decl !== "object") return;
3495
+ const d = decl;
3496
+ if (d.type === "FunctionDeclaration") {
3497
+ checkFunction2(d);
3498
+ return;
3499
+ }
3500
+ if (d.type === "VariableDeclaration") {
3501
+ for (const declNode of d.declarations ?? []) {
3502
+ const init = declNode.init;
3503
+ if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
3504
+ checkFunction2(init);
3505
+ }
3506
+ }
3507
+ }
3508
+ }
3509
+ return {
3510
+ // Only inspect the PUBLIC INTERFACE of the tissue file — the default or
3511
+ // Named export. Inner helper functions (not exported) are file-scoped
3512
+ // Implementation details; their props don't define the tissue's
3513
+ // Contract, so they're out of scope for this rule.
3514
+ ExportDefaultDeclaration(node) {
3515
+ checkDeclaration(node.declaration);
3516
+ },
3517
+ ExportNamedDeclaration(node) {
3518
+ checkDeclaration(node.declaration);
3519
+ }
3520
+ };
3521
+ },
3522
+ fixShape: `A tissue takes \`children\`, ReactNode slot props, layout variants, \`params\` and \`searchParams\` \u2014 and
3523
+ nothing else. A prop typed \`string\`, \`number\`, an array or a domain type (\`id\`, \`initialData\`,
3524
+ \`documents\`) means the file owns data, which makes it a CELL: move it to \`cells/\`. A tissue gets its
3525
+ content as slots, never as data.`,
3526
+ meta: {
3527
+ docs: {
3528
+ description: "Tissues must not own data. Props on a tissue must be limited to `children`, ReactNode slot props, `params`, `searchParams`, icon types, or layout-variant literal unions. Any primitive (`string`/`number`/`boolean`), array type, or arbitrary domain type reference means the file is actually a cell \u2014 move it to cells/. That is lint-theater.2 (Tissue self-sufficiency rule)."
3529
+ },
3530
+ messages: {
3531
+ dataProp: "Tissue prop '{{prop}}' has type '{{type}}' which is not allowed on a tissue. Tissues arrange children without owning data. Move the file from features/*/tissues/ to features/*/cells/ \u2014 cells own data. Allowed tissue props: children, params, searchParams, ReactNode slots, literal-union variants, icon types."
3532
+ },
3533
+ schema: [],
3534
+ type: "problem"
3535
+ }
3536
+ };
3537
+ var tissue_no_data_props_default = tissueNoDataProps;
3538
+
3539
+ // src/rules/tissue-no-hooks.ts
3540
+ var FORBIDDEN_HOOKS = /* @__PURE__ */ new Set([
3541
+ "useCallback",
3542
+ "useEffect",
3543
+ "useImperativeHandle",
3544
+ "useLayoutEffect",
3545
+ "useMemo",
3546
+ "useReducer",
3547
+ "useRef",
3548
+ "useState"
3549
+ ]);
3550
+ var HOOK_CALL_PATTERN = /^use[A-Z]/;
3551
+ var TISSUE_FILE_PATTERN2 = /(?:features\/[^/]+\/)?tissues\//;
3552
+ var tissueNoHooks = {
3553
+ create(context) {
3554
+ const filename = context.filename;
3555
+ const normalized = filename.replaceAll(/\\/g, "/");
3556
+ const isTissue = TISSUE_FILE_PATTERN2.test(normalized);
3557
+ const shortName = normalized.split("/").pop() || normalized;
3558
+ const isEntrypoint = isTissue && shortName.endsWith(".tsx") && shortName !== "index.tsx" && !/\.(parts|sections|summary|layouts|icons|types|hooks|utils|context|store|sidebar|data|config|stories|spec|test)\.tsx$/.test(
3559
+ shortName
3560
+ );
3561
+ if (!isEntrypoint) return {};
3562
+ return {
3563
+ CallExpression(node) {
3564
+ const called = node.callee;
3565
+ if (called?.type !== "Identifier") return;
3566
+ const name = called.name;
3567
+ if (name === void 0 || !HOOK_CALL_PATTERN.test(name)) return;
3568
+ context.report({
3569
+ data: { file: shortName, hook: name },
3570
+ messageId: "called",
3571
+ node
3572
+ });
3573
+ },
3574
+ ImportDeclaration(node) {
3575
+ if (node.importKind === "type") return;
3576
+ if (node.source.value !== "react") return;
3577
+ const specifiers = node.specifiers ?? [];
3578
+ for (const spec of specifiers) {
3579
+ if (spec.type !== "ImportSpecifier") continue;
3580
+ const importedName = spec.imported?.name;
3581
+ if (!importedName) continue;
3582
+ if (FORBIDDEN_HOOKS.has(importedName)) {
3583
+ context.report({
3584
+ data: { file: shortName, hook: importedName },
3585
+ messageId: "forbidden",
3586
+ node
3587
+ });
3588
+ }
3589
+ }
3590
+ }
3591
+ };
3592
+ },
3593
+ fixShape: `A tissue holds no state: no \`useState\`, no \`useEffect\`, and no call to ANY \`use*()\` \u2014 including a
3594
+ hook defined in \`lib/\` that reads a store behind your back. If you need the hook, the file is a cell:
3595
+ move it to \`cells/\`, or push the call into a cell the tissue arranges. Private siblings
3596
+ (\`.parts.tsx\`, \`.sections.tsx\`) are not the tissue entrypoint and may hold local UI state.`,
3597
+ meta: {
3598
+ docs: {
3599
+ description: "Tissues are pure arrangements of cells and cannot hold state. Tissues cannot import stateful React hooks. State lives only at the organelle and cell levels. If you need hooks in a tissue, the file is actually a cell - promote it or push the logic into a constituent cell."
3600
+ },
3601
+ messages: {
3602
+ called: 'Tissue "{{file}}" calls "{{hook}}". A hook call is state, wherever the hook is defined \u2014 a rule that only reads imports never sees the state a `lib/` hook reaches through. Move this file to cells/, or push the call into a cell it arranges.',
3603
+ forbidden: 'Tissue "{{file}}" imports "{{hook}}" from react. Tissues are pure arrangements of cells and cannot hold state. State lives only at organelle/cell levels. Either push this logic into a cell, or if this file is truly doing cell work, move it to the cells/ tier.'
3604
+ },
3605
+ schema: [],
3606
+ type: "problem"
3607
+ }
3608
+ };
3609
+ var tissue_no_hooks_default = tissueNoHooks;
3610
+
3611
+ // src/rules/tissue-no-organelles.ts
3612
+ var TISSUE_FILE_PATTERN3 = /(?:features\/[^/]+\/)?tissues\//;
3613
+ var tissueNoOrganelles = {
3614
+ create(context) {
3615
+ const filename = context.filename;
3616
+ const normalized = filename.replaceAll(/\\/g, "/");
3617
+ const isTissue = TISSUE_FILE_PATTERN3.test(normalized);
3618
+ if (!isTissue) return {};
3619
+ const shortName = normalized.split("/").pop() || normalized;
3620
+ return {
3621
+ ImportDeclaration(node) {
3622
+ if (node.importKind === "type") return;
3623
+ const source = node.source.value;
3624
+ if (/\/organelles\//.test(source)) {
3625
+ context.report({
3626
+ data: { file: shortName, source },
3627
+ messageId: "forbidden",
3628
+ node
3629
+ });
3630
+ }
3631
+ }
3632
+ };
3633
+ },
3634
+ fixShape: `A tissue never imports an organelle. Organelles live INSIDE cells; go through a cell that hosts the
3635
+ organelle and let the tissue arrange the cell. Reaching past the cell is how state gets into a layer
3636
+ that is supposed to have none.`,
3637
+ meta: {
3638
+ docs: {
3639
+ description: "Tissues cannot directly import organelles. In biology, tissues arrange cells; organelles live INSIDE cells. To reach an organelle from a tissue, go through a cell. This forces tissues to stay at the cell-arrangement level and keeps state encapsulated in the cells that own their organelles."
3640
+ },
3641
+ messages: {
3642
+ forbidden: 'Tissue "{{file}}" cannot import organelle "{{source}}". Tissues arrange cells; organelles live inside cells. Host this organelle inside a cell, then compose the cell from this tissue.'
3643
+ },
3644
+ schema: [],
3645
+ type: "problem"
3646
+ }
3647
+ };
3648
+ var tissue_no_organelles_default = tissueNoOrganelles;
3649
+
3650
+ // src/rules/tissue-no-stores.ts
3651
+ var TISSUE_FILE_PATTERN4 = /(?:features\/[^/]+\/)?tissues\//;
3652
+ var tissueNoStores = {
3653
+ create(context) {
3654
+ const filename = context.filename;
3655
+ const normalized = filename.replaceAll(/\\/g, "/");
3656
+ const isTissue = TISSUE_FILE_PATTERN4.test(normalized);
3657
+ if (!isTissue) return {};
3658
+ const shortName = normalized.split("/").pop() || normalized;
3659
+ return {
3660
+ ImportDeclaration(node) {
3661
+ if (node.importKind === "type") return;
3662
+ const source = node.source.value;
3663
+ if (/stores\//.test(source)) {
3664
+ context.report({
3665
+ data: { file: shortName, source },
3666
+ messageId: "forbidden",
3667
+ node
3668
+ });
3669
+ }
3670
+ }
3671
+ };
3672
+ },
3673
+ fixShape: `A tissue reads no store. State lives at the organelle and cell tiers only \u2014 a tissue that needs store
3674
+ data is a cell wearing the wrong folder. Move it to \`cells/\`, or have a cell read the store and let
3675
+ the tissue arrange that cell.`,
3676
+ meta: {
3677
+ docs: {
3678
+ description: "Tissues cannot import from stores. Tissues arrange cells but must not directly access state. State lives at organelle and cell levels. (App route files are classified as organs - see organ-no-stores.)"
3679
+ },
3680
+ messages: {
3681
+ forbidden: 'Tissue "{{file}}" cannot import from "{{source}}". Tissues arrange cells but must not directly access stores. Push the store read into a constituent cell.'
3682
+ },
3683
+ schema: [],
3684
+ type: "problem"
3685
+ }
3686
+ };
3687
+ var tissue_no_stores_default = tissueNoStores;
3688
+
3689
+ // src/rules/worker-handles-are-scoped.ts
3690
+ var cellWorkerPattern = /\/apps\/api\/src\//;
3691
+ var handlerKeys = /* @__PURE__ */ new Set(["fetch", "queue", "scheduled", "email"]);
3692
+ var entrypointMakers = /* @__PURE__ */ new Set([
3693
+ "WorkflowEntrypoint",
3694
+ "entrypointFor",
3695
+ "createWorkflowEntrypoint"
3696
+ ]);
3697
+ var cellHandlesAreScoped = {
3698
+ create(context) {
3699
+ const normalized = context.filename.replace(/\\/g, "/");
3700
+ if (!cellWorkerPattern.test(normalized)) return {};
3701
+ let handler = null;
3702
+ let handlerNode = null;
3703
+ let scoped = false;
3704
+ return {
3705
+ ExportDefaultDeclaration(node) {
3706
+ const keys = (node.declaration?.properties ?? []).map((property) => property.key?.name);
3707
+ const named = keys.find((key) => key !== void 0 && handlerKeys.has(key));
3708
+ if (node.declaration?.type !== "ObjectExpression" || named === void 0) return;
3709
+ handler = `the ${named} handler`;
3710
+ handlerNode = node;
3711
+ },
3712
+ CallExpression(node) {
3713
+ if (node.callee?.type !== "Identifier") return;
3714
+ if (node.callee.name === "withCells") scoped = true;
3715
+ if (node.callee.name === "createWorkflowEntrypoint") {
3716
+ handler = "the workflow entrypoint";
3717
+ handlerNode = node;
3718
+ }
3719
+ },
3720
+ // `extends WorkflowEntrypoint`, `extends entrypointFor(...)`, `extends createWorkflowEntrypoint(...)`
3721
+ ClassDeclaration(node) {
3722
+ const parent = node.superClass;
3723
+ const name = parent?.type === "Identifier" ? parent.name : parent?.type === "CallExpression" && parent.callee?.type === "Identifier" ? parent.callee.name : void 0;
3724
+ if (name === void 0 || !entrypointMakers.has(name)) return;
3725
+ handler = "the workflow entrypoint";
3726
+ handlerNode = node;
3727
+ },
3728
+ "Program:exit"(node) {
3729
+ if (handler === null || scoped) return;
3730
+ context.report({
3731
+ data: { handler },
3732
+ messageId: "handlerOutsideWithCells",
3733
+ node: handlerNode ?? node
3734
+ });
3735
+ }
3736
+ };
3737
+ },
3738
+ fixShape: `A worker that opens a database-cell handle wraps every invocation in \`withCells\` \u2014 the module
3739
+ handlers (\`fetch\`, \`queue\`, \`scheduled\`, \`email\`) and the workflow entrypoint alike \u2014 so the handle
3740
+ is closed when the invocation ends: \`withCells(() => \u2026, (closing) => ctx.waitUntil(closing))\`. A
3741
+ handler that declares itself and never calls it opens a pool per call and never closes one.`,
3742
+ meta: {
3743
+ docs: {
3744
+ description: "Every invocation of a cell worker \u2014 the module handlers and the Workflow entrypoint \u2014 runs inside `withCells`, which shares one cell handle per invocation and closes it when the invocation ends. A file that declares such an invocation and never calls `withCells` opens a pool it never closes."
3745
+ },
3746
+ messages: {
3747
+ handlerOutsideWithCells: "This file declares {{handler}} and never calls `withCells`. Wrap the invocation \u2014 `withCells(() => \u2026, (closing) => ctx.waitUntil(closing))` \u2014 so the cell handle it opens is closed when it ends."
3748
+ },
3749
+ schema: [],
3750
+ type: "problem"
3751
+ }
3752
+ };
3753
+ var worker_handles_are_scoped_default = cellHandlesAreScoped;
3754
+
3755
+ // src/rules/zustand-v5-best-practices.ts
3756
+ var STORE_HOOK_PATTERN2 = /^use\w+Store$/;
3757
+ var zustandV5BestPractices = {
3758
+ create(context) {
3759
+ const storeCallsByScope = /* @__PURE__ */ new Map();
3760
+ const functionStack = [];
3761
+ function currentScope() {
3762
+ return functionStack.at(-1) ?? null;
3763
+ }
3764
+ function enterFunction(node) {
3765
+ functionStack.push(node);
3766
+ }
3767
+ function exitFunction() {
3768
+ const scope = functionStack.pop();
3769
+ const storeCalls = storeCallsByScope.get(scope);
3770
+ if (!storeCalls) return;
3771
+ for (const [hookName, data] of storeCalls) {
3772
+ if (data.count > 1) {
3773
+ context.report({
3774
+ data: { count: String(data.count), hook: hookName },
3775
+ messageId: "multipleSelectors",
3776
+ node: data.nodes[1]
3777
+ });
3778
+ }
3779
+ }
3780
+ storeCallsByScope.delete(scope);
3781
+ }
3782
+ return {
3783
+ ArrowFunctionExpression(node) {
3784
+ enterFunction(node);
3785
+ },
3786
+ "ArrowFunctionExpression:exit"() {
3787
+ exitFunction();
3788
+ },
3789
+ CallExpression(node) {
3790
+ if (node.callee.type !== "Identifier") return;
3791
+ const name = node.callee.name;
3792
+ if (!name || !STORE_HOOK_PATTERN2.test(name)) return;
3793
+ if (node.arguments.length === 0) {
3794
+ context.report({
3795
+ data: { hook: name },
3796
+ messageId: "noSelector",
3797
+ node
3798
+ });
3799
+ return;
3800
+ }
3801
+ const firstArg = node.arguments[0];
3802
+ if (firstArg?.type === "CallExpression" && firstArg.callee?.name === "useShallow") return;
3803
+ const scope = currentScope();
3804
+ if (!scope) return;
3805
+ let scopeMap = storeCallsByScope.get(scope);
3806
+ if (!scopeMap) {
3807
+ scopeMap = /* @__PURE__ */ new Map();
3808
+ storeCallsByScope.set(scope, scopeMap);
3809
+ }
3810
+ const existing = scopeMap.get(name) || { count: 0, nodes: [] };
3811
+ existing.nodes.push(node);
3812
+ existing.count++;
3813
+ scopeMap.set(name, existing);
3814
+ },
3815
+ FunctionDeclaration(node) {
3816
+ enterFunction(node);
3817
+ },
3818
+ "FunctionDeclaration:exit"() {
3819
+ exitFunction();
3820
+ },
3821
+ FunctionExpression(node) {
3822
+ enterFunction(node);
3823
+ },
3824
+ "FunctionExpression:exit"() {
3825
+ exitFunction();
3826
+ }
3827
+ };
3828
+ },
3829
+ fixShape: `Never \`useStore()\` with no selector \u2014 that subscribes to the whole store and re-renders on every
3830
+ change. One field: \`useStore((s) => s.field)\`. Several fields: ONE call with
3831
+ \`useShallow((s) => ({ a: s.a, b: s.b }))\`, never several selector calls in the same component.`,
3832
+ meta: {
3833
+ docs: {
3834
+ description: "Enforce Zustand v5 best practices. (1) Require selector on store hooks \u2014 useStore() without args subscribes to entire state. (2) One call per store per component \u2014 multiple individual selectors must be consolidated with useShallow."
3835
+ },
3836
+ messages: {
3837
+ multipleSelectors: '"{{hook}}" called {{count}} times with individual selectors. Use one call with useShallow: const { ... } = {{hook}}(useShallow((s) => ({ ... })))',
3838
+ noSelector: '"{{hook}}()" called without a selector \u2014 subscribes to ENTIRE store, re-renders on ANY change. Use: {{hook}}((s) => s.field) or {{hook}}(useShallow((s) => ({ ... })))'
3839
+ },
3840
+ schema: [],
3841
+ type: "problem"
3842
+ }
3843
+ };
3844
+ var zustand_v5_best_practices_default = zustandV5BestPractices;
3845
+
3846
+ // src/index.ts
3847
+ var rules = {
3848
+ "atom-no-deps": atom_no_deps_default,
3849
+ "cell-must-be-stateful": cell_must_be_stateful_default,
3850
+ "cell-must-not-compose-cell": cell_must_not_compose_cell_default,
3851
+ "cell-no-tissues": cell_no_tissues_default,
3852
+ "cells-folder-index-is-barrel": cells_folder_index_is_barrel_default,
3853
+ "compound-must-be-stateless": compound_must_be_stateless_default,
3854
+ "compound-no-stores": compound_no_stores_default,
3855
+ "constants-in-constants-file": constants_in_constants_file_default,
3856
+ "dialect-through-the-seam": dialect_through_the_seam_default,
3857
+ "document-sagas-are-generic": document_sagas_are_generic_default,
3858
+ "documents-share-one-table": documents_share_one_table_default,
3859
+ "effect-hook-naming": effect_hook_naming_default,
3860
+ "max-comment-density": max_comment_density_default,
3861
+ "molecule-atoms-only": molecule_atoms_only_default,
3862
+ "molecule-must-compose": molecule_must_compose_default,
3863
+ "next-route-segment-is-thin-delegate": next_route_segment_is_thin_delegate_default,
3864
+ "no-brand-names": no_brand_names_default,
3865
+ "no-card-shaped-div": no_card_shaped_div_default,
3866
+ "no-cross-feature-stores": no_cross_feature_stores_default,
3867
+ "no-d1-transaction": no_d1_transaction_default,
3868
+ "no-duplicate-jsx-patterns": no_duplicate_jsx_patterns_default,
3869
+ "no-hook-in-component-disguise": no_hook_in_component_disguise_default,
3870
+ "no-inert-hidden-jsx": no_inert_hidden_jsx_default,
3871
+ "no-inline-data-in-jsx": no_inline_data_in_jsx_default,
3872
+ "no-invalid-feature-folders": no_invalid_feature_folders_default,
3873
+ "no-logic-in-component-files": no_logic_in_component_files_default,
3874
+ "no-orm-outside-db": no_orm_outside_db_default,
3875
+ "no-raw-html-atoms": no_raw_html_atoms_default,
3876
+ "no-raw-sql-outside-allowed": no_raw_sql_outside_allowed_default,
3877
+ "no-react-namespace": no_react_namespace_default,
3878
+ "no-renamed-html-props": no_renamed_html_props_default,
3879
+ "no-render-prop-reader": no_render_prop_reader_default,
3880
+ "no-trivial-wrapper-component": no_trivial_wrapper_component_default,
3881
+ "no-ts-in-bio-folders": no_ts_in_bio_folders_default,
3882
+ "no-type-definitions-in-components": no_type_definitions_in_components_default,
3883
+ "no-void-port": no_void_port_default,
3884
+ "organelle-dependency": organelle_dependency_default,
3885
+ "organelle-single-source": organelle_single_source_default,
3886
+ "queries-require-org-scope": queries_require_org_scope_default,
3887
+ "queue-loop-is-the-library": queue_loop_is_the_library_default,
3888
+ "ssot-no-inline-facts": ssot_no_inline_facts_default,
3889
+ "ssot-no-process-env": ssot_no_process_env_default,
3890
+ "step-opens-its-own-cell": step_opens_its_own_cell_default,
3891
+ "store-route-scopes-tenant-data": store_route_scopes_tenant_data_default,
3892
+ "tables-declare-their-plane": tables_declare_their_plane_default,
3893
+ "tenant-tables-carry-org-id": tenant_tables_carry_org_id_default,
3894
+ "time-through-the-door": time_through_the_door_default,
3895
+ "tissue-must-compose": tissue_must_compose_default,
3896
+ "tissue-no-data-props": tissue_no_data_props_default,
3897
+ "tissue-no-hooks": tissue_no_hooks_default,
3898
+ "tissue-no-organelles": tissue_no_organelles_default,
3899
+ "tissue-no-stores": tissue_no_stores_default,
3900
+ "worker-handles-are-scoped": worker_handles_are_scoped_default,
3901
+ "zustand-v5-best-practices": zustand_v5_best_practices_default
3902
+ };
3903
+ var plugin = {
3904
+ configs: PRESETS,
3905
+ meta: {
3906
+ name: "biological-architecture",
3907
+ version: "0.1.0"
3908
+ },
3909
+ rules
3910
+ };
3911
+ var index_default = plugin;
3912
+ export {
3913
+ PRESETS,
3914
+ index_default as default,
3915
+ rules
3916
+ };