@geonosis/oxlint-plugin-biological-architecture 2.7.0 → 2.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @geonosis/oxlint-plugin-biological-architecture
2
2
 
3
+ ## 2.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e35392e: `tokens-only-colors` sees a colour that arrives as a default.
8
+
9
+ `({ color = '#090909' })` is never written in a JSX attribute, so the rule's only visitor could not
10
+ see it: one kit carried the same hex in 72 components, under this rule, at `"error"`, for its whole
11
+ life. A bare word is still left alone — a default is as often `variant = 'primary'` as a colour.
12
+
13
+ ## 2.8.0
14
+
15
+ ### Minor Changes
16
+
17
+ - e880a6c: The tiers have no dead ends a consumer can reach (#1, #2). Three shapes from one migration had no
18
+ legal expression, because two rules together forbade every place the file could live:
19
+
20
+ - **`tissue-no-data-props`** accepts layout configuration: a `number` or `boolean` prop the tissue
21
+ gives a literal default (`initialCount = 10`, `wrap = false`). A stateless adapter between two
22
+ cells is a tissue, and now it can be one. A string, a default that is not a literal of the prop's
23
+ type, and a primitive with no default are still data.
24
+ - **`no-hook-in-component-disguise`** accepts the client-boundary beacon: a file whose directive
25
+ prologue says `'use client'`, whose component calls a hook and returns only `null`. Its parent — a
26
+ Server Component, or a tissue under `tissue-no-hooks` — cannot call the hook, so there was no
27
+ real component to move it into. A `'use client'` component that returns its `children`, or calls
28
+ no hook, is still reported. The no-hook message now tells a layout about metadata
29
+ (`passthroughLayout`) and tells any other component nothing of the kind.
30
+ - **`no-invalid-feature-folders`** takes `roots` (default: none). Directly under each declared
31
+ directory, a folder that is not a tier is a finding per file, so an unmigrated pile
32
+ (`src/components/organisms/`) is debt the ratchet counts and moving a file into it grows the
33
+ count, instead of silently clearing every tier rule.
34
+
35
+ `cell-must-not-compose-cell` keeps no exception; its fix now names the stateful host: two cells
36
+ side by side, coordinating through the store, arranged by a tissue.
37
+
3
38
  ## 2.7.0
4
39
 
5
40
  ### Minor Changes
package/RULES.md CHANGED
@@ -867,7 +867,7 @@ option not named here is one no rule reads.
867
867
  | `no-index-count-as-exact` | `estimateFields`, `labelKey` |
868
868
  | `no-inert-hidden-jsx` | — |
869
869
  | `no-inline-data-in-jsx` | — |
870
- | `no-invalid-feature-folders` | |
870
+ | `no-invalid-feature-folders` | `roots` |
871
871
  | `no-loader-side-effect-in-index-file` | `directories`, `resourceFactories` |
872
872
  | `no-logic-in-component-files` | — |
873
873
  | `no-mock-db-in-integration` | `dbModules`, `integrationDirs`, `mockNames` |
@@ -0,0 +1,11 @@
1
+ export const DefaultedIcon = ({
2
+ color = '#090909',
3
+ size = 24
4
+ }: {
5
+ color?: string
6
+ size?: number
7
+ }) => (
8
+ <svg width={size} height={size} viewBox="0 0 24 24">
9
+ <path d="M4 4h16v16H4z" fill={color} />
10
+ </svg>
11
+ )
package/dist/index.js CHANGED
@@ -538,7 +538,12 @@ var cellMustNotComposeCell = directionRule({
538
538
  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.",
539
539
  fixShape: `A cell never composes another cell \u2014 not same-feature, not \`shared/\`, no exception. If you want to,
540
540
  one of the two is misclassified: arrangement goes up to a \`tissues/\` file, a stateless display goes
541
- down to \`compounds/\`, a store adapter goes sideways to \`organelles/\`. Private siblings of the SAME
541
+ down to \`compounds/\`, a store adapter goes sideways to \`organelles/\`. A parent that holds state AND
542
+ hosts a cell (a summary card that opens a modal-hosted form) is two cells side by side: the child
543
+ hosts its own modal and reads whether it is open from the store, the parent writes it, and a
544
+ \`tissues/\` file arranges both. Cells coordinate through a store, never through props from one to
545
+ the other. A stateless adapter between cells is a tissue \u2014 it may take number and boolean layout
546
+ configuration with a literal default (\`step = 10\`). Private siblings of the SAME
542
547
  component (\`invoice-editor.parts.tsx\` from \`invoice-editor.tsx\`) are one cell and are fine. The
543
548
  barrels \`cells/index.ts\`, \`cells/dynamic.ts\` and \`cells/dynamic.tsx\` are exempt \u2014 re-exporting
544
549
  siblings is the shape \`cells-folder-index-is-barrel\` requires of them.`,
@@ -2621,6 +2626,7 @@ var EXPLICIT_SIDE_EFFECT_HOOKS = /* @__PURE__ */ new Set([
2621
2626
  ]);
2622
2627
  var CUSTOM_EFFECT_HOOK_PATTERN = /^use[A-Z][A-Za-z0-9]*Effect$/;
2623
2628
  var STORE_HOOK_PATTERN = /^use[A-Z][A-Za-z0-9]*Store$/;
2629
+ var ANY_HOOK_PATTERN = /^use[A-Z]/;
2624
2630
  function isSideEffectHook(name) {
2625
2631
  if (EXPLICIT_SIDE_EFFECT_HOOKS.has(name)) return true;
2626
2632
  if (CUSTOM_EFFECT_HOOK_PATTERN.test(name)) return true;
@@ -2634,6 +2640,7 @@ var ROUTE_FALLBACK_BASENAMES = /* @__PURE__ */ new Set([
2634
2640
  "loading.tsx",
2635
2641
  "not-found.tsx"
2636
2642
  ]);
2643
+ var LAYOUT_BASENAMES = /* @__PURE__ */ new Set(["layout.tsx", "template.tsx"]);
2637
2644
  var R3F_PACKAGES = /* @__PURE__ */ new Set(["@react-three/drei", "@react-three/fiber"]);
2638
2645
  function isPascalCase(name) {
2639
2646
  return /^[A-Z]/.test(name);
@@ -2666,11 +2673,21 @@ function isIdentityFragment(n) {
2666
2673
  return false;
2667
2674
  }
2668
2675
  function classifyReturnShape(arg) {
2669
- if (isNullLiteral(arg)) return "identity";
2670
- if (isChildrenIdentifier(arg)) return "identity";
2671
- if (isIdentityFragment(arg)) return "identity";
2676
+ if (isNullLiteral(arg)) return "null";
2677
+ if (isChildrenIdentifier(arg)) return "passthrough";
2678
+ if (isIdentityFragment(arg)) return "passthrough";
2672
2679
  return "real";
2673
2680
  }
2681
+ function recordReturn(scope, arg) {
2682
+ if (!scope) return;
2683
+ const shape = classifyReturnShape(arg);
2684
+ if (shape === "real") {
2685
+ scope.hasNonIdentityReturn = true;
2686
+ return;
2687
+ }
2688
+ scope.hasIdentityReturn = true;
2689
+ if (shape === "passthrough") scope.hasPassthroughReturn = true;
2690
+ }
2674
2691
  function suggestedHookName(componentName) {
2675
2692
  const trimmed = componentName.replace(/(Provider|Sync|Listener|Hydration)$/, "");
2676
2693
  const kebab = trimmed.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
@@ -2684,6 +2701,7 @@ var noHookInComponentDisguise = {
2684
2701
  const basename = normalized.split("/").pop() ?? "";
2685
2702
  if (ROUTE_FALLBACK_BASENAMES.has(basename)) return {};
2686
2703
  let fileUsesR3F = false;
2704
+ let fileIsClientBoundary = false;
2687
2705
  const scopeStack = [];
2688
2706
  let pendingName = null;
2689
2707
  function currentScope() {
@@ -2691,9 +2709,11 @@ var noHookInComponentDisguise = {
2691
2709
  }
2692
2710
  function enterFunction(name, node) {
2693
2711
  scopeStack.push({
2712
+ calledAnyHook: false,
2694
2713
  calledHooks: /* @__PURE__ */ new Set(),
2695
2714
  hasIdentityReturn: false,
2696
2715
  hasNonIdentityReturn: false,
2716
+ hasPassthroughReturn: false,
2697
2717
  name,
2698
2718
  node
2699
2719
  });
@@ -2706,14 +2726,16 @@ var noHookInComponentDisguise = {
2706
2726
  if (scope.hasNonIdentityReturn) return;
2707
2727
  if (!scope.hasIdentityReturn) return;
2708
2728
  if (basename === "page.tsx" && scope.calledHooks.size === 0) return;
2729
+ if (fileIsClientBoundary && scope.calledAnyHook && !scope.hasPassthroughReturn) return;
2709
2730
  const hasHooks = scope.calledHooks.size > 0;
2731
+ const passthrough = LAYOUT_BASENAMES.has(basename) ? "passthroughLayout" : "passthroughComponent";
2710
2732
  context.report({
2711
2733
  data: {
2712
2734
  hooks: hasHooks ? [...scope.calledHooks].toSorted((a, b) => a.localeCompare(b)).join(", ") : "",
2713
2735
  name: scope.name,
2714
2736
  suggested: suggestedHookName(scope.name)
2715
2737
  },
2716
- messageId: hasHooks ? "hookInDisguise" : "passthroughComponent",
2738
+ messageId: hasHooks ? "hookInDisguise" : passthrough,
2717
2739
  node: scope.node
2718
2740
  });
2719
2741
  }
@@ -2723,12 +2745,7 @@ var noHookInComponentDisguise = {
2723
2745
  pendingName = null;
2724
2746
  enterFunction(name, node);
2725
2747
  if (node.body && node.body.type !== "BlockStatement") {
2726
- const shape = classifyReturnShape(node.body);
2727
- const scope = currentScope();
2728
- if (scope) {
2729
- if (shape === "identity") scope.hasIdentityReturn = true;
2730
- else scope.hasNonIdentityReturn = true;
2731
- }
2748
+ recordReturn(currentScope(), node.body);
2732
2749
  }
2733
2750
  },
2734
2751
  "ArrowFunctionExpression:exit"() {
@@ -2740,6 +2757,7 @@ var noHookInComponentDisguise = {
2740
2757
  if (!scope) return;
2741
2758
  const name = node.callee.name;
2742
2759
  if (!name) return;
2760
+ if (ANY_HOOK_PATTERN.test(name)) scope.calledAnyHook = true;
2743
2761
  if (isSideEffectHook(name)) {
2744
2762
  scope.calledHooks.add(name);
2745
2763
  }
@@ -2765,12 +2783,16 @@ var noHookInComponentDisguise = {
2765
2783
  fileUsesR3F = true;
2766
2784
  }
2767
2785
  },
2786
+ // Only the prologue is a directive: the same string after an import is an expression
2787
+ // statement, and React reads it as nothing.
2788
+ Program(node) {
2789
+ for (const statement2 of node.body ?? []) {
2790
+ if (typeof statement2.directive !== "string") break;
2791
+ if (statement2.directive === "use client") fileIsClientBoundary = true;
2792
+ }
2793
+ },
2768
2794
  ReturnStatement(node) {
2769
- const scope = currentScope();
2770
- if (!scope) return;
2771
- const shape = classifyReturnShape(node.argument);
2772
- if (shape === "identity") scope.hasIdentityReturn = true;
2773
- else scope.hasNonIdentityReturn = true;
2795
+ recordReturn(currentScope(), node.argument);
2774
2796
  },
2775
2797
  VariableDeclarator(node) {
2776
2798
  if (node.id?.type === "Identifier" && node.init && (node.init.type === "ArrowFunctionExpression" || node.init.type === "FunctionExpression")) {
@@ -2781,14 +2803,19 @@ var noHookInComponentDisguise = {
2781
2803
  },
2782
2804
  fixShape: `A component that calls hooks, renders nothing, and exists only to run an effect is a hook wearing
2783
2805
  \`.tsx\`. Move it to \`organelles/use-<thing>.ts\`, export it as \`use<Thing>\`, and let the cell call it.
2784
- A component returns UI; a hook returns state.`,
2806
+ A component returns UI; a hook returns state. The one exception is the client-boundary beacon, for a
2807
+ parent that cannot call hooks \u2014 a Server Component, or a tissue: \`'use client'\` as the file's first
2808
+ statement, a hook call, and \`return null\`. Render it beside the children, never around them \u2014 a
2809
+ component that returns its \`children\` is still a disguise. A component that calls no hook and
2810
+ returns nothing is not a beacon either; delete it.`,
2785
2811
  meta: {
2786
2812
  docs: {
2787
- 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."
2813
+ 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), files importing from @react-three/fiber or @react-three/drei, and the client-boundary beacon \u2014 a 'use client' file whose component calls a hook and returns only null. That is lint-theater."
2788
2814
  },
2789
2815
  messages: {
2790
- 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.",
2791
- 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."
2816
+ 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. If its parent cannot call hooks (a Server Component, a tissue), make it the client boundary instead: 'use client' first, and return null \u2014 rendered beside the children, not around them.",
2817
+ passthroughComponent: "Component '{{name}}' renders only identity/null and calls no hook. It serves no architectural purpose: delete it and render its children where it is used, or give it real content.",
2818
+ passthroughLayout: "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."
2792
2819
  },
2793
2820
  schema: [],
2794
2821
  type: "problem"
@@ -3017,10 +3044,31 @@ var VALID_FOLDERS = /* @__PURE__ */ new Set([
3017
3044
  "tissues"
3018
3045
  ]);
3019
3046
  var FEATURE_PATH_PATTERN = /features\/([^/]+)\/([^/]+)\//;
3047
+ var folderUnderRoot = (normalized, roots) => {
3048
+ const path2 = `/${normalized}`;
3049
+ for (const declared of roots) {
3050
+ const root = declared.replaceAll(/^\/+|\/+$/g, "");
3051
+ const at = path2.indexOf(`/${root}/`);
3052
+ if (root === "" || at === -1) continue;
3053
+ const rest = path2.slice(at + root.length + 2);
3054
+ const slash = rest.indexOf("/");
3055
+ if (slash !== -1) return { folder: rest.slice(0, slash), root };
3056
+ }
3057
+ return void 0;
3058
+ };
3020
3059
  var noInvalidFeatureFolders = {
3021
3060
  create(context) {
3022
3061
  const normalized = normalise(context.filename);
3023
3062
  const match = normalized.match(FEATURE_PATH_PATTERN);
3063
+ const { roots = [] } = context.options?.[0] ?? {};
3064
+ const underRoot = match ? void 0 : folderUnderRoot(normalized, roots);
3065
+ if (underRoot && underRoot.folder !== "features" && !VALID_FOLDERS.has(underRoot.folder)) {
3066
+ return {
3067
+ Program(node) {
3068
+ context.report({ data: underRoot, messageId: "untiered", node });
3069
+ }
3070
+ };
3071
+ }
3024
3072
  if (!match) return {};
3025
3073
  const feature = match[1] ?? "";
3026
3074
  const folder = match[2] ?? "";
@@ -3050,16 +3098,28 @@ var noInvalidFeatureFolders = {
3050
3098
  fixShape: `A feature folder holds only the tiers and their two neighbours: \`atoms\`, \`molecules\`, \`compounds\`,
3051
3099
  \`organelles\`, \`cells\`, \`tissues\`, \`stores\`, \`lib\`. Anything else (\`components\`, \`utils\`, \`helpers\`,
3052
3100
  \`types\`) is an unclassified pile \u2014 put the file in the tier that describes what it does, and pure
3053
- logic in \`lib/\`.`,
3101
+ logic in \`lib/\`. A repo whose components live outside \`features/\` names those directories in
3102
+ \`roots\` (default: none), and the same holds directly under each one. An unmigrated folder there
3103
+ (\`organisms/\`) is a finding per file \u2014 debt the ratchet counts, never a place to park a file whose
3104
+ tier would not accept it.`,
3054
3105
  meta: {
3055
3106
  docs: {
3056
- 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.'
3107
+ description: 'Files inside features/*/ \u2014 and directly under each directory the `roots` option declares \u2014 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.'
3057
3108
  },
3058
3109
  messages: {
3059
3110
  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.',
3060
- invalid: 'File is in "features/{{feature}}/{{folder}}/" which is not a valid biological hierarchy folder. Valid folders: compounds, organelles, cells, tissues, stores, lib.'
3111
+ invalid: 'File is in "features/{{feature}}/{{folder}}/" which is not a valid biological hierarchy folder. Valid folders: compounds, organelles, cells, tissues, stores, lib.',
3112
+ untiered: 'File is in "{{root}}/{{folder}}/", which is not a tier, so no tier rule inspects it. Move it to the tier that describes what it does: atoms, molecules, compounds, organelles, cells, tissues, stores or lib.'
3061
3113
  },
3062
- schema: [],
3114
+ schema: [
3115
+ {
3116
+ additionalProperties: false,
3117
+ properties: {
3118
+ roots: { items: { type: "string" }, type: "array" }
3119
+ },
3120
+ type: "object"
3121
+ }
3122
+ ],
3063
3123
  type: "problem"
3064
3124
  }
3065
3125
  };
@@ -6297,6 +6357,12 @@ function isAllowedType(t) {
6297
6357
  return true;
6298
6358
  }
6299
6359
  }
6360
+ var literalDefaultsOf = (param) => new Map(
6361
+ (param.properties ?? []).flatMap(
6362
+ (property) => property.type === "Property" && property.key?.type === "Identifier" && property.value?.type === "AssignmentPattern" && property.value.right?.type === "Literal" ? [[property.key.name ?? "", property.value.right.value]] : []
6363
+ )
6364
+ );
6365
+ var isLayoutConfiguration = (t, defaults, prop) => t.type === "TSNumberKeyword" && typeof defaults.get(prop) === "number" || t.type === "TSBooleanKeyword" && typeof defaults.get(prop) === "boolean";
6300
6366
  var tissueNoDataProps = {
6301
6367
  create(context) {
6302
6368
  const normalized = normalise(context.filename);
@@ -6312,6 +6378,7 @@ var tissueNoDataProps = {
6312
6378
  const typeLiteral = firstParam.typeAnnotation?.typeAnnotation;
6313
6379
  if (!typeLiteral || typeLiteral.type !== "TSTypeLiteral") return;
6314
6380
  const members = typeLiteral.members ?? [];
6381
+ const defaults = literalDefaultsOf(firstParam);
6315
6382
  for (const member of members) {
6316
6383
  if (member.key?.type !== "Identifier") continue;
6317
6384
  const propName = member.key.name ?? "";
@@ -6320,6 +6387,7 @@ var tissueNoDataProps = {
6320
6387
  const t = member.typeAnnotation?.typeAnnotation;
6321
6388
  if (!t) continue;
6322
6389
  if (isAllowedType(t)) continue;
6390
+ if (isLayoutConfiguration(t, defaults, propName)) continue;
6323
6391
  context.report({
6324
6392
  data: { prop: propName, type: describeTypeAnnotation(t) },
6325
6393
  messageId: "dataProp",
@@ -6359,13 +6427,14 @@ var tissueNoDataProps = {
6359
6427
  fixShape: `A tissue takes \`children\`, ReactNode slot props, layout variants, \`params\` and \`searchParams\` \u2014 and
6360
6428
  nothing else. A prop typed \`string\`, \`number\`, an array or a domain type (\`id\`, \`initialData\`,
6361
6429
  \`documents\`) means the file owns data, which makes it a CELL: move it to \`cells/\`. A tissue gets its
6362
- content as slots, never as data.`,
6430
+ content as slots, never as data. Layout configuration is not data: a \`number\` or \`boolean\` prop the
6431
+ tissue gives a literal default (\`{ step = 10, wrap = false }\`) is allowed.`,
6363
6432
  meta: {
6364
6433
  docs: {
6365
6434
  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)."
6366
6435
  },
6367
6436
  messages: {
6368
- 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."
6437
+ 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, and number/boolean layout configuration with a literal default."
6369
6438
  },
6370
6439
  schema: [],
6371
6440
  type: "problem"
@@ -6797,6 +6866,10 @@ var COLOR_UTILITIES = /* @__PURE__ */ new Set([
6797
6866
  var isColorUtility = (utility) => COLOR_UTILITIES.has(utility.replace(/^.*:/, ""));
6798
6867
 
6799
6868
  // src/rules/tokens-only-colors.ts
6869
+ var defaultedName = (node) => {
6870
+ const left = node.left;
6871
+ return left?.type === "Identifier" ? left.name ?? "default" : "default";
6872
+ };
6800
6873
  var tokenOffence = (value, prefixes, local) => {
6801
6874
  const variable = variableOf(value);
6802
6875
  if (variable !== void 0) {
@@ -6837,6 +6910,28 @@ var tokensOnlyColors = {
6837
6910
  report("colorLiteral", { property: utility, value: inner });
6838
6911
  }
6839
6912
  }
6913
+ },
6914
+ /**
6915
+ * A colour that arrives as a DEFAULT — `({ color = '#090909' })` — is never written in a JSX
6916
+ * attribute, so the visitor above cannot see it. It reaches the element through a prop and
6917
+ * paints in the brand it was typed for, from the one place nobody looked: seventy-two icons
6918
+ * in one kit carried the same hex this way, under this rule, at "error", for its whole life.
6919
+ * A default is a value the component CHOOSES, which is exactly what a token is for.
6920
+ *
6921
+ * A bare word is left alone. `variant = 'primary'` and `size = 'small'` are defaults too, and
6922
+ * a rule that read every English word as a colour would be turned off within the week.
6923
+ */
6924
+ AssignmentPattern(node) {
6925
+ const right = node.right;
6926
+ if (right?.type !== "Literal" || typeof right.value !== "string") return;
6927
+ const offence = tokenOffence(right.value, prefixes, /* @__PURE__ */ new Set());
6928
+ if (offence === void 0 || offence === "colorLiteral" && isNamedOnly(right.value))
6929
+ return;
6930
+ context.report({
6931
+ data: { property: defaultedName(node), value: right.value },
6932
+ messageId: offence,
6933
+ node
6934
+ });
6840
6935
  }
6841
6936
  };
6842
6937
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/oxlint-plugin-biological-architecture",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "Biological tier architecture as lint — the union of the dielime and during.day rule sets, shipped as presets.",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  "oxlint": ">=1.77"
46
46
  },
47
47
  "devDependencies": {
48
- "@geonosis/lint-parity": "2.7.0"
48
+ "@geonosis/lint-parity": "2.9.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=22"