@geonosis/oxlint-plugin-biological-architecture 0.2.0 → 0.3.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.
@@ -35,6 +35,7 @@
35
35
  }
36
36
  ],
37
37
  "biological-architecture/effect-hook-naming": "error",
38
+ "biological-architecture/emit-declares-attempts": "error",
38
39
  "biological-architecture/layer-walls": [
39
40
  "error",
40
41
  {
@@ -0,0 +1,13 @@
1
+ // A file in no tier folder at all — a Medusa admin widget, a Next route, a one-off surface. The
2
+ // atom exists there too, so no-raw-html-atoms still has something to say.
3
+ import { Card } from '@kit/ui/atoms/card'
4
+
5
+ export function OrderWidget({ onRefresh }: { onRefresh: () => void }) {
6
+ return (
7
+ <Card>
8
+ <button onClick={onRefresh} type="button">
9
+ Refresh
10
+ </button>
11
+ </Card>
12
+ )
13
+ }
@@ -0,0 +1,9 @@
1
+ import { useMemo } from 'react'
2
+
3
+ const weights: Record<string, number> = { bold: 700, regular: 400 }
4
+
5
+ export function Spacer({ weight = 'regular' }: { weight?: string }) {
6
+ const value = useMemo<number>(() => weights[weight] ?? 400, [weight])
7
+
8
+ return <div style={{ fontWeight: value }} />
9
+ }
@@ -0,0 +1,15 @@
1
+ import type { HTMLAttributes } from 'react'
2
+
3
+ interface NativeSelectProps extends Omit<HTMLAttributes<HTMLSelectElement>, 'onChange'> {
4
+ onPick?: (value: string) => void
5
+ }
6
+
7
+ export function NativeSelect({ onPick, children, ...props }: NativeSelectProps) {
8
+ return (
9
+ <div className="relative">
10
+ <select onChange={(event) => onPick?.(event.target.value)} {...props}>
11
+ {children}
12
+ </select>
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,7 @@
1
+ export function CheckIcon({ size = 16 }: { size?: number }) {
2
+ return (
3
+ <svg fill="none" height={size} viewBox="0 0 24 24" width={size}>
4
+ <path d="M20 6 9 17l-5-5" stroke="currentColor" strokeWidth="2" />
5
+ </svg>
6
+ )
7
+ }
@@ -0,0 +1,3 @@
1
+ export function Label({ children }: { children?: unknown }) {
2
+ return <label className="text-sm">{children}</label>
3
+ }
@@ -0,0 +1,27 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+ import { Slot } from 'radix-ui'
3
+ import type { ComponentProps, Ref } from 'react'
4
+
5
+ const buttonVariants = cva('inline-flex', {
6
+ variants: { size: { medium: 'px-7', small: 'px-5' } },
7
+ })
8
+
9
+ type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>['size']>
10
+
11
+ const labels: Record<string, string> = { medium: 'Medium', small: 'Small' }
12
+
13
+ interface ButtonProps extends ComponentProps<'button'>, VariantProps<typeof buttonVariants> {
14
+ asChild?: boolean
15
+ ref?: Ref<HTMLButtonElement>
16
+ }
17
+
18
+ export function Button({ asChild = false, size, children, ...props }: ButtonProps) {
19
+ const Comp = asChild ? Slot.Root : 'button'
20
+ const resolved: ButtonSize = size ?? 'medium'
21
+
22
+ return (
23
+ <Comp aria-label={labels[resolved]} {...props}>
24
+ {children}
25
+ </Comp>
26
+ )
27
+ }
@@ -0,0 +1,11 @@
1
+ import type { ComponentPropsWithoutRef } from 'react'
2
+
3
+ type TextAreaProps = ComponentPropsWithoutRef<'textarea'> & { invalid?: boolean }
4
+
5
+ export function TextArea({ invalid = false, ...props }: TextAreaProps) {
6
+ return (
7
+ <div className="relative">
8
+ <textarea data-invalid={invalid} {...props} />
9
+ </div>
10
+ )
11
+ }
@@ -0,0 +1,13 @@
1
+ import * as React from 'react'
2
+
3
+ interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
4
+ invalid?: boolean
5
+ }
6
+
7
+ export function TextInput({ invalid = false, ...props }: TextInputProps) {
8
+ return (
9
+ <div className="relative">
10
+ <input data-invalid={invalid} {...props} />
11
+ </div>
12
+ )
13
+ }
@@ -0,0 +1,9 @@
1
+ import { LinkAtom } from './slot-anchor'
2
+
3
+ export default { component: LinkAtom, title: 'Atoms/LinkAtom' }
4
+
5
+ export const Loading = () => (
6
+ <figure>
7
+ <progress max={100} value={40} />
8
+ </figure>
9
+ )
@@ -0,0 +1,15 @@
1
+ import { Slot } from 'radix-ui'
2
+
3
+ import { cn } from '#lib/utils'
4
+
5
+ interface LinkAtomProps {
6
+ asChild?: boolean
7
+ children?: unknown
8
+ className?: string
9
+ }
10
+
11
+ export function LinkAtom({ asChild = false, children, className }: LinkAtomProps) {
12
+ const Comp = asChild ? Slot.Root : 'a'
13
+
14
+ return <Comp className={cn('underline', className)}>{children}</Comp>
15
+ }
@@ -0,0 +1,7 @@
1
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
2
+
3
+ export const emitMilestone = (data: { tracking_number: string }) =>
4
+ emitEventStep({
5
+ data,
6
+ eventName: 'shipment.milestone',
7
+ })
@@ -0,0 +1,11 @@
1
+ import { EventPriority } from '@medusajs/framework/utils'
2
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
3
+
4
+ // An options bag that names a priority and no attempts: configured to the eye, droppable to the
5
+ // worker, which reads only `attempts > 1` as "a retry was asked for".
6
+ export const emitSampleRequested = (data: { order_id: string }) =>
7
+ emitEventStep({
8
+ data,
9
+ eventName: 'sample.requested',
10
+ options: { priority: EventPriority.CRITICAL },
11
+ }).config({ name: 'emit-sample-requested' })
@@ -0,0 +1,8 @@
1
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
2
+
3
+ export const emitConverted = (data: { order_id: string }) =>
4
+ emitEventStep({
5
+ data,
6
+ eventName: 'sample.converted',
7
+ options: { attempts: 1 },
8
+ })
@@ -0,0 +1,9 @@
1
+ import { EventPriority } from '@medusajs/framework/utils'
2
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
3
+
4
+ export const emitOrderPlaced = (data: { id: string }) =>
5
+ emitEventStep({
6
+ data,
7
+ eventName: 'order.placed',
8
+ options: { attempts: 3, priority: EventPriority.CRITICAL },
9
+ }).config({ name: 'emit-order-placed' })
@@ -0,0 +1,10 @@
1
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
2
+
3
+ import { RETRY_BACKGROUND } from './retry'
4
+
5
+ export const emitProofReady = (data: { proof_id: string }) =>
6
+ emitEventStep({
7
+ data,
8
+ eventName: 'proof.ready',
9
+ options: { attempts: RETRY_BACKGROUND.attempts },
10
+ })
@@ -0,0 +1,3 @@
1
+ // The shared retry budgets. One place, so a change to what "background" means reaches every emit.
2
+ export const RETRY_BACKGROUND = { attempts: 5, backoff: 2000 }
3
+ export const RETRY_CRITICAL = { attempts: 9, backoff: 500 }
@@ -13,6 +13,7 @@
13
13
  "biological-architecture/document-sagas-are-generic",
14
14
  "biological-architecture/documents-share-one-table",
15
15
  "biological-architecture/effect-hook-naming",
16
+ "biological-architecture/emit-declares-attempts",
16
17
  "biological-architecture/layer-walls",
17
18
  "biological-architecture/max-comment-density",
18
19
  "biological-architecture/molecule-atoms-only",
@@ -0,0 +1,10 @@
1
+ // A molecule hand-rolling the element an atom already wraps: level 2, where the rule bites.
2
+ import { Icon } from '../atoms/icon'
3
+
4
+ export function IconAction({ onPress }: { onPress: () => void }) {
5
+ return (
6
+ <button onClick={onPress} type="button">
7
+ <Icon name="plus" />
8
+ </button>
9
+ )
10
+ }
@@ -52,7 +52,7 @@ var BACKEND_WORKFLOWS = [
52
52
  ];
53
53
  var BACKEND_D1 = ["no-d1-transaction", "worker-handles-are-scoped"];
54
54
  var BACKEND_SAGAFLOW_CF = ["step-opens-its-own-cell"];
55
- var BACKEND_MEDUSA = ["store-route-scopes-tenant-data"];
55
+ var BACKEND_MEDUSA = ["emit-declares-attempts", "store-route-scopes-tenant-data"];
56
56
  var TENANT_SCOPING = [
57
57
  "queries-require-org-scope",
58
58
  "store-route-scopes-tenant-data",
@@ -34,6 +34,18 @@ declare function buildAtomRuleRestrictions(atomsDir: string, importPathPrefix?:
34
34
  * Variants are kept as alternatives in the error message.
35
35
  */
36
36
  declare function scanAtomMappings(atomsDir: string): AtomMapping[];
37
+ /**
38
+ * The native element an atom wraps, read from the one place every atom says so: its props type.
39
+ *
40
+ * `ComponentProps<'button'>`, `ComponentPropsWithoutRef<'textarea'>`,
41
+ * `HTMLAttributes<HTMLSelectElement>` and `React.ButtonHTMLAttributes<HTMLButtonElement>` all name
42
+ * the element outright, and an atom that types its props any other way is an atom nobody can
43
+ * compose. The Radix `asChild ? Slot.Root : 'button'` form comes next, and the JSX-tag scan is
44
+ * only the fallback — it was the whole detector once, and on a real tree it returned `string`,
45
+ * `typeof` and `path` while missing every shadcn button in the repo.
46
+ *
47
+ * The name is kept because consumers import it.
48
+ */
37
49
  declare function detectFirstNativeJsxElement(source: string): null | string;
38
50
 
39
51
  export { buildAtomRestrictions, buildAtomRuleRestrictions, detectFirstNativeJsxElement, scanAtomMappings };
@@ -2,6 +2,164 @@
2
2
  import { readdirSync, readFileSync } from "fs";
3
3
  import { basename, join } from "path";
4
4
  var GENERIC_ELEMENTS = /* @__PURE__ */ new Set(["div", "span"]);
5
+ var HTML_ELEMENTS = /* @__PURE__ */ new Set([
6
+ "a",
7
+ "abbr",
8
+ "address",
9
+ "area",
10
+ "article",
11
+ "aside",
12
+ "audio",
13
+ "b",
14
+ "bdi",
15
+ "bdo",
16
+ "blockquote",
17
+ "br",
18
+ "button",
19
+ "canvas",
20
+ "caption",
21
+ "cite",
22
+ "code",
23
+ "col",
24
+ "colgroup",
25
+ "data",
26
+ "datalist",
27
+ "dd",
28
+ "del",
29
+ "details",
30
+ "dfn",
31
+ "dialog",
32
+ "div",
33
+ "dl",
34
+ "dt",
35
+ "em",
36
+ "embed",
37
+ "fieldset",
38
+ "figcaption",
39
+ "figure",
40
+ "footer",
41
+ "form",
42
+ "h1",
43
+ "h2",
44
+ "h3",
45
+ "h4",
46
+ "h5",
47
+ "h6",
48
+ "header",
49
+ "hgroup",
50
+ "hr",
51
+ "i",
52
+ "iframe",
53
+ "img",
54
+ "input",
55
+ "ins",
56
+ "kbd",
57
+ "label",
58
+ "legend",
59
+ "li",
60
+ "main",
61
+ "map",
62
+ "mark",
63
+ "menu",
64
+ "meter",
65
+ "nav",
66
+ "noscript",
67
+ "object",
68
+ "ol",
69
+ "optgroup",
70
+ "option",
71
+ "output",
72
+ "p",
73
+ "picture",
74
+ "pre",
75
+ "progress",
76
+ "q",
77
+ "rp",
78
+ "rt",
79
+ "ruby",
80
+ "s",
81
+ "samp",
82
+ "search",
83
+ "section",
84
+ "select",
85
+ "small",
86
+ "source",
87
+ "span",
88
+ "strong",
89
+ "sub",
90
+ "summary",
91
+ "sup",
92
+ "table",
93
+ "tbody",
94
+ "td",
95
+ "template",
96
+ "textarea",
97
+ "tfoot",
98
+ "th",
99
+ "thead",
100
+ "time",
101
+ "tr",
102
+ "track",
103
+ "u",
104
+ "ul",
105
+ "var",
106
+ "video",
107
+ "wbr"
108
+ ]);
109
+ var TAG_OF_DOM_INTERFACE = {
110
+ HTMLAnchorElement: "a",
111
+ HTMLAreaElement: "area",
112
+ HTMLAudioElement: "audio",
113
+ HTMLBRElement: "br",
114
+ HTMLButtonElement: "button",
115
+ HTMLCanvasElement: "canvas",
116
+ HTMLDListElement: "dl",
117
+ HTMLDataElement: "data",
118
+ HTMLDataListElement: "datalist",
119
+ HTMLDetailsElement: "details",
120
+ HTMLDialogElement: "dialog",
121
+ HTMLDivElement: "div",
122
+ HTMLEmbedElement: "embed",
123
+ HTMLFieldSetElement: "fieldset",
124
+ HTMLFormElement: "form",
125
+ HTMLHRElement: "hr",
126
+ HTMLIFrameElement: "iframe",
127
+ HTMLImageElement: "img",
128
+ HTMLInputElement: "input",
129
+ HTMLLIElement: "li",
130
+ HTMLLabelElement: "label",
131
+ HTMLLegendElement: "legend",
132
+ HTMLMapElement: "map",
133
+ HTMLMenuElement: "menu",
134
+ HTMLMeterElement: "meter",
135
+ HTMLOListElement: "ol",
136
+ HTMLObjectElement: "object",
137
+ HTMLOptGroupElement: "optgroup",
138
+ HTMLOptionElement: "option",
139
+ HTMLOutputElement: "output",
140
+ HTMLParagraphElement: "p",
141
+ HTMLPictureElement: "picture",
142
+ HTMLPreElement: "pre",
143
+ HTMLProgressElement: "progress",
144
+ HTMLSelectElement: "select",
145
+ HTMLSourceElement: "source",
146
+ HTMLSpanElement: "span",
147
+ HTMLTableCaptionElement: "caption",
148
+ HTMLTableColElement: "col",
149
+ HTMLTableElement: "table",
150
+ HTMLTableRowElement: "tr",
151
+ HTMLTextAreaElement: "textarea",
152
+ HTMLTimeElement: "time",
153
+ HTMLTrackElement: "track",
154
+ HTMLUListElement: "ul",
155
+ HTMLVideoElement: "video"
156
+ };
157
+ var TAG_OF_ATTRIBUTES_PREFIX = { anchor: "a" };
158
+ var NOT_AN_ATOM = /\.(?:stories|test|spec)\.tsx$/;
159
+ var COMPONENT_PROPS = /(?:React\.)?ComponentProps(?:WithoutRef|WithRef)?\s*<\s*['"]([a-z][a-z0-9]*)['"]\s*>/g;
160
+ var HTML_ATTRIBUTES = /(?:React\.)?([A-Z][A-Za-z]*?)?HTMLAttributes\s*<\s*(HTML[A-Za-z]*Element)\s*>/g;
161
+ var SLOT_FALLBACK = /Slot(?:\.Root)?\s*:\s*['"]([a-z][a-z0-9]*)['"]/;
162
+ var JSX_TAG = /(?<![A-Za-z0-9_$])<([a-z][a-z0-9]*)(?=[\s/>])/g;
5
163
  function buildAtomRestrictions(atomsDir, importPathPrefix = "@kit/ui/atoms") {
6
164
  const mappings = scanAtomMappings(atomsDir);
7
165
  return mappings.map(({ atomName, nativeElement }) => ({
@@ -23,6 +181,7 @@ function scanAtomMappings(atomsDir) {
23
181
  for (const entry of entries) {
24
182
  if (!entry.isFile()) continue;
25
183
  if (!entry.name.endsWith(".tsx")) continue;
184
+ if (NOT_AN_ATOM.test(entry.name)) continue;
26
185
  const atomFile = entry.name;
27
186
  const atomName = basename(atomFile, ".tsx");
28
187
  const content = readFileSync(join(atomsDir, atomFile), "utf8");
@@ -42,11 +201,44 @@ function scanAtomMappings(atomsDir) {
42
201
  return mappings;
43
202
  }
44
203
  function detectFirstNativeJsxElement(source) {
45
- const slotMatch = source.match(/Slot\s*:\s*['"]([a-z][a-z0-9]*)['"]/);
46
- if (slotMatch?.[1]) return slotMatch[1];
47
- const cleaned = source.replaceAll(/\/\/.*$/gm, "").replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""');
48
- const match = cleaned.match(/<([a-z][a-z0-9]*)\b/);
49
- return match?.[1] ?? null;
204
+ const withoutComments = stripComments(source);
205
+ const declared = elementFromPropsType(withoutComments);
206
+ if (declared) return declared;
207
+ const slot = SLOT_FALLBACK.exec(withoutComments)?.[1];
208
+ if (slot && HTML_ELEMENTS.has(slot)) return slot;
209
+ return elementFromJsx(withoutComments);
210
+ }
211
+ function elementFromPropsType(source) {
212
+ const found = [];
213
+ for (const match of source.matchAll(COMPONENT_PROPS)) {
214
+ const element = match[1];
215
+ if (element && HTML_ELEMENTS.has(element)) found.push({ at: match.index, element });
216
+ }
217
+ for (const match of source.matchAll(HTML_ATTRIBUTES)) {
218
+ const element = elementOfAttributes(match[1], match[2]);
219
+ if (element) found.push({ at: match.index, element });
220
+ }
221
+ return found.toSorted((a, b) => a.at - b.at)[0]?.element ?? null;
222
+ }
223
+ function elementOfAttributes(prefix, domInterface) {
224
+ if (prefix !== void 0) {
225
+ const lower = prefix.toLowerCase();
226
+ const named = TAG_OF_ATTRIBUTES_PREFIX[lower] ?? lower;
227
+ if (HTML_ELEMENTS.has(named)) return named;
228
+ }
229
+ const mapped = domInterface === void 0 ? void 0 : TAG_OF_DOM_INTERFACE[domInterface];
230
+ return mapped !== void 0 && HTML_ELEMENTS.has(mapped) ? mapped : null;
231
+ }
232
+ function elementFromJsx(withoutComments) {
233
+ const cleaned = withoutComments.replaceAll(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""');
234
+ for (const match of cleaned.matchAll(JSX_TAG)) {
235
+ const tag = match[1];
236
+ if (tag && HTML_ELEMENTS.has(tag)) return tag;
237
+ }
238
+ return null;
239
+ }
240
+ function stripComments(source) {
241
+ return source.replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/(?<!:)\/\/.*$/gm, "");
50
242
  }
51
243
  function toPascalCase(kebab) {
52
244
  return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PRESETS,
3
3
  rulesOfPreset
4
- } from "./chunk-HXBZSOIU.js";
4
+ } from "./chunk-55NK7NJN.js";
5
5
 
6
6
  // src/index.ts
7
7
  import { createRequire } from "module";
@@ -743,6 +743,80 @@ file and the export together \u2014 half the pair still fires.`,
743
743
  };
744
744
  var effect_hook_naming_default = effectHookNaming;
745
745
 
746
+ // src/rules/emit-declares-attempts.ts
747
+ var RULE = "emit-declares-attempts";
748
+ var DEFAULT_EMITTERS = ["emitEventStep"];
749
+ var OPTIONS = "options";
750
+ var ATTEMPTS = "attempts";
751
+ var EVENT_NAME = "eventName";
752
+ var NO_RETRY = 1;
753
+ var objectOf = (node) => node?.type === "ObjectExpression" ? node : null;
754
+ var propertiesOf = (object) => {
755
+ const properties = object.properties ?? [];
756
+ return properties.every((one) => one.type === "Property") ? properties : null;
757
+ };
758
+ var valueOf = (properties, name) => properties.find((one) => one.key?.type === "Identifier" && one.key.name === name)?.value;
759
+ var emitDeclaresAttempts = {
760
+ create(context) {
761
+ const emitters = new Set(
762
+ requireOption(RULE, context.options?.[0]?.emitters ?? DEFAULT_EMITTERS, "emitters")
763
+ );
764
+ return {
765
+ CallExpression(node) {
766
+ if (node.callee?.type !== "Identifier") return;
767
+ const emitter = node.callee.name ?? "";
768
+ if (!emitters.has(emitter)) return;
769
+ const payload = objectOf(node.arguments?.[0]);
770
+ if (payload === null) return;
771
+ const properties = propertiesOf(payload);
772
+ if (properties === null) return;
773
+ const named = valueOf(properties, EVENT_NAME);
774
+ const site = named?.type === "Literal" && typeof named.value === "string" ? named.value : emitter;
775
+ const options = valueOf(properties, OPTIONS);
776
+ if (options !== void 0 && objectOf(options) === null) return;
777
+ const bag = options === void 0 ? [] : propertiesOf(objectOf(options));
778
+ if (bag === null) return;
779
+ const attempts = valueOf(bag, ATTEMPTS);
780
+ if (attempts === void 0) {
781
+ context.report({ data: { emitter, site }, messageId: "noBudget", node });
782
+ return;
783
+ }
784
+ if (attempts.type !== "Literal" || typeof attempts.value !== "number") return;
785
+ if (attempts.value > NO_RETRY) return;
786
+ context.report({
787
+ data: { attempts: String(attempts.value), emitter, site },
788
+ messageId: "tooFew",
789
+ node
790
+ });
791
+ }
792
+ };
793
+ },
794
+ fixShape: `Every emit carries a retry budget: \`options.attempts\`, taken from the shared retry constants rather
795
+ than written as a number at the call site. \`attempts\` defaults to 1 and the worker reads only
796
+ \`attempts > 1\` as "a retry was configured", so an emit with \`{ priority }\` and no \`attempts\` is
797
+ droppable while looking deliberate. Pass \`options: { attempts: RETRY_BACKGROUND.attempts, priority }\`.
798
+ A budget the rule cannot evaluate \u2014 a constant, a computed value \u2014 passes: it says nothing rather
799
+ than pushing the number back inline. Configure \`emitters\` if the repo wraps the emit under another name.`,
800
+ meta: {
801
+ docs: {
802
+ description: "An emit declares how many times it may be retried. Medusa's `emitEventStep` defaults `attempts` to 1 and the worker treats only `attempts > 1` as retry-configured, so an options bag without one is a droppable event that reads as a deliberate one. `emitters` is an option; a literal `attempts` is judged, anything else passes."
803
+ },
804
+ messages: {
805
+ noBudget: '`{{emitter}}` emits "{{site}}" with no `options.attempts`. `attempts` defaults to 1 and the worker treats only `attempts > 1` as a configured retry, so this event is dropped on its first failure. Give it a budget from the shared retry constants.',
806
+ tooFew: '`{{emitter}}` emits "{{site}}" with `attempts: {{attempts}}`, which is the default spelled out \u2014 one attempt is no retry. Take the budget from the shared retry constants instead.'
807
+ },
808
+ schema: [
809
+ {
810
+ additionalProperties: false,
811
+ properties: { emitters: { items: { type: "string" }, type: "array" } },
812
+ type: "object"
813
+ }
814
+ ],
815
+ type: "problem"
816
+ }
817
+ };
818
+ var emit_declares_attempts_default = emitDeclaresAttempts;
819
+
746
820
  // src/rules/layer-walls.ts
747
821
  import * as path2 from "path";
748
822
  var REQUIRE = "require";
@@ -2112,14 +2186,34 @@ below the seam, and a caller that names it has crossed it.`,
2112
2186
  };
2113
2187
  var no_orm_outside_db_default = noOrmOutsideDb;
2114
2188
 
2189
+ // src/rules/lib/tiers.ts
2190
+ var TIERS = ["atoms", "molecules", "compounds", "organelles", "cells", "tissues"];
2191
+ var PATTERNS = {
2192
+ atoms: /(?:^|\/)(?:features\/[^/]+\/)?atoms\//,
2193
+ cells: /(?:^|\/)(?:features\/[^/]+\/)?cells\//,
2194
+ compounds: /(?:^|\/)(?:features\/[^/]+\/)?compounds\//,
2195
+ molecules: /(?:^|\/)(?:features\/[^/]+\/)?molecules\//,
2196
+ organelles: /(?:^|\/)(?:features\/[^/]+\/)?organelles\//,
2197
+ tissues: /(?:^|\/)(?:features\/[^/]+\/)?tissues\//
2198
+ };
2199
+ var inTier = (filename, tier) => PATTERNS[tier].test(filename.replaceAll("\\", "/"));
2200
+ var requireTiers = (rule, names) => names.map((name) => {
2201
+ if (TIERS.includes(name)) return name;
2202
+ throw new Error(
2203
+ `biological-architecture/${rule} is scoped to "${name}", which is not a tier, so that scope can never match a file. The tiers are ${TIERS.join(", ")}.`
2204
+ );
2205
+ });
2206
+
2115
2207
  // src/rules/no-raw-html-atoms.ts
2208
+ var RULE2 = "no-raw-html-atoms";
2209
+ var WHERE_RAW_HTML_IS_THE_POINT = "atoms";
2116
2210
  var noRawHtmlAtoms = {
2117
2211
  create(context) {
2118
- const restrictions = requireOption(
2119
- "no-raw-html-atoms",
2120
- context.options[0],
2121
- "element\u2192atom map"
2122
- );
2212
+ const restrictions = requireOption(RULE2, context.options[0], "element\u2192atom map");
2213
+ const scope = context.options[1]?.scope;
2214
+ const filename = context.filename;
2215
+ const inScope = scope === void 0 ? !inTier(filename, WHERE_RAW_HTML_IS_THE_POINT) : requireTiers(RULE2, scope).some((tier) => inTier(filename, tier));
2216
+ if (!inScope) return {};
2123
2217
  const elementMap = /* @__PURE__ */ new Map();
2124
2218
  for (const r of restrictions) {
2125
2219
  elementMap.set(r.element, r);
@@ -2144,15 +2238,17 @@ var noRawHtmlAtoms = {
2144
2238
  };
2145
2239
  },
2146
2240
  fixShape: `A native element that already has an atom wrapper (\`<button>\`, \`<input>\`, \`<td>\`, \`<header>\`, \u2026) is
2147
- written as the atom in levels 2-6. Import the atom named in the rule's options and use it. Configure the element\u2192atom map per repo;
2148
- enabled without one the rule refuses the run, because a rule with no map to check reads exactly like
2149
- a tree with no raw elements in it.`,
2241
+ written as the atom everywhere above the atom tier. Import the atom named in the rule's options and use it.
2242
+ Inside \`atoms/\` the raw element is the point of the file, so the rule says nothing there; every other
2243
+ file is in scope, tier folder or not, and \`scope\` narrows it to named tiers when a repo wants that.
2244
+ Configure the element\u2192atom map per repo; enabled without one the rule refuses the run, because a rule
2245
+ with no map to check reads exactly like a tree with no raw elements in it.`,
2150
2246
  meta: {
2151
2247
  docs: {
2152
- description: "Forbid raw native HTML elements when an atom wrapper exists. Use the atom the options name instead."
2248
+ description: "Forbid raw native HTML elements when an atom wrapper exists. Use the atom the options name instead. Silent inside the atom tier, where the raw element is the point; `scope` narrows it to named tiers."
2153
2249
  },
2154
2250
  messages: {
2155
- 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.'
2251
+ forbidden: 'Use <{{atom}}> from "{{importPath}}" instead of raw <{{element}}>. Native HTML elements that have an atom wrapper must not be used directly above the atom tier - use the atom to preserve consistent styling and behavior.'
2156
2252
  },
2157
2253
  schema: [
2158
2254
  {
@@ -2166,6 +2262,13 @@ a tree with no raw elements in it.`,
2166
2262
  type: "object"
2167
2263
  },
2168
2264
  type: "array"
2265
+ },
2266
+ {
2267
+ additionalProperties: false,
2268
+ properties: {
2269
+ scope: { items: { enum: [...TIERS], type: "string" }, type: "array" }
2270
+ },
2271
+ type: "object"
2169
2272
  }
2170
2273
  ],
2171
2274
  type: "problem"
@@ -3132,14 +3235,14 @@ can be misspelled and a second default nobody agreed to.`,
3132
3235
  var ssot_no_process_env_default = ssotNoProcessEnv;
3133
3236
 
3134
3237
  // src/rules/step-opens-its-own-cell.ts
3135
- var RULE = "step-opens-its-own-cell";
3238
+ var RULE3 = "step-opens-its-own-cell";
3136
3239
  var STEP = "step";
3137
3240
  var readable = (pattern) => pattern.replaceAll("\\", "").replace(/\$$/, "");
3138
3241
  var stepOpensItsOwnCell = {
3139
3242
  create(context) {
3140
3243
  const { wrapper } = context.options?.[0] ?? {};
3141
- const engine = requireOption(RULE, context.options?.[0]?.engine, "engine");
3142
- const within = requireOption(RULE, context.options?.[0]?.within, "within");
3244
+ const engine = requireOption(RULE3, context.options?.[0]?.engine, "engine");
3245
+ const within = requireOption(RULE3, context.options?.[0]?.within, "within");
3143
3246
  const filename = context.filename.replaceAll("\\", "/");
3144
3247
  if (!new RegExp(within).test(filename)) return {};
3145
3248
  if (wrapper !== void 0 && new RegExp(wrapper).test(filename)) return {};
@@ -4002,6 +4105,7 @@ var rules = {
4002
4105
  "document-sagas-are-generic": document_sagas_are_generic_default,
4003
4106
  "documents-share-one-table": documents_share_one_table_default,
4004
4107
  "effect-hook-naming": effect_hook_naming_default,
4108
+ "emit-declares-attempts": emit_declares_attempts_default,
4005
4109
  "layer-walls": layer_walls_default,
4006
4110
  "max-comment-density": max_comment_density_default,
4007
4111
  "molecule-atoms-only": molecule_atoms_only_default,
package/dist/presets.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PRESETS,
3
3
  rulesOfPreset
4
- } from "./chunk-HXBZSOIU.js";
4
+ } from "./chunk-55NK7NJN.js";
5
5
  export {
6
6
  PRESETS,
7
7
  rulesOfPreset
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/oxlint-plugin-biological-architecture",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Biological tier architecture as lint — the union of the dielime and during.day rule sets, shipped as presets.",
5
5
  "keywords": [
6
6
  "oxlint",
@@ -33,7 +33,7 @@
33
33
  "oxlint": ">=1.77"
34
34
  },
35
35
  "devDependencies": {
36
- "@geonosis/lint-parity": "0.2.0"
36
+ "@geonosis/lint-parity": "0.3.0"
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=22"