@geonosis/oxlint-plugin-biological-architecture 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +48 -5
  3. package/RULES.md +201 -98
  4. package/corpus/.oxlintrc.json +6 -0
  5. package/corpus/data/bad-dynamic-orm.ts +5 -0
  6. package/corpus/data/bad-type-only-orm.ts +3 -0
  7. package/corpus/e2e/bad-list-response.spec.ts +5 -0
  8. package/corpus/e2e/bad-timeout.spec.ts +5 -0
  9. package/corpus/e2e/good-detail-response.spec.ts +5 -0
  10. package/corpus/e2e/good-state.spec.ts +5 -0
  11. package/corpus/icon-controls/bad-unnamed-icon-button.tsx +8 -0
  12. package/corpus/icon-controls/good-named-icon-button.tsx +19 -0
  13. package/corpus/manifest.json +3 -0
  14. package/corpus/tissues/bad-awaits-data.tsx +8 -0
  15. package/corpus/tissues/bad-fetches-data.tsx +7 -0
  16. package/corpus/tissues/good-takes-products.tsx +5 -0
  17. package/dist/discover-atoms.d.ts +2 -37
  18. package/dist/discover-atoms.js +1 -104
  19. package/dist/index.d.ts +3 -12
  20. package/dist/index.js +19 -174
  21. package/dist/presets.d.ts +11 -19
  22. package/dist/presets.js +89 -3
  23. package/dist/registry.d.ts +2 -0
  24. package/dist/registry.js +172 -0
  25. package/dist/rules/atom-no-deps.d.ts +1 -0
  26. package/dist/rules/atom-no-deps.js +10 -11
  27. package/dist/rules/boundary-time-is-primitive.d.ts +0 -7
  28. package/dist/rules/cell-must-be-stateful.d.ts +2 -2
  29. package/dist/rules/cell-must-be-stateful.js +24 -23
  30. package/dist/rules/cell-must-not-compose-cell.d.ts +1 -0
  31. package/dist/rules/cell-must-not-compose-cell.js +10 -3
  32. package/dist/rules/cell-no-cross-feature-organelles.d.ts +1 -0
  33. package/dist/rules/cell-no-cross-feature-organelles.js +7 -0
  34. package/dist/rules/cell-no-tissues.d.ts +1 -0
  35. package/dist/rules/cell-no-tissues.js +11 -7
  36. package/dist/rules/cells-folder-index-is-barrel.d.ts +12 -10
  37. package/dist/rules/cells-folder-index-is-barrel.js +66 -34
  38. package/dist/rules/compound-must-be-stateless.d.ts +2 -0
  39. package/dist/rules/compound-must-be-stateless.js +24 -26
  40. package/dist/rules/compound-no-stores.d.ts +1 -0
  41. package/dist/rules/compound-no-stores.js +12 -10
  42. package/dist/rules/constants-in-constants-file.d.ts +2 -8
  43. package/dist/rules/constants-in-constants-file.js +5 -6
  44. package/dist/rules/dialect-through-the-seam.d.ts +0 -13
  45. package/dist/rules/durable-body-reads-through-steps.d.ts +0 -14
  46. package/dist/rules/durable-body-reads-through-steps.js +8 -3
  47. package/dist/rules/effect-hook-naming.d.ts +9 -11
  48. package/dist/rules/effect-hook-naming.js +33 -31
  49. package/dist/rules/emit-declares-attempts.d.ts +0 -17
  50. package/dist/rules/emit-declares-attempts.js +5 -10
  51. package/dist/rules/font-roles-only.d.ts +0 -18
  52. package/dist/rules/font-roles-only.js +19 -22
  53. package/dist/rules/hermetic-e2e-no-wall-clock-waits.d.ts +53 -0
  54. package/dist/rules/hermetic-e2e-no-wall-clock-waits.js +100 -0
  55. package/dist/rules/icon-control-has-name.d.ts +53 -0
  56. package/dist/rules/icon-control-has-name.js +71 -0
  57. package/dist/rules/layer-walls.d.ts +0 -25
  58. package/dist/rules/layer-walls.js +7 -13
  59. package/dist/rules/lib/atom-map.d.ts +24 -0
  60. package/dist/rules/lib/atom-map.js +16 -0
  61. package/dist/rules/lib/cells-barrel.d.ts +0 -1
  62. package/dist/rules/lib/colors.d.ts +0 -19
  63. package/dist/rules/lib/colors.js +10 -243
  64. package/dist/rules/lib/comments.d.ts +19 -0
  65. package/dist/rules/lib/comments.js +5 -0
  66. package/dist/rules/lib/context.d.ts +0 -10
  67. package/dist/rules/lib/direction-rule.d.ts +3 -21
  68. package/dist/rules/lib/direction-rule.js +12 -3
  69. package/dist/rules/lib/doors.d.ts +2 -18
  70. package/dist/rules/lib/doors.js +79 -41
  71. package/dist/rules/lib/hooks.d.ts +2 -17
  72. package/dist/rules/lib/hooks.js +14 -9
  73. package/dist/rules/lib/imports.d.ts +2 -23
  74. package/dist/rules/lib/imports.js +7 -2
  75. package/dist/rules/lib/jsx.d.ts +31 -0
  76. package/dist/rules/lib/jsx.js +11 -0
  77. package/dist/rules/lib/landing.d.ts +0 -40
  78. package/dist/rules/lib/literal-call.d.ts +1 -0
  79. package/dist/rules/lib/literal-call.js +2 -0
  80. package/dist/rules/lib/module-text.d.ts +3 -27
  81. package/dist/rules/lib/module-text.js +19 -4
  82. package/dist/rules/lib/mutations.d.ts +0 -14
  83. package/dist/rules/lib/nodes.d.ts +1 -18
  84. package/dist/rules/lib/nodes.js +1 -0
  85. package/dist/rules/lib/objects.d.ts +2 -11
  86. package/dist/rules/lib/objects.js +21 -0
  87. package/dist/rules/lib/options.d.ts +2 -52
  88. package/dist/rules/lib/options.js +44 -18
  89. package/dist/rules/lib/package-imports.d.ts +0 -5
  90. package/dist/rules/lib/package-imports.js +4 -10
  91. package/dist/rules/lib/paths.d.ts +15 -41
  92. package/dist/rules/lib/paths.js +34 -1
  93. package/dist/rules/lib/pattern-match.d.ts +2 -12
  94. package/dist/rules/lib/pattern-match.js +5 -1
  95. package/dist/rules/lib/probe.d.ts +0 -43
  96. package/dist/rules/lib/reexports.d.ts +0 -18
  97. package/dist/rules/lib/render-tracking.d.ts +0 -8
  98. package/dist/rules/lib/shared-features.d.ts +0 -8
  99. package/dist/rules/lib/shared-features.js +2 -9
  100. package/dist/rules/lib/state-sources.d.ts +8 -0
  101. package/dist/rules/lib/state-sources.js +55 -0
  102. package/dist/rules/lib/steps.d.ts +3 -5
  103. package/dist/rules/lib/steps.js +6 -1
  104. package/dist/rules/lib/styling.d.ts +9 -42
  105. package/dist/rules/lib/styling.js +36 -10
  106. package/dist/rules/lib/tiers.d.ts +11 -24
  107. package/dist/rules/lib/tiers.js +41 -1
  108. package/dist/rules/lib/tissue-entrypoint.d.ts +0 -1
  109. package/dist/rules/lib/tissue-entrypoint.js +4 -5
  110. package/dist/rules/lib/tsconfig-paths.d.ts +0 -4
  111. package/dist/rules/lib/tsconfig-paths.js +47 -15
  112. package/dist/rules/lib/type-members.d.ts +6 -0
  113. package/dist/rules/lib/type-members.js +165 -0
  114. package/dist/rules/lib/visitors.d.ts +0 -22
  115. package/dist/rules/lib/workspace-members.d.ts +2 -0
  116. package/dist/rules/lib/workspace-members.js +115 -0
  117. package/dist/rules/list-take-without-order.js +2 -8
  118. package/dist/rules/max-comment-density.d.ts +3 -18
  119. package/dist/rules/max-comment-density.js +3 -3
  120. package/dist/rules/molecule-atoms-only.d.ts +1 -0
  121. package/dist/rules/molecule-atoms-only.js +11 -7
  122. package/dist/rules/molecule-must-compose.d.ts +3 -1
  123. package/dist/rules/molecule-must-compose.js +17 -21
  124. package/dist/rules/mutating-step-declares-retry.d.ts +2 -7
  125. package/dist/rules/mutating-step-declares-retry.js +29 -11
  126. package/dist/rules/named-when-block.d.ts +0 -2
  127. package/dist/rules/named-when-block.js +2 -8
  128. package/dist/rules/next-route-segment-is-thin-delegate.d.ts +4 -19
  129. package/dist/rules/next-route-segment-is-thin-delegate.js +47 -71
  130. package/dist/rules/no-brand-names.d.ts +0 -9
  131. package/dist/rules/no-brand-names.js +1 -1
  132. package/dist/rules/no-card-shaped-div.d.ts +4 -42
  133. package/dist/rules/no-card-shaped-div.js +12 -4
  134. package/dist/rules/no-cross-feature-stores.d.ts +1 -0
  135. package/dist/rules/no-cross-feature-stores.js +9 -5
  136. package/dist/rules/no-d1-transaction.d.ts +2 -5
  137. package/dist/rules/no-d1-transaction.js +7 -0
  138. package/dist/rules/no-date-string-compare.d.ts +2 -0
  139. package/dist/rules/no-date-string-compare.js +14 -8
  140. package/dist/rules/no-duplicate-jsx-patterns.d.ts +2 -0
  141. package/dist/rules/no-duplicate-jsx-patterns.js +16 -1
  142. package/dist/rules/no-generic-utility-package.d.ts +48 -2
  143. package/dist/rules/no-generic-utility-package.js +48 -9
  144. package/dist/rules/no-hook-in-component-disguise.d.ts +27 -13
  145. package/dist/rules/no-hook-in-component-disguise.js +38 -30
  146. package/dist/rules/no-index-count-as-exact.d.ts +0 -3
  147. package/dist/rules/no-inert-hidden-jsx.d.ts +4 -21
  148. package/dist/rules/no-inert-hidden-jsx.js +13 -12
  149. package/dist/rules/no-inline-data-in-jsx.d.ts +2 -0
  150. package/dist/rules/no-inline-data-in-jsx.js +17 -2
  151. package/dist/rules/no-invalid-feature-folders.d.ts +2 -0
  152. package/dist/rules/no-invalid-feature-folders.js +12 -4
  153. package/dist/rules/no-loader-side-effect-in-index-file.d.ts +0 -7
  154. package/dist/rules/no-logic-in-component-files.d.ts +6 -5
  155. package/dist/rules/no-logic-in-component-files.js +52 -31
  156. package/dist/rules/no-mock-db-in-integration.d.ts +0 -15
  157. package/dist/rules/no-mock-db-in-integration.js +3 -4
  158. package/dist/rules/no-orm-outside-db.d.ts +1 -0
  159. package/dist/rules/no-orm-outside-db.js +9 -7
  160. package/dist/rules/no-raw-html-atoms.d.ts +3 -22
  161. package/dist/rules/no-raw-html-atoms.js +5 -17
  162. package/dist/rules/no-raw-sql-outside-allowed.d.ts +2 -0
  163. package/dist/rules/no-raw-sql-outside-allowed.js +10 -15
  164. package/dist/rules/no-react-namespace.d.ts +2 -0
  165. package/dist/rules/no-react-namespace.js +13 -0
  166. package/dist/rules/no-renamed-html-props.d.ts +2 -0
  167. package/dist/rules/no-renamed-html-props.js +11 -2
  168. package/dist/rules/no-render-prop-reader.d.ts +4 -20
  169. package/dist/rules/no-render-prop-reader.js +13 -5
  170. package/dist/rules/no-runtime-dead-affordance.d.ts +3 -26
  171. package/dist/rules/no-runtime-dead-affordance.js +7 -20
  172. package/dist/rules/no-swallowed-catch.js +2 -8
  173. package/dist/rules/no-trivial-wrapper-component.d.ts +4 -19
  174. package/dist/rules/no-trivial-wrapper-component.js +24 -33
  175. package/dist/rules/no-ts-in-bio-folders.d.ts +2 -13
  176. package/dist/rules/no-ts-in-bio-folders.js +9 -7
  177. package/dist/rules/no-type-definitions-in-components.d.ts +2 -0
  178. package/dist/rules/no-type-definitions-in-components.js +12 -1
  179. package/dist/rules/no-unregistered-plugin-dir.d.ts +0 -41
  180. package/dist/rules/no-unregistered-plugin-dir.js +1 -9
  181. package/dist/rules/no-void-port.d.ts +2 -4
  182. package/dist/rules/no-void-port.js +7 -0
  183. package/dist/rules/organelle-dependency.d.ts +1 -0
  184. package/dist/rules/organelle-dependency.js +10 -8
  185. package/dist/rules/organelle-single-source.d.ts +2 -0
  186. package/dist/rules/organelle-single-source.js +58 -9
  187. package/dist/rules/plugin-route-namespaced.d.ts +0 -11
  188. package/dist/rules/plugin-route-namespaced.js +2 -2
  189. package/dist/rules/queries-require-org-scope.d.ts +0 -3
  190. package/dist/rules/queries-require-org-scope.js +3 -10
  191. package/dist/rules/queue-loop-is-the-library.js +3 -2
  192. package/dist/rules/route-no-inline-mutation.d.ts +1 -25
  193. package/dist/rules/route-no-inline-mutation.js +2 -7
  194. package/dist/rules/router-schema-parity.d.ts +0 -9
  195. package/dist/rules/router-schema-parity.js +5 -9
  196. package/dist/rules/service-name-not-reserved.d.ts +0 -3
  197. package/dist/rules/service-name-not-reserved.js +1 -34
  198. package/dist/rules/ssot-no-inline-facts.js +2 -5
  199. package/dist/rules/ssot-no-process-env.js +4 -12
  200. package/dist/rules/step-declares-compensation-or-none.d.ts +2 -1
  201. package/dist/rules/step-declares-compensation-or-none.js +17 -1
  202. package/dist/rules/step-opens-its-own-cell.d.ts +2 -25
  203. package/dist/rules/step-opens-its-own-cell.js +8 -13
  204. package/dist/rules/store-route-scopes-tenant-data.js +8 -26
  205. package/dist/rules/subpath-over-barrel.d.ts +2 -10
  206. package/dist/rules/subpath-over-barrel.js +48 -9
  207. package/dist/rules/subscriber-declares-id.d.ts +0 -2
  208. package/dist/rules/subscriber-declares-id.js +2 -8
  209. package/dist/rules/tables-declare-their-plane.d.ts +0 -2
  210. package/dist/rules/tables-declare-their-plane.js +4 -10
  211. package/dist/rules/tenant-tables-carry-org-id.d.ts +1 -4
  212. package/dist/rules/tenant-tables-carry-org-id.js +5 -12
  213. package/dist/rules/test-no-clock-polling-for-promise-flips.d.ts +2 -0
  214. package/dist/rules/test-no-clock-polling-for-promise-flips.js +18 -1
  215. package/dist/rules/time-through-the-door.d.ts +0 -24
  216. package/dist/rules/time-through-the-door.js +5 -5
  217. package/dist/rules/tissue-must-compose.d.ts +4 -2
  218. package/dist/rules/tissue-must-compose.js +25 -20
  219. package/dist/rules/tissue-no-data-props.d.ts +56 -0
  220. package/dist/rules/tissue-no-data-props.js +114 -32
  221. package/dist/rules/tissue-no-fetch.d.ts +26 -0
  222. package/dist/rules/tissue-no-fetch.js +57 -0
  223. package/dist/rules/tissue-no-hooks.d.ts +20 -22
  224. package/dist/rules/tissue-no-hooks.js +22 -4
  225. package/dist/rules/tissue-no-organelles.d.ts +1 -0
  226. package/dist/rules/tissue-no-organelles.js +9 -1
  227. package/dist/rules/tissue-no-stores.d.ts +1 -0
  228. package/dist/rules/tissue-no-stores.js +9 -1
  229. package/dist/rules/todo-names-its-plan.d.ts +1 -28
  230. package/dist/rules/todo-names-its-plan.js +2 -2
  231. package/dist/rules/tokens-only-colors.d.ts +2 -37
  232. package/dist/rules/tokens-only-colors.js +33 -18
  233. package/dist/rules/unique-query-step-name.d.ts +0 -7
  234. package/dist/rules/unique-query-step-name.js +6 -10
  235. package/dist/rules/worker-handles-are-scoped.d.ts +11 -8
  236. package/dist/rules/worker-handles-are-scoped.js +12 -5
  237. package/dist/rules/zustand-v5-best-practices.d.ts +8 -18
  238. package/dist/rules/zustand-v5-best-practices.js +32 -56
  239. package/package.json +2 -2
