@geonosis/oxlint-plugin-biological-architecture 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/RULES.md +37 -18
  2. package/corpus/.oxlintrc.json +48 -10
  3. package/corpus/affordances/bad-dead-control.tsx +6 -0
  4. package/corpus/affordances/good-live-control.tsx +12 -0
  5. package/corpus/api/store/history/find-orders.ts +8 -0
  6. package/corpus/api/store/history/route.ts +7 -0
  7. package/corpus/api/store/track/find-tracked-order.ts +12 -0
  8. package/corpus/api/store/track/route.ts +10 -0
  9. package/corpus/dates/bad-string-order.ts +7 -0
  10. package/corpus/dates/good-time-order.ts +7 -0
  11. package/corpus/engine/good-durable-read-through-step-proxy.ts +13 -0
  12. package/corpus/failure/good-refresh.ts +17 -0
  13. package/corpus/failure/logged.ts +6 -0
  14. package/corpus/failure/refresh.ts +4 -0
  15. package/corpus/failure/reload.ts +6 -0
  16. package/corpus/manifest.json +6 -0
  17. package/corpus/medusa/queries/bad-unordered-page.ts +20 -0
  18. package/corpus/medusa/queries/good-ordered-page.ts +23 -0
  19. package/corpus/messaging/providers/email-gmail.ts +2 -0
  20. package/corpus/messaging/providers/sms-twilio.ts +2 -0
  21. package/corpus/messaging/providers/whatsapp-360dialog.ts +2 -0
  22. package/corpus/messaging/registry.ts +5 -0
  23. package/corpus/packages/pricing/package.json +5 -0
  24. package/corpus/packages/pricing/src/index.ts +2 -0
  25. package/corpus/packages/utils/package.json +5 -0
  26. package/corpus/packages/utils/src/index.ts +4 -0
  27. package/corpus/suites/__tests__/awaits.ts +17 -0
  28. package/corpus/suites/__tests__/settles.ts +14 -0
  29. package/dist/{chunk-LTDP2733.js → chunk-KV7JRNAP.js} +7 -2
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +812 -61
  32. package/dist/presets.d.ts +3 -0
  33. package/dist/presets.js +1 -1
  34. package/package.json +2 -2
  35. /package/corpus/api/store/orders/{scoped.route.ts → scoped/route.ts} +0 -0
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  PRESUMED_ENGINES,
4
4
  RENAMED_TO,
5
5
  rulesOfPreset
6
- } from "./chunk-LTDP2733.js";
6
+ } from "./chunk-KV7JRNAP.js";
7
7
 
8
8
  // src/index.ts
9
9
  import { createRequire } from "module";
@@ -30,6 +30,11 @@ var isDoor = (filename, door) => {
30
30
  return false;
31
31
  }
32
32
  };
