@geonosis/oxlint-plugin-biological-architecture 2.6.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  PRESUMED_ENGINES,
4
4
  RENAMED_TO,
5
5
  rulesOfPreset
6
- } from "./chunk-KV7JRNAP.js";
6
+ } from "./chunk-24K5232K.js";
7
7
 
8
8
  // src/index.ts
9
9
  import { createRequire } from "module";
@@ -257,16 +257,6 @@ var requireOption = (rule, value, option, accepts) => {
257
257
  }
258
258
  return value;
259
259
  };
260
- var requireOptionHere = (rule, value, option, file, accepts) => {
261
- if (isMissing(value)) {
262
- throw new OptionMissing(
263
- rule,
264
- option,
265
- `${refusalHeader(rule, option)} It is enabled over "${file}", which it would judge, and its \`${option}\` option is ${NOTHING}. Configure \`${option}\`, or turn the rule off for this tree.${accepts === void 0 ? "" : ` It accepts ${accepts}.`}`
266
- );
267
- }
268
- return value;
269
- };
270
260
  var raisedOver = /* @__PURE__ */ new Map();
271
261
  var fileOf2 = (context) => {
272
262
  const named = context?.filename;
@@ -548,7 +538,12 @@ var cellMustNotComposeCell = directionRule({
548
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.",
549
539
  fixShape: `A cell never composes another cell \u2014 not same-feature, not \`shared/\`, no exception. If you want to,
550
540
  one of the two is misclassified: arrangement goes up to a \`tissues/\` file, a stateless display goes
551
- 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
552
547
  component (\`invoice-editor.parts.tsx\` from \`invoice-editor.tsx\`) are one cell and are fine. The
553
548
  barrels \`cells/index.ts\`, \`cells/dynamic.ts\` and \`cells/dynamic.tsx\` are exempt \u2014 re-exporting
554
549
  siblings is the shape \`cells-folder-index-is-barrel\` requires of them.`,
@@ -884,155 +879,6 @@ configs. Everywhere else, use the dialect-free aliases the seam exports (\`Datab
884
879
  };
885
880
  var dialect_through_the_seam_default = dialectThroughTheSeam;
886
881
 
887
- // src/rules/document-sagas-are-generic.ts
888
- var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
889
- var RULE2 = "document-sagas-are-generic";
890
- var DEFAULT_FACTORIES = ["saga", "createWorkflow"];
891
- var perEntityNameOf = (node, factories) => {
892
- for (const factory of factories) {
893
- const name = literalNamedBy(node, factory);
894
- if (name !== null) return name;
895
- }
896
- return null;
897
- };
898
- var declaresAFlow = (text, factories) => factories.some((factory) => new RegExp(`\\b${escapeForRegex(factory)}\\s*\\(`).test(text));
899
- var documentSagasAreGeneric = {
900
- create(context) {
901
- const factories = requireOption(
902
- RULE2,
903
- context.options?.[0]?.factories ?? DEFAULT_FACTORIES,
904
- "factories"
905
- );
906
- const filename = normalise(context.filename);
907
- const named = context.options?.[0]?.within;
908
- const insideTree = named === void 0 || named === "" ? void 0 : new RegExp(named).test(filename);
909
- if (insideTree === false) return {};
910
- if (insideTree === void 0 && !declaresAFlow(context.sourceCode?.text ?? "", factories)) {
911
- return {};
912
- }
913
- const prefixes = requireOptionHere(
914
- RULE2,
915
- context.options?.[0]?.perKindPrefixes,
916
- "perKindPrefixes",
917
- filename
918
- );
919
- const within = requireOptionHere(RULE2, named, "within", filename);
920
- if (!new RegExp(within).test(filename)) return {};
921
- const perKindPrefix = new RegExp(`^(?:${prefixes.map(escapeForRegex).join("|")})\\.`);
922
- return {
923
- CallExpression(node) {
924
- const name = perEntityNameOf(node, factories);
925
- if (name === null || !perKindPrefix.test(name)) return;
926
- context.report({ data: { name }, messageId: "perKindSaga", node });
927
- }
928
- };
929
- },
930
- fixShape: `A saga named for one document kind (\`invoice.issue\`, \`quote.accept\`) is the hand-written fork the
931
- generic base entity replaced. Mint it from the generic factory \u2014 \`documentSagas(entity, effects)\` \u2014
932
- and pass the kind as data. The same holds for a workflow: \`createWorkflow('invoice.create', \u2026)\` is
933
- the fork, \`documentWorkflows(entity)\` is the mint. A name assembled from the entity \u2014 a template
934
- literal \u2014 is what minting looks like and is left alone by construction. Configure the kind vocabulary
935
- through \`perKindPrefixes\`, the tree that holds the flows through \`within\` (a regex over the
936
- filename), and \`factories\` if the repo declares them under other names; enabled without
937
- \`perKindPrefixes\` or \`within\` the rule refuses the run.`,
938
- meta: {
939
- floor: { name: "document", pending: true },
940
- docs: {
941
- description: "Document sagas are minted, not written: `documentSagas(entity, effects)` gives every entity that extends Document its create/save/setStatus/remove/send/convert/duplicate/share from one body. A saga or workflow named with a LITERAL for one entity is a fork; a name assembled from the entity is the mint and is unreadable here on purpose. `factories`, `perKindPrefixes` and `within` are options."
942
- },
943
- messages: {
944
- perKindSaga: '"{{name}}" is a hand-written flow for one entity. Every entity that extends Document gets its sagas from `documentSagas(entity, effects)` and its workflows from the same mint; put what this entity decides in its `Document.extend` definition or in its `DocumentEffects`.'
945
- },
946
- schema: [
947
- {
948
- additionalProperties: false,
949
- properties: {
950
- factories: { items: { type: "string" }, type: "array" },
951
- perKindPrefixes: { items: { type: "string" }, type: "array" },
952
- within: { type: "string" }
953
- },
954
- type: "object"
955
- }
956
- ],
957
- type: "problem"
958
- },
959
- /** Every half comes from the options now: the vocabulary, the tree, and the factory it is minted by. */
960
- probe(options) {
961
- const { perKindPrefixes = [], within = "" } = bagOf(options);
962
- const factory = firstOf(bagOf(options).factories, DEFAULT_FACTORIES);
963
- return {
964
- path: pathMatching(within, "geonosis-probe-saga.ts"),
965
- source: `import { ${factory} } from 'sagaflow-js'
966
-
967
- export const geonosisProbe = ${factory}('${perKindPrefixes[0] ?? ""}.geonosis-probe', {})
968
- `
969
- };
970
- }
971
- };
972
- var document_sagas_are_generic_default = documentSagasAreGeneric;
973
-
974
- // src/rules/documents-share-one-table.ts
975
- var TABLE_CALLEES = ["sqliteTable", "pgTable"];
976
- var documentsShareOneTable = {
977
- create(context) {
978
- const perKindTables = new Set(
979
- requireOption(
980
- "documents-share-one-table",
981
- context.options?.[0]?.perKindTables,
982
- "perKindTables"
983
- )
984
- );
985
- if (!/\/packages\/db\/src\/schema\//.test(normalise(context.filename))) return {};
986
- return {
987
- CallExpression(node) {
988
- for (const callee of TABLE_CALLEES) {
989
- const table = literalNamedBy(node, callee);
990
- if (table === null || !perKindTables.has(table)) continue;
991
- context.report({ data: { table }, messageId: "perKindTable", node });
992
- return;
993
- }
994
- }
995
- };
996
- },
997
- fixShape: `Every document kind is a row of the one \`documents\` table with a \`kind\` column, not a table of its
998
- own. A \`pgTable('invoices', \u2026)\` beside a \`pgTable('quotes', \u2026)\` is the second storage the base entity
999
- retired: write the row, not the table. Both constructors count \u2014 \`pgTable\` and \`sqliteTable\` \u2014 since
1000
- the dialect is a seam fact, not a reason for a kind to own storage. The forbidden table names are the rule's
1001
- \`perKindTables\` option; enabled without it the rule refuses the run rather than passing every
1002
- schema silently.`,
1003
- meta: {
1004
- floor: { name: "document", pending: true },
1005
- docs: {
1006
- description: "Documents share ONE table: `documents` holds the head every kind derives and the body its kind declares. A table named for one kind of document is a second storage."
1007
- },
1008
- messages: {
1009
- perKindTable: '"{{table}}" is a table for one kind of document. Every kind is a row of `documents` \u2014 add the kind to `documentEntities` and let its `headOf` fill the columns.'
1010
- },
1011
- schema: [
1012
- {
1013
- additionalProperties: false,
1014
- properties: {
1015
- perKindTables: { items: { type: "string" }, type: "array" }
1016
- },
1017
- type: "object"
1018
- }
1019
- ],
1020
- type: "problem"
1021
- },
1022
- /** The schema directory is compiled in; the forbidden table names are this repo's vocabulary. */
1023
- probe(options) {
1024
- const { perKindTables = [] } = bagOf(options);
1025
- return {
1026
- path: "packages/db/src/schema/geonosis-probe-table.ts",
1027
- source: `import { pgTable, text } from 'drizzle-orm/pg-core'
1028
-
1029
- export const geonosisProbe = pgTable('${perKindTables[0] ?? ""}', { id: text('id') })
1030
- `
1031
- };
1032
- }
1033
- };
1034
- var documents_share_one_table_default = documentsShareOneTable;
1035
-
1036
882
  // src/rules/lib/objects.ts
1037
883
  var propertiesOf = (node) => {
1038
884
  const object = node;
@@ -1084,7 +930,7 @@ var stepNameOf = (node, config) => {
1084
930
  };
1085
931
 
1086
932
  // src/rules/durable-body-reads-through-steps.ts
1087
- var RULE3 = "durable-body-reads-through-steps";
933
+ var RULE2 = "durable-body-reads-through-steps";
1088
934
  var ACCEPTS2 = "{ ports: string[] (required), durableKey: string, durableValue: boolean | string, workflowFactories: string[], stepFactories: string[], stepProxies: string[] }";
1089
935
  var DEFAULT_WORKFLOW_FACTORIES = ["createWorkflow"];
1090
936
  var DEFAULT_DURABLE_KEY = "execution";
@@ -1116,13 +962,13 @@ var insideAStep = (node, factories) => {
1116
962
  var durableBodyReadsThroughSteps = {
1117
963
  create(context) {
1118
964
  const bag = context.options?.[0] ?? {};
1119
- const ports = new Set(requireOption(RULE3, bag.ports, "ports", ACCEPTS2));
965
+ const ports = new Set(requireOption(RULE2, bag.ports, "ports", ACCEPTS2));
1120
966
  const proxies = new Set(bag.stepProxies ?? []);
1121
- const steps = stepFactoriesOf(RULE3, bag.stepFactories);
967
+ const steps = stepFactoriesOf(RULE2, bag.stepFactories);
1122
968
  const workflows = new Set(
1123
- requireOption(RULE3, bag.workflowFactories ?? DEFAULT_WORKFLOW_FACTORIES, "workflowFactories")
969
+ requireOption(RULE2, bag.workflowFactories ?? DEFAULT_WORKFLOW_FACTORIES, "workflowFactories")
1124
970
  );
1125
- const durableKey = requireOption(RULE3, bag.durableKey ?? DEFAULT_DURABLE_KEY, "durableKey");
971
+ const durableKey = requireOption(RULE2, bag.durableKey ?? DEFAULT_DURABLE_KEY, "durableKey");
1126
972
  const durableValue = bag.durableValue ?? DEFAULT_DURABLE_VALUE;
1127
973
  return {
1128
974
  CallExpression(node) {
@@ -1272,7 +1118,7 @@ file and the export together \u2014 half the pair still fires.`,
1272
1118
  var effect_hook_naming_default = effectHookNaming;
1273
1119
 
1274
1120
  // src/rules/emit-declares-attempts.ts
1275
- var RULE4 = "emit-declares-attempts";
1121
+ var RULE3 = "emit-declares-attempts";
1276
1122
  var DEFAULT_EMITTERS = ["emitEventStep"];
1277
1123
  var OPTIONS = "options";
1278
1124
  var ATTEMPTS = "attempts";
@@ -1287,7 +1133,7 @@ var valueOf2 = (properties, name) => properties.find((one) => one.key?.type ===
1287
1133
  var emitDeclaresAttempts = {
1288
1134
  create(context) {
1289
1135
  const emitters = new Set(
1290
- requireOption(RULE4, context.options?.[0]?.emitters ?? DEFAULT_EMITTERS, "emitters")
1136
+ requireOption(RULE3, context.options?.[0]?.emitters ?? DEFAULT_EMITTERS, "emitters")
1291
1137
  );
1292
1138
  const budgetHelpers = new Set(context.options?.[0]?.budgetHelpers ?? []);
1293
1139
  return {
@@ -1957,21 +1803,21 @@ var writesWith = (prefixes) => (node) => {
1957
1803
  var writeNameOf = (node) => propertyNameOf(node.callee ?? {}) ?? "a service write";
1958
1804
 
1959
1805
  // src/rules/mutating-step-declares-retry.ts
1960
- var RULE5 = "mutating-step-declares-retry";
1806
+ var RULE4 = "mutating-step-declares-retry";
1961
1807
  var DEFAULT_RETRY_KEY = "maxRetries";
1962
1808
  var DEFAULT_RETRY_LIMIT_KEY = "limit";
1963
1809
  var NO_RETRY2 = 0;
1964
1810
  var mutatingStepDeclaresRetry = {
1965
1811
  create(context) {
1966
- const factories = stepFactoriesOf(RULE5, context.options?.[0]?.stepFactories);
1967
- const isWrite = writesWith(mutationPrefixesOf(RULE5, context.options?.[0]?.mutationPrefixes));
1812
+ const factories = stepFactoriesOf(RULE4, context.options?.[0]?.stepFactories);
1813
+ const isWrite = writesWith(mutationPrefixesOf(RULE4, context.options?.[0]?.mutationPrefixes));
1968
1814
  const retryKey = requireOption(
1969
- RULE5,
1815
+ RULE4,
1970
1816
  context.options?.[0]?.retryKey ?? DEFAULT_RETRY_KEY,
1971
1817
  "retryKey"
1972
1818
  );
1973
1819
  const retryLimitKey = requireOption(
1974
- RULE5,
1820
+ RULE4,
1975
1821
  context.options?.[0]?.retryLimitKey ?? DEFAULT_RETRY_LIMIT_KEY,
1976
1822
  "retryLimitKey"
1977
1823
  );
@@ -2048,13 +1894,13 @@ the repo wraps \`createStep\`, and \`mutationPrefixes\` if its writes are named
2048
1894
  var mutating_step_declares_retry_default = mutatingStepDeclaresRetry;
2049
1895
 
2050
1896
  // src/rules/named-when-block.ts
2051
- var RULE6 = "named-when-block";
1897
+ var RULE5 = "named-when-block";
2052
1898
  var DEFAULT_BLOCKS = ["when"];
2053
1899
  var NAMED = 3;
2054
1900
  var namedWhenBlock = {
2055
1901
  create(context) {
2056
1902
  const blocks = new Set(
2057
- requireOption(RULE6, context.options?.[0]?.blocks ?? DEFAULT_BLOCKS, "blocks")
1903
+ requireOption(RULE5, context.options?.[0]?.blocks ?? DEFAULT_BLOCKS, "blocks")
2058
1904
  );
2059
1905
  return {
2060
1906
  CallExpression(node) {
@@ -2272,16 +2118,16 @@ var next_route_segment_is_thin_delegate_default = nextRouteSegmentIsThinDelegate
2272
2118
  // src/rules/no-brand-names.ts
2273
2119
  import * as fs3 from "fs";
2274
2120
  var EXEMPT_PATH = /\/examples\//;
2275
- var escapeForRegex2 = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
2121
+ var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
2276
2122
  var noBrandNames = {
2277
2123
  create(context) {
2278
2124
  const { allowedSubstrings = [], brands = [] } = context.options?.[0] ?? {};
2279
2125
  requireOption("no-brand-names", brands, "brands");
2280
2126
  const filename = normalise(context.filename);
2281
2127
  if (EXEMPT_PATH.test(filename)) return {};
2282
- const brandPattern = new RegExp(`\\b(?:${brands.map(escapeForRegex2).join("|")})\\b`, "i");
2128
+ const brandPattern = new RegExp(`\\b(?:${brands.map(escapeForRegex).join("|")})\\b`, "i");
2283
2129
  const allowedPatterns = allowedSubstrings.map(
2284
- (allowed) => new RegExp(escapeForRegex2(allowed), "gi")
2130
+ (allowed) => new RegExp(escapeForRegex(allowed), "gi")
2285
2131
  );
2286
2132
  let programNode = null;
2287
2133
  return {
@@ -2780,6 +2626,7 @@ var EXPLICIT_SIDE_EFFECT_HOOKS = /* @__PURE__ */ new Set([
2780
2626
  ]);
2781
2627
  var CUSTOM_EFFECT_HOOK_PATTERN = /^use[A-Z][A-Za-z0-9]*Effect$/;
2782
2628
  var STORE_HOOK_PATTERN = /^use[A-Z][A-Za-z0-9]*Store$/;
2629
+ var ANY_HOOK_PATTERN = /^use[A-Z]/;
2783
2630
  function isSideEffectHook(name) {
2784
2631
  if (EXPLICIT_SIDE_EFFECT_HOOKS.has(name)) return true;
2785
2632
  if (CUSTOM_EFFECT_HOOK_PATTERN.test(name)) return true;
@@ -2793,6 +2640,7 @@ var ROUTE_FALLBACK_BASENAMES = /* @__PURE__ */ new Set([
2793
2640
  "loading.tsx",
2794
2641
  "not-found.tsx"
2795
2642
  ]);
2643
+ var LAYOUT_BASENAMES = /* @__PURE__ */ new Set(["layout.tsx", "template.tsx"]);
2796
2644
  var R3F_PACKAGES = /* @__PURE__ */ new Set(["@react-three/drei", "@react-three/fiber"]);
2797
2645
  function isPascalCase(name) {
2798
2646
  return /^[A-Z]/.test(name);
@@ -2825,11 +2673,21 @@ function isIdentityFragment(n) {
2825
2673
  return false;
2826
2674
  }
2827
2675
  function classifyReturnShape(arg) {
2828
- if (isNullLiteral(arg)) return "identity";
2829
- if (isChildrenIdentifier(arg)) return "identity";
2830
- if (isIdentityFragment(arg)) return "identity";
2676
+ if (isNullLiteral(arg)) return "null";
2677
+ if (isChildrenIdentifier(arg)) return "passthrough";
2678
+ if (isIdentityFragment(arg)) return "passthrough";
2831
2679
  return "real";
2832
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
+ }
2833
2691
  function suggestedHookName(componentName) {
2834
2692
  const trimmed = componentName.replace(/(Provider|Sync|Listener|Hydration)$/, "");
2835
2693
  const kebab = trimmed.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
@@ -2843,6 +2701,7 @@ var noHookInComponentDisguise = {
2843
2701
  const basename = normalized.split("/").pop() ?? "";
2844
2702
  if (ROUTE_FALLBACK_BASENAMES.has(basename)) return {};
2845
2703
  let fileUsesR3F = false;
2704
+ let fileIsClientBoundary = false;
2846
2705
  const scopeStack = [];
2847
2706
  let pendingName = null;
2848
2707
  function currentScope() {
@@ -2850,9 +2709,11 @@ var noHookInComponentDisguise = {
2850
2709
  }
2851
2710
  function enterFunction(name, node) {
2852
2711
  scopeStack.push({
2712
+ calledAnyHook: false,
2853
2713
  calledHooks: /* @__PURE__ */ new Set(),
2854
2714
  hasIdentityReturn: false,
2855
2715
  hasNonIdentityReturn: false,
2716
+ hasPassthroughReturn: false,
2856
2717
  name,
2857
2718
  node
2858
2719
  });
@@ -2865,14 +2726,16 @@ var noHookInComponentDisguise = {
2865
2726
  if (scope.hasNonIdentityReturn) return;
2866
2727
  if (!scope.hasIdentityReturn) return;
2867
2728
  if (basename === "page.tsx" && scope.calledHooks.size === 0) return;
2729
+ if (fileIsClientBoundary && scope.calledAnyHook && !scope.hasPassthroughReturn) return;
2868
2730
  const hasHooks = scope.calledHooks.size > 0;
2731
+ const passthrough = LAYOUT_BASENAMES.has(basename) ? "passthroughLayout" : "passthroughComponent";
2869
2732
  context.report({
2870
2733
  data: {
2871
2734
  hooks: hasHooks ? [...scope.calledHooks].toSorted((a, b) => a.localeCompare(b)).join(", ") : "",
2872
2735
  name: scope.name,
2873
2736
  suggested: suggestedHookName(scope.name)
2874
2737
  },
2875
- messageId: hasHooks ? "hookInDisguise" : "passthroughComponent",
2738
+ messageId: hasHooks ? "hookInDisguise" : passthrough,
2876
2739
  node: scope.node
2877
2740
  });
2878
2741
  }
@@ -2882,12 +2745,7 @@ var noHookInComponentDisguise = {
2882
2745
  pendingName = null;
2883
2746
  enterFunction(name, node);
2884
2747
  if (node.body && node.body.type !== "BlockStatement") {
2885
- const shape = classifyReturnShape(node.body);
2886
- const scope = currentScope();
2887
- if (scope) {
2888
- if (shape === "identity") scope.hasIdentityReturn = true;
2889
- else scope.hasNonIdentityReturn = true;
2890
- }
2748
+ recordReturn(currentScope(), node.body);
2891
2749
  }
2892
2750
  },
2893
2751
  "ArrowFunctionExpression:exit"() {
@@ -2899,6 +2757,7 @@ var noHookInComponentDisguise = {
2899
2757
  if (!scope) return;
2900
2758
  const name = node.callee.name;
2901
2759
  if (!name) return;
2760
+ if (ANY_HOOK_PATTERN.test(name)) scope.calledAnyHook = true;
2902
2761
  if (isSideEffectHook(name)) {
2903
2762
  scope.calledHooks.add(name);
2904
2763
  }
@@ -2924,12 +2783,16 @@ var noHookInComponentDisguise = {
2924
2783
  fileUsesR3F = true;
2925
2784
  }
2926
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
+ },
2927
2794
  ReturnStatement(node) {
2928
- const scope = currentScope();
2929
- if (!scope) return;
2930
- const shape = classifyReturnShape(node.argument);
2931
- if (shape === "identity") scope.hasIdentityReturn = true;
2932
- else scope.hasNonIdentityReturn = true;
2795
+ recordReturn(currentScope(), node.argument);
2933
2796
  },
2934
2797
  VariableDeclarator(node) {
2935
2798
  if (node.id?.type === "Identifier" && node.init && (node.init.type === "ArrowFunctionExpression" || node.init.type === "FunctionExpression")) {
@@ -2940,14 +2803,19 @@ var noHookInComponentDisguise = {
2940
2803
  },
2941
2804
  fixShape: `A component that calls hooks, renders nothing, and exists only to run an effect is a hook wearing
2942
2805
  \`.tsx\`. Move it to \`organelles/use-<thing>.ts\`, export it as \`use<Thing>\`, and let the cell call it.
2943
- 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.`,
2944
2811
  meta: {
2945
2812
  docs: {
2946
- 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."
2947
2814
  },
2948
2815
  messages: {
2949
- 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.",
2950
- 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."
2951
2819
  },
2952
2820
  schema: [],
2953
2821
  type: "problem"
@@ -2956,20 +2824,20 @@ A component returns UI; a hook returns state.`,
2956
2824
  var no_hook_in_component_disguise_default = noHookInComponentDisguise;
2957
2825
 
2958
2826
  // src/rules/no-index-count-as-exact.ts
2959
- var RULE7 = "no-index-count-as-exact";
2827
+ var RULE6 = "no-index-count-as-exact";
2960
2828
  var DEFAULT_ESTIMATE_FIELDS = ["estimate_count", "count_estimate"];
2961
2829
  var DEFAULT_LABEL_KEY = "count_is_estimate";
2962
2830
  var noIndexCountAsExact = {
2963
2831
  create(context) {
2964
2832
  const fields = new Set(
2965
2833
  requireOption(
2966
- RULE7,
2834
+ RULE6,
2967
2835
  context.options?.[0]?.estimateFields ?? DEFAULT_ESTIMATE_FIELDS,
2968
2836
  "estimateFields"
2969
2837
  )
2970
2838
  );
2971
2839
  const label = requireOption(
2972
- RULE7,
2840
+ RULE6,
2973
2841
  context.options?.[0]?.labelKey ?? DEFAULT_LABEL_KEY,
2974
2842
  "labelKey"
2975
2843
  );
@@ -3176,10 +3044,31 @@ var VALID_FOLDERS = /* @__PURE__ */ new Set([
3176
3044
  "tissues"
3177
3045
  ]);
3178
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
+ };
3179
3059
  var noInvalidFeatureFolders = {
3180
3060
  create(context) {
3181
3061
  const normalized = normalise(context.filename);
3182
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
+ }
3183
3072
  if (!match) return {};
3184
3073
  const feature = match[1] ?? "";
3185
3074
  const folder = match[2] ?? "";
@@ -3209,36 +3098,48 @@ var noInvalidFeatureFolders = {
3209
3098
  fixShape: `A feature folder holds only the tiers and their two neighbours: \`atoms\`, \`molecules\`, \`compounds\`,
3210
3099
  \`organelles\`, \`cells\`, \`tissues\`, \`stores\`, \`lib\`. Anything else (\`components\`, \`utils\`, \`helpers\`,
3211
3100
  \`types\`) is an unclassified pile \u2014 put the file in the tier that describes what it does, and pure
3212
- 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.`,
3213
3105
  meta: {
3214
3106
  docs: {
3215
- 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.'
3216
3108
  },
3217
3109
  messages: {
3218
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.',
3219
- 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.'
3220
3113
  },
3221
- schema: [],
3114
+ schema: [
3115
+ {
3116
+ additionalProperties: false,
3117
+ properties: {
3118
+ roots: { items: { type: "string" }, type: "array" }
3119
+ },
3120
+ type: "object"
3121
+ }
3122
+ ],
3222
3123
  type: "problem"
3223
3124
  }
3224
3125
  };
3225
3126
  var no_invalid_feature_folders_default = noInvalidFeatureFolders;
3226
3127
 
3227
3128
  // src/rules/no-loader-side-effect-in-index-file.ts
3228
- var RULE8 = "no-loader-side-effect-in-index-file";
3129
+ var RULE7 = "no-loader-side-effect-in-index-file";
3229
3130
  var DEFAULT_DIRECTORIES = ["(?:^|/)subscribers/", "(?:^|/)jobs/"];
3230
3131
  var DEFAULT_RESOURCE_FACTORIES = ["createWorkflow", "createStep", "createHook"];
3231
3132
  var INDEX = /(?:^|\/)index\.[cm]?[jt]sx?$/;
3232
3133
  var noLoaderSideEffectInIndexFile = {
3233
3134
  create(context) {
3234
3135
  const directories = requireOption(
3235
- RULE8,
3136
+ RULE7,
3236
3137
  context.options?.[0]?.directories ?? DEFAULT_DIRECTORIES,
3237
3138
  "directories"
3238
3139
  ).map((source) => new RegExp(source));
3239
3140
  const factories = new Set(
3240
3141
  requireOption(
3241
- RULE8,
3142
+ RULE7,
3242
3143
  context.options?.[0]?.resourceFactories ?? DEFAULT_RESOURCE_FACTORIES,
3243
3144
  "resourceFactories"
3244
3145
  )
@@ -3401,7 +3302,7 @@ the same component. Thin preset wrappers over one imported component do not coun
3401
3302
  var no_logic_in_component_files_default = noLogicInComponentFiles;
3402
3303
 
3403
3304
  // src/rules/no-mock-db-in-integration.ts
3404
- var RULE9 = "no-mock-db-in-integration";
3305
+ var RULE8 = "no-mock-db-in-integration";
3405
3306
  var BUN_MOCK = "mock.module";
3406
3307
  var MOCK_CALLS = /* @__PURE__ */ new Set(["jest.mock", BUN_MOCK, "vi.mock"]);
3407
3308
  var calleeOf = (node) => {
@@ -3419,8 +3320,8 @@ var inDirectory = (filename, directory) => {
3419
3320
  var noMockDbInIntegration = {
3420
3321
  create(context) {
3421
3322
  const options = context.options?.[0] ?? {};
3422
- const dirs = requireOption(RULE9, options.integrationDirs, "integrationDirs");
3423
- const modules = requireOption(RULE9, options.dbModules, "dbModules");
3323
+ const dirs = requireOption(RULE8, options.integrationDirs, "integrationDirs");
3324
+ const modules = requireOption(RULE8, options.dbModules, "dbModules");
3424
3325
  const mockNames = new Set(options.mockNames ?? []);
3425
3326
  const filename = normalise(context.filename);
3426
3327
  const spans = /* @__PURE__ */ new Set();
@@ -3521,14 +3422,14 @@ below the seam, and a caller that names it has crossed it.`,
3521
3422
  var no_orm_outside_db_default = noOrmOutsideDb;
3522
3423
 
3523
3424
  // src/rules/no-raw-html-atoms.ts
3524
- var RULE10 = "no-raw-html-atoms";
3425
+ var RULE9 = "no-raw-html-atoms";
3525
3426
  var WHERE_RAW_HTML_IS_THE_POINT = "atoms";
3526
3427
  var noRawHtmlAtoms = {
3527
3428
  create(context) {
3528
- const restrictions = requireOption(RULE10, context.options[0], "element\u2192atom map");
3429
+ const restrictions = requireOption(RULE9, context.options[0], "element\u2192atom map");
3529
3430
  const scope = context.options[1]?.scope;
3530
3431
  const filename = context.filename;
3531
- const inScope = scope === void 0 ? !inTier(filename, WHERE_RAW_HTML_IS_THE_POINT) : requireTiers(RULE10, scope).some((tier) => inTier(filename, tier));
3432
+ const inScope = scope === void 0 ? !inTier(filename, WHERE_RAW_HTML_IS_THE_POINT) : requireTiers(RULE9, scope).some((tier) => inTier(filename, tier));
3532
3433
  if (!inScope) return {};
3533
3434
  const elementMap = /* @__PURE__ */ new Map();
3534
3435
  for (const r of restrictions) {
@@ -4178,7 +4079,7 @@ var no_type_definitions_in_components_default = noTypeDefinitionsInComponents;
4178
4079
 
4179
4080
  // src/rules/no-unregistered-plugin-dir.ts
4180
4081
  import { readFileSync as readFileSync4 } from "fs";
4181
- var RULE11 = "no-unregistered-plugin-dir";
4082
+ var RULE10 = "no-unregistered-plugin-dir";
4182
4083
  var ACCEPTS3 = "{ roots: (string | { path: string, files: boolean })[], registry: string | string[], manifests: string[] }";
4183
4084
  var trimSlashes = (path2) => path2.replace(/^\/+/, "").replace(/\/+$/, "");
4184
4085
  var withoutComments = (source) => source.replaceAll(/\/\*[\s\S]*?\*\//g, " ").replaceAll(/\/\/[^\n]*/g, " ");
@@ -4206,12 +4107,12 @@ var oneFile = (segments) => {
4206
4107
  var noUnregisteredPluginDir = {
4207
4108
  create(context) {
4208
4109
  const options = context.options?.[0] ?? {};
4209
- const roots = requireOption(RULE11, options.roots, "roots", ACCEPTS3).map(
4110
+ const roots = requireOption(RULE10, options.roots, "roots", ACCEPTS3).map(
4210
4111
  (one) => typeof one === "string" ? { files: false, path: trimSlashes(one) } : { files: one.files === true, path: trimSlashes(one.path) }
4211
4112
  );
4212
- const declared = requireOption(RULE11, options.registry, "registry", ACCEPTS3);
4113
+ const declared = requireOption(RULE10, options.registry, "registry", ACCEPTS3);
4213
4114
  const registries = (typeof declared === "string" ? [declared] : declared).map(trimSlashes);
4214
- const manifests = new Set(requireOption(RULE11, options.manifests, "manifests", ACCEPTS3));
4115
+ const manifests = new Set(requireOption(RULE10, options.manifests, "manifests", ACCEPTS3));
4215
4116
  const filename = normalise(context.filename);
4216
4117
  return {
4217
4118
  Program(node) {
@@ -4488,16 +4389,16 @@ source. The CELL is the only level where several sources meet; it derives and pa
4488
4389
  var organelle_single_source_default = organelleSingleSource;
4489
4390
 
4490
4391
  // src/rules/plugin-route-namespaced.ts
4491
- var RULE12 = "plugin-route-namespaced";
4392
+ var RULE11 = "plugin-route-namespaced";
4492
4393
  var DEFAULT_SCOPES = ["store", "admin"];
4493
4394
  var ROUTE_FILE = /(?:^|\/)api\/(.+\/)?route\.[cm]?[jt]sx?$/;
4494
4395
  var NAMESPACED_FROM = 2;
4495
4396
  var readPackages = (configured) => {
4496
- const rules2 = requireOption(RULE12, configured, "packages");
4397
+ const rules2 = requireOption(RULE11, configured, "packages");
4497
4398
  for (const rule of rules2) {
4498
4399
  if (typeof rule.under !== "string" || rule.under === "") {
4499
4400
  throw new Error(
4500
- `biological-architecture/${RULE12}: every \`packages\` entry needs \`under\` \u2014 the directory whose children are packages. Without it the rule cannot tell which files belong to a plugin.`
4401
+ `biological-architecture/${RULE11}: every \`packages\` entry needs \`under\` \u2014 the directory whose children are packages. Without it the rule cannot tell which files belong to a plugin.`
4501
4402
  );
4502
4403
  }
4503
4404
  }
@@ -4524,7 +4425,7 @@ var pluginRouteNamespaced = {
4524
4425
  create(context) {
4525
4426
  const rules2 = readPackages(context.options?.[0]?.packages);
4526
4427
  const scopes = new Set(
4527
- requireOption(RULE12, context.options?.[0]?.scopes ?? DEFAULT_SCOPES, "scopes")
4428
+ requireOption(RULE11, context.options?.[0]?.scopes ?? DEFAULT_SCOPES, "scopes")
4528
4429
  );
4529
4430
  const path2 = normalise(context.filename);
4530
4431
  const route = routePathOf(path2);
@@ -4710,8 +4611,55 @@ decides what an event MEANS and throws when it cannot.`,
4710
4611
  };
4711
4612
  var queue_loop_is_the_library_default = queueLoopIsTheLibrary;
4712
4613
 
4614
+ // src/rules/retired.ts
4615
+ var SINCE = "2.7.0";
4616
+ var REASON = "it enforced one consumer's document kinds, which is vocabulary the kit does not hold";
4617
+ var announced = /* @__PURE__ */ new Set();
4618
+ var retired = (name, schema) => ({
4619
+ create: () => {
4620
+ if (!announced.has(name)) {
4621
+ announced.add(name);
4622
+ process.stderr.write(
4623
+ `biological-architecture: rule "${name}" is retired since ${SINCE} \u2014 ${REASON}. It reports nothing and goes at the next major. Remove it from your config.
4624
+ `
4625
+ );
4626
+ }
4627
+ return {};
4628
+ },
4629
+ fixShape: `\`${name}\` is retired since ${SINCE} and reports nothing. Remove it from the config.`,
4630
+ meta: {
4631
+ docs: {
4632
+ description: `Retired since ${SINCE}: ${REASON}. Reports nothing; goes at the next major.`
4633
+ },
4634
+ messages: {},
4635
+ retired: { reason: REASON, since: SINCE },
4636
+ schema,
4637
+ type: "problem"
4638
+ }
4639
+ });
4640
+ var RETIRED_RULES = {
4641
+ "document-sagas-are-generic": retired("document-sagas-are-generic", [
4642
+ {
4643
+ additionalProperties: false,
4644
+ properties: {
4645
+ factories: { items: { type: "string" }, type: "array" },
4646
+ perKindPrefixes: { items: { type: "string" }, type: "array" },
4647
+ within: { type: "string" }
4648
+ },
4649
+ type: "object"
4650
+ }
4651
+ ]),
4652
+ "documents-share-one-table": retired("documents-share-one-table", [
4653
+ {
4654
+ additionalProperties: false,
4655
+ properties: { perKindTables: { items: { type: "string" }, type: "array" } },
4656
+ type: "object"
4657
+ }
4658
+ ])
4659
+ };
4660
+
4713
4661
  // src/rules/route-no-inline-mutation.ts
4714
- var RULE13 = "route-no-inline-mutation";
4662
+ var RULE12 = "route-no-inline-mutation";
4715
4663
  var DEFAULT_PATHS = [String.raw`(?:^|/)api/.*/route\.[cm]?[jt]sx?$`];
4716
4664
  var DEFAULT_INLINE_MUTATION_OK = "arch:inline-mutation-ok";
4717
4665
  var commentsOf2 = (sourceCode) => sourceCode?.getAllComments?.() ?? sourceCode?.ast?.comments ?? [];
@@ -4755,15 +4703,15 @@ var annotated = (found, comments, marker) => {
4755
4703
  };
4756
4704
  var routeNoInlineMutation = {
4757
4705
  create(context) {
4758
- const paths = requireOption(RULE13, context.options?.[0]?.paths ?? DEFAULT_PATHS, "paths").map(
4706
+ const paths = requireOption(RULE12, context.options?.[0]?.paths ?? DEFAULT_PATHS, "paths").map(
4759
4707
  (source) => new RegExp(source)
4760
4708
  );
4761
4709
  const marker = requireOption(
4762
- RULE13,
4710
+ RULE12,
4763
4711
  context.options?.[0]?.inlineMutationOk ?? DEFAULT_INLINE_MUTATION_OK,
4764
4712
  "inlineMutationOk"
4765
4713
  );
4766
- const isWrite = writesWith(mutationPrefixesOf(RULE13, context.options?.[0]?.mutationPrefixes));
4714
+ const isWrite = writesWith(mutationPrefixesOf(RULE12, context.options?.[0]?.mutationPrefixes));
4767
4715
  const filename = normalise(context.filename);
4768
4716
  if (!paths.some((pattern) => pattern.test(filename))) return {};
4769
4717
  return {
@@ -4833,7 +4781,7 @@ var route_no_inline_mutation_default = routeNoInlineMutation;
4833
4781
 
4834
4782
  // src/rules/router-schema-parity.ts
4835
4783
  import { existsSync as existsSync2 } from "fs";
4836
- var RULE14 = "router-schema-parity";
4784
+ var RULE13 = "router-schema-parity";
4837
4785
  var EXTENSIONS2 = [".ts", ".tsx"];
4838
4786
  var trimSlashes2 = (path2) => path2.replace(/^\/+/, "").replace(/\/+$/, "");
4839
4787
  var baseOf = (file) => file.replace(/\.[cm]?[jt]sx?$/, "");
@@ -4851,8 +4799,8 @@ var directlyUnder = (filename, directory) => {
4851
4799
  var routerSchemaParity = {
4852
4800
  create(context) {
4853
4801
  const options = context.options?.[0] ?? {};
4854
- const routers = trimSlashes2(requireOption(RULE14, options.routers, "routers"));
4855
- const schemas = trimSlashes2(requireOption(RULE14, options.schemas, "schemas"));
4802
+ const routers = trimSlashes2(requireOption(RULE13, options.routers, "routers"));
4803
+ const schemas = trimSlashes2(requireOption(RULE13, options.schemas, "schemas"));
4856
4804
  const suffix = options.suffix ?? "";
4857
4805
  const ignored = new Set(options.ignore ?? []);
4858
4806
  const mounts = options.mounts ?? {};
@@ -4924,7 +4872,7 @@ repo-root-relative, because in one consumer they live in different packages.`,
4924
4872
  var router_schema_parity_default = routerSchemaParity;
4925
4873
 
4926
4874
  // src/rules/service-name-not-reserved.ts
4927
- var RULE15 = "service-name-not-reserved";
4875
+ var RULE14 = "service-name-not-reserved";
4928
4876
  var DEFAULT_MODULE_FACTORIES = ["Module"];
4929
4877
  var MODULES_2_19 = [
4930
4878
  "analytics",
@@ -4964,13 +4912,13 @@ var serviceNameNotReserved = {
4964
4912
  create(context) {
4965
4913
  const factories = new Set(
4966
4914
  requireOption(
4967
- RULE15,
4915
+ RULE14,
4968
4916
  context.options?.[0]?.moduleFactories ?? DEFAULT_MODULE_FACTORIES,
4969
4917
  "moduleFactories"
4970
4918
  )
4971
4919
  );
4972
4920
  const reserved = new Set(
4973
- requireOption(RULE15, context.options?.[0]?.reserved ?? MODULES_2_19, "reserved")
4921
+ requireOption(RULE14, context.options?.[0]?.reserved ?? MODULES_2_19, "reserved")
4974
4922
  );
4975
4923
  const constants = /* @__PURE__ */ new Map();
4976
4924
  const pending = [];
@@ -5281,12 +5229,12 @@ can be misspelled and a second default nobody agreed to.`,
5281
5229
  var ssot_no_process_env_default = ssotNoProcessEnv;
5282
5230
 
5283
5231
  // src/rules/step-declares-compensation-or-none.ts
5284
- var RULE16 = "step-declares-compensation-or-none";
5232
+ var RULE15 = "step-declares-compensation-or-none";
5285
5233
  var NO_COMPENSATION = "noCompensation";
5286
5234
  var WITH_COMPENSATION = 3;
5287
5235
  var stepDeclaresCompensationOrNone = {
5288
5236
  create(context) {
5289
- const factories = stepFactoriesOf(RULE16, context.options?.[0]?.stepFactories);
5237
+ const factories = stepFactoriesOf(RULE15, context.options?.[0]?.stepFactories);
5290
5238
  return {
5291
5239
  CallExpression(node) {
5292
5240
  if (!isStepCall(node, factories)) return;
@@ -5330,7 +5278,7 @@ variable \u2014 passes, since the flag may be in the base. Configure \`stepFacto
5330
5278
  var step_declares_compensation_or_none_default = stepDeclaresCompensationOrNone;
5331
5279
 
5332
5280
  // src/rules/step-opens-its-own-cell.ts
5333
- var RULE17 = "step-opens-its-own-cell";
5281
+ var RULE16 = "step-opens-its-own-cell";
5334
5282
  var STEP = "step";
5335
5283
  var ACCEPTS4 = "{ within: string (required), hosts: string[], scopeKey: string, engine: string, wrapper: string }";
5336
5284
  var DEFAULT_HOSTS = ["createRuntime", "entrypointFor"];
@@ -5365,9 +5313,9 @@ var declaresScope = (call, scopeKey) => {
5365
5313
  var stepOpensItsOwnCell = {
5366
5314
  create(context) {
5367
5315
  const bag = context.options?.[0] ?? {};
5368
- const within = requireOption(RULE17, bag.within, "within", ACCEPTS4);
5369
- const hosts = new Set(requireOption(RULE17, bag.hosts ?? DEFAULT_HOSTS, "hosts", ACCEPTS4));
5370
- const scopeKey = requireOption(RULE17, bag.scopeKey ?? DEFAULT_SCOPE_KEY, "scopeKey", ACCEPTS4);
5316
+ const within = requireOption(RULE16, bag.within, "within", ACCEPTS4);
5317
+ const hosts = new Set(requireOption(RULE16, bag.hosts ?? DEFAULT_HOSTS, "hosts", ACCEPTS4));
5318
+ const scopeKey = requireOption(RULE16, bag.scopeKey ?? DEFAULT_SCOPE_KEY, "scopeKey", ACCEPTS4);
5371
5319
  const { engine, wrapper } = bag;
5372
5320
  const filename = normalise(context.filename);
5373
5321
  if (!new RegExp(within).test(filename)) return {};
@@ -5470,7 +5418,7 @@ without \`within\` the rule refuses the run.`,
5470
5418
  const path2 = pathMatching(within, "geonosis-probe-engine-step.ts");
5471
5419
  if (wrapper !== void 0 && new RegExp(wrapper).test(path2)) {
5472
5420
  throw new Error(
5473
- `biological-architecture/${RULE17}: the probe path "${path2}" is also the \`wrapper\`, which is the one file allowed to reach \`${engine}\` \u2014 no single file inside \`within\` can both land where the rule looks and be somewhere it speaks.`
5421
+ `biological-architecture/${RULE16}: the probe path "${path2}" is also the \`wrapper\`, which is the one file allowed to reach \`${engine}\` \u2014 no single file inside \`within\` can both land where the rule looks and be somewhere it speaks.`
5474
5422
  );
5475
5423
  }
5476
5424
  return {
@@ -5675,10 +5623,10 @@ data, it belongs on the admin surface.`,
5675
5623
  var store_route_scopes_tenant_data_default = storeRouteScopesTenantData;
5676
5624
 
5677
5625
  // src/rules/subpath-over-barrel.ts
5678
- var RULE18 = "subpath-over-barrel";
5626
+ var RULE17 = "subpath-over-barrel";
5679
5627
  var ACCEPTS5 = "{ packages: { name: string, subpaths: { [subpath: string]: string[] } }[] (required) }";
5680
5628
  var homesOf = (declared) => {
5681
- const subpaths = requireOption(RULE18, Object.keys(declared.subpaths ?? {}), "subpaths", ACCEPTS5);
5629
+ const subpaths = requireOption(RULE17, Object.keys(declared.subpaths ?? {}), "subpaths", ACCEPTS5);
5682
5630
  return new Map(
5683
5631
  subpaths.flatMap(
5684
5632
  (subpath) => (declared.subpaths?.[subpath] ?? []).map((name) => [name, subpath])
@@ -5701,9 +5649,9 @@ var importedNamesOf = (node) => {
5701
5649
  };
5702
5650
  var subpathOverBarrel = {
5703
5651
  create(context) {
5704
- const declared = requireOption(RULE18, context.options?.[0]?.packages, "packages", ACCEPTS5);
5652
+ const declared = requireOption(RULE17, context.options?.[0]?.packages, "packages", ACCEPTS5);
5705
5653
  const homes = new Map(
5706
- declared.map((one) => [requireOption(RULE18, one.name, "name", ACCEPTS5), homesOf(one)])
5654
+ declared.map((one) => [requireOption(RULE17, one.name, "name", ACCEPTS5), homesOf(one)])
5707
5655
  );
5708
5656
  return {
5709
5657
  ImportDeclaration(node) {
@@ -5773,7 +5721,7 @@ is a type-only import, which erases before any of that happens. Declare the map
5773
5721
  const [exported] = first?.subpaths?.[subpath ?? ""] ?? [];
5774
5722
  if (exported === void 0) {
5775
5723
  throw new Error(
5776
- `biological-architecture/${RULE18}: the configured packages declare no subpath with a name in it, so there is nothing a probe could import from the root.`
5724
+ `biological-architecture/${RULE17}: the configured packages declare no subpath with a name in it, so there is nothing a probe could import from the root.`
5777
5725
  );
5778
5726
  }
5779
5727
  return {
@@ -5788,7 +5736,7 @@ export const geonosisProbe = ${exported}
5788
5736
  var subpath_over_barrel_default = subpathOverBarrel;
5789
5737
 
5790
5738
  // src/rules/subscriber-declares-id.ts
5791
- var RULE19 = "subscriber-declares-id";
5739
+ var RULE18 = "subscriber-declares-id";
5792
5740
  var DEFAULT_PATHS2 = ["(?:^|/)subscribers/"];
5793
5741
  var CONFIG = "config";
5794
5742
  var CONTEXT = "context";
@@ -5802,7 +5750,7 @@ var declaredName2 = (node) => {
5802
5750
  };
5803
5751
  var subscriberDeclaresId = {
5804
5752
  create(context) {
5805
- const paths = requireOption(RULE19, context.options?.[0]?.paths ?? DEFAULT_PATHS2, "paths").map(
5753
+ const paths = requireOption(RULE18, context.options?.[0]?.paths ?? DEFAULT_PATHS2, "paths").map(
5806
5754
  (source) => new RegExp(source)
5807
5755
  );
5808
5756
  const filename = normalise(context.filename);
@@ -6065,7 +6013,7 @@ are tests is \`testGlobs\` and which calls poll is \`pollers\`, both defaulted.`
6065
6013
  var test_no_clock_polling_for_promise_flips_default = testNoClockPollingForPromiseFlips;
6066
6014
 
6067
6015
  // src/rules/time-through-the-door.ts
6068
- var RULE20 = "time-through-the-door";
6016
+ var RULE19 = "time-through-the-door";
6069
6017
  var ACCEPTS6 = "{ door: string (a regex over the filename), exempt: string[] (regex sources, default the test paths: __tests__/, .test., .spec.), libraries: string[] (default ['dayjs']) }";
6070
6018
  var DEFAULT_LIBRARIES = ["dayjs"];
6071
6019
  var DEFAULT_EXEMPT = ["__tests__/", String.raw`\.test\.`, String.raw`\.spec\.`];
@@ -6093,11 +6041,11 @@ var memberName = (callee) => ({
6093
6041
  var readable2 = (pattern) => pattern.replaceAll("\\", "").replace(/\$$/, "");
6094
6042
  var timeThroughTheDoor = {
6095
6043
  create(context) {
6096
- const door = requireOption(RULE20, context.options?.[0]?.door, "door", ACCEPTS6);
6044
+ const door = requireOption(RULE19, context.options?.[0]?.door, "door", ACCEPTS6);
6097
6045
  const exempt = context.options?.[0]?.exempt ?? DEFAULT_EXEMPT;
6098
6046
  const exemptModules = context.options?.[0]?.exemptModules ?? [];
6099
6047
  const libraries = requireOption(
6100
- RULE20,
6048
+ RULE19,
6101
6049
  context.options?.[0]?.libraries ?? DEFAULT_LIBRARIES,
6102
6050
  "libraries",
6103
6051
  ACCEPTS6
@@ -6201,7 +6149,7 @@ it internally cannot be tested at a chosen instant.`,
6201
6149
  );
6202
6150
  if (outside === void 0) {
6203
6151
  throw new Error(
6204
- `biological-architecture/${RULE20}: every path a probe could be written at is inside the \`door\` (${door}) or one of its \`exempt\` patterns, so there is nowhere left for this rule to speak.`
6152
+ `biological-architecture/${RULE19}: every path a probe could be written at is inside the \`door\` (${door}) or one of its \`exempt\` patterns, so there is nowhere left for this rule to speak.`
6205
6153
  );
6206
6154
  }
6207
6155
  return {
@@ -6409,6 +6357,12 @@ function isAllowedType(t) {
6409
6357
  return true;
6410
6358
  }
6411
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";
6412
6366
  var tissueNoDataProps = {
6413
6367
  create(context) {
6414
6368
  const normalized = normalise(context.filename);
@@ -6424,6 +6378,7 @@ var tissueNoDataProps = {
6424
6378
  const typeLiteral = firstParam.typeAnnotation?.typeAnnotation;
6425
6379
  if (!typeLiteral || typeLiteral.type !== "TSTypeLiteral") return;
6426
6380
  const members = typeLiteral.members ?? [];
6381
+ const defaults = literalDefaultsOf(firstParam);
6427
6382
  for (const member of members) {
6428
6383
  if (member.key?.type !== "Identifier") continue;
6429
6384
  const propName = member.key.name ?? "";
@@ -6432,6 +6387,7 @@ var tissueNoDataProps = {
6432
6387
  const t = member.typeAnnotation?.typeAnnotation;
6433
6388
  if (!t) continue;
6434
6389
  if (isAllowedType(t)) continue;
6390
+ if (isLayoutConfiguration(t, defaults, propName)) continue;
6435
6391
  context.report({
6436
6392
  data: { prop: propName, type: describeTypeAnnotation(t) },
6437
6393
  messageId: "dataProp",
@@ -6471,13 +6427,14 @@ var tissueNoDataProps = {
6471
6427
  fixShape: `A tissue takes \`children\`, ReactNode slot props, layout variants, \`params\` and \`searchParams\` \u2014 and
6472
6428
  nothing else. A prop typed \`string\`, \`number\`, an array or a domain type (\`id\`, \`initialData\`,
6473
6429
  \`documents\`) means the file owns data, which makes it a CELL: move it to \`cells/\`. A tissue gets its
6474
- 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.`,
6475
6432
  meta: {
6476
6433
  docs: {
6477
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)."
6478
6435
  },
6479
6436
  messages: {
6480
- 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."
6481
6438
  },
6482
6439
  schema: [],
6483
6440
  type: "problem"
@@ -6588,13 +6545,13 @@ var tissue_no_stores_default = tissueNoStores;
6588
6545
  // src/rules/todo-names-its-plan.ts
6589
6546
  var ID_DEFAULT = String.raw`\d{3,}`;
6590
6547
  var commentsOf3 = (sourceCode) => sourceCode?.getAllComments?.() ?? sourceCode?.ast?.comments ?? [];
6591
- var escapeForRegex3 = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
6548
+ var escapeForRegex2 = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
6592
6549
  var todoNamesItsPlan = {
6593
6550
  create(context) {
6594
6551
  const { id = ID_DEFAULT, markers = [] } = context.options?.[0] ?? {};
6595
6552
  requireOption("todo-names-its-plan", markers, "markers", "{ markers: string[], id?: string }");
6596
6553
  const marker = new RegExp(
6597
- `\\b(${markers.map(escapeForRegex3).join("|")})\\b(?:\\(([^)]*)\\))?`,
6554
+ `\\b(${markers.map(escapeForRegex2).join("|")})\\b(?:\\(([^)]*)\\))?`,
6598
6555
  "g"
6599
6556
  );
6600
6557
  const names2 = new RegExp(`^${id}$`);
@@ -7011,7 +6968,7 @@ var isNamedOnly = (value) => /^[a-z]+$/i.test(value.trim());
7011
6968
  var tokens_only_colors_default = tokensOnlyColors;
7012
6969
 
7013
6970
  // src/rules/unique-query-step-name.ts
7014
- var RULE21 = "unique-query-step-name";
6971
+ var RULE20 = "unique-query-step-name";
7015
6972
  var ACCEPTS7 = "{ queryStepCalls: string[] (required), workflowFactories: string[] }";
7016
6973
  var DEFAULT_WORKFLOW_FACTORIES2 = ["createWorkflow"];
7017
6974
  var CONFIG2 = "config";
@@ -7055,13 +7012,13 @@ var uniqueQueryStepName = {
7055
7012
  create(context) {
7056
7013
  const factories = new Set(
7057
7014
  requireOption(
7058
- RULE21,
7015
+ RULE20,
7059
7016
  context.options?.[0]?.workflowFactories ?? DEFAULT_WORKFLOW_FACTORIES2,
7060
7017
  "workflowFactories"
7061
7018
  )
7062
7019
  );
7063
7020
  const calls = new Set(
7064
- requireOption(RULE21, context.options?.[0]?.queryStepCalls, "queryStepCalls", ACCEPTS7)
7021
+ requireOption(RULE20, context.options?.[0]?.queryStepCalls, "queryStepCalls", ACCEPTS7)
7065
7022
  );
7066
7023
  return {
7067
7024
  CallExpression(node) {
@@ -7142,7 +7099,7 @@ because the collision it reports is one framework's step-handler map and not eve
7142
7099
  var unique_query_step_name_default = uniqueQueryStepName;
7143
7100
 
7144
7101
  // src/rules/worker-handles-are-scoped.ts
7145
- var RULE22 = "worker-handles-are-scoped";
7102
+ var RULE21 = "worker-handles-are-scoped";
7146
7103
  var ACCEPTS8 = "{ within: string (a regex over the filename), wrapper: string[] }";
7147
7104
  var handlerKeys = /* @__PURE__ */ new Set(["fetch", "queue", "scheduled", "email"]);
7148
7105
  var entrypointMakers = /* @__PURE__ */ new Set([
@@ -7152,8 +7109,8 @@ var entrypointMakers = /* @__PURE__ */ new Set([
7152
7109
  ]);
7153
7110
  var cellHandlesAreScoped = {
7154
7111
  create(context) {
7155
- const within = requireOption(RULE22, context.options?.[0]?.within, "within", ACCEPTS8);
7156
- const wrappers = requireOption(RULE22, context.options?.[0]?.wrapper, "wrapper", ACCEPTS8);
7112
+ const within = requireOption(RULE21, context.options?.[0]?.within, "within", ACCEPTS8);
7113
+ const wrappers = requireOption(RULE21, context.options?.[0]?.wrapper, "wrapper", ACCEPTS8);
7157
7114
  if (!new RegExp(within).test(normalise(context.filename))) return {};
7158
7115
  const scopedBy = new Set(wrappers);
7159
7116
  let handler = null;
@@ -7356,8 +7313,6 @@ var rules = refusingOnce(
7356
7313
  "compound-no-stores": compound_no_stores_default,
7357
7314
  "constants-in-constants-file": constants_in_constants_file_default,
7358
7315
  "dialect-through-the-seam": dialect_through_the_seam_default,
7359
- "document-sagas-are-generic": document_sagas_are_generic_default,
7360
- "documents-share-one-table": documents_share_one_table_default,
7361
7316
  "durable-body-reads-through-steps": durable_body_reads_through_steps_default,
7362
7317
  "effect-hook-naming": effect_hook_naming_default,
7363
7318
  "emit-declares-attempts": emit_declares_attempts_default,
@@ -7430,18 +7385,22 @@ var rules = refusingOnce(
7430
7385
  })
7431
7386
  );
7432
7387
  var { version } = createRequire(import.meta.url)("../package.json");
7388
+ var loaded = { ...rules, ...RETIRED_RULES };
7433
7389
  var plugin = {
7434
7390
  configs: PRESETS,
7435
7391
  meta: {
7436
7392
  name: "biological-architecture",
7437
7393
  version
7438
7394
  },
7439
- rules
7395
+ // A retired id resolves here and nowhere else: a config that still names one parses, and the id
7396
+ // reports nothing. `rules` — what the kit's own tooling reads as its rule set — does not carry it.
7397
+ rules: loaded
7440
7398
  };
7441
7399
  var index_default = plugin;
7442
7400
  export {
7443
7401
  PRESETS,
7444
7402
  RENAMED_TO,
7403
+ RETIRED_RULES,
7445
7404
  index_default as default,
7446
7405
  rules,
7447
7406
  rulesOfPreset