@geonosis/oxlint-plugin-biological-architecture 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/RULES.md +51 -16
  2. package/corpus/.oxlintrc.json +43 -9
  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/failure/good-refresh.ts +17 -0
  12. package/corpus/failure/logged.ts +6 -0
  13. package/corpus/failure/refresh.ts +4 -0
  14. package/corpus/failure/reload.ts +6 -0
  15. package/corpus/manifest.json +6 -0
  16. package/corpus/medusa/queries/bad-unordered-page.ts +20 -0
  17. package/corpus/medusa/queries/good-ordered-page.ts +23 -0
  18. package/corpus/messaging/providers/email-gmail.ts +2 -0
  19. package/corpus/messaging/providers/sms-twilio.ts +2 -0
  20. package/corpus/messaging/providers/whatsapp-360dialog.ts +2 -0
  21. package/corpus/messaging/registry.ts +5 -0
  22. package/corpus/packages/pricing/package.json +5 -0
  23. package/corpus/packages/pricing/src/index.ts +2 -0
  24. package/corpus/packages/utils/package.json +5 -0
  25. package/corpus/packages/utils/src/index.ts +4 -0
  26. package/corpus/suites/__tests__/awaits.ts +17 -0
  27. package/corpus/suites/__tests__/settles.ts +14 -0
  28. package/dist/{chunk-LTDP2733.js → chunk-KV7JRNAP.js} +7 -2
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.js +787 -52
  31. package/dist/presets.d.ts +3 -0
  32. package/dist/presets.js +1 -1
  33. package/package.json +2 -2
  34. /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";
@@ -899,6 +904,7 @@ through \`perKindPrefixes\`, the tree that holds the flows through \`within\` (a
899
904
  filename), and \`factories\` if the repo declares them under other names; enabled without
900
905
  \`perKindPrefixes\` or \`within\` the rule refuses the run.`,