33
+ var isModule = (filename, module) => {
34
+ const path2 = normalise(filename);
35
+ const named = module.replace(/^\.?\//, "");
36
+ return path2 === named || path2.endsWith(`/${named}`);
37
+ };
33
38
 
34
39
  // src/rules/lib/imports.ts
35
40
  var REQUIRE = "require";
@@ -1048,7 +1053,7 @@ var stepNameOf = (node, config) => {
1048
1053
 
1049
1054
  // src/rules/durable-body-reads-through-steps.ts
1050
1055
  var RULE3 = "durable-body-reads-through-steps";
1051
- var ACCEPTS2 = "{ ports: string[] (required), durableKey: string, durableValue: boolean | string, workflowFactories: string[], stepFactories: string[] }";
1056
+ var ACCEPTS2 = "{ ports: string[] (required), durableKey: string, durableValue: boolean | string, workflowFactories: string[], stepFactories: string[], stepProxies: string[] }";
1052
1057
  var DEFAULT_WORKFLOW_FACTORIES = ["createWorkflow"];
1053
1058
  var DEFAULT_DURABLE_KEY = "execution";
1054
1059
  var DEFAULT_DURABLE_VALUE = "durable";
@@ -1080,6 +1085,7 @@ var durableBodyReadsThroughSteps = {
1080
1085
  create(context) {
1081
1086
  const bag = context.options?.[0] ?? {};
1082
1087
  const ports = new Set(requireOption(RULE3, bag.ports, "ports", ACCEPTS2));
1088
+ const proxies = new Set(bag.stepProxies ?? []);
1083
1089
  const steps = stepFactoriesOf(RULE3, bag.stepFactories);
1084
1090
  const workflows = new Set(
1085
1091
  requireOption(RULE3, bag.workflowFactories ?? DEFAULT_WORKFLOW_FACTORIES, "workflowFactories")
@@ -1097,7 +1103,7 @@ var durableBodyReadsThroughSteps = {
1097
1103
  walkNode(node, (inner) => {
1098
1104
  if (inner.type !== "CallExpression" || memoised.has(inner)) return;
1099
1105
  const port = portReachedBy(inner, ports);
1100
- if (port === null) return;
1106
+ if (port === null || portReachedBy(inner, proxies) !== null) return;
1101
1107
  context.report({ data: { port, workflow }, messageId: "freeRead", node: inner });
1102
1108
  });
1103
1109
  }
@@ -1108,7 +1114,9 @@ input or from a step result. A port read written straight into the body \u2014 \
1108
1114
  inline-only: the platform memoises steps by name and does not journal reads, so on a replay the read
1109
1115
  sees what the first invocation wrote, the branch flips, and the run unwinds work it had completed.
1110
1116
  Move the decision into a step (\`const doc = await ensureDraft(id)\`) and it is memoised with the run.
1111
- Name this repo's ports in \`ports\`; enabled without them the rule refuses the run. What it can SEE is a
1117
+ Name this repo's ports in \`ports\`; enabled without them the rule refuses the run. A receiver the
1118
+ engine already made steps of \u2014 \`stepsOf\`' proxy over bound queries \u2014 goes in \`stepProxies\`: a call
1119
+ through it is journalled under its own name and is what this rule asks for. What it can SEE is a
1112
1120
  call on a name \`ports\` lists: a read moved one function out \u2014 \`readStatus(id)\`, with the port inside
1113
1121
  it \u2014 is invisible here, and no lint rule that reads one file can follow it. Name the helper in
1114
1122
  \`ports\` too when a body reaches the outside world through one; a helper that is really a step is
@@ -1130,6 +1138,7 @@ better made one. \`durableKey\` /
1130
1138
  durableValue: { type: ["boolean", "string"] },
1131
1139
  ports: { items: { type: "string" }, type: "array" },
1132
1140
  stepFactories: { items: { type: "string" }, type: "array" },
1141
+ stepProxies: { items: { type: "string" }, type: "array" },
1133
1142
  workflowFactories: { items: { type: "string" }, type: "array" }
1134
1143
  },
1135
1144
  type: "object"
@@ -1429,9 +1438,10 @@ var variableOf = (value) => /^var\(\s*(--[A-Za-z0-9_-]+)/.exec(value.trim())?.[1
1429
1438
  var WEIGHT = /^\d+(?:\.\d+)?$/;
1430
1439
  var fontRolesOnly = {
1431
1440
  create(context) {
1432
- const { allow = [], prefix = "--font-", roles } = context.options?.[0] ?? {};
1441
+ const { allow = [], exemptModules = [], prefix = "--font-", roles } = context.options?.[0] ?? {};
1433
1442
  const declared = requireOption("font-roles-only", roles, "roles");
1434
1443
  const filename = normalise(context.filename);
1444
+ if (exemptModules.some((module) => isModule(filename, module))) return {};
1435
1445
  if (allow.some((path2) => filename.includes(path2))) return {};
1436
1446
  return {
1437
1447
  JSXOpeningElement(node) {
@@ -1462,11 +1472,19 @@ var fontRolesOnly = {
1462
1472
  sans-serif"\` or \`font-[Helvetica]\` with the role variable \u2014 \`var(--font-body)\`, \`var(--font-display)\`
1463
1473
  \u2014 or the utility that reads it. A \`var(--font-\u2026)\` naming a role the contract does not declare is the
1464
1474
  same offence: add the role to the theme, or use one that is there. Declare your role names in
1465
- \`roles\` and, if your variables are spelled differently, set \`prefix\`; put the theme file and any
1466
- \`@font-face\` sheet in \`allow\`. Bare \`font-*\` utility classes are deliberately not policed, because
1475
+ \`roles\` and, if your variables are spelled differently, set \`prefix\`; NAME the theme file and any
1476
+ \`@font-face\` sheet in \`exemptModules\`, one module per entry \u2014 \`allow\` still takes patterns and still
1477
+ grants its exemption to wherever the pattern stops, which is a boundary nobody decided and nobody
1478
+ counts. Bare \`font-*\` utility classes are deliberately not policed, because
1467
1479
  Tailwind puts weight and style under the same prefix. Enabled with no \`roles\` the rule refuses the
1468
1480
  run.`,
1469
1481
  meta: {
1482
+ /**
1483
+ * #139: which option grants by PATTERN and which one ENUMERATES. Declared on the rule so the
1484
+ * doctor needs no list of its own — a rule that gains a path grant and forgets to say so is a
1485
+ * grant nothing counts, which is the defect this pair exists to end.
1486
+ */
1487
+ grants: { modules: "exemptModules", paths: "allow" },
1470
1488
  docs: {
1471
1489
  description: "Font families are asked for by role. A family literal in a className or an inline style fires, and so does a var() naming a role outside the declared set. Configure `roles` (and `prefix` when the variables are spelled differently); enabled without `roles` it refuses the run."
1472
1490
  },
@@ -1479,6 +1497,7 @@ run.`,
1479
1497
  additionalProperties: false,
1480
1498
  properties: {
1481
1499
  allow: { items: { type: "string" }, type: "array" },
1500
+ exemptModules: { items: { type: "string" }, type: "array" },
1482
1501
  prefix: { type: "string" },
1483
1502
  roles: { items: { type: "string" }, type: "array" }
1484
1503
  },
@@ -1647,6 +1666,81 @@ export const geonosisProbeWall = geonosisProbe
1647
1666
  };
1648
1667
  var layer_walls_default = layerWalls;
1649
1668
 
1669
+ // src/rules/list-take-without-order.ts
1670
+ var CALLEES = [String.raw`^(?:\w+\.)?list[A-Z]`, String.raw`^(?:\w+\.)?graph$`];
1671
+ var ORDER_KEYS = ["order", "orderBy"];
1672
+ var TAKE = "take";
1673
+ var keyNameOf = (node) => {
1674
+ const key = node.key;
1675
+ if (key?.type === "Identifier" && typeof key.name === "string") return key.name;
1676
+ return key?.type === "Literal" && typeof key.value === "string" ? key.value : null;
1677
+ };
1678
+ var spellingOf = (callee) => {
1679
+ if (callee === void 0) return null;
1680
+ if (callee.type === "Identifier") return callee.name ?? null;
1681
+ const property = propertyNameOf(callee);
1682
+ if (property === null) return null;
1683
+ const object = callee.object;
1684
+ return object?.type === "Identifier" ? `${String(object.name)}.${property}` : property;
1685
+ };
1686
+ var listTakeWithoutOrder = {
1687
+ create(context) {
1688
+ const options = bagOf(context.options);
1689
+ const callees = (options.callees ?? CALLEES).map((pattern) => new RegExp(pattern));
1690
+ const orderKeys = options.orderKeys ?? ORDER_KEYS;
1691
+ return {
1692
+ CallExpression(node) {
1693
+ const callee = spellingOf(node.callee);
1694
+ if (callee === null || !callees.some((pattern) => pattern.test(callee))) return;
1695
+ let takes = false;
1696
+ let orders = false;
1697
+ let unreadable = false;
1698
+ for (const argument of node.arguments ?? []) {
1699
+ walkNode(argument, (inner) => {
1700
+ if (inner.type === "SpreadElement") unreadable = true;
1701
+ if (inner.type !== "Property") return;
1702
+ const key = keyNameOf(inner);
1703
+ if (key === TAKE) takes = true;
1704
+ if (key !== null && orderKeys.includes(key)) orders = true;
1705
+ });
1706
+ }
1707
+ if (!takes || orders || unreadable) return;
1708
+ context.report({
1709
+ data: { callee, order: orderKeys[0] ?? "" },
1710
+ messageId: "unordered",
1711
+ node
1712
+ });
1713
+ }
1714
+ };
1715
+ },
1716
+ fixShape: `A page taken in no order answers a question about the page, not about the table. A \`list*()\`
1717
+ or \`query.graph\` call carrying \`take\` names an \`order\` beside it \u2014 the column the page is a page OF \u2014
1718
+ so the first row is the first row every time, and the row a previous stage just wrote is where the
1719
+ reader expects it. Without one the database returns whatever is cheapest, which is stable in
1720
+ development and different under load. A spread the rule cannot read is left alone. Which calls are
1721
+ list calls is the \`callees\` option and which key declares the ordering is \`orderKeys\`.`,
1722
+ meta: {
1723
+ docs: {
1724
+ description: "A list or graph call that takes a page without declaring an order. The rows come back in whatever order is cheapest, so `[0]`, `.find()` and `.sort()` over the result answer a question about the page rather than about the table."
1725
+ },
1726
+ messages: {
1727
+ unordered: "`{{callee}}` takes a page with no `{{order}}`: the rows come back in whatever order the database finds cheapest, so the first row is a different row under load. Name the column the page is a page of."
1728
+ },
1729
+ schema: [
1730
+ {
1731
+ additionalProperties: false,
1732
+ properties: {
1733
+ callees: { items: { type: "string" }, type: "array" },
1734
+ orderKeys: { items: { type: "string" }, type: "array" }
1735
+ },
1736
+ type: "object"
1737
+ }
1738
+ ],
1739
+ type: "problem"
1740
+ }
1741
+ };
1742
+ var list_take_without_order_default = listTakeWithoutOrder;
1743
+
1650
1744
  // src/rules/max-comment-density.ts
1651
1745
  var exemptFiles2 = [
1652
1746
  /\/__tests__\//,
@@ -2428,6 +2522,79 @@ saga, not the transaction.`,
2428
2522
  };
2429
2523
  var no_d1_transaction_default = noD1Transaction;
2430
2524
 
2525
+ // src/rules/no-date-string-compare.ts
2526
+ var FIELD_PATTERN = "(?:_at|[a-z]At)$";
2527
+ var RELATIONAL = /* @__PURE__ */ new Set(["<", "<=", ">", ">="]);
2528
+ var COMPARE = "localeCompare";
2529
+ var STRINGIFY = "String";
2530
+ var fieldStringified = (node, holdsATime) => {
2531
+ if (node === void 0) return null;
2532
+ const named = (subject) => {
2533
+ if (subject === void 0) return null;
2534
+ const property = propertyNameOf(subject);
2535
+ if (property !== null) return holdsATime.test(property) ? property : null;
2536
+ return subject.type === "Identifier" && typeof subject.name === "string" && holdsATime.test(subject.name) ? subject.name : null;
2537
+ };
2538
+ if (node.type === "CallExpression" && calleeNameOf(node) === STRINGIFY) {
2539
+ const [argument] = node.arguments ?? [];
2540
+ return named(argument);
2541
+ }
2542
+ if (node.type !== "TemplateLiteral") return null;
2543
+ const expressions = node.expressions ?? [];
2544
+ const quasis = node.quasis ?? [];
2545
+ const empty = quasis.every((one) => (one.value?.raw ?? "") === "");
2546
+ return expressions.length === 1 && empty ? named(expressions[0]) : null;
2547
+ };
2548
+ var noDateStringCompare = {
2549
+ create(context) {
2550
+ const holdsATime = new RegExp(bagOf(context.options).fieldPattern ?? FIELD_PATTERN);
2551
+ const reportFirst = (node, sides, ordering) => {
2552
+ for (const side of sides) {
2553
+ const field = fieldStringified(side, holdsATime);
2554
+ if (field === null) continue;
2555
+ context.report({ data: { field, ordering }, messageId: "textOrder", node });
2556
+ return;
2557
+ }
2558
+ };
2559
+ return {
2560
+ BinaryExpression(node) {
2561
+ if (!RELATIONAL.has(node.operator)) return;
2562
+ reportFirst(node, [node.left, node.right], node.operator);
2563
+ },
2564
+ CallExpression(node) {
2565
+ if (propertyNameOf(node.callee) !== COMPARE) return;
2566
+ const receiver = node.callee.object;
2567
+ const [argument] = node.arguments ?? [];
2568
+ reportFirst(node, [receiver, argument], COMPARE);
2569
+ }
2570
+ };
2571
+ },
2572
+ fixShape: `Order a timestamp by its time, never by its text. \`String(row.created_at)\` on a Date is
2573
+ "Fri Aug 30 2026 \u2026", so "newest first" becomes "starts with F" and the sort is stable, plausible and
2574
+ wrong. Compare \`new Date(a.created_at).getTime()\` against \`Date.parse(b.created_at)\`, or sort on the
2575
+ ISO string the API returned \u2014 an ISO-8601 string does order lexicographically, which is why the
2576
+ defect survives review. The rule reports only a field turned INTO text: a raw timestamp under \`<\`
2577
+ and a template with copy in it are both left alone. Which names hold a timestamp is the
2578
+ \`fieldPattern\` option, defaulting to the \`_at\` / \`\u2026At\` spellings.`,
2579
+ meta: {
2580
+ docs: {
2581
+ description: "A `*_at` / `*At` field turned into a string with `String()` or a bare template, then ordered with `localeCompare` or a relational operator. A stringified Date sorts by its weekday name, not by its time."
2582
+ },
2583
+ messages: {
2584
+ textOrder: 'Ordering "{{field}}" by {{ordering}} on a string compares the TEXT of a timestamp: a Date stringifies to "Fri Aug 30 2026 \u2026", so this sorts by weekday name. Compare `.getTime()` or `Date.parse()`, or order on the ISO string the API returned.'
2585
+ },
2586
+ schema: [
2587
+ {
2588
+ additionalProperties: false,
2589
+ properties: { fieldPattern: { type: "string" } },
2590
+ type: "object"
2591
+ }
2592
+ ],
2593
+ type: "problem"
2594
+ }
2595
+ };
2596
+ var no_date_string_compare_default = noDateStringCompare;
2597
+
2431
2598
  // src/rules/no-duplicate-jsx-patterns.ts
2432
2599
  var COMPONENT_FILE_PATTERN = /(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
2433
2600
  var MIN_CLASS_LENGTH = 30;
@@ -2520,6 +2687,90 @@ their text are one component with a prop.`,
2520
2687
  };
2521
2688
  var no_duplicate_jsx_patterns_default = noDuplicateJsxPatterns;
2522
2689
 
2690
+ // src/rules/no-generic-utility-package.ts
2691
+ import { readFileSync as readFileSync3 } from "fs";
2692
+ var NAMES = ["common", "helpers", "misc", "shared", "utils"];
2693
+ var ENTRIES = [
2694
+ "index.ts",
2695
+ "index.tsx",
2696
+ "index.js",
2697
+ "index.jsx",
2698
+ "src/index.ts",
2699
+ "src/index.tsx",
2700
+ "src/index.js",
2701
+ "src/index.jsx"
2702
+ ];
2703
+ var MANIFEST = "package.json";
2704
+ var nameOf2 = (root) => {
2705
+ try {
2706
+ const manifest = JSON.parse(readFileSync3(`${root}/${MANIFEST}`, "utf8"));
2707
+ return typeof manifest.name === "string" ? manifest.name : null;
2708
+ } catch {
2709
+ return null;
2710
+ }
2711
+ };
2712
+ var noGenericUtilityPackage = {
2713
+ create(context) {
2714
+ const options = bagOf(context.options);
2715
+ const names2 = new Set(options.names ?? NAMES);
2716
+ const entries = options.entries ?? ENTRIES;
2717
+ const filename = normalise(context.filename);
2718
+ return {
2719
+ Program(node) {
2720
+ const entry = entries.toSorted((a, b) => b.length - a.length).find((one) => filename.endsWith(`/${one}`));
2721
+ if (entry === void 0) return;
2722
+ const name = nameOf2(filename.slice(0, filename.length - entry.length - 1));
2723
+ if (name === null) return;
2724
+ const last = name.split("/").at(-1) ?? "";
2725
+ if (!names2.has(last)) return;
2726
+ context.report({ data: { generic: last, name }, messageId: "generic", node });
2727
+ }
2728
+ };
2729
+ },
2730
+ fixShape: `A package is named for what it does. \`utils\`, \`common\`, \`helpers\`, \`shared\`, \`misc\` say nothing
2731
+ about what is inside, so nothing can be refused entry and everything arrives: web helpers, a constants
2732
+ object, a time door. Split it by what each export is FOR and give each half a name that would make a
2733
+ wrong import obviously wrong \u2014 the formatting goes to the feature's own lib, the constants go beside
2734
+ their owner, a door gets its own package named after the thing it is a door to. Delete the rest. The
2735
+ anti-names are the \`names\` option and the default is the universal list, never a repo's vocabulary.`,
2736
+ meta: {
2737
+ docs: {
2738
+ description: "A workspace package named `utils` / `common` / `helpers` / `shared` / `misc` is named for nothing, so it accretes everything. Reported once, at the package entry, from the name in its own manifest."
2739
+ },
2740
+ messages: {
2741
+ generic: 'The package "{{name}}" is named for nothing: "{{generic}}" says what is inside to no one, so nothing can be refused entry and everything arrives. Split it by what each export is for and name each half after the thing it does.'
2742
+ },
2743
+ schema: [
2744
+ {
2745
+ additionalProperties: false,
2746
+ properties: {
2747
+ entries: { items: { type: "string" }, type: "array" },
2748
+ names: { items: { type: "string" }, type: "array" }
2749
+ },
2750
+ type: "object"
2751
+ }
2752
+ ],
2753
+ type: "problem"
2754
+ },
2755
+ /** The manifest AND the entry: the name is in the manifest, and the finding lands on the entry. */
2756
+ probe(options) {
2757
+ const bag = bagOf(options);
2758
+ const generic = (bag.names ?? NAMES)[0] ?? "";
2759
+ const entry = (bag.entries ?? ENTRIES)[0] ?? "";
2760
+ return {
2761
+ files: [
2762
+ { path: `packages/${generic}/${entry}`, source: "export const entry = 1\n" },
2763
+ {
2764
+ path: `packages/${generic}/${MANIFEST}`,
2765
+ source: `{ "name": "${generic}", "version": "0.0.0" }
2766
+ `
2767
+ }
2768
+ ]
2769
+ };
2770
+ }
2771
+ };
2772
+ var no_generic_utility_package_default = noGenericUtilityPackage;
2773
+
2523
2774
  // src/rules/no-hook-in-component-disguise.ts
2524
2775
  var EXPLICIT_SIDE_EFFECT_HOOKS = /* @__PURE__ */ new Set([
2525
2776
  "useEffect",
@@ -3567,6 +3818,154 @@ The state goes in an organelle that renders its own JSX; the arrangement goes in
3567
3818
  };
3568
3819
  var no_render_prop_reader_default = noRenderPropReader;
3569
3820
 
3821
+ // src/rules/no-runtime-dead-affordance.ts
3822
+ var HANDS_IT_ON = /* @__PURE__ */ new Set(["asChild", "disabled", "form", "formAction"]);
3823
+ var PRESSED_BY_THE_FORM = /* @__PURE__ */ new Set(["submit", "reset"]);
3824
+ var nameOf3 = (attribute) => attribute.name?.type === "JSXIdentifier" ? attribute.name.name ?? "" : "";
3825
+ var arms = (attribute) => {
3826
+ if (attribute.type !== "JSXAttribute") return true;
3827
+ const name = nameOf3(attribute);
3828
+ if (name.startsWith("on") && name.length > 2) return true;
3829
+ if (HANDS_IT_ON.has(name)) return true;
3830
+ if (name !== "type") return false;
3831
+ const value = attribute.value;
3832
+ if (value === null || value === void 0) return false;
3833
+ return value.type === "Literal" ? PRESSED_BY_THE_FORM.has(String(value.value)) : (
3834
+ // An expression is a type this rule cannot read.
3835
+ true
3836
+ );
3837
+ };
3838
+ var noRuntimeDeadAffordance = {
3839
+ create(context) {
3840
+ const controls = new Set((context.options[0] ?? []).map((one) => one.atom));
3841
+ return {
3842
+ JSXOpeningElement(node) {
3843
+ if (node?.name?.type !== "JSXIdentifier") return;
3844
+ const atom = node.name.name ?? "";
3845
+ if (!controls.has(atom)) return;
3846
+ if ((node.attributes ?? []).some(arms)) return;
3847
+ context.report({ data: { atom }, messageId: "dead", node });
3848
+ }
3849
+ };
3850
+ },
3851
+ fixShape: `A control that renders and does nothing is worse than no control: it looks pressable and
3852
+ answers nothing. Every control atom the rule's map names carries one of \`onClick\` (or another
3853
+ \`on\u2026\` handler), \`asChild\` so the child it wraps carries the behaviour, \`type="submit"\`/\`"reset"\` so
3854
+ the form presses it, \`form\`/\`formAction\`, or \`disabled\` when it is inert on purpose. \`type="button"\`
3855
+ is the default and presses nothing, so it arms nothing. A spread is a prop bag the rule cannot read
3856
+ and is left alone. Configure the same element\u2192atom map \`no-raw-html-atoms\` takes; enabled without
3857
+ one the rule refuses the run.`,
3858
+ meta: {
3859
+ docs: {
3860
+ description: "A control atom rendered with no handler, no `asChild`, no submit role and no `disabled` is a dead affordance \u2014 it renders, it looks pressable, and pressing it does nothing. Reads the same element\u2192atom map as `no-raw-html-atoms`."
3861
+ },
3862
+ messages: {
3863
+ dead: '<{{atom}}> has no onClick, no other handler, no asChild and no submit role, so pressing it does nothing. Give it a handler, hand the behaviour to a child with asChild, make it type="submit" inside its form, or mark it disabled if it is meant to be inert.'
3864
+ },
3865
+ schema: [
3866
+ {
3867
+ items: {
3868
+ properties: {
3869
+ atom: { type: "string" },
3870
+ element: { type: "string" },
3871
+ importPath: { type: "string" }
3872
+ },
3873
+ required: ["element", "atom", "importPath"],
3874
+ type: "object"
3875
+ },
3876
+ type: "array"
3877
+ }
3878
+ ],
3879
+ type: "problem"
3880
+ },
3881
+ /** The map is POSITIONAL, so the probe reads `options[0]` rather than a bag. */
3882
+ probe(options) {
3883
+ const map = Array.isArray(options[0]) ? options[0] : [];
3884
+ const atom = map[0]?.atom;
3885
+ if (atom === void 0) {
3886
+ throw new Error(
3887
+ "biological-architecture/no-runtime-dead-affordance has no element\u2192atom map here, so there is no control for a probe to render \u2014 the rule is inert by design until one is configured."
3888
+ );
3889
+ }
3890
+ return {
3891
+ path: "cells/geonosis-probe.tsx",
3892
+ source: `export const GeonosisProbe = () => <${atom} />
3893
+ `
3894
+ };
3895
+ }
3896
+ };
3897
+ var no_runtime_dead_affordance_default = noRuntimeDeadAffordance;
3898
+
3899
+ // src/rules/no-swallowed-catch.ts
3900
+ var CATCH = "catch";
3901
+ var LOGGER = "console";
3902
+ var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression"]);
3903
+ var isLogging = (node) => {
3904
+ const callee = node.callee;
3905
+ const object = callee?.object;
3906
+ return object?.type === "Identifier" && object.name === LOGGER;
3907
+ };
3908
+ var swallows = (body) => {
3909
+ let answers = false;
3910
+ walkNode(body, (node) => {
3911
+ if (node.type === "ThrowStatement") answers = true;
3912
+ if (node.type === "AssignmentExpression" || node.type === "UpdateExpression") answers = true;
3913
+ if (node.type === "CallExpression" && !isLogging(node)) answers = true;
3914
+ });
3915
+ return !answers;
3916
+ };
3917
+ var noSwallowedCatch = {
3918
+ create(context) {
3919
+ const paths = bagOf(context.options).paths ?? [];
3920
+ const filename = normalise(context.filename);
3921
+ if (!paths.some((pattern) => new RegExp(pattern).test(filename))) return {};
3922
+ const report = (node, shape) => {
3923
+ context.report({ data: { shape }, messageId: "swallowed", node });
3924
+ };
3925
+ return {
3926
+ CallExpression(node) {
3927
+ if (propertyNameOf(node.callee) !== CATCH) return;
3928
+ const [handler] = node.arguments ?? [];
3929
+ if (handler === void 0 || !FUNCTIONS.has(handler.type ?? "")) return;
3930
+ if (swallows(handler.body)) report(handler, ".catch()");
3931
+ },
3932
+ CatchClause(node) {
3933
+ if (swallows(node.body)) report(node, "catch block");
3934
+ }
3935
+ };
3936
+ },
3937
+ fixShape: `A rejection on a fetch or refresh path is answered, never dropped. An empty \`.catch(() => {})\`,
3938
+ an empty \`catch {}\` and a handler that only logs all leave the caller reading a failed refresh as a
3939
+ successful one. Write the failure somewhere the caller can see it \u2014 set an error flag, put the state
3940
+ back, mark the value stale \u2014 or rethrow it and let the boundary above decide. Which modules are the
3941
+ fetch/refresh surface is the \`paths\` option; enabled without it the rule refuses the run, because a
3942
+ rule with no surface to watch reads exactly like a tree with no swallowed rejection in it.`,
3943
+ meta: {
3944
+ docs: {
3945
+ description: "A `.catch()` handler or a `catch` block on a declared fetch/refresh path must write state or rethrow. An empty handler, or one that only logs, turns a failed refresh into a successful one for everything downstream."
3946
+ },
3947
+ messages: {
3948
+ swallowed: "This {{shape}} drops the rejection: it neither writes state nor rethrows, so a failed refresh is indistinguishable from a successful one. Set the error where the caller reads it, or rethrow."
3949
+ },
3950
+ schema: [
3951
+ {
3952
+ additionalProperties: false,
3953
+ properties: { paths: { items: { type: "string" }, type: "array" } },
3954
+ type: "object"
3955
+ }
3956
+ ],
3957
+ type: "problem"
3958
+ },
3959
+ probe(options) {
3960
+ return {
3961
+ path: pathMatching(firstOf(bagOf(options).paths, []), "refresh.ts"),
3962
+ source: `export const refresh = (load: () => Promise<void>) => load().catch(() => {})
3963
+ `
3964
+ };
3965
+ }
3966
+ };
3967
+ var no_swallowed_catch_default = noSwallowedCatch;
3968
+
3570
3969
  // src/rules/no-trivial-wrapper-component.ts
3571
3970
  var ROUTE_BASENAMES = /* @__PURE__ */ new Set([
3572
3971
  "default.tsx",
@@ -3802,9 +4201,9 @@ and is imported.`,
3802
4201
  var no_type_definitions_in_components_default = noTypeDefinitionsInComponents;
3803
4202
 
3804
4203
  // src/rules/no-unregistered-plugin-dir.ts
3805
- import { readFileSync as readFileSync3 } from "fs";
4204
+ import { readFileSync as readFileSync4 } from "fs";
3806
4205
  var RULE11 = "no-unregistered-plugin-dir";
3807
- var ACCEPTS3 = "{ roots: string[], registry: string | string[], manifests: string[] }";
4206
+ var ACCEPTS3 = "{ roots: (string | { path: string, files: boolean })[], registry: string | string[], manifests: string[] }";
3808
4207
  var trimSlashes = (path2) => path2.replace(/^\/+/, "").replace(/\/+$/, "");
3809
4208
  var withoutComments = (source) => source.replaceAll(/\/\*[\s\S]*?\*\//g, " ").replaceAll(/\/\/[^\n]*/g, " ");
3810
4209
  var literalsOf = (source) => [...withoutComments(source).matchAll(/['"`]([^'"`\n]+)['"`]/g)].map((match) => match[1] ?? "");
@@ -3818,10 +4217,22 @@ var rootedAt = (filename, root) => {
3818
4217
  if (at < 0) return null;
3819
4218
  return { after: filename.slice(at + marker.length), repoRoot: filename.slice(0, at) };
3820
4219
  };
4220
+ var oneDirectory = (segments, manifests) => {
4221
+ if (segments.length !== 2) return null;
4222
+ const [directory, file] = segments;
4223
+ return directory !== void 0 && file !== void 0 && manifests.has(file) ? directory : null;
4224
+ };
4225
+ var oneFile = (segments) => {
4226
+ if (segments.length !== 1) return null;
4227
+ const [file] = segments;
4228
+ return file === void 0 ? null : file.replace(/\.[cm]?[jt]sx?$/, "");
4229
+ };
3821
4230
  var noUnregisteredPluginDir = {
3822
4231
  create(context) {
3823
4232
  const options = context.options?.[0] ?? {};
3824
- const roots = requireOption(RULE11, options.roots, "roots", ACCEPTS3).map(trimSlashes);
4233
+ const roots = requireOption(RULE11, options.roots, "roots", ACCEPTS3).map(
4234
+ (one) => typeof one === "string" ? { files: false, path: trimSlashes(one) } : { files: one.files === true, path: trimSlashes(one.path) }
4235
+ );
3825
4236
  const declared = requireOption(RULE11, options.registry, "registry", ACCEPTS3);
3826
4237
  const registries = (typeof declared === "string" ? [declared] : declared).map(trimSlashes);
3827
4238
  const manifests = new Set(requireOption(RULE11, options.manifests, "manifests", ACCEPTS3));
@@ -3829,21 +4240,24 @@ var noUnregisteredPluginDir = {
3829
4240
  return {
3830
4241
  Program(node) {
3831
4242
  for (const root of roots) {
3832
- const rooted = rootedAt(filename, root);
4243
+ const rooted = rootedAt(filename, root.path);
3833
4244
  if (rooted === null) continue;
3834
4245
  const segments = rooted.after.split("/");
3835
- if (segments.length !== 2) continue;
3836
- const [directory, file] = segments;
3837
- if (directory === void 0 || file === void 0 || !manifests.has(file)) continue;
4246
+ const registrant = root.files ? oneFile(segments) : oneDirectory(segments, manifests);
4247
+ if (registrant === null) continue;
3838
4248
  let source = "";
3839
4249
  try {
3840
- source = registries.map((half) => readFileSync3(`${rooted.repoRoot}/${half}`, "utf8")).join("\n");
4250
+ source = registries.map((half) => readFileSync4(`${rooted.repoRoot}/${half}`, "utf8")).join("\n");
3841
4251
  } catch {
3842
4252
  return;
3843
4253
  }
3844
- if (literalsOf(source).some((literal) => names(literal, directory))) return;
4254
+ if (literalsOf(source).some((literal) => names(literal, registrant))) return;
3845
4255
  const registry = registries.join(", ");
3846
- context.report({ data: { directory, registry, root }, messageId: "unregistered", node });
4256
+ context.report({
4257
+ data: { directory: registrant, registry, root: root.path },
4258
+ messageId: "unregistered",
4259
+ node
4260
+ });
3847
4261
  return;
3848
4262
  }
3849
4263
  }
@@ -3856,7 +4270,9 @@ eight of them. Configure \`roots\`, \`registry\` and \`manifests\` \u2014 all th
3856
4270
  registry is SPLIT across files (a config that imports a second config): name every half, because the
3857
4271
  rule reads source and does not follow imports. \`manifests\` has no default on purpose: dielime's plugin directories hold \`service.ts\` and no index, so a defaulted list
3858
4272
  would match nothing and report a clean tree it never looked at. A seam whose providers are flat files
3859
- rather than directories is out of scope \u2014 nothing under a root matches, so nothing is reported.`,
4273
+ rather than directories is judged when its root says \`files: true\` \u2014 \`{ path, files: true }\` makes each
4274
+ FILE under that root a registrant named by its own basename \u2014 and is out of scope under a bare root,
4275
+ where nothing matches and nothing is reported.`,
3860
4276
  meta: {
3861
4277
  docs: {
3862
4278
  description: "Every directory under a plugin root must be named by the registry file. Midday ships 35 app-store directories and 8 are reachable from nothing. The registry is read as source, so an id or an import path both count, and `registry` takes a LIST when the registry is split across files \u2014 reading only the named half reported two registered carrier directories as dead weight on a real consumer. `roots`, `registry` and `manifests` are all required; `manifests` has no default because a defaulted entry filename matched nothing in a real consumer and reported a clean tree it had never looked at."
@@ -3875,7 +4291,23 @@ rather than directories is out of scope \u2014 nothing under a root matches, so
3875
4291
  { items: { type: "string" }, type: "array" }
3876
4292
  ]
3877
4293
  },
3878
- roots: { items: { type: "string" }, type: "array" }
4294
+ roots: {
4295
+ items: {
4296
+ anyOf: [
4297
+ { type: "string" },
4298
+ {
4299
+ additionalProperties: false,
4300
+ properties: {
4301
+ files: { type: "boolean" },
4302
+ path: { type: "string" }
4303
+ },
4304
+ required: ["path"],
4305
+ type: "object"
4306
+ }
4307
+ ]
4308
+ },
4309
+ type: "array"
4310
+ }
3879
4311
  },
3880
4312
  type: "object"
3881
4313
  }
@@ -3896,12 +4328,14 @@ rather than directories is out of scope \u2014 nothing under a root matches, so
3896
4328
  */
3897
4329
  probe(options) {
3898
4330
  const { manifests = [], registry = "", roots = [] } = bagOf(options);
3899
- const root = trimSlashes(roots[0] ?? "");
4331
+ const first = roots[0] ?? "";
4332
+ const root = typeof first === "string" ? { files: false, path: first } : first;
4333
+ const under = trimSlashes(root.path);
3900
4334
  const half = typeof registry === "string" ? registry : registry[0] ?? "";
3901
4335
  return {
3902
4336
  files: [
3903
4337
  {
3904
- path: `${root}/geonosis-probe/${manifests[0] ?? ""}`,
4338
+ path: root.files === true ? `${under}/geonosis-probe.ts` : `${under}/geonosis-probe/${manifests[0] ?? ""}`,
3905
4339
  source: "export const geonosisProbe = { id: 'geonosis-probe' }\n"
3906
4340
  },
3907
4341
  { path: trimSlashes(half), source: "export const registry = { providers: [] }\n" }
@@ -4706,13 +5140,14 @@ var styleAttributes = /* @__PURE__ */ new Set([
4706
5140
  "autoComplete"
4707
5141
  ]);
4708
5142
  var styleCallees = /* @__PURE__ */ new Set(["cn", "clsx", "twMerge", "cva", "tv"]);
4709
- var declarationCallees = /* @__PURE__ */ new Set([
5143
+ var DECLARATION_CALLEES = [
4710
5144
  "declare",
4711
5145
  "saga",
4712
5146
  "step",
4713
5147
  "action",
4714
5148
  "defineEvent",
4715
5149
  "defineWorkflow",
5150
+ "createWorkflow",
4716
5151
  "createStep",
4717
5152
  "sqliteTable",
4718
5153
  "text",
@@ -4726,7 +5161,7 @@ var declarationCallees = /* @__PURE__ */ new Set([
4726
5161
  "check",
4727
5162
  "literal",
4728
5163
  "enum"
4729
- ]);
5164
+ ];
4730
5165
  var formFieldCallees = /* @__PURE__ */ new Set([
4731
5166
  "register",
4732
5167
  "watch",
@@ -4752,23 +5187,23 @@ var isFactShaped = (literal) => {
4752
5187
  if (/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/.test(literal)) return true;
4753
5188
  return literal.length >= 16 && !/^[a-z0-9:\-[\]/.%#!]+$/.test(literal);
4754
5189
  };
4755
- var nameOf2 = (node) => {
5190
+ var nameOf4 = (node) => {
4756
5191
  if (node === void 0) return void 0;
4757
5192
  if (typeof node === "string") return node;
4758
5193
  if (node.type === "Identifier" || node.type === "JSXIdentifier") return node.name;
4759
5194
  if (node.type === "Literal") return String(node.value);
4760
5195
  return void 0;
4761
5196
  };
4762
- var isSpelledAsStyleOrModule = (node) => {
5197
+ var isSpelledAsStyleOrModule = (node, declarationCallees) => {
4763
5198
  let current = node.parent;
4764
5199
  let child = node;
4765
5200
  while (current && typeof current.type === "string" && current.type !== "Program") {
4766
- if (current.type === "JSXAttribute") return styleAttributes.has(nameOf2(current.name) ?? "");
5201
+ if (current.type === "JSXAttribute") return styleAttributes.has(nameOf4(current.name) ?? "");
4767
5202
  if (current.type === "Property" && current.key === child) return true;
4768
- if (current.type === "Property" && styleAttributes.has(nameOf2(current.key) ?? "")) return true;
5203
+ if (current.type === "Property" && styleAttributes.has(nameOf4(current.key) ?? "")) return true;
4769
5204
  if (current.type === "CallExpression") {
4770
5205
  const callee = current.callee;
4771
- const calleeName = callee?.type === "MemberExpression" ? nameOf2(callee.property) : nameOf2(callee);
5206
+ const calleeName = callee?.type === "MemberExpression" ? nameOf4(callee.property) : nameOf4(callee);
4772
5207
  if (styleCallees.has(calleeName ?? "") || formFieldCallees.has(calleeName ?? "")) return true;
4773
5208
  const firstArgument = current.arguments?.[0];
4774
5209
  if (declarationCallees.has(calleeName ?? "") && firstArgument === child) return true;
@@ -4786,9 +5221,13 @@ var ssotNoInlineFacts = {
4786
5221
  create(context) {
4787
5222
  const normalized = normalise(context.filename);
4788
5223
  if (factOwners.some((owner) => owner.test(normalized))) return {};
5224
+ const declarationCallees = /* @__PURE__ */ new Set([
5225
+ ...DECLARATION_CALLEES,
5226
+ ...context.options?.[0]?.declarationCallees ?? []
5227
+ ]);
4789
5228
  const check = (literal, node) => {
4790
5229
  if (!isFactShaped(literal)) return;
4791
- if (isSpelledAsStyleOrModule(node)) return;
5230
+ if (isSpelledAsStyleOrModule(node, declarationCallees)) return;
4792
5231
  context.report({ data: { literal }, messageId: "inlineFact", node });
4793
5232
  };
4794
5233
  return {
@@ -4803,7 +5242,9 @@ var ssotNoInlineFacts = {
4803
5242
  },
4804
5243
  fixShape: `A route, a URL, a code, a dotted key: a fact is declared once, in the module that owns it, and
4805
5244
  imported. An inline literal used as configuration is a second copy waiting to drift \u2014 name it and put
4806
- it where its owner lives.`,
5245
+ it where its owner lives. A literal that DECLARES rather than repeats \u2014 the first argument to
5246
+ \`createWorkflow\`, \`createStep\`, \`defineEvent\` \u2014 is the declaration itself and is left alone; name
5247
+ this repo's own factories in \`declarationCallees\` and they are added to those.`,
4807
5248
  meta: {
4808
5249
  docs: {
4809
5250
  description: "A fact \u2014 a route, a URL, a CODE, a dotted.name, a long key \u2014 is declared once, in its owner module (the owning module, lib/routes.ts, lib/env.ts, @during/client), and imported everywhere else. Spelling it inline makes a second source."
@@ -4811,7 +5252,15 @@ it where its owner lives.`,
4811
5252
  messages: {
4812
5253
  inlineFact: '"{{literal}}" is a fact spelled inline. Declare it once in its owner module and import it.'
4813
5254
  },
4814
- schema: [],
5255
+ schema: [
5256
+ {
5257
+ additionalProperties: false,
5258
+ properties: {
5259
+ declarationCallees: { items: { type: "string" }, type: "array" }
5260
+ },
5261
+ type: "object"
5262
+ }
5263
+ ],
4815
5264
  type: "problem"
4816
5265
  }
4817
5266
  };
@@ -5060,8 +5509,8 @@ export const probe = step('geonosis-probe', async () => ({ ok: true }))
5060
5509
  var step_opens_its_own_cell_default = stepOpensItsOwnCell;
5061
5510
 
5062
5511
  // src/rules/store-route-scopes-tenant-data.ts
5063
- var STORE_ROUTE_PATTERN = /\/api\/store\/.*\/route\.tsx?$/;
5064
- var TENANT_ENTITIES = /* @__PURE__ */ new Set([
5512
+ import { readFileSync as readFileSync5 } from "fs";
5513
+ var ENTITIES = [
5065
5514
  "cart",
5066
5515
  "claim",
5067
5516
  "company",
@@ -5071,27 +5520,136 @@ var TENANT_ENTITIES = /* @__PURE__ */ new Set([
5071
5520
  "planned_order",
5072
5521
  "production_order",
5073
5522
  "quote"
5523
+ ];
5524
+ var ROUTES = [String.raw`/api/store/.*/route\.tsx?$`];
5525
+ var SCOPING_CALLS = ["auth_context", "actor_id", "customer_id", "company_id"];
5526
+ var ENTITY_KEY = "entity";
5527
+ var SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
5528
+ var keyNameOf2 = (node) => {
5529
+ const key = node.key;
5530
+ if (key?.type === "Identifier" && typeof key.name === "string") return key.name;
5531
+ return key?.type === "Literal" && typeof key.value === "string" ? key.value : null;
5532
+ };
5533
+ var stringValueOf = (node) => {
5534
+ const value = node.value;
5535
+ return value?.type === "Literal" && typeof value.value === "string" ? value.value : null;
5536
+ };
5537
+ var withoutComments2 = (text) => {
5538
+ let out = "";
5539
+ let at = 0;
5540
+ let quote = "";
5541
+ while (at < text.length) {
5542
+ const here = text[at] ?? "";
5543
+ const next = text[at + 1] ?? "";
5544
+ if (quote !== "") {
5545
+ if (here === "\\") {
5546
+ out += " ";
5547
+ at += 2;
5548
+ continue;
5549
+ }
5550
+ if (here === quote) quote = "";
5551
+ out += here;
5552
+ at += 1;
5553
+ continue;
5554
+ }
5555
+ if (here === '"' || here === "'" || here === "`") {
5556
+ quote = here;
5557
+ out += here;
5558
+ at += 1;
5559
+ continue;
5560
+ }
5561
+ if (here === "/" && next === "/") {
5562
+ while (at < text.length && text[at] !== "\n") {
5563
+ out += " ";
5564
+ at += 1;
5565
+ }
5566
+ continue;
5567
+ }
5568
+ if (here === "/" && next === "*") {
5569
+ const end = text.indexOf("*/", at + 2);
5570
+ const stop = end === -1 ? text.length : end + 2;
5571
+ for (; at < stop; at += 1) out += text[at] === "\n" ? "\n" : " ";
5572
+ continue;
5573
+ }
5574
+ out += here;
5575
+ at += 1;
5576
+ }
5577
+ return out;
5578
+ };
5579
+ var siblingSourceOf = (importer, source) => {
5580
+ if (!source.startsWith(".")) return null;
5581
+ const landed = resolveRelative(importer, source);
5582
+ for (const suffix of SUFFIXES) {
5583
+ try {
5584
+ return withoutComments2(readFileSync5(`${landed}${suffix}`, "utf8"));
5585
+ } catch {
5586
+ continue;
5587
+ }
5588
+ }
5589
+ return null;
5590
+ };
5591
+ var MODULE_NODES = /* @__PURE__ */ new Set([
5592
+ "CallExpression",
5593
+ "ExportAllDeclaration",
5594
+ "ExportNamedDeclaration",
5595
+ "ImportDeclaration",
5596
+ "ImportExpression"
5074
5597
  ]);
5075
- var SCOPE_MARKERS = /auth_context|actor_id|customer_id|company_id|\bcustomer\b\s*:/;
5598
+ var readIn = (text, entities) => {
5599
+ for (const entity of entities) {
5600
+ if (new RegExp(`${ENTITY_KEY}\\s*:\\s*['"\`]${entity}['"\`]`).test(text)) return entity;
5601
+ }
5602
+ return null;
5603
+ };
5604
+ var namesAny = (text, names2) => names2.some((name) => new RegExp(`\\b${name}\\b`).test(text));
5076
5605
  var storeRouteScopesTenantData = {
5077
5606
  create(context) {
5078
- const normalized = normalise(context.filename);
5079
- if (!STORE_ROUTE_PATTERN.test(normalized)) return {};
5080
- const shortName = normalized.split("/").slice(-3).join("/");
5607
+ const options = bagOf(context.options);
5608
+ const routes = options.routes ?? ROUTES;
5609
+ const entities = options.entities ?? ENTITIES;
5610
+ const narrowing = [...options.scopingCalls ?? SCOPING_CALLS, ...options.evidenceCalls ?? []];
5611
+ const filename = normalise(context.filename);
5612
+ if (!routes.some((pattern) => new RegExp(pattern).test(filename))) return {};
5613
+ const shortName = filename.split("/").slice(-3).join("/");
5081
5614
  return {
5082
- Program(node) {
5083
- const text = context.sourceCode?.text ?? "";
5084
- if (text === "" || SCOPE_MARKERS.test(text)) return;
5085
- for (const entity of TENANT_ENTITIES) {
5086
- const reads = new RegExp(`entity:\\s*['"\`]${entity}['"\`]`).test(text);
5087
- if (reads) {
5088
- context.report({
5089
- data: { entity, file: shortName },
5090
- messageId: "unscoped",
5091
- node
5092
- });
5093
- return;
5615
+ Program(program) {
5616
+ let read = null;
5617
+ let scoped = false;
5618
+ const sources = [];
5619
+ walkNode(program, (node) => {
5620
+ if (node.type === "Property" && keyNameOf2(node) === ENTITY_KEY) {
5621
+ const entity = stringValueOf(node);
5622
+ if (entity !== null && entities.includes(entity) && read === null) {
5623
+ read = { entity, node };
5624
+ }
5094
5625
  }
5626
+ if (node.type === "Identifier" && narrowing.includes(node.name)) scoped = true;
5627
+ if (MODULE_NODES.has(node.type ?? "")) {
5628
+ const source = sourceOf(node);
5629
+ if (source !== null) sources.push(source);
5630
+ }
5631
+ });
5632
+ if (scoped) return;
5633
+ if (read !== null) {
5634
+ const found = read;
5635
+ context.report({
5636
+ data: { entity: found.entity, file: shortName },
5637
+ messageId: "unscoped",
5638
+ node: found.node
5639
+ });
5640
+ return;
5641
+ }
5642
+ for (const source of sources) {
5643
+ const text = siblingSourceOf(filename, source);
5644
+ if (text === null || namesAny(text, narrowing)) continue;
5645
+ const entity = readIn(text, entities);
5646
+ if (entity === null) continue;
5647
+ context.report({
5648
+ data: { entity, file: shortName, through: source },
5649
+ messageId: "unscopedSibling",
5650
+ node: program
5651
+ });
5652
+ return;
5095
5653
  }
5096
5654
  }
5097
5655
  };
@@ -5099,17 +5657,43 @@ var storeRouteScopesTenantData = {
5099
5657
  fixShape: `A customer-facing store route that reads a customer-owned entity (cart, order, quote, company, \u2026)
5100
5658
  narrows the read to the authenticated caller \u2014 \`auth_context\`, \`actor_id\`, \`customer_id\` or
5101
5659
  \`company_id\` must appear in the handler. Without it a publishable key returns every tenant's rows.
5102
- Global catalog entities are deliberately exempt; if the data is genuinely not customer data, it
5103
- belongs on the admin surface.`,
5660
+ A route the caller proves entitlement to with EVIDENCE instead of a session \u2014 an order reference
5661
+ plus the email it was placed with \u2014 calls a predicate the config names in \`evidenceCalls\`; the rule
5662
+ trusts no predicate it was not given. Moving the read into the module beside the route does not
5663
+ answer the question: the rule follows a relative import one hop and reads the scoping there. Which
5664
+ files are the route surface, which entities are customer-owned and which names count as scoping are
5665
+ all options. Global catalog entities are deliberately exempt; if the data is genuinely not customer
5666
+ data, it belongs on the admin surface.`,
5104
5667
  meta: {
5105
5668
  docs: {
5106
- description: "A /store/** route that reads a customer-owned entity must narrow the read to the authenticated caller. Without it the handler returns every tenant\u2019s rows to anyone holding a publishable key. Global catalog entities are exempt."
5669
+ description: "A /store/** route that reads a customer-owned entity must narrow the read to the authenticated caller, or verify caller-supplied evidence with a predicate the config names. Without either, the handler returns every tenant\u2019s rows to anyone holding a publishable key. The read is followed one hop into a relative sibling; global catalog entities are exempt."
5107
5670
  },
5108
5671
  messages: {
5109
- unscoped: 'Store route "{{file}}" reads the customer-owned entity "{{entity}}" without any caller scoping (no auth_context / actor_id / customer_id / company_id in the file). Narrow the read to the authenticated caller, or move it to /admin if it is genuinely not customer data.'
5672
+ unscoped: 'Store route "{{file}}" reads the customer-owned entity "{{entity}}" without any caller scoping (no auth_context / actor_id / customer_id / company_id in the file, and no evidence check the config names). Narrow the read to the authenticated caller, name your evidence predicate in `evidenceCalls`, or move it to /admin if it is genuinely not customer data.',
5673
+ unscopedSibling: 'Store route "{{file}}" reads the customer-owned entity "{{entity}}" through "{{through}}", and neither file narrows it to a caller. Scope the read where it happens, or name the predicate that checks the caller\u2019s evidence in `evidenceCalls`.'
5110
5674
  },
5111
- schema: [],
5675
+ schema: [
5676
+ {
5677
+ additionalProperties: false,
5678
+ properties: {
5679
+ entities: { items: { type: "string" }, type: "array" },
5680
+ evidenceCalls: { items: { type: "string" }, type: "array" },
5681
+ routes: { items: { type: "string" }, type: "array" },
5682
+ scopingCalls: { items: { type: "string" }, type: "array" }
5683
+ },
5684
+ type: "object"
5685
+ }
5686
+ ],
5112
5687
  type: "problem"
5688
+ },
5689
+ probe(options) {
5690
+ const bag = bagOf(options);
5691
+ return {
5692
+ path: pathMatching(firstOf(bag.routes, ROUTES), "route.ts"),
5693
+ source: `export const GET = async (query: { graph: (c: unknown) => Promise<unknown> }) =>
5694
+ query.graph({ entity: '${firstOf(bag.entities, ENTITIES)}' })
5695
+ `
5696
+ };
5113
5697
  }
5114
5698
  };
5115
5699
  var store_route_scopes_tenant_data_default = storeRouteScopesTenantData;
@@ -5364,6 +5948,146 @@ table to \`schema/control/\`.`,
5364
5948
  };
5365
5949
  var tenant_tables_carry_org_id_default = tenantTablesCarryOrgId;
5366
5950
 
5951
+ // src/rules/test-no-clock-polling-for-promise-flips.ts
5952
+ var POLLERS = ["waitFor"];
5953
+ var TEST_GLOBS = [String.raw`\.(?:test|spec)\.[cm]?[jt]sx?$`, "/__tests__/"];
5954
+ var ASSERT = "expect";
5955
+ var FUNCTIONS2 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionDeclaration", "FunctionExpression"]);
5956
+ var walkWithin = (root, visit) => {
5957
+ const seen = /* @__PURE__ */ new Set();
5958
+ const stack = [root];
5959
+ while (stack.length > 0) {
5960
+ const at = stack.pop();
5961
+ if (at === null || typeof at !== "object" || seen.has(at)) continue;
5962
+ seen.add(at);
5963
+ if (Array.isArray(at)) {
5964
+ stack.push(...at);
5965
+ continue;
5966
+ }
5967
+ const node = at;
5968
+ if (typeof node.type === "string") {
5969
+ if (at !== root && FUNCTIONS2.has(node.type)) continue;
5970
+ visit(node);
5971
+ }
5972
+ for (const [key, value] of Object.entries(node)) {
5973
+ if (key === "parent") continue;
5974
+ if (value !== null && typeof value === "object") stack.push(value);
5975
+ }
5976
+ }
5977
+ };
5978
+ var rootCallOf = (node) => {
5979
+ let at = node;
5980
+ for (; ; ) {
5981
+ const callee = at.callee;
5982
+ if (callee?.type !== "MemberExpression") return at;
5983
+ const object = callee.object;
5984
+ if (object?.type !== "CallExpression") return at;
5985
+ at = object;
5986
+ }
5987
+ };
5988
+ var onlyAsserts = (body) => {
5989
+ let calls = 0;
5990
+ let asserts = 0;
5991
+ walkWithin(body, (node) => {
5992
+ if (node.type !== "CallExpression") return;
5993
+ calls += 1;
5994
+ if (calleeNameOf(rootCallOf(node)) === ASSERT) asserts += 1;
5995
+ });
5996
+ return calls > 0 && calls === asserts;
5997
+ };
5998
+ var promisesIn = (scope) => {
5999
+ const created = /* @__PURE__ */ new Map();
6000
+ const proven = /* @__PURE__ */ new Set();
6001
+ walkWithin(scope, (node) => {
6002
+ if (node.type === "VariableDeclarator") {
6003
+ const id = node.id;
6004
+ const init = node.init;
6005
+ if (id?.type === "Identifier" && init?.type === "CallExpression") {
6006
+ created.set(id.name, init);
6007
+ }
6008
+ }
6009
+ if (node.type === "AwaitExpression") {
6010
+ const argument = node.argument;
6011
+ if (argument?.type === "Identifier") proven.add(argument.name);
6012
+ }
6013
+ if (node.type === "MemberExpression") {
6014
+ const object = node.object;
6015
+ const property = propertyNameOf(node);
6016
+ if (object?.type === "Identifier" && (property === "then" || property === "catch" || property === "finally")) {
6017
+ proven.add(object.name);
6018
+ }
6019
+ }
6020
+ if (node.type === "CallExpression") {
6021
+ const object = node.callee?.object;
6022
+ if (object?.type === "Identifier" && object.name === "Promise") {
6023
+ for (const argument of node.arguments ?? []) {
6024
+ for (const element of argument.elements ?? [argument]) {
6025
+ if (element?.type === "Identifier") proven.add(element.name);
6026
+ }
6027
+ }
6028
+ }
6029
+ }
6030
+ });
6031
+ return new Set([...created.keys()].filter((name) => proven.has(name)));
6032
+ };
6033
+ var testNoClockPollingForPromiseFlips = {
6034
+ create(context) {
6035
+ const options = bagOf(context.options);
6036
+ const pollers = options.pollers ?? POLLERS;
6037
+ const globs = options.testGlobs ?? TEST_GLOBS;
6038
+ const filename = normalise(context.filename);
6039
+ if (!globs.some((pattern) => new RegExp(pattern).test(filename))) return {};
6040
+ const inScope = (node) => {
6041
+ const held = promisesIn(node);
6042
+ if (held.size === 0) return;
6043
+ const [promise] = [...held].toSorted();
6044
+ walkWithin(node, (inner) => {
6045
+ if (inner.type !== "CallExpression") return;
6046
+ if (!pollers.includes(calleeNameOf(inner) ?? "")) return;
6047
+ const [callback] = inner.arguments ?? [];
6048
+ if (callback === void 0 || !FUNCTIONS2.has(callback.type ?? "")) return;
6049
+ if (!onlyAsserts(callback.body)) return;
6050
+ context.report({
6051
+ data: { poller: calleeNameOf(inner) ?? "", promise: promise ?? "" },
6052
+ messageId: "clockPoll",
6053
+ node: inner
6054
+ });
6055
+ });
6056
+ };
6057
+ return {
6058
+ ArrowFunctionExpression: inScope,
6059
+ FunctionDeclaration: inScope,
6060
+ FunctionExpression: inScope
6061
+ };
6062
+ },
6063
+ fixShape: `A test that already holds a promise awaits it; it does not poll a clock for the flip. A
6064
+ \`waitFor\` whose body is nothing but \`expect\` assertions, in a test that created a promise and proves
6065
+ it is one by awaiting or chaining it, is asking the scheduler a question the promise answers exactly:
6066
+ move the \`await\` above the assertions, or drain microtasks, and assert once. Keep \`waitFor\` for state
6067
+ nothing in the test can await \u2014 an element a click will render, a timer the runtime owns. Which files
6068
+ are tests is \`testGlobs\` and which calls poll is \`pollers\`, both defaulted.`,
6069
+ meta: {
6070
+ docs: {
6071
+ description: "In a test file, a `waitFor` whose callback only asserts, in a scope that already holds a promise, is a clock poll for a flip the promise decides. Await the promise instead."
6072
+ },
6073
+ messages: {
6074
+ clockPoll: 'This {{poller}} only asserts, and the test already holds the promise "{{promise}}". Await "{{promise}}" before asserting rather than polling the clock for the flip it decides \u2014 a poll that passes is a poll that was slower than the microtask, and a poll that fails is a timeout with no cause.'
6075
+ },
6076
+ schema: [
6077
+ {
6078
+ additionalProperties: false,
6079
+ properties: {
6080
+ pollers: { items: { type: "string" }, type: "array" },
6081
+ testGlobs: { items: { type: "string" }, type: "array" }
6082
+ },
6083
+ type: "object"
6084
+ }
6085
+ ],
6086
+ type: "problem"
6087
+ }
6088
+ };
6089
+ var test_no_clock_polling_for_promise_flips_default = testNoClockPollingForPromiseFlips;
6090
+
5367
6091
  // src/rules/time-through-the-door.ts
5368
6092
  var RULE20 = "time-through-the-door";
5369
6093
  var ACCEPTS6 = "{ door: string (a regex over the filename), exempt: string[] (regex sources, default the test paths: __tests__/, .test., .spec.), libraries: string[] (default ['dayjs']) }";
@@ -5395,6 +6119,7 @@ var timeThroughTheDoor = {
5395
6119
  create(context) {
5396
6120
  const door = requireOption(RULE20, context.options?.[0]?.door, "door", ACCEPTS6);
5397
6121
  const exempt = context.options?.[0]?.exempt ?? DEFAULT_EXEMPT;
6122
+ const exemptModules = context.options?.[0]?.exemptModules ?? [];
5398
6123
  const libraries = requireOption(
5399
6124
  RULE20,
5400
6125
  context.options?.[0]?.libraries ?? DEFAULT_LIBRARIES,
@@ -5403,6 +6128,7 @@ var timeThroughTheDoor = {
5403
6128
  );
5404
6129
  const filename = normalise(context.filename);
5405
6130
  if (isDoor(filename, door)) return {};
6131
+ if (exemptModules.some((module) => isModule(filename, module))) return {};
5406
6132
  if (exempt.some((pattern) => new RegExp(pattern).test(filename))) return {};
5407
6133
  const where = readable2(door);
5408
6134
  return {
@@ -5444,7 +6170,9 @@ the plugins, the zone and the formats. Import \`isoDate\` / \`today\` / \`addDay
5444
6170
  operation there. A second module that imports the library is a second configured library: plugin
5445
6171
  state is global, so the two disagree the moment one of them extends something the other did not.
5446
6172
  Name the library in \`libraries\` (default \`['dayjs']\`) \u2014 a repo on date-fns or luxon says so and
5447
- gets the whole rule rather than its raw-\`Date\` half.
6173
+ gets the whole rule rather than its raw-\`Date\` half. Name the modules that are outside the door in
6174
+ \`exemptModules\`, one per entry: \`exempt\` still takes patterns and still grants its exemption to
6175
+ wherever the pattern stops, which is a boundary nobody decided and nobody counts.
5448
6176
 
5449
6177
  Three kinds of time, never mixed. An INSTANT is epoch milliseconds. A BUSINESS DATE is an ISO date
5450
6178
  string, \`YYYY-MM-DD\`, parsed strictly in UTC \u2014 a calendar day is not an instant and rendering one as
@@ -5455,6 +6183,12 @@ The clock is injected, never ambient. Take \`nowMs: number\` as a parameter and
5455
6183
  root pass \`Date.now()\` in \u2014 \`Date.now()\` is the one call allowed anywhere, and a function that reads
5456
6184
  it internally cannot be tested at a chosen instant.`,
5457
6185
  meta: {
6186
+ /**
6187
+ * #139: which option grants by PATTERN and which one ENUMERATES. Declared on the rule so the
6188
+ * doctor needs no list of its own — a rule that gains a path grant and forgets to say so is a
6189
+ * grant nothing counts, which is the defect this pair exists to end.
6190
+ */
6191
+ grants: { modules: "exemptModules", paths: "exempt" },
5458
6192
  docs: {
5459
6193
  description: "Time is handled behind one module \u2014 the `door` option names it \u2014 and nothing else constructs, parses or formats a Date or imports dayjs. Date.now() is the one call allowed everywhere. Enabled without a `door` it refuses the run."
5460
6194
  },
@@ -5468,6 +6202,7 @@ it internally cannot be tested at a chosen instant.`,
5468
6202
  properties: {
5469
6203
  door: { type: "string" },
5470
6204
  exempt: { items: { type: "string" }, type: "array" },
6205
+ exemptModules: { items: { type: "string" }, type: "array" },
5471
6206
  libraries: { items: { type: "string" }, type: "array" }
5472
6207
  },
5473
6208
  type: "object"
@@ -5976,7 +6711,7 @@ export const geonosisProbeTodo = 1
5976
6711
  var todo_names_its_plan_default = todoNamesItsPlan;
5977
6712
 
5978
6713
  // src/rules/lib/colors.ts
5979
- var FUNCTIONS = [
6714
+ var FUNCTIONS3 = [
5980
6715
  "color",
5981
6716
  "color-mix",
5982
6717
  "hsl",
@@ -5990,7 +6725,7 @@ var FUNCTIONS = [
5990
6725
  "rgba"
5991
6726
  ];
5992
6727
  var HEX = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
5993
- var FUNCTION_CALL = new RegExp(`\\b(?:${FUNCTIONS.join("|")})\\(`, "i");
6728
+ var FUNCTION_CALL = new RegExp(`\\b(?:${FUNCTIONS3.join("|")})\\(`, "i");
5994
6729
  var HEX_ANYWHERE = /(?:^|[\s(,])#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i;
5995
6730
  var NAMED2 = /* @__PURE__ */ new Set([
5996
6731
  "aliceblue",
@@ -6147,7 +6882,7 @@ var isColorLiteral = (value) => {
6147
6882
  if (trimmed === "") return false;
6148
6883
  if (HEX.test(trimmed)) return true;
6149
6884
  if (NAMED2.has(trimmed.toLowerCase())) return true;
6150
- return new RegExp(`^(?:${FUNCTIONS.join("|")})\\(`, "i").test(trimmed);
6885
+ return new RegExp(`^(?:${FUNCTIONS3.join("|")})\\(`, "i").test(trimmed);
6151
6886
  };
6152
6887
  var containsColorLiteral = (value) => HEX_ANYWHERE.test(value) || FUNCTION_CALL.test(value);
6153
6888
  var COLOR_PROPERTIES = /* @__PURE__ */ new Set([
@@ -6232,10 +6967,11 @@ var tokenOffence = (value, prefixes, local) => {
6232
6967
  };
6233
6968
  var tokensOnlyColors = {
6234
6969
  create(context) {
6235
- const { allow = [], tokenPrefix } = context.options?.[0] ?? {};
6970
+ const { allow = [], exemptModules = [], tokenPrefix } = context.options?.[0] ?? {};
6236
6971
  const prefixes = [requireOption("tokens-only-colors", tokenPrefix, "tokenPrefix")].flat();
6237
6972
  requireOption("tokens-only-colors", prefixes, "tokenPrefix");
6238
6973
  const filename = normalise(context.filename);
6974
+ if (exemptModules.some((module) => isModule(filename, module))) return {};
6239
6975
  if (allow.some((path2) => filename.includes(path2))) return {};
6240
6976
  return {
6241
6977
  JSXOpeningElement(node) {
@@ -6269,11 +7005,19 @@ var tokensOnlyColors = {
6269
7005
  \`style={{ color: 'var(--mc-primary)' }}\`, \`className="bg-brand-primary"\`. A \`var()\` that is NOT a
6270
7006
  token (\`var(--legacy-blue)\`) is the same offence spelled differently: it reaches around the theme, so
6271
7007
  the brand switches and the component does not \u2014 add the value to the theme and point at it there.
6272
- Configure \`tokenPrefix\` with the prefixes your themed properties use, and put the token layer itself
6273
- in \`allow\`, because that is the one place a literal belongs. Enabled with no \`tokenPrefix\` the rule
7008
+ Configure \`tokenPrefix\` with the prefixes your themed properties use, and NAME the token layer's
7009
+ modules in \`exemptModules\`, one per entry, because that is the one place a literal belongs \u2014
7010
+ \`allow\` still takes patterns and still grants its exemption to wherever the pattern stops, which is
7011
+ a boundary nobody decided and nobody counts. Enabled with no \`tokenPrefix\` the rule
6274
7012
  refuses the run: it cannot tell a token from a foreign variable, and a rule that cannot tell reads
6275
7013
  exactly like a clean tree.`,
6276
7014
  meta: {
7015
+ /**
7016
+ * #139: which option grants by PATTERN and which one ENUMERATES. Declared on the rule so the
7017
+ * doctor needs no list of its own — a rule that gains a path grant and forgets to say so is a
7018
+ * grant nothing counts, which is the defect this pair exists to end.
7019
+ */
7020
+ grants: { modules: "exemptModules", paths: "allow" },
6277
7021
  docs: {
6278
7022
  description: "Colours in a component come from the token layer. A colour literal in a className or an inline style fires, and so does a var() naming a custom property outside the configured token prefixes. Configure `tokenPrefix`; put the token layer in `allow`. Enabled without `tokenPrefix` it refuses the run."
6279
7023
  },
@@ -6286,6 +7030,7 @@ exactly like a clean tree.`,
6286
7030
  additionalProperties: false,
6287
7031
  properties: {
6288
7032
  allow: { items: { type: "string" }, type: "array" },
7033
+ exemptModules: { items: { type: "string" }, type: "array" },
6289
7034
  tokenPrefix: {
6290
7035
  anyOf: [
6291
7036
  { type: "string" },
@@ -6666,6 +7411,7 @@ var rules = refusingOnce(
6666
7411
  "emit-declares-attempts": emit_declares_attempts_default,
6667
7412
  "font-roles-only": font_roles_only_default,
6668
7413
  "layer-walls": layer_walls_default,
7414
+ "list-take-without-order": list_take_without_order_default,
6669
7415
  "max-comment-density": max_comment_density_default,
6670
7416
  "molecule-atoms-only": molecule_atoms_only_default,
6671
7417
  "molecule-must-compose": molecule_must_compose_default,
@@ -6679,7 +7425,9 @@ var rules = refusingOnce(
6679
7425
  "no-card-shaped-div": no_card_shaped_div_default,
6680
7426
  "no-cross-feature-stores": no_cross_feature_stores_default,
6681
7427
  "no-d1-transaction": no_d1_transaction_default,
7428
+ "no-date-string-compare": no_date_string_compare_default,
6682
7429
  "no-duplicate-jsx-patterns": no_duplicate_jsx_patterns_default,
7430
+ "no-generic-utility-package": no_generic_utility_package_default,
6683
7431
  "no-hook-in-component-disguise": no_hook_in_component_disguise_default,
6684
7432
  "no-index-count-as-exact": no_index_count_as_exact_default,
6685
7433
  "no-inert-hidden-jsx": no_inert_hidden_jsx_default,
@@ -6694,6 +7442,8 @@ var rules = refusingOnce(
6694
7442
  "no-react-namespace": no_react_namespace_default,
6695
7443
  "no-renamed-html-props": no_renamed_html_props_default,
6696
7444
  "no-render-prop-reader": no_render_prop_reader_default,
7445
+ "no-swallowed-catch": no_swallowed_catch_default,
7446
+ "no-runtime-dead-affordance": no_runtime_dead_affordance_default,
6697
7447
  "no-trivial-wrapper-component": no_trivial_wrapper_component_default,
6698
7448
  "no-ts-in-bio-folders": no_ts_in_bio_folders_default,
6699
7449
  "no-type-definitions-in-components": no_type_definitions_in_components_default,
@@ -6714,6 +7464,7 @@ var rules = refusingOnce(
6714
7464
  "subscriber-declares-id": subscriber_declares_id_default,
6715
7465
  "tables-declare-their-plane": tables_declare_their_plane_default,
6716
7466
  "tenant-tables-carry-org-id": tenant_tables_carry_org_id_default,
7467
+ "test-no-clock-polling-for-promise-flips": test_no_clock_polling_for_promise_flips_default,
6717
7468
  "time-through-the-door": time_through_the_door_default,
6718
7469
  "tokens-only-colors": tokens_only_colors_default,
6719
7470
  "tissue-must-compose": tissue_must_compose_default,