@@ -1,20 +1,8 @@
1
1
  import { type RuleContextWithOptions } from './lib/context.js';
2
+ import { type Probe } from './lib/probe.js';
2
3
  type Options = {
3
4
  extraFolders?: readonly string[] | string;
4
5
  };
5
- /**
6
- * Issue #5. This rule used to carry its own list of "the architecture's folders" — `atoms,
7
- * molecules, compounds, cells, tissues, gutenberg, emails, organs`. It omitted `organelles`, which
8
- * LAW.md declares a tier, and named three folders the law never mentions; `no-invalid-feature-
9
- * folders` held a second, different list beside it. Which folders are tiers is not this rule's
10
- * decision: `lib/tiers.ts` declares them, held against LAW.md by a test, and both rules read it.
11
- * What IS this rule's decision is the one it is named for — a file sitting in a tier folder is not
12
- * a bare `.ts`.
13
- *
14
- * `gutenberg`, `emails` and `organs` were one repo's vocabulary in shared code, which LAW 6
15
- * forbids, so they are gone from the default and reachable as `extraFolders` by a consumer that
16
- * genuinely holds components somewhere the law does not name.
17
- */
18
6
  declare const noTsInBioFolders: {
19
7
  create(context: RuleContextWithOptions<Options>): {
20
8
  Program?: undefined;
@@ -48,5 +36,6 @@ declare const noTsInBioFolders: {
48
36
  }[];
49
37
  type: 'problem';
50
38
  };
39
+ probe(): Probe;
51
40
  };