901
906
  meta: {
907
+ floor: { name: "document", pending: true },
902
908
  docs: {
903
909
  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."
904
910
  },
@@ -963,6 +969,7 @@ the dialect is a seam fact, not a reason for a kind to own storage. The forbidde
963
969
  \`perKindTables\` option; enabled without it the rule refuses the run rather than passing every
964
970
  schema silently.`,
965
971
  meta: {
972
+ floor: { name: "document", pending: true },
966
973
  docs: {
967
974
  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."
968
975
  },
@@ -1427,9 +1434,10 @@ var variableOf = (value) => /^var\(\s*(--[A-Za-z0-9_-]+)/.exec(value.trim())?.[1
1427
1434
  var WEIGHT = /^\d+(?:\.\d+)?$/;
1428
1435
  var fontRolesOnly = {
1429
1436
  create(context) {
1430
- const { allow = [], prefix = "--font-", roles } = context.options?.[0] ?? {};
1437
+ const { allow = [], exemptModules = [], prefix = "--font-", roles } = context.options?.[0] ?? {};
1431
1438
  const declared = requireOption("font-roles-only", roles, "roles");
1432
1439
  const filename = normalise(context.filename);
1440
+ if (exemptModules.some((module) => isModule(filename, module))) return {};
1433
1441
  if (allow.some((path2) => filename.includes(path2))) return {};
1434
1442
  return {
1435
1443
  JSXOpeningElement(node) {
@@ -1460,11 +1468,19 @@ var fontRolesOnly = {
1460
1468
  sans-serif"\` or \`font-[Helvetica]\` with the role variable \u2014 \`var(--font-body)\`, \`var(--font-display)\`
1461
1469
  \u2014 or the utility that reads it. A \`var(--font-\u2026)\` naming a role the contract does not declare is the
1462
1470
  same offence: add the role to the theme, or use one that is there. Declare your role names in
1463
- \`roles\` and, if your variables are spelled differently, set \`prefix\`; put the theme file and any
1464
- \`@font-face\` sheet in \`allow\`. Bare \`font-*\` utility classes are deliberately not policed, because
1471
+ \`roles\` and, if your variables are spelled differently, set \`prefix\`; NAME the theme file and any
1472
+ \`@font-face\` sheet in \`exemptModules\`, one module per entry \u2014 \`allow\` still takes patterns and still
1473
+ grants its exemption to wherever the pattern stops, which is a boundary nobody decided and nobody
1474
+ counts. Bare \`font-*\` utility classes are deliberately not policed, because
1465
1475
  Tailwind puts weight and style under the same prefix. Enabled with no \`roles\` the rule refuses the
1466
1476
  run.`,
1467
1477
  meta: {
1478
+ /**
1479
+ * #139: which option grants by PATTERN and which one ENUMERATES. Declared on the rule so the
1480
+ * doctor needs no list of its own — a rule that gains a path grant and forgets to say so is a
1481
+ * grant nothing counts, which is the defect this pair exists to end.
1482
+ */
1483
+ grants: { modules: "exemptModules", paths: "allow" },
1468
1484
  docs: {
1469
1485
  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."
1470
1486
  },
@@ -1477,6 +1493,7 @@ run.`,
1477
1493
  additionalProperties: false,
1478
1494
  properties: {
1479
1495
  allow: { items: { type: "string" }, type: "array" },
1496
+ exemptModules: { items: { type: "string" }, type: "array" },
1480
1497
  prefix: { type: "string" },
1481
1498
  roles: { items: { type: "string" }, type: "array" }
1482
1499
  },
@@ -1645,6 +1662,81 @@ export const geonosisProbeWall = geonosisProbe
1645
1662
  };
1646
1663
  var layer_walls_default = layerWalls;
1647
1664
 
1665
+ // src/rules/list-take-without-order.ts
1666
+ var CALLEES = [String.raw`^(?:\w+\.)?list[A-Z]`, String.raw`^(?:\w+\.)?graph$`];
1667
+ var ORDER_KEYS = ["order", "orderBy"];
1668
+ var TAKE = "take";
1669
+ var keyNameOf = (node) => {
1670
+ const key = node.key;
1671
+ if (key?.type === "Identifier" && typeof key.name === "string") return key.name;
1672
+ return key?.type === "Literal" && typeof key.value === "string" ? key.value : null;
1673
+ };
1674
+ var spellingOf = (callee) => {
1675
+ if (callee === void 0) return null;
1676
+ if (callee.type === "Identifier") return callee.name ?? null;
1677
+ const property = propertyNameOf(callee);
1678
+ if (property === null) return null;
1679
+ const object = callee.object;
1680
+ return object?.type === "Identifier" ? `${String(object.name)}.${property}` : property;
1681
+ };
1682
+ var listTakeWithoutOrder = {
1683
+ create(context) {
1684
+ const options = bagOf(context.options);
1685
+ const callees = (options.callees ?? CALLEES).map((pattern) => new RegExp(pattern));
1686
+ const orderKeys = options.orderKeys ?? ORDER_KEYS;
1687
+ return {
1688
+ CallExpression(node) {
1689
+ const callee = spellingOf(node.callee);
1690
+ if (callee === null || !callees.some((pattern) => pattern.test(callee))) return;
1691
+ let takes = false;
1692
+ let orders = false;
1693
+ let unreadable = false;
1694
+ for (const argument of node.arguments ?? []) {
1695
+ walkNode(argument, (inner) => {
1696
+ if (inner.type === "SpreadElement") unreadable = true;
1697
+ if (inner.type !== "Property") return;
1698
+ const key = keyNameOf(inner);
1699
+ if (key === TAKE) takes = true;
1700
+ if (key !== null && orderKeys.includes(key)) orders = true;
1701
+ });
1702
+ }
1703
+ if (!takes || orders || unreadable) return;
1704
+ context.report({
1705
+ data: { callee, order: orderKeys[0] ?? "" },
1706
+ messageId: "unordered",
1707
+ node
1708
+ });
1709
+ }
1710
+ };
1711
+ },
1712
+ fixShape: `A page taken in no order answers a question about the page, not about the table. A \`list*()\`
1713
+ or \`query.graph\` call carrying \`take\` names an \`order\` beside it \u2014 the column the page is a page OF \u2014
1714
+ so the first row is the first row every time, and the row a previous stage just wrote is where the
1715
+ reader expects it. Without one the database returns whatever is cheapest, which is stable in
1716
+ development and different under load. A spread the rule cannot read is left alone. Which calls are
1717
+ list calls is the \`callees\` option and which key declares the ordering is \`orderKeys\`.`,
1718
+ meta: {
1719
+ docs: {
1720
+ 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."
1721
+ },
1722
+ messages: {
1723
+ 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."
1724
+ },
1725
+ schema: [
1726
+ {
1727
+ additionalProperties: false,
1728
+ properties: {
1729
+ callees: { items: { type: "string" }, type: "array" },
1730
+ orderKeys: { items: { type: "string" }, type: "array" }
1731
+ },
1732
+ type: "object"
1733
+ }
1734
+ ],
1735
+ type: "problem"
1736
+ }
1737
+ };
1738
+ var list_take_without_order_default = listTakeWithoutOrder;
1739
+
1648
1740
  // src/rules/max-comment-density.ts
1649
1741
  var exemptFiles2 = [
1650
1742
  /\/__tests__\//,
@@ -2426,6 +2518,79 @@ saga, not the transaction.`,
2426
2518
  };
2427
2519
  var no_d1_transaction_default = noD1Transaction;
2428
2520
 
2521
+ // src/rules/no-date-string-compare.ts
2522
+ var FIELD_PATTERN = "(?:_at|[a-z]At)$";
2523
+ var RELATIONAL = /* @__PURE__ */ new Set(["<", "<=", ">", ">="]);
2524
+ var COMPARE = "localeCompare";
2525
+ var STRINGIFY = "String";
2526
+ var fieldStringified = (node, holdsATime) => {
2527
+ if (node === void 0) return null;
2528
+ const named = (subject) => {
2529
+ if (subject === void 0) return null;
2530
+ const property = propertyNameOf(subject);
2531
+ if (property !== null) return holdsATime.test(property) ? property : null;
2532
+ return subject.type === "Identifier" && typeof subject.name === "string" && holdsATime.test(subject.name) ? subject.name : null;
2533
+ };
2534
+ if (node.type === "CallExpression" && calleeNameOf(node) === STRINGIFY) {
2535
+ const [argument] = node.arguments ?? [];
2536
+ return named(argument);
2537
+ }
2538
+ if (node.type !== "TemplateLiteral") return null;
2539
+ const expressions = node.expressions ?? [];
2540
+ const quasis = node.quasis ?? [];
2541
+ const empty = quasis.every((one) => (one.value?.raw ?? "") === "");
2542
+ return expressions.length === 1 && empty ? named(expressions[0]) : null;
2543
+ };
2544
+ var noDateStringCompare = {
2545
+ create(context) {
2546
+ const holdsATime = new RegExp(bagOf(context.options).fieldPattern ?? FIELD_PATTERN);
2547
+ const reportFirst = (node, sides, ordering) => {
2548
+ for (const side of sides) {
2549
+ const field = fieldStringified(side, holdsATime);
2550
+ if (field === null) continue;
2551
+ context.report({ data: { field, ordering }, messageId: "textOrder", node });
2552
+ return;
2553
+ }
2554
+ };
2555
+ return {
2556
+ BinaryExpression(node) {
2557
+ if (!RELATIONAL.has(node.operator)) return;
2558
+ reportFirst(node, [node.left, node.right], node.operator);
2559
+ },
2560
+ CallExpression(node) {
2561
+ if (propertyNameOf(node.callee) !== COMPARE) return;
2562
+ const receiver = node.callee.object;
2563
+ const [argument] = node.arguments ?? [];
2564
+ reportFirst(node, [receiver, argument], COMPARE);
2565
+ }
2566
+ };
2567
+ },
2568
+ fixShape: `Order a timestamp by its time, never by its text. \`String(row.created_at)\` on a Date is
2569
+ "Fri Aug 30 2026 \u2026", so "newest first" becomes "starts with F" and the sort is stable, plausible and
2570
+ wrong. Compare \`new Date(a.created_at).getTime()\` against \`Date.parse(b.created_at)\`, or sort on the
2571
+ ISO string the API returned \u2014 an ISO-8601 string does order lexicographically, which is why the
2572
+ defect survives review. The rule reports only a field turned INTO text: a raw timestamp under \`<\`
2573
+ and a template with copy in it are both left alone. Which names hold a timestamp is the
2574
+ \`fieldPattern\` option, defaulting to the \`_at\` / \`\u2026At\` spellings.`,
2575
+ meta: {
2576
+ docs: {
2577
+ 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."
2578
+ },
2579
+ messages: {
2580
+ 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.'
2581
+ },
2582
+ schema: [
2583
+ {
2584
+ additionalProperties: false,
2585
+ properties: { fieldPattern: { type: "string" } },
2586
+ type: "object"
2587
+ }
2588
+ ],
2589
+ type: "problem"
2590
+ }
2591
+ };
2592
+ var no_date_string_compare_default = noDateStringCompare;
2593
+
2429
2594
  // src/rules/no-duplicate-jsx-patterns.ts
2430
2595
  var COMPONENT_FILE_PATTERN = /(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
2431
2596
  var MIN_CLASS_LENGTH = 30;
@@ -2518,6 +2683,90 @@ their text are one component with a prop.`,
2518
2683
  };
2519
2684
  var no_duplicate_jsx_patterns_default = noDuplicateJsxPatterns;
2520
2685
 
2686
+ // src/rules/no-generic-utility-package.ts
2687
+ import { readFileSync as readFileSync3 } from "fs";
2688
+ var NAMES = ["common", "helpers", "misc", "shared", "utils"];
2689
+ var ENTRIES = [
2690
+ "index.ts",
2691
+ "index.tsx",
2692
+ "index.js",
2693
+ "index.jsx",
2694
+ "src/index.ts",
2695
+ "src/index.tsx",
2696
+ "src/index.js",
2697
+ "src/index.jsx"
2698
+ ];
2699
+ var MANIFEST = "package.json";
2700
+ var nameOf2 = (root) => {
2701
+ try {
2702
+ const manifest = JSON.parse(readFileSync3(`${root}/${MANIFEST}`, "utf8"));
2703
+ return typeof manifest.name === "string" ? manifest.name : null;
2704
+ } catch {
2705
+ return null;
2706
+ }
2707
+ };
2708
+ var noGenericUtilityPackage = {
2709
+ create(context) {
2710
+ const options = bagOf(context.options);
2711
+ const names2 = new Set(options.names ?? NAMES);
2712
+ const entries = options.entries ?? ENTRIES;
2713
+ const filename = normalise(context.filename);
2714
+ return {
2715
+ Program(node) {
2716
+ const entry = entries.toSorted((a, b) => b.length - a.length).find((one) => filename.endsWith(`/${one}`));
2717
+ if (entry === void 0) return;
2718
+ const name = nameOf2(filename.slice(0, filename.length - entry.length - 1));
2719
+ if (name === null) return;
2720
+ const last = name.split("/").at(-1) ?? "";
2721
+ if (!names2.has(last)) return;
2722
+ context.report({ data: { generic: last, name }, messageId: "generic", node });
2723
+ }
2724
+ };
2725
+ },
2726
+ fixShape: `A package is named for what it does. \`utils\`, \`common\`, \`helpers\`, \`shared\`, \`misc\` say nothing
2727
+ about what is inside, so nothing can be refused entry and everything arrives: web helpers, a constants
2728
+ object, a time door. Split it by what each export is FOR and give each half a name that would make a
2729
+ wrong import obviously wrong \u2014 the formatting goes to the feature's own lib, the constants go beside
2730
+ their owner, a door gets its own package named after the thing it is a door to. Delete the rest. The
2731
+ anti-names are the \`names\` option and the default is the universal list, never a repo's vocabulary.`,
2732
+ meta: {
2733
+ docs: {
2734
+ 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."
2735
+ },
2736
+ messages: {
2737
+ 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.'
2738
+ },
2739
+ schema: [
2740
+ {
2741
+ additionalProperties: false,
2742
+ properties: {
2743
+ entries: { items: { type: "string" }, type: "array" },
2744
+ names: { items: { type: "string" }, type: "array" }
2745
+ },
2746
+ type: "object"
2747
+ }
2748
+ ],
2749
+ type: "problem"
2750
+ },
2751
+ /** The manifest AND the entry: the name is in the manifest, and the finding lands on the entry. */
2752
+ probe(options) {
2753
+ const bag = bagOf(options);
2754
+ const generic = (bag.names ?? NAMES)[0] ?? "";
2755
+ const entry = (bag.entries ?? ENTRIES)[0] ?? "";
2756
+ return {
2757
+ files: [
2758
+ { path: `packages/${generic}/${entry}`, source: "export const entry = 1\n" },
2759
+ {
2760
+ path: `packages/${generic}/${MANIFEST}`,
2761
+ source: `{ "name": "${generic}", "version": "0.0.0" }
2762
+ `
2763
+ }
2764
+ ]
2765
+ };
2766
+ }
2767
+ };
2768
+ var no_generic_utility_package_default = noGenericUtilityPackage;
2769
+
2521
2770
  // src/rules/no-hook-in-component-disguise.ts
2522
2771
  var EXPLICIT_SIDE_EFFECT_HOOKS = /* @__PURE__ */ new Set([
2523
2772
  "useEffect",
@@ -3565,6 +3814,154 @@ The state goes in an organelle that renders its own JSX; the arrangement goes in
3565
3814
  };
3566
3815
  var no_render_prop_reader_default = noRenderPropReader;
3567
3816
 
3817
+ // src/rules/no-runtime-dead-affordance.ts
3818
+ var HANDS_IT_ON = /* @__PURE__ */ new Set(["asChild", "disabled", "form", "formAction"]);
3819
+ var PRESSED_BY_THE_FORM = /* @__PURE__ */ new Set(["submit", "reset"]);
3820
+ var nameOf3 = (attribute) => attribute.name?.type === "JSXIdentifier" ? attribute.name.name ?? "" : "";
3821
+ var arms = (attribute) => {
3822
+ if (attribute.type !== "JSXAttribute") return true;
3823
+ const name = nameOf3(attribute);
3824
+ if (name.startsWith("on") && name.length > 2) return true;
3825
+ if (HANDS_IT_ON.has(name)) return true;
3826
+ if (name !== "type") return false;
3827
+ const value = attribute.value;
3828
+ if (value === null || value === void 0) return false;
3829
+ return value.type === "Literal" ? PRESSED_BY_THE_FORM.has(String(value.value)) : (
3830
+ // An expression is a type this rule cannot read.
3831
+ true
3832
+ );
3833
+ };
3834
+ var noRuntimeDeadAffordance = {
3835
+ create(context) {
3836
+ const controls = new Set((context.options[0] ?? []).map((one) => one.atom));
3837
+ return {
3838
+ JSXOpeningElement(node) {
3839
+ if (node?.name?.type !== "JSXIdentifier") return;
3840
+ const atom = node.name.name ?? "";
3841
+ if (!controls.has(atom)) return;
3842
+ if ((node.attributes ?? []).some(arms)) return;
3843
+ context.report({ data: { atom }, messageId: "dead", node });
3844
+ }
3845
+ };
3846
+ },
3847
+ fixShape: `A control that renders and does nothing is worse than no control: it looks pressable and
3848
+ answers nothing. Every control atom the rule's map names carries one of \`onClick\` (or another
3849
+ \`on\u2026\` handler), \`asChild\` so the child it wraps carries the behaviour, \`type="submit"\`/\`"reset"\` so
3850
+ the form presses it, \`form\`/\`formAction\`, or \`disabled\` when it is inert on purpose. \`type="button"\`
3851
+ is the default and presses nothing, so it arms nothing. A spread is a prop bag the rule cannot read
3852
+ and is left alone. Configure the same element\u2192atom map \`no-raw-html-atoms\` takes; enabled without
3853
+ one the rule refuses the run.`,
3854
+ meta: {
3855
+ docs: {
3856
+ 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`."
3857
+ },
3858
+ messages: {
3859
+ 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.'
3860
+ },
3861
+ schema: [
3862
+ {
3863
+ items: {
3864
+ properties: {
3865
+ atom: { type: "string" },
3866
+ element: { type: "string" },
3867
+ importPath: { type: "string" }
3868
+ },
3869
+ required: ["element", "atom", "importPath"],
3870
+ type: "object"
3871
+ },
3872
+ type: "array"
3873
+ }
3874
+ ],
3875
+ type: "problem"
3876
+ },
3877
+ /** The map is POSITIONAL, so the probe reads `options[0]` rather than a bag. */
3878
+ probe(options) {
3879
+ const map = Array.isArray(options[0]) ? options[0] : [];
3880
+ const atom = map[0]?.atom;
3881
+ if (atom === void 0) {
3882
+ throw new Error(
3883
+ "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."
3884
+ );
3885
+ }
3886
+ return {
3887
+ path: "cells/geonosis-probe.tsx",
3888
+ source: `export const GeonosisProbe = () => <${atom} />
3889
+ `
3890
+ };
3891
+ }
3892
+ };
3893
+ var no_runtime_dead_affordance_default = noRuntimeDeadAffordance;
3894
+
3895
+ // src/rules/no-swallowed-catch.ts
3896
+ var CATCH = "catch";
3897
+ var LOGGER = "console";
3898
+ var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression"]);
3899
+ var isLogging = (node) => {
3900
+ const callee = node.callee;
3901
+ const object = callee?.object;
3902
+ return object?.type === "Identifier" && object.name === LOGGER;
3903
+ };
3904
+ var swallows = (body) => {
3905
+ let answers = false;
3906
+ walkNode(body, (node) => {
3907
+ if (node.type === "ThrowStatement") answers = true;
3908
+ if (node.type === "AssignmentExpression" || node.type === "UpdateExpression") answers = true;
3909
+ if (node.type === "CallExpression" && !isLogging(node)) answers = true;
3910
+ });
3911
+ return !answers;
3912
+ };
3913
+ var noSwallowedCatch = {
3914
+ create(context) {
3915
+ const paths = bagOf(context.options).paths ?? [];
3916
+ const filename = normalise(context.filename);
3917
+ if (!paths.some((pattern) => new RegExp(pattern).test(filename))) return {};
3918
+ const report = (node, shape) => {
3919
+ context.report({ data: { shape }, messageId: "swallowed", node });
3920
+ };
3921
+ return {
3922
+ CallExpression(node) {
3923
+ if (propertyNameOf(node.callee) !== CATCH) return;
3924
+ const [handler] = node.arguments ?? [];
3925
+ if (handler === void 0 || !FUNCTIONS.has(handler.type ?? "")) return;
3926
+ if (swallows(handler.body)) report(handler, ".catch()");
3927
+ },
3928
+ CatchClause(node) {
3929
+ if (swallows(node.body)) report(node, "catch block");
3930
+ }
3931
+ };
3932
+ },
3933
+ fixShape: `A rejection on a fetch or refresh path is answered, never dropped. An empty \`.catch(() => {})\`,
3934
+ an empty \`catch {}\` and a handler that only logs all leave the caller reading a failed refresh as a
3935
+ successful one. Write the failure somewhere the caller can see it \u2014 set an error flag, put the state
3936
+ back, mark the value stale \u2014 or rethrow it and let the boundary above decide. Which modules are the
3937
+ fetch/refresh surface is the \`paths\` option; enabled without it the rule refuses the run, because a
3938
+ rule with no surface to watch reads exactly like a tree with no swallowed rejection in it.`,
3939
+ meta: {
3940
+ docs: {
3941
+ 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."
3942
+ },
3943
+ messages: {
3944
+ 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."
3945
+ },
3946
+ schema: [
3947
+ {
3948
+ additionalProperties: false,
3949
+ properties: { paths: { items: { type: "string" }, type: "array" } },
3950
+ type: "object"
3951
+ }
3952
+ ],
3953
+ type: "problem"
3954
+ },
3955
+ probe(options) {
3956
+ return {
3957
+ path: pathMatching(firstOf(bagOf(options).paths, []), "refresh.ts"),
3958
+ source: `export const refresh = (load: () => Promise<void>) => load().catch(() => {})
3959
+ `
3960
+ };
3961
+ }
3962
+ };
3963
+ var no_swallowed_catch_default = noSwallowedCatch;
3964
+
3568
3965
  // src/rules/no-trivial-wrapper-component.ts
3569
3966
  var ROUTE_BASENAMES = /* @__PURE__ */ new Set([
3570
3967
  "default.tsx",
@@ -3800,9 +4197,9 @@ and is imported.`,
3800
4197
  var no_type_definitions_in_components_default = noTypeDefinitionsInComponents;
3801
4198
 
3802
4199
  // src/rules/no-unregistered-plugin-dir.ts
3803
- import { readFileSync as readFileSync3 } from "fs";
4200
+ import { readFileSync as readFileSync4 } from "fs";
3804
4201
  var RULE11 = "no-unregistered-plugin-dir";
3805
- var ACCEPTS3 = "{ roots: string[], registry: string | string[], manifests: string[] }";
4202
+ var ACCEPTS3 = "{ roots: (string | { path: string, files: boolean })[], registry: string | string[], manifests: string[] }";
3806
4203
  var trimSlashes = (path2) => path2.replace(/^\/+/, "").replace(/\/+$/, "");
3807
4204
  var withoutComments = (source) => source.replaceAll(/\/\*[\s\S]*?\*\//g, " ").replaceAll(/\/\/[^\n]*/g, " ");
3808
4205
  var literalsOf = (source) => [...withoutComments(source).matchAll(/['"`]([^'"`\n]+)['"`]/g)].map((match) => match[1] ?? "");
@@ -3816,10 +4213,22 @@ var rootedAt = (filename, root) => {
3816
4213
  if (at < 0) return null;
3817
4214
  return { after: filename.slice(at + marker.length), repoRoot: filename.slice(0, at) };
3818
4215
  };
4216
+ var oneDirectory = (segments, manifests) => {
4217
+ if (segments.length !== 2) return null;
4218
+ const [directory, file] = segments;
4219
+ return directory !== void 0 && file !== void 0 && manifests.has(file) ? directory : null;
4220
+ };
4221
+ var oneFile = (segments) => {
4222
+ if (segments.length !== 1) return null;
4223
+ const [file] = segments;
4224
+ return file === void 0 ? null : file.replace(/\.[cm]?[jt]sx?$/, "");
4225
+ };
3819
4226
  var noUnregisteredPluginDir = {
3820
4227
  create(context) {
3821
4228
  const options = context.options?.[0] ?? {};
3822
- const roots = requireOption(RULE11, options.roots, "roots", ACCEPTS3).map(trimSlashes);
4229
+ const roots = requireOption(RULE11, options.roots, "roots", ACCEPTS3).map(
4230
+ (one) => typeof one === "string" ? { files: false, path: trimSlashes(one) } : { files: one.files === true, path: trimSlashes(one.path) }
4231
+ );
3823
4232
  const declared = requireOption(RULE11, options.registry, "registry", ACCEPTS3);
3824
4233
  const registries = (typeof declared === "string" ? [declared] : declared).map(trimSlashes);
3825
4234
  const manifests = new Set(requireOption(RULE11, options.manifests, "manifests", ACCEPTS3));
@@ -3827,21 +4236,24 @@ var noUnregisteredPluginDir = {
3827
4236
  return {
3828
4237
  Program(node) {
3829
4238
  for (const root of roots) {
3830
- const rooted = rootedAt(filename, root);
4239
+ const rooted = rootedAt(filename, root.path);
3831
4240
  if (rooted === null) continue;
3832
4241
  const segments = rooted.after.split("/");
3833
- if (segments.length !== 2) continue;
3834
- const [directory, file] = segments;
3835
- if (directory === void 0 || file === void 0 || !manifests.has(file)) continue;
4242
+ const registrant = root.files ? oneFile(segments) : oneDirectory(segments, manifests);
4243
+ if (registrant === null) continue;
3836
4244
  let source = "";
3837
4245
  try {
3838
- source = registries.map((half) => readFileSync3(`${rooted.repoRoot}/${half}`, "utf8")).join("\n");
4246
+ source = registries.map((half) => readFileSync4(`${rooted.repoRoot}/${half}`, "utf8")).join("\n");
3839
4247
  } catch {
3840
4248
  return;
3841
4249
  }
3842
- if (literalsOf(source).some((literal) => names(literal, directory))) return;
4250
+ if (literalsOf(source).some((literal) => names(literal, registrant))) return;
3843
4251
  const registry = registries.join(", ");
3844
- context.report({ data: { directory, registry, root }, messageId: "unregistered", node });
4252
+ context.report({
4253
+ data: { directory: registrant, registry, root: root.path },
4254
+ messageId: "unregistered",
4255
+ node
4256
+ });
3845
4257
  return;
3846
4258
  }
3847
4259
  }
@@ -3854,7 +4266,9 @@ eight of them. Configure \`roots\`, \`registry\` and \`manifests\` \u2014 all th
3854
4266
  registry is SPLIT across files (a config that imports a second config): name every half, because the
3855
4267
  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
3856
4268
  would match nothing and report a clean tree it never looked at. A seam whose providers are flat files
3857
- rather than directories is out of scope \u2014 nothing under a root matches, so nothing is reported.`,
4269
+ rather than directories is judged when its root says \`files: true\` \u2014 \`{ path, files: true }\` makes each
4270
+ FILE under that root a registrant named by its own basename \u2014 and is out of scope under a bare root,
4271
+ where nothing matches and nothing is reported.`,
3858
4272
  meta: {
3859
4273
  docs: {
3860
4274
  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."
@@ -3873,7 +4287,23 @@ rather than directories is out of scope \u2014 nothing under a root matches, so
3873
4287
  { items: { type: "string" }, type: "array" }
3874
4288
  ]
3875
4289
  },
3876
- roots: { items: { type: "string" }, type: "array" }
4290
+ roots: {
4291
+ items: {
4292
+ anyOf: [
4293
+ { type: "string" },
4294
+ {
4295
+ additionalProperties: false,
4296
+ properties: {
4297
+ files: { type: "boolean" },
4298
+ path: { type: "string" }
4299
+ },
4300
+ required: ["path"],
4301
+ type: "object"
4302
+ }
4303
+ ]
4304
+ },
4305
+ type: "array"
4306
+ }
3877
4307
  },
3878
4308
  type: "object"
3879
4309
  }
@@ -3894,12 +4324,14 @@ rather than directories is out of scope \u2014 nothing under a root matches, so
3894
4324
  */
3895
4325
  probe(options) {
3896
4326
  const { manifests = [], registry = "", roots = [] } = bagOf(options);
3897
- const root = trimSlashes(roots[0] ?? "");
4327
+ const first = roots[0] ?? "";
4328
+ const root = typeof first === "string" ? { files: false, path: first } : first;
4329
+ const under = trimSlashes(root.path);
3898
4330
  const half = typeof registry === "string" ? registry : registry[0] ?? "";
3899
4331
  return {
3900
4332
  files: [
3901
4333
  {
3902
- path: `${root}/geonosis-probe/${manifests[0] ?? ""}`,
4334
+ path: root.files === true ? `${under}/geonosis-probe.ts` : `${under}/geonosis-probe/${manifests[0] ?? ""}`,
3903
4335
  source: "export const geonosisProbe = { id: 'geonosis-probe' }\n"
3904
4336
  },
3905
4337
  { path: trimSlashes(half), source: "export const registry = { providers: [] }\n" }
@@ -4750,7 +5182,7 @@ var isFactShaped = (literal) => {
4750
5182
  if (/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/.test(literal)) return true;
4751
5183
  return literal.length >= 16 && !/^[a-z0-9:\-[\]/.%#!]+$/.test(literal);
4752
5184
  };
4753
- var nameOf2 = (node) => {
5185
+ var nameOf4 = (node) => {
4754
5186
  if (node === void 0) return void 0;
4755
5187
  if (typeof node === "string") return node;
4756
5188
  if (node.type === "Identifier" || node.type === "JSXIdentifier") return node.name;
@@ -4761,12 +5193,12 @@ var isSpelledAsStyleOrModule = (node) => {
4761
5193
  let current = node.parent;
4762
5194
  let child = node;
4763
5195
  while (current && typeof current.type === "string" && current.type !== "Program") {
4764
- if (current.type === "JSXAttribute") return styleAttributes.has(nameOf2(current.name) ?? "");
5196
+ if (current.type === "JSXAttribute") return styleAttributes.has(nameOf4(current.name) ?? "");
4765
5197
  if (current.type === "Property" && current.key === child) return true;
4766
- if (current.type === "Property" && styleAttributes.has(nameOf2(current.key) ?? "")) return true;
5198
+ if (current.type === "Property" && styleAttributes.has(nameOf4(current.key) ?? "")) return true;
4767
5199
  if (current.type === "CallExpression") {
4768
5200
  const callee = current.callee;
4769
- const calleeName = callee?.type === "MemberExpression" ? nameOf2(callee.property) : nameOf2(callee);
5201
+ const calleeName = callee?.type === "MemberExpression" ? nameOf4(callee.property) : nameOf4(callee);
4770
5202
  if (styleCallees.has(calleeName ?? "") || formFieldCallees.has(calleeName ?? "")) return true;
4771
5203
  const firstArgument = current.arguments?.[0];
4772
5204
  if (declarationCallees.has(calleeName ?? "") && firstArgument === child) return true;
@@ -5003,6 +5435,7 @@ On \`sagaflow-js\` there is no such seam and the older shape stays reachable: se
5003
5435
  Configure \`within\`; \`hosts\`, \`scopeKey\`, \`engine\` and \`wrapper\` have defaults or are off. Enabled
5004
5436
  without \`within\` the rule refuses the run.`,
5005
5437
  meta: {
5438
+ floor: { name: "workflows" },
5006
5439
  docs: {
5007
5440
  description: "A durable host declares the scope its steps run in \u2014 `scope.perStep` on the runtime it builds \u2014 because a durable run hibernates and retries, so a handle opened once per run is dead by the time a later step uses it. A repo on an engine with no such seam names its `engine` and `wrapper` instead, and a saga importing the engine's `step` around that wrapper is the finding. The guarded tree is required; enabled without it the rule refuses the run."
5008
5441
  },
@@ -5057,8 +5490,8 @@ export const probe = step('geonosis-probe', async () => ({ ok: true }))
5057
5490
  var step_opens_its_own_cell_default = stepOpensItsOwnCell;
5058
5491
 
5059
5492
  // src/rules/store-route-scopes-tenant-data.ts
5060
- var STORE_ROUTE_PATTERN = /\/api\/store\/.*\/route\.tsx?$/;
5061
- var TENANT_ENTITIES = /* @__PURE__ */ new Set([
5493
+ import { readFileSync as readFileSync5 } from "fs";
5494
+ var ENTITIES = [
5062
5495
  "cart",
5063
5496
  "claim",
5064
5497
  "company",
@@ -5068,27 +5501,136 @@ var TENANT_ENTITIES = /* @__PURE__ */ new Set([
5068
5501
  "planned_order",
5069
5502
  "production_order",
5070
5503
  "quote"
5504
+ ];
5505
+ var ROUTES = [String.raw`/api/store/.*/route\.tsx?$`];
5506
+ var SCOPING_CALLS = ["auth_context", "actor_id", "customer_id", "company_id"];
5507
+ var ENTITY_KEY = "entity";
5508
+ var SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
5509
+ var keyNameOf2 = (node) => {
5510
+ const key = node.key;
5511
+ if (key?.type === "Identifier" && typeof key.name === "string") return key.name;
5512
+ return key?.type === "Literal" && typeof key.value === "string" ? key.value : null;
5513
+ };
5514
+ var stringValueOf = (node) => {
5515
+ const value = node.value;
5516
+ return value?.type === "Literal" && typeof value.value === "string" ? value.value : null;
5517
+ };
5518
+ var withoutComments2 = (text) => {
5519
+ let out = "";
5520
+ let at = 0;
5521
+ let quote = "";
5522
+ while (at < text.length) {
5523
+ const here = text[at] ?? "";
5524
+ const next = text[at + 1] ?? "";
5525
+ if (quote !== "") {
5526
+ if (here === "\\") {
5527
+ out += " ";
5528
+ at += 2;
5529
+ continue;
5530
+ }
5531
+ if (here === quote) quote = "";
5532
+ out += here;
5533
+ at += 1;
5534
+ continue;
5535
+ }
5536
+ if (here === '"' || here === "'" || here === "`") {
5537
+ quote = here;
5538
+ out += here;
5539
+ at += 1;
5540
+ continue;
5541
+ }
5542
+ if (here === "/" && next === "/") {
5543
+ while (at < text.length && text[at] !== "\n") {
5544
+ out += " ";
5545
+ at += 1;
5546
+ }
5547
+ continue;
5548
+ }
5549
+ if (here === "/" && next === "*") {
5550
+ const end = text.indexOf("*/", at + 2);
5551
+ const stop = end === -1 ? text.length : end + 2;
5552
+ for (; at < stop; at += 1) out += text[at] === "\n" ? "\n" : " ";
5553
+ continue;
5554
+ }
5555
+ out += here;
5556
+ at += 1;
5557
+ }
5558
+ return out;
5559
+ };
5560
+ var siblingSourceOf = (importer, source) => {
5561
+ if (!source.startsWith(".")) return null;
5562
+ const landed = resolveRelative(importer, source);
5563
+ for (const suffix of SUFFIXES) {
5564
+ try {
5565
+ return withoutComments2(readFileSync5(`${landed}${suffix}`, "utf8"));
5566
+ } catch {
5567
+ continue;
5568
+ }
5569
+ }
5570
+ return null;
5571
+ };
5572
+ var MODULE_NODES = /* @__PURE__ */ new Set([
5573
+ "CallExpression",
5574
+ "ExportAllDeclaration",
5575
+ "ExportNamedDeclaration",
5576
+ "ImportDeclaration",
5577
+ "ImportExpression"
5071
5578
  ]);
5072
- var SCOPE_MARKERS = /auth_context|actor_id|customer_id|company_id|\bcustomer\b\s*:/;
5579
+ var readIn = (text, entities) => {
5580
+ for (const entity of entities) {
5581
+ if (new RegExp(`${ENTITY_KEY}\\s*:\\s*['"\`]${entity}['"\`]`).test(text)) return entity;
5582
+ }
5583
+ return null;
5584
+ };
5585
+ var namesAny = (text, names2) => names2.some((name) => new RegExp(`\\b${name}\\b`).test(text));
5073
5586
  var storeRouteScopesTenantData = {
5074
5587
  create(context) {
5075
- const normalized = normalise(context.filename);
5076
- if (!STORE_ROUTE_PATTERN.test(normalized)) return {};
5077
- const shortName = normalized.split("/").slice(-3).join("/");
5588
+ const options = bagOf(context.options);
5589
+ const routes = options.routes ?? ROUTES;
5590
+ const entities = options.entities ?? ENTITIES;
5591
+ const narrowing = [...options.scopingCalls ?? SCOPING_CALLS, ...options.evidenceCalls ?? []];
5592
+ const filename = normalise(context.filename);
5593
+ if (!routes.some((pattern) => new RegExp(pattern).test(filename))) return {};
5594
+ const shortName = filename.split("/").slice(-3).join("/");
5078
5595
  return {
5079
- Program(node) {
5080
- const text = context.sourceCode?.text ?? "";
5081
- if (text === "" || SCOPE_MARKERS.test(text)) return;
5082
- for (const entity of TENANT_ENTITIES) {
5083
- const reads = new RegExp(`entity:\\s*['"\`]${entity}['"\`]`).test(text);
5084
- if (reads) {
5085
- context.report({
5086
- data: { entity, file: shortName },
5087
- messageId: "unscoped",
5088
- node
5089
- });
5090
- return;
5596
+ Program(program) {
5597
+ let read = null;
5598
+ let scoped = false;
5599
+ const sources = [];
5600
+ walkNode(program, (node) => {
5601
+ if (node.type === "Property" && keyNameOf2(node) === ENTITY_KEY) {
5602
+ const entity = stringValueOf(node);
5603
+ if (entity !== null && entities.includes(entity) && read === null) {
5604
+ read = { entity, node };
5605
+ }
5606
+ }
5607
+ if (node.type === "Identifier" && narrowing.includes(node.name)) scoped = true;
5608
+ if (MODULE_NODES.has(node.type ?? "")) {
5609
+ const source = sourceOf(node);
5610
+ if (source !== null) sources.push(source);
5091
5611
  }
5612
+ });
5613
+ if (scoped) return;
5614
+ if (read !== null) {
5615
+ const found = read;
5616
+ context.report({
5617
+ data: { entity: found.entity, file: shortName },
5618
+ messageId: "unscoped",
5619
+ node: found.node
5620
+ });
5621
+ return;
5622
+ }
5623
+ for (const source of sources) {
5624
+ const text = siblingSourceOf(filename, source);
5625
+ if (text === null || namesAny(text, narrowing)) continue;
5626
+ const entity = readIn(text, entities);
5627
+ if (entity === null) continue;
5628
+ context.report({
5629
+ data: { entity, file: shortName, through: source },
5630
+ messageId: "unscopedSibling",
5631
+ node: program
5632
+ });
5633
+ return;
5092
5634
  }
5093
5635
  }
5094
5636
  };
@@ -5096,17 +5638,43 @@ var storeRouteScopesTenantData = {
5096
5638
  fixShape: `A customer-facing store route that reads a customer-owned entity (cart, order, quote, company, \u2026)
5097
5639
  narrows the read to the authenticated caller \u2014 \`auth_context\`, \`actor_id\`, \`customer_id\` or
5098
5640
  \`company_id\` must appear in the handler. Without it a publishable key returns every tenant's rows.
5099
- Global catalog entities are deliberately exempt; if the data is genuinely not customer data, it
5100
- belongs on the admin surface.`,
5641
+ A route the caller proves entitlement to with EVIDENCE instead of a session \u2014 an order reference
5642
+ plus the email it was placed with \u2014 calls a predicate the config names in \`evidenceCalls\`; the rule
5643
+ trusts no predicate it was not given. Moving the read into the module beside the route does not
5644
+ answer the question: the rule follows a relative import one hop and reads the scoping there. Which
5645
+ files are the route surface, which entities are customer-owned and which names count as scoping are
5646
+ all options. Global catalog entities are deliberately exempt; if the data is genuinely not customer
5647
+ data, it belongs on the admin surface.`,
5101
5648
  meta: {
5102
5649
  docs: {
5103
- 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."
5650
+ 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."
5104
5651
  },
5105
5652
  messages: {
5106
- 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.'
5653
+ 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.',
5654
+ 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`.'
5107
5655
  },
5108
- schema: [],
5656
+ schema: [
5657
+ {
5658
+ additionalProperties: false,
5659
+ properties: {
5660
+ entities: { items: { type: "string" }, type: "array" },
5661
+ evidenceCalls: { items: { type: "string" }, type: "array" },
5662
+ routes: { items: { type: "string" }, type: "array" },
5663
+ scopingCalls: { items: { type: "string" }, type: "array" }
5664
+ },
5665
+ type: "object"
5666
+ }
5667
+ ],
5109
5668
  type: "problem"
5669
+ },
5670
+ probe(options) {
5671
+ const bag = bagOf(options);
5672
+ return {
5673
+ path: pathMatching(firstOf(bag.routes, ROUTES), "route.ts"),
5674
+ source: `export const GET = async (query: { graph: (c: unknown) => Promise<unknown> }) =>
5675
+ query.graph({ entity: '${firstOf(bag.entities, ENTITIES)}' })
5676
+ `
5677
+ };
5110
5678
  }
5111
5679
  };
5112
5680
  var store_route_scopes_tenant_data_default = storeRouteScopesTenantData;
@@ -5361,6 +5929,146 @@ table to \`schema/control/\`.`,
5361
5929
  };
5362
5930
  var tenant_tables_carry_org_id_default = tenantTablesCarryOrgId;
5363
5931
 
5932
+ // src/rules/test-no-clock-polling-for-promise-flips.ts
5933
+ var POLLERS = ["waitFor"];
5934
+ var TEST_GLOBS = [String.raw`\.(?:test|spec)\.[cm]?[jt]sx?$`, "/__tests__/"];
5935
+ var ASSERT = "expect";
5936
+ var FUNCTIONS2 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionDeclaration", "FunctionExpression"]);
5937
+ var walkWithin = (root, visit) => {
5938
+ const seen = /* @__PURE__ */ new Set();
5939
+ const stack = [root];
5940
+ while (stack.length > 0) {
5941
+ const at = stack.pop();
5942
+ if (at === null || typeof at !== "object" || seen.has(at)) continue;
5943
+ seen.add(at);
5944
+ if (Array.isArray(at)) {
5945
+ stack.push(...at);
5946
+ continue;
5947
+ }
5948
+ const node = at;
5949
+ if (typeof node.type === "string") {
5950
+ if (at !== root && FUNCTIONS2.has(node.type)) continue;
5951
+ visit(node);
5952
+ }
5953
+ for (const [key, value] of Object.entries(node)) {
5954
+ if (key === "parent") continue;
5955
+ if (value !== null && typeof value === "object") stack.push(value);
5956
+ }
5957
+ }
5958
+ };
5959
+ var rootCallOf = (node) => {
5960
+ let at = node;
5961
+ for (; ; ) {
5962
+ const callee = at.callee;
5963
+ if (callee?.type !== "MemberExpression") return at;
5964
+ const object = callee.object;
5965
+ if (object?.type !== "CallExpression") return at;
5966
+ at = object;
5967
+ }
5968
+ };
5969
+ var onlyAsserts = (body) => {
5970
+ let calls = 0;
5971
+ let asserts = 0;
5972
+ walkWithin(body, (node) => {
5973
+ if (node.type !== "CallExpression") return;
5974
+ calls += 1;
5975
+ if (calleeNameOf(rootCallOf(node)) === ASSERT) asserts += 1;
5976
+ });
5977
+ return calls > 0 && calls === asserts;
5978
+ };
5979
+ var promisesIn = (scope) => {
5980
+ const created = /* @__PURE__ */ new Map();
5981
+ const proven = /* @__PURE__ */ new Set();
5982
+ walkWithin(scope, (node) => {
5983
+ if (node.type === "VariableDeclarator") {
5984
+ const id = node.id;
5985
+ const init = node.init;
5986
+ if (id?.type === "Identifier" && init?.type === "CallExpression") {
5987
+ created.set(id.name, init);
5988
+ }
5989
+ }
5990
+ if (node.type === "AwaitExpression") {
5991
+ const argument = node.argument;
5992
+ if (argument?.type === "Identifier") proven.add(argument.name);
5993
+ }
5994
+ if (node.type === "MemberExpression") {
5995
+ const object = node.object;
5996
+ const property = propertyNameOf(node);
5997
+ if (object?.type === "Identifier" && (property === "then" || property === "catch" || property === "finally")) {
5998
+ proven.add(object.name);
5999
+ }
6000
+ }
6001
+ if (node.type === "CallExpression") {
6002
+ const object = node.callee?.object;
6003
+ if (object?.type === "Identifier" && object.name === "Promise") {
6004
+ for (const argument of node.arguments ?? []) {
6005
+ for (const element of argument.elements ?? [argument]) {
6006
+ if (element?.type === "Identifier") proven.add(element.name);
6007
+ }
6008
+ }
6009
+ }
6010
+ }
6011
+ });
6012
+ return new Set([...created.keys()].filter((name) => proven.has(name)));
6013
+ };
6014
+ var testNoClockPollingForPromiseFlips = {
6015
+ create(context) {
6016
+ const options = bagOf(context.options);
6017
+ const pollers = options.pollers ?? POLLERS;
6018
+ const globs = options.testGlobs ?? TEST_GLOBS;
6019
+ const filename = normalise(context.filename);
6020
+ if (!globs.some((pattern) => new RegExp(pattern).test(filename))) return {};
6021
+ const inScope = (node) => {
6022
+ const held = promisesIn(node);
6023
+ if (held.size === 0) return;
6024
+ const [promise] = [...held].toSorted();
6025
+ walkWithin(node, (inner) => {
6026
+ if (inner.type !== "CallExpression") return;
6027
+ if (!pollers.includes(calleeNameOf(inner) ?? "")) return;
6028
+ const [callback] = inner.arguments ?? [];
6029
+ if (callback === void 0 || !FUNCTIONS2.has(callback.type ?? "")) return;
6030
+ if (!onlyAsserts(callback.body)) return;
6031
+ context.report({
6032
+ data: { poller: calleeNameOf(inner) ?? "", promise: promise ?? "" },
6033
+ messageId: "clockPoll",
6034
+ node: inner
6035
+ });
6036
+ });
6037
+ };
6038
+ return {
6039
+ ArrowFunctionExpression: inScope,
6040
+ FunctionDeclaration: inScope,
6041
+ FunctionExpression: inScope
6042
+ };
6043
+ },
6044
+ fixShape: `A test that already holds a promise awaits it; it does not poll a clock for the flip. A
6045
+ \`waitFor\` whose body is nothing but \`expect\` assertions, in a test that created a promise and proves
6046
+ it is one by awaiting or chaining it, is asking the scheduler a question the promise answers exactly:
6047
+ move the \`await\` above the assertions, or drain microtasks, and assert once. Keep \`waitFor\` for state
6048
+ nothing in the test can await \u2014 an element a click will render, a timer the runtime owns. Which files
6049
+ are tests is \`testGlobs\` and which calls poll is \`pollers\`, both defaulted.`,
6050
+ meta: {
6051
+ docs: {
6052
+ 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."
6053
+ },
6054
+ messages: {
6055
+ 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.'
6056
+ },
6057
+ schema: [
6058
+ {
6059
+ additionalProperties: false,
6060
+ properties: {
6061
+ pollers: { items: { type: "string" }, type: "array" },
6062
+ testGlobs: { items: { type: "string" }, type: "array" }
6063
+ },
6064
+ type: "object"
6065
+ }
6066
+ ],
6067
+ type: "problem"
6068
+ }
6069
+ };
6070
+ var test_no_clock_polling_for_promise_flips_default = testNoClockPollingForPromiseFlips;
6071
+
5364
6072
  // src/rules/time-through-the-door.ts
5365
6073
  var RULE20 = "time-through-the-door";
5366
6074
  var ACCEPTS6 = "{ door: string (a regex over the filename), exempt: string[] (regex sources, default the test paths: __tests__/, .test., .spec.), libraries: string[] (default ['dayjs']) }";
@@ -5392,6 +6100,7 @@ var timeThroughTheDoor = {
5392
6100
  create(context) {
5393
6101
  const door = requireOption(RULE20, context.options?.[0]?.door, "door", ACCEPTS6);
5394
6102
  const exempt = context.options?.[0]?.exempt ?? DEFAULT_EXEMPT;
6103
+ const exemptModules = context.options?.[0]?.exemptModules ?? [];
5395
6104
  const libraries = requireOption(
5396
6105
  RULE20,
5397
6106
  context.options?.[0]?.libraries ?? DEFAULT_LIBRARIES,
@@ -5400,6 +6109,7 @@ var timeThroughTheDoor = {
5400
6109
  );
5401
6110
  const filename = normalise(context.filename);
5402
6111
  if (isDoor(filename, door)) return {};
6112
+ if (exemptModules.some((module) => isModule(filename, module))) return {};
5403
6113
  if (exempt.some((pattern) => new RegExp(pattern).test(filename))) return {};
5404
6114
  const where = readable2(door);
5405
6115
  return {
@@ -5441,7 +6151,9 @@ the plugins, the zone and the formats. Import \`isoDate\` / \`today\` / \`addDay
5441
6151
  operation there. A second module that imports the library is a second configured library: plugin
5442
6152
  state is global, so the two disagree the moment one of them extends something the other did not.
5443
6153
  Name the library in \`libraries\` (default \`['dayjs']\`) \u2014 a repo on date-fns or luxon says so and
5444
- gets the whole rule rather than its raw-\`Date\` half.
6154
+ gets the whole rule rather than its raw-\`Date\` half. Name the modules that are outside the door in
6155
+ \`exemptModules\`, one per entry: \`exempt\` still takes patterns and still grants its exemption to
6156
+ wherever the pattern stops, which is a boundary nobody decided and nobody counts.
5445
6157
 
5446
6158
  Three kinds of time, never mixed. An INSTANT is epoch milliseconds. A BUSINESS DATE is an ISO date
5447
6159
  string, \`YYYY-MM-DD\`, parsed strictly in UTC \u2014 a calendar day is not an instant and rendering one as
@@ -5452,6 +6164,12 @@ The clock is injected, never ambient. Take \`nowMs: number\` as a parameter and
5452
6164
  root pass \`Date.now()\` in \u2014 \`Date.now()\` is the one call allowed anywhere, and a function that reads
5453
6165
  it internally cannot be tested at a chosen instant.`,
5454
6166
  meta: {
6167
+ /**
6168
+ * #139: which option grants by PATTERN and which one ENUMERATES. Declared on the rule so the
6169
+ * doctor needs no list of its own — a rule that gains a path grant and forgets to say so is a
6170
+ * grant nothing counts, which is the defect this pair exists to end.
6171
+ */
6172
+ grants: { modules: "exemptModules", paths: "exempt" },
5455
6173
  docs: {
5456
6174
  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."
5457
6175
  },
@@ -5465,6 +6183,7 @@ it internally cannot be tested at a chosen instant.`,
5465
6183
  properties: {
5466
6184
  door: { type: "string" },
5467
6185
  exempt: { items: { type: "string" }, type: "array" },
6186
+ exemptModules: { items: { type: "string" }, type: "array" },
5468
6187
  libraries: { items: { type: "string" }, type: "array" }
5469
6188
  },
5470
6189
  type: "object"
@@ -5973,7 +6692,7 @@ export const geonosisProbeTodo = 1
5973
6692
  var todo_names_its_plan_default = todoNamesItsPlan;
5974
6693
 
5975
6694
  // src/rules/lib/colors.ts
5976
- var FUNCTIONS = [
6695
+ var FUNCTIONS3 = [
5977
6696
  "color",
5978
6697
  "color-mix",
5979
6698
  "hsl",
@@ -5987,7 +6706,7 @@ var FUNCTIONS = [
5987
6706
  "rgba"
5988
6707
  ];
5989
6708
  var HEX = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
5990
- var FUNCTION_CALL = new RegExp(`\\b(?:${FUNCTIONS.join("|")})\\(`, "i");
6709
+ var FUNCTION_CALL = new RegExp(`\\b(?:${FUNCTIONS3.join("|")})\\(`, "i");
5991
6710
  var HEX_ANYWHERE = /(?:^|[\s(,])#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i;
5992
6711
  var NAMED2 = /* @__PURE__ */ new Set([
5993
6712
  "aliceblue",
@@ -6144,7 +6863,7 @@ var isColorLiteral = (value) => {
6144
6863
  if (trimmed === "") return false;
6145
6864
  if (HEX.test(trimmed)) return true;
6146
6865
  if (NAMED2.has(trimmed.toLowerCase())) return true;
6147
- return new RegExp(`^(?:${FUNCTIONS.join("|")})\\(`, "i").test(trimmed);
6866
+ return new RegExp(`^(?:${FUNCTIONS3.join("|")})\\(`, "i").test(trimmed);
6148
6867
  };
6149
6868
  var containsColorLiteral = (value) => HEX_ANYWHERE.test(value) || FUNCTION_CALL.test(value);
6150
6869
  var COLOR_PROPERTIES = /* @__PURE__ */ new Set([
@@ -6229,10 +6948,11 @@ var tokenOffence = (value, prefixes, local) => {
6229
6948
  };
6230
6949
  var tokensOnlyColors = {
6231
6950
  create(context) {
6232
- const { allow = [], tokenPrefix } = context.options?.[0] ?? {};
6951
+ const { allow = [], exemptModules = [], tokenPrefix } = context.options?.[0] ?? {};
6233
6952
  const prefixes = [requireOption("tokens-only-colors", tokenPrefix, "tokenPrefix")].flat();
6234
6953
  requireOption("tokens-only-colors", prefixes, "tokenPrefix");
6235
6954
  const filename = normalise(context.filename);
6955
+ if (exemptModules.some((module) => isModule(filename, module))) return {};
6236
6956
  if (allow.some((path2) => filename.includes(path2))) return {};
6237
6957
  return {
6238
6958
  JSXOpeningElement(node) {
@@ -6266,11 +6986,19 @@ var tokensOnlyColors = {
6266
6986
  \`style={{ color: 'var(--mc-primary)' }}\`, \`className="bg-brand-primary"\`. A \`var()\` that is NOT a
6267
6987
  token (\`var(--legacy-blue)\`) is the same offence spelled differently: it reaches around the theme, so
6268
6988
  the brand switches and the component does not \u2014 add the value to the theme and point at it there.
6269
- Configure \`tokenPrefix\` with the prefixes your themed properties use, and put the token layer itself
6270
- in \`allow\`, because that is the one place a literal belongs. Enabled with no \`tokenPrefix\` the rule
6989
+ Configure \`tokenPrefix\` with the prefixes your themed properties use, and NAME the token layer's
6990
+ modules in \`exemptModules\`, one per entry, because that is the one place a literal belongs \u2014
6991
+ \`allow\` still takes patterns and still grants its exemption to wherever the pattern stops, which is
6992
+ a boundary nobody decided and nobody counts. Enabled with no \`tokenPrefix\` the rule
6271
6993
  refuses the run: it cannot tell a token from a foreign variable, and a rule that cannot tell reads
6272
6994
  exactly like a clean tree.`,
6273
6995
  meta: {
6996
+ /**
6997
+ * #139: which option grants by PATTERN and which one ENUMERATES. Declared on the rule so the
6998
+ * doctor needs no list of its own — a rule that gains a path grant and forgets to say so is a
6999
+ * grant nothing counts, which is the defect this pair exists to end.
7000
+ */
7001
+ grants: { modules: "exemptModules", paths: "allow" },
6274
7002
  docs: {
6275
7003
  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."
6276
7004
  },
@@ -6283,6 +7011,7 @@ exactly like a clean tree.`,
6283
7011
  additionalProperties: false,
6284
7012
  properties: {
6285
7013
  allow: { items: { type: "string" }, type: "array" },
7014
+ exemptModules: { items: { type: "string" }, type: "array" },
6286
7015
  tokenPrefix: {
6287
7016
  anyOf: [
6288
7017
  { type: "string" },
@@ -6663,6 +7392,7 @@ var rules = refusingOnce(
6663
7392
  "emit-declares-attempts": emit_declares_attempts_default,
6664
7393
  "font-roles-only": font_roles_only_default,
6665
7394
  "layer-walls": layer_walls_default,
7395
+ "list-take-without-order": list_take_without_order_default,
6666
7396
  "max-comment-density": max_comment_density_default,
6667
7397
  "molecule-atoms-only": molecule_atoms_only_default,
6668
7398
  "molecule-must-compose": molecule_must_compose_default,
@@ -6676,7 +7406,9 @@ var rules = refusingOnce(
6676
7406
  "no-card-shaped-div": no_card_shaped_div_default,
6677
7407
  "no-cross-feature-stores": no_cross_feature_stores_default,
6678
7408
  "no-d1-transaction": no_d1_transaction_default,
7409
+ "no-date-string-compare": no_date_string_compare_default,
6679
7410
  "no-duplicate-jsx-patterns": no_duplicate_jsx_patterns_default,
7411
+ "no-generic-utility-package": no_generic_utility_package_default,
6680
7412
  "no-hook-in-component-disguise": no_hook_in_component_disguise_default,
6681
7413
  "no-index-count-as-exact": no_index_count_as_exact_default,
6682
7414
  "no-inert-hidden-jsx": no_inert_hidden_jsx_default,
@@ -6691,6 +7423,8 @@ var rules = refusingOnce(
6691
7423
  "no-react-namespace": no_react_namespace_default,
6692
7424
  "no-renamed-html-props": no_renamed_html_props_default,
6693
7425
  "no-render-prop-reader": no_render_prop_reader_default,
7426
+ "no-swallowed-catch": no_swallowed_catch_default,
7427
+ "no-runtime-dead-affordance": no_runtime_dead_affordance_default,
6694
7428
  "no-trivial-wrapper-component": no_trivial_wrapper_component_default,
6695
7429
  "no-ts-in-bio-folders": no_ts_in_bio_folders_default,
6696
7430
  "no-type-definitions-in-components": no_type_definitions_in_components_default,
@@ -6711,6 +7445,7 @@ var rules = refusingOnce(
6711
7445
  "subscriber-declares-id": subscriber_declares_id_default,
6712
7446
  "tables-declare-their-plane": tables_declare_their_plane_default,
6713
7447
  "tenant-tables-carry-org-id": tenant_tables_carry_org_id_default,
7448
+ "test-no-clock-polling-for-promise-flips": test_no_clock_polling_for_promise_flips_default,
6714
7449
  "time-through-the-door": time_through_the_door_default,
6715
7450
  "tokens-only-colors": tokens_only_colors_default,
6716
7451
  "tissue-must-compose": tissue_must_compose_default,