52
41
  export default noTsInBioFolders;
@@ -1,6 +1,7 @@
1
1
  // src/rules/no-ts-in-bio-folders.ts
2
2
  import { PATTERNS, patternsOf } from "./lib/options.js";
3
3
  import { fileOf, normalise } from "./lib/paths.js";
4
+ import { optionsBag } from "./lib/schema.js";
4
5
  import { tierFolderOf } from "./lib/tiers.js";
5
6
  var HOOK_FILE = /^use-|^use[A-Z]/;
6
7
  var noTsInBioFolders = {
@@ -42,14 +43,15 @@ law does not name (\`emails/\`) names it in \`extraFolders\` (default: none).`,
42
43
  messages: {
43
44
  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/.'
44
45
  },
45
- schema: [
46
- {
47
- additionalProperties: false,
48
- properties: { extraFolders: PATTERNS },
49
- type: "object"
50
- }
51
- ],
46
+ schema: optionsBag({ extraFolders: PATTERNS }),
52
47
  type: "problem"
48
+ },
49
+ probe() {
50
+ return {
51
+ path: "src/cells/geonosis-probe.ts",
52
+ source: `export const computeThing = (n: number) => n * 2
53
+ `
54
+ };
53
55
  }
54
56
  };
55
57
  var no_ts_in_bio_folders_default = noTsInBioFolders;
@@ -1,4 +1,5 @@
1
1
  import { type RuleContext } from './lib/context.js';
2
+ import { type Probe } from './lib/probe.js';
2
3
  declare const noTypeDefinitionsInComponents: {
3
4
  create(context: RuleContext): {
4
5
  TSInterfaceDeclaration?: undefined;
@@ -29,5 +30,6 @@ declare const noTypeDefinitionsInComponents: {
29
30
  schema: never[];
30
31
  type: 'problem';
31
32
  };
33
+ probe(): Probe;
32
34
  };
33
35
  export default noTypeDefinitionsInComponents;
@@ -1,6 +1,6 @@
1
1
  // src/rules/no-type-definitions-in-components.ts
2
2
  import { normalise } from "./lib/paths.js";
3
- var COMPONENT_FILE_PATTERN = /(?:^|\/)(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
3
+ import { COMPONENT_FILE_PATTERN } from "./lib/tiers.js";
4
4
  var noTypeDefinitionsInComponents = {
5
5
  create(context) {
6
6
  const normalized = normalise(context.filename);
@@ -40,6 +40,17 @@ and is imported.`,
40
40
  },
41
41
  schema: [],
42
42
  type: "problem"
43
+ },
44
+ probe() {
45
+ return {
46
+ path: "src/cells/geonosis-probe.tsx",
47
+ source: `type Label = string
48
+
49
+ export function GeonosisProbe({ label }: { label: Label }) {
50
+ return <button type="button">{label}</button>
51
+ }
52
+ `
53
+ };
43
54
  }
44
55
  };
45
56
  var no_type_definitions_in_components_default = noTypeDefinitionsInComponents;
@@ -1,37 +1,8 @@
1
1
  import { type RuleContextWithOptions } from './lib/context.js';
2
2
  import { type Probe } from './lib/probe.js';
3
3
  interface Options {
4
- /**
5
- * What a plugin directory's entry file is called. REQUIRED, and it used to have a default.
6
- *
7
- * Run read-only over a Medusa storefront with a default of `index.ts` / `config.ts`, the rule
8
- * reported zero findings on two roots whose directories are all registered — which read as a
9
- * pass. It was not one: a Medusa storefront's plugin directories contain `service.ts` and no
10
- * index at all, so the rule matched no file and answered a question it had never asked. The
11
- * control run that names a registry containing none of them also reported zero, which is what
12
- * caught it. A default that happens not to match a repo's layout is the silent zero
13
- * `requireOption` exists to prevent.
14
- */
15
4
  manifests?: string[];
16
- /**
17
- * The file that names them, read as source. Repo-root-relative.
18
- *
19
- * A LIST when the registry is split across files — a Medusa storefront's `medusa-config.ts`
20
- * imports `medusa-config.fulfillment.ts`, and the carrier directories are registered in the half
21
- * that is imported. Reading only the file the option named reported two registered carriers as
22
- * dead weight, and a false positive on a working tree is how a rule gets turned off.
23
- *
24
- * The halves are listed rather than followed: chasing `import` statements out of a registry would
25
- * need the repo's whole module resolution set up inside a linter, and would guess.
26
- */
27
5
  registry?: string | string[];
28
- /**
29
- * The directories whose children are registrants. Repo-root-relative.
30
- *
31
- * A bare string means the children are DIRECTORIES, each holding one of `manifests`. A root
32
- * written `{ path, files: true }` means the children are files, and each file is a registrant
33
- * named by its own basename — a seam with no directories in it at all.
34
- */
35
6
  roots?: (Root | string)[];
36
7
  }
37
8
  interface Root {
@@ -97,18 +68,6 @@ declare const noUnregisteredPluginDir: {
97
68
  }[];
98
69
  type: 'problem';
99
70
  };
100
- /**
101
- * A directory under a configured root, carrying a manifest, that the registry does not name — and
102
- * the registry itself, because the rule reads it off disk and returns silently when it is not
103
- * there. One file cannot be both the directory under `roots` and the registry at the consumer's
104
- * own path, so the probe declares two. It was a single file until 2026-08-30, and every consumer
105
- * enforcing this rule over a real registry read as a permanent FAIL for it: the probe fired
106
- * nothing, which was the truth about the probe and a lie about the repo.
107
- *
108
- * The registry it writes carries no string literal at all, so nothing in it can name the
109
- * directory beside it — the probe must be a directory the registry does NOT reach, or the rule is
110
- * right to stay quiet.
111
- */
112
71
  probe(options: readonly unknown[]): {
113
72
  files: Probe[];
114
73
  };
@@ -2,24 +2,16 @@
2
2
  import { readFileSync } from "node:fs";
3
3
  import { stripComments } from "./lib/module-text.js";
4
4
  import { requireOption } from "./lib/options.js";
5
- import { normalise } from "./lib/paths.js";
5
+ import { normalise, rootedAt, trimSlashes } from "./lib/paths.js";
6
6
  import { bagOf } from "./lib/probe.js";
7
7
  import { optionsBag, STRING, STRINGS } from "./lib/schema.js";
8
8
  var RULE = "no-unregistered-plugin-dir";
9
9
  var ACCEPTS = "{ roots: (string | { path: string, files: boolean })[], registry: string | string[], manifests: string[] }";
10
- var trimSlashes = (path) => path.replace(/^\/+/, "").replace(/\/+$/, "");
11
10
  var literalsOf = (source) => [...stripComments(source).matchAll(/['"`]([^'"`\n]+)['"`]/g)].map((match) => match[1] ?? "");
12
11
  var names = (literal, directory) => {
13
12
  const segments = trimSlashes(literal.replace(/^\.\/?/, "")).split("/");
14
13
  return segments.includes(directory);
15
14
  };
16
- var rootedAt = (filename, root) => {
17
- const marker = `/${root}/`;
18
- const at = filename.indexOf(marker);
19
- if (at < 0)
20
- return null;
21
- return { after: filename.slice(at + marker.length), repoRoot: filename.slice(0, at) };
22
- };
23
15
  var oneDirectory = (segments, manifests) => {
24
16
  if (segments.length !== 2)
25
17
  return null;
@@ -1,9 +1,6 @@
1
1
  import { type RuleContextWithOptions } from './lib/context.js';
2
+ import type { Probe } from './lib/probe.js';
2
3
  interface Options {
3
- /**
4
- * Regexes over the names of exported function-type aliases whose failure channel is an engine
5
- * that catches the throw. Empty by default: a repo names its own engine contracts.
6
- */
7
4
  engineCallbacks?: string[];
8
5
  }
9
6
  declare const voidPromise: (annotation: {
@@ -61,5 +58,6 @@ declare const noVoidPort: {
61
58
  }[];
62
59
  type: 'problem';
63
60
  };
61
+ probe(): Probe;
64
62
  };
65
63
  export default noVoidPort;
@@ -45,6 +45,13 @@ empty by default); a \`Result\` there would be ignored by the engine, never read
45
45
  engineCallbacks: STRINGS
46
46
  }),
47
47
  type: "problem"
48
+ },
49
+ probe() {
50
+ return {
51
+ path: "geonosis-probe-port.ts",
52
+ source: `export type GeonosisProbePort = () => Promise<void>
53
+ `
54
+ };
48
55
  }
49
56
  };
50
57
  var no_void_port_default = noVoidPort;
@@ -9,6 +9,7 @@ declare const organelleDependency: {
9
9
  }) => void;
10
10
  }): import('./lib/visitors.js').Visitor;
11
11
  fixShape: string;
12
+ probe?: ((options: readonly unknown[]) => import('./lib/probe.js').Probe | import('./lib/probe.js').ProbeTree) | undefined;
12
13
  meta: {
13
14
  docs: {
14
15
  description: string;
@@ -4,13 +4,8 @@ import {
4
4
  crossesFeatureBoundary,
5
5
  SHARED_FEATURES_SCHEMA
6
6
  } from "./lib/shared-features.js";
7
- var ABOVE_AN_ORGANELLE = [
8
- /\/cells\//,
9
- /\/tissues\//,
10
- /\/organs\//,
11
- /(?:^|\/)layouts\//,
12
- /(?:^|\/)app\//
13
- ];
7
+ import { APP_FOLDER, inOrgans, inTier, LAYOUTS_FOLDER } from "./lib/tiers.js";
8
+ var aboveAnOrganelle = (path) => inTier(path, "cells") || inTier(path, "tissues") || inOrgans(path) || LAYOUTS_FOLDER.test(path) || APP_FOLDER.test(path);
14
9
  var CROSS_FEATURE_ORGANELLE = /(?:^|[/@])features\/([^/]+)\/organelles\//;
15
10
  var organelleDependency = directionRule({
16
11
  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 the same feature or one the `sharedFeatures` option names (default: shared).",
@@ -19,7 +14,7 @@ tissues, organs and layouts are above it — a cell contains the organelle, neve
19
14
  Cross-feature organelle imports are allowed only from the features named in the rule's
20
15
  \`sharedFeatures\` option (default: \`shared\`); anything else flows through the cell as props.`,
21
16
  forbidden: (target, importer, options) => {
22
- if (ABOVE_AN_ORGANELLE.some((pattern) => pattern.test(target.path)))
17
+ if (aboveAnOrganelle(target.path))
23
18
  return true;
24
19
  const importedFeature = CROSS_FEATURE_ORGANELLE.exec(target.path)?.[1];
25
20
  return crossesFeatureBoundary(importedFeature, importer.feature, options);
@@ -28,6 +23,13 @@ Cross-feature organelle imports are allowed only from the features named in the
28
23
  forbidden: 'Organelle "{{file}}" cannot import from "{{source}}". Organelles may depend on atoms, molecules, compounds, stores, domains, and sub-organelles (same feature, or one the `sharedFeatures` option names).'
29
24
  },
30
25
  optionsFrom: (raw) => raw ?? {},
26
+ probe: () => ({
27
+ path: "src/organelles/geonosis-probe.tsx",
28
+ source: `import { Up } from '../cells/geonosis-up'
29
+
30
+ export const GeonosisProbe = Up
31
+ `
32
+ }),
31
33
  scope: "organelles",
32
34
  schema: SHARED_FEATURES_SCHEMA
33
35
  });
@@ -1,4 +1,5 @@
1
1
  import { type RuleContext } from './lib/context.js';
2
+ import { type Probe } from './lib/probe.js';
2
3
  declare const organelleSingleSource: {
3
4
  create(context: RuleContext): import('./lib/visitors.js').Visitor;
4
5
  fixShape: string;
@@ -15,5 +16,6 @@ declare const organelleSingleSource: {
15
16
  schema: never[];
16
17
  type: 'problem';
17
18
  };
19
+ probe(): Probe;
18
20
  };
19
21
  export default organelleSingleSource;
@@ -1,27 +1,40 @@
1
1
  // src/rules/organelle-single-source.ts
2
+ import { STORE_HOOK_NAME } from "./lib/hooks.js";
3
+ import { isRelative } from "./lib/imports.js";
4
+ import { fileFor } from "./lib/module-text.js";
2
5
  import { normalise } from "./lib/paths.js";
6
+ import {
7
+ FORM_CONTEXT_IMPORT_PATTERN,
8
+ FORM_SOURCES,
9
+ isProvidersSource,
10
+ stateSourcesOf
11
+ } from "./lib/state-sources.js";
12
+ import { inTier } from "./lib/tiers.js";
3
13
  import { mergeVisitors, onImportSource } from "./lib/visitors.js";
4
- var ORGANELLE_FILE_PATTERN = /(?:^|\/)(?:features\/[^/]+\/)?organelles\//;
5
- var ZUSTAND_STORE_PATTERN = /^use\w+Store$/;
6
- var FORM_SOURCES = new Set(["react-hook-form"]);
7
- var FORM_CONTEXT_IMPORT_PATTERN = /FormContext$/;
8
14
  var organelleSingleSource = {
9
15
  create(context) {
10
16
  const normalized = normalise(context.filename);
11
- if (!ORGANELLE_FILE_PATTERN.test(normalized))
17
+ if (!inTier(normalized, "organelles"))
12
18
  return {};
13
19
  const file = normalized.split("/").pop()?.replace(/\.tsx?$/, "") || "unknown";
14
20
  const storeHooks = new Map;
15
21
  let formNode = null;
16
22
  let formSource = "";
17
23
  let programNode = null;
24
+ const called = new Set;
25
+ const siblings = [];
18
26
  const fromModule = (source, node) => {
19
27
  if (FORM_SOURCES.has(source) && !formNode) {
20
28
  formNode = node;
21
29
  formSource = source;
22
30
  return;
23
31
  }
24
- if (/(?:^|\/)providers(?:\/|$)/.test(source)) {
32
+ if (isRelative(source)) {
33
+ const locals = (node.specifiers ?? []).flatMap((spec) => spec.local?.name ?? []);
34
+ if (locals.length > 0)
35
+ siblings.push({ locals, node, source });
36
+ }
37
+ if (isProvidersSource(source)) {
25
38
  const specs = node.specifiers ?? [];
26
39
  for (const spec of specs) {
27
40
  const localName = spec.local?.name || "";
@@ -39,7 +52,8 @@ var organelleSingleSource = {
39
52
  const name = node.callee.name;
40
53
  if (!name)
41
54
  return;
42
- if (ZUSTAND_STORE_PATTERN.test(name) && !storeHooks.has(name)) {
55
+ called.add(name);
56
+ if (STORE_HOOK_NAME.test(name) && !storeHooks.has(name)) {
43
57
  storeHooks.set(name, node);
44
58
  }
45
59
  },
@@ -47,6 +61,24 @@ var organelleSingleSource = {
47
61
  programNode = node;
48
62
  },
49
63
  "Program:exit"() {
64
+ for (const sibling of siblings) {
65
+ if (!sibling.locals.some((local) => called.has(local)))
66
+ continue;
67
+ const home = fileFor(context.filename, sibling.source);
68
+ if (home === undefined || home === context.filename || !inTier(home, "organelles")) {
69
+ continue;
70
+ }
71
+ const reached = stateSourcesOf(home);
72
+ for (const store of reached.stores) {
73
+ if (!storeHooks.has(store))
74
+ storeHooks.set(store, sibling.node);
75
+ }
76
+ const [form] = reached.forms;
77
+ if (form !== undefined && !formNode) {
78
+ formNode = sibling.node;
79
+ formSource = form;
80
+ }
81
+ }
50
82
  const storeNames = [...storeHooks.keys()];
51
83
  const isTsx = normalized.endsWith(".tsx");
52
84
  if (storeNames.length > 0 && formNode) {
@@ -76,10 +108,12 @@ var organelleSingleSource = {
76
108
  },
77
109
  fixShape: `One organelle, one state source: at most ONE Zustand store, and never Zustand and react-hook-form in
78
110
  the same file. Mixing them means DECOMPOSE, not promote — split into one focused organelle per
79
- source. The CELL is the only level where several sources meet; it derives and passes props down.`,
111
+ source. The CELL is the only level where several sources meet; it derives and passes props down.
112
+ A source read by a sibling organelle hook this file calls is this file's source too: moving a read
113
+ next door does not split the organelle, it only hides one of its two sources.`,
80
114
  meta: {
81
115
  docs: {
82
- description: "Organelles must have a single state source. (1) An organelle must not mix Zustand and react-hook-form — the cell coordinates across sources. (2) An organelle must read from at most ONE Zustand store — if it needs multiple stores, the cell should derive and pass the data down."
116
+ description: "Organelles must have a single state source, counting the sources of the sibling organelle hooks they call. (1) An organelle must not mix Zustand and react-hook-form — the cell coordinates across sources. (2) An organelle must read from at most ONE Zustand store — if it needs multiple stores, the cell should derive and pass the data down."
83
117
  },
84
118
  messages: {
85
119
  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.',
@@ -89,6 +123,21 @@ source. The CELL is the only level where several sources meet; it derives and pa
89
123
  },
90
124
  schema: [],
91
125
  type: "problem"
126
+ },
127
+ probe() {
128
+ return {
129
+ path: "src/organelles/geonosis-probe.tsx",
130
+ source: `declare const useGeonosisAStore: <T>(select: (state: { a: T }) => T) => T
131
+ declare const useGeonosisBStore: <T>(select: (state: { b: T }) => T) => T
132
+
133
+ export function GeonosisProbe() {
134
+ const a = useGeonosisAStore((state) => state.a)
135
+ const b = useGeonosisBStore((state) => state.b)
136
+
137
+ return <div>{String(a) + String(b)}</div>
138
+ }
139
+ `
140
+ };
92
141
  }
93
142
  };
94
143
  var organelle_single_source_default = organelleSingleSource;
@@ -1,19 +1,13 @@
1
1
  import { type RuleContextWithOptions } from './lib/context.js';
2
2
  import { type Probe } from './lib/probe.js';
3
3
  interface PackageRule {
4
- /** The package-name template `{dir}` is substituted into. Used only in messages. */
5
4
  name?: string;
6
- /** The namespace segment routes must sit under. `{dir}` is the directory. Optional — see below. */
7
5
  namespace?: string;
8
- /** Whether routes owned by this group must carry a namespace segment. */
9
6
  namespaced?: boolean;
10
- /** The directory whose children are packages. */
11
7
  under: string;
12
8
  }
13
9
  interface Options {
14
- /** Which directories hold packages, and which of them are namespace-checked. Required. */
15
10
  packages?: PackageRule[];
16
- /** The scopes a plugin route may live under. Default: Medusa's two. */
17
11
  scopes?: string[];
18
12
  }
19
13
  declare const pluginRouteNamespaced: {
@@ -69,11 +63,6 @@ declare const pluginRouteNamespaced: {
69
63
  }[];
70
64
  type: 'problem';
71
65
  };
72
- /**
73
- * A plugin route sitting bare on a scope: `/store` and nothing of the plugin's own after it. The
74
- * package directory is invented rather than read, because the offence is a PATH shape and a
75
- * directory that does not exist yet has the same one.
76
- */
77
66
  probe(options: readonly unknown[]): Probe;
78
67
  };
79
68
  export default pluginRouteNamespaced;
@@ -1,6 +1,6 @@
1
1
  // src/rules/plugin-route-namespaced.ts
2
2
  import { requireOption } from "./lib/options.js";
3
- import { normalise } from "./lib/paths.js";
3
+ import { normalise, trimSlashes } from "./lib/paths.js";
4
4
  import { bagOf, firstOf } from "./lib/probe.js";
5
5
  import { optionsBag, STRING, STRINGS } from "./lib/schema.js";
6
6
  var RULE = "plugin-route-namespaced";
@@ -18,7 +18,7 @@ var readPackages = (configured) => {
18
18
  };
19
19
  var ownerOf = (path, rules) => {
20
20
  for (const rule of rules) {
21
- const prefix = `/${rule.under.replaceAll(/^\/+|\/+$/g, "")}/`;
21
+ const prefix = `/${trimSlashes(rule.under)}/`;
22
22
  const at = `/${path}`.indexOf(prefix);
23
23
  if (at === -1)
24
24
  continue;
@@ -1,9 +1,7 @@
1
1
  import { type RuleContextWithOptions } from './lib/context.js';
2
2
  import { type Probe } from './lib/probe.js';
3
3
  interface Options {
4
- /** Regex source(s), tested against the normalised filename: the query modules. */
5
4
  queries?: string | string[];
6
- /** The field a tenant is scoped by. Default: `orgId`. */
7
5
  tenantKey?: string;
8
6
  }
9
7
  type TypeNode = {
@@ -82,7 +80,6 @@ declare const queriesRequireOrgScope: {
82
80
  }[];
83
81
  type: 'problem';
84
82
  };
85
- /** An exported query in the tree the options name, whose params never write `orgId`. */
86
83
  probe(options: readonly unknown[]): Probe;
87
84
  };
88
85
  export default queriesRequireOrgScope;
@@ -1,9 +1,8 @@
1
1
  // src/rules/queries-require-org-scope.ts
2
- import { PATTERNS, patternsOf, requireOption } from "./lib/options.js";
2
+ import { DEFAULT_TENANT_KEY, PATTERNS, patternsOf, requireOption } from "./lib/options.js";
3
3
  import { normalise } from "./lib/paths.js";
4
4
  import { bagOf, firstOf, pathMatching } from "./lib/probe.js";
5
- import { STRING } from "./lib/schema.js";
6
- var DEFAULT_TENANT_KEY = "orgId";
5
+ import { optionsBag, STRING } from "./lib/schema.js";
7
6
  var RULE = "queries-require-org-scope";
8
7
  var ACCEPTS = "{ queries: string | string[] (regex sources over the filename: the query modules) }";
9
8
  function writesKey(annotation, key) {
@@ -59,13 +58,7 @@ nothing.`,
59
58
  messages: {
60
59
  missingOrgScope: 'Query "{{name}}" does not declare {{key}}. Give it a (db, params) signature whose params type literal includes "{{key}}: string" — every read and write is scoped to its tenant.'
61
60
  },
62
- schema: [
63
- {
64
- additionalProperties: false,
65
- properties: { queries: PATTERNS, tenantKey: STRING },
66
- type: "object"
67
- }
68
- ],
61
+ schema: optionsBag({ queries: PATTERNS, tenantKey: STRING }),
69
62
  type: "problem"
70
63
  },
71
64
  probe(options) {
@@ -1,9 +1,10 @@
1
1
  // src/rules/queue-loop-is-the-library.ts
2
- import { normalise } from "./lib/paths.js";
2
+ import { isTestFile, normalise } from "./lib/paths.js";
3
3
  var queueMessageMethods = new Set(["ack", "retry", "ackAll", "retryAll"]);
4
4
  var queueLoopIsTheLibrary = {
5
5
  create(context) {
6
- if (/node_modules|\/__tests__\//.test(normalise(context.filename)))
6
+ const path = normalise(context.filename);
7
+ if (path.includes("node_modules") || isTestFile(path))
7
8
  return {};
8
9
  return {
9
10
  CallExpression(node) {
@@ -1,30 +1,11 @@
1
+ import { type SourceCode } from './lib/comments.js';
1
2
  import type { AnyNode } from './lib/nodes.js';
2
3
  import { type Probe } from './lib/probe.js';
3
4
  interface Options {
4
- /** The annotation that marks a deliberately bare write. Default: the scanner's vocabulary. */
5
5
  inlineMutationOk?: string;
6
- /** The call names that generate a service write. Default: what `MedusaService` generates. */
7
6
  mutationPrefixes?: string[];
8
- /** Regex sources for the files that are route handlers. Default: Medusa's `api/**\/route.ts`. */
9
7
  paths?: string[];
10
8
  }
11
- type Comment = {
12
- loc?: {
13
- end?: {
14
- line?: number;
15
- };
16
- start?: {
17
- line?: number;
18
- };
19
- };
20
- value?: string;
21
- };
22
- type SourceCode = {
23
- ast?: {
24
- comments?: Comment[];
25
- };
26
- getAllComments?: () => Comment[];
27
- };
28
9
  declare const routeNoInlineMutation: {
29
10
  create(context: {
30
11
  filename: string;
@@ -71,11 +52,6 @@ declare const routeNoInlineMutation: {
71
52
  }[];
72
53
  type: 'problem';
73
54
  };
74
- /**
75
- * A route writing through a service in its own body, with no escape marker on it. The write name
76
- * is built from the configured prefix, because `writesWith` reads a capital after the prefix and
77
- * a probe spelling `creatething` would pass the rule it is meant to trip.
78
- */
79
55
  probe(options: readonly unknown[]): Probe;
80
56
  };
81
57
  export default routeNoInlineMutation;
@@ -1,4 +1,5 @@
1
1
  // src/rules/route-no-inline-mutation.ts
2
+ import { commentsOf } from "./lib/comments.js";
2
3
  import {
3
4
  DEFAULT_MUTATION_PREFIXES,
4
5
  mutationPrefixesOf,
@@ -12,7 +13,6 @@ import { optionsBag, STRING, STRINGS } from "./lib/schema.js";
12
13
  var RULE = "route-no-inline-mutation";
13
14
  var DEFAULT_PATHS = [String.raw`(?:^|/)api/.*/route\.[cm]?[jt]sx?$`];
14
15
  var DEFAULT_INLINE_MUTATION_OK = "arch:inline-mutation-ok";
15
- var commentsOf = (sourceCode) => sourceCode?.getAllComments?.() ?? sourceCode?.ast?.comments ?? [];
16
16
  var isStatement = (node) => typeof node.type === "string" && (node.type.endsWith("Statement") || node.type === "VariableDeclaration");
17
17
  var lineOf = (node, edge) => {
18
18
  const loc = node.loc;
@@ -51,12 +51,7 @@ var annotated = (found, comments, marker) => {
51
51
  const end = lineOf(anchor, "end") ?? start;
52
52
  if (start === null || end === null)
53
53
  return false;
54
- return comments.some((comment) => {
55
- if (!(comment.value ?? "").includes(marker))
56
- return false;
57
- const line = comment.loc?.start?.line;
58
- return typeof line === "number" && line >= start - 1 && line <= end;
59
- });
54
+ return comments.some((comment) => comment.value.includes(marker) && comment.loc.start.line >= start - 1 && comment.loc.start.line <= end);
60
55
  };
61
56
  var routeNoInlineMutation = {
62
57
  create(context) {
@@ -1,15 +1,10 @@
1
1
  import { type RuleContextWithOptions } from './lib/context.js';
2
2
  import { type Probe } from './lib/probe.js';
3
3
  interface Options {
4
- /** Basenames that are neither a router nor a schema — `_app`, `index`, `errors`, `pagination`. */
5
4
  ignore?: string[];
6
- /** Router basename -> the schema basenames it serves. */
7
5
  mounts?: Record<string, string[]>;
8
- /** Repo-root-relative directory of the routers. */
9
6
  routers?: string;
10
- /** Repo-root-relative directory of the schemas. */
11
7
  schemas?: string;
12
- /** What a schema file adds to the shared basename, if anything — `.schema`. */
13
8
  suffix?: string;
14
9
  }
15
10
  declare const routerSchemaParity: {
@@ -51,10 +46,6 @@ declare const routerSchemaParity: {
51
46
  }[];
52
47
  type: 'problem';
53
48
  };
54
- /**
55
- * A router with no schema half. The two directories are repo-root-relative, and the missing half
56
- * is a question about the tree the run is over: nothing writes the twin, so nothing is there.
57
- */
58
49
  probe(options: readonly unknown[]): Probe;
59
50
  };
60
51
  export default routerSchemaParity;
@@ -1,24 +1,20 @@
1
1
  // src/rules/router-schema-parity.ts
2
2
  import { existsSync } from "node:fs";
3
3
  import { requireOption } from "./lib/options.js";
4
- import { normalise } from "./lib/paths.js";
4
+ import { normalise, rootedAt, trimSlashes } from "./lib/paths.js";
5
5
  import { bagOf } from "./lib/probe.js";
6
6
  import { optionsBag, STRING, STRINGS } from "./lib/schema.js";
7
7
  var RULE = "router-schema-parity";
8
8
  var EXTENSIONS = [".ts", ".tsx"];
9
- var trimSlashes = (path) => path.replace(/^\/+/, "").replace(/\/+$/, "");
10
9
  var baseOf = (file) => file.replace(/\.[cm]?[jt]sx?$/, "");
11
10
  var NEITHER_HALF = /\.(?:test|spec|d)$/;
12
11
  var existsWithAnyExtension = (withoutExtension) => EXTENSIONS.map((extension) => `${withoutExtension}${extension}`).find((path) => existsSync(path)) ?? null;
13
12
  var directlyUnder = (filename, directory) => {
14
- const marker = `/${directory}/`;
15
- const at = filename.indexOf(marker);
16
- if (at < 0)
13
+ const rooted = rootedAt(filename, directory);
14
+ if (rooted === null || rooted.after.includes("/") || NEITHER_HALF.test(baseOf(rooted.after))) {
17
15
  return null;
18
- const after = filename.slice(at + marker.length);
19
- if (after.includes("/") || NEITHER_HALF.test(baseOf(after)))
20
- return null;
21
- return { base: baseOf(after), repoRoot: filename.slice(0, at) };
16
+ }
17
+ return { base: baseOf(rooted.after), repoRoot: rooted.repoRoot };
22
18
  };
23
19
  var routerSchemaParity = {
24
20
  create(context) {
@@ -2,9 +2,7 @@ import { type RuleContextWithOptions } from './lib/context.js';
2
2
  import { type AnyNode } from './lib/nodes.js';
3
3
  import { type Probe } from './lib/probe.js';
4
4
  interface Options {
5
- /** The call names that register a module. Default: Medusa's `Module`. */
6
5
  moduleFactories?: string[];
7
- /** The keys the framework owns. Default: the 32 values of `Modules` at 2.19.0. */
8
6
  reserved?: string[];
9
7
  }
10
8
  declare const serviceNameNotReserved: {
@@ -41,7 +39,6 @@ declare const serviceNameNotReserved: {
41
39
  }[];
42
40
  type: 'problem';
43
41
  };
44
- /** A module registered under a key the framework owns — the first of the configured list. */
45
42
  probe(options: readonly unknown[]): Probe;
46
43
  };
47
44
  export default serviceNameNotReserved;