oxlint-plugin-arch 0.12.6 → 0.13.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.
@@ -2,38 +2,20 @@ import { createRequire } from "node:module";
2
2
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
4
  // src/preset/preset.ts
5
- import { createRequire as createRequire2 } from "node:module";
5
+ import { globSync } from "node:fs";
6
+ import { createRequire as createRequire3 } from "node:module";
7
+ import { matchesGlob as matchesGlob2, posix as posix9 } from "node:path";
6
8
 
7
9
  // src/preset/groups/backend.ts
8
- import { posix as posix3, resolve as resolve3 } from "node:path";
10
+ import { posix as posix4, resolve as resolve4 } from "node:path";
9
11
 
10
12
  // src/preset/preset-detect.ts
11
- import { existsSync, readdirSync, readFileSync as readFileSync2 } from "node:fs";
12
- import { matchesGlob, posix as posix2, resolve as resolve2 } from "node:path";
13
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
14
+ import { matchesGlob, posix as posix3, resolve as resolve3 } from "node:path";
13
15
 
14
- // src/preset/preset-recipes.ts
15
- var presetUi = { react: true, kit: true, tailwind: true };
16
- var presetRecipes = {
17
- tanstack: { react: true, tanstack: true, tailwind: true, shadcn: true, schemas: true, forms: true },
18
- next: { react: true, next: true, tailwind: true, shadcn: true, schemas: true, forms: true },
19
- expo: { react: true, expo: true, schemas: true, forms: true },
20
- backend: { backend: true, schemas: true, sql: true, layout: { strict: true } },
21
- react: { react: true, tailwind: true, schemas: true },
22
- desktop: { console: true },
23
- library: { schemas: true },
24
- effect: { schemas: true, effect: true },
25
- ui: presetUi,
26
- kit: presetUi,
27
- cli: { console: true }
28
- };
29
- var presetTemplates = {
30
- monorepo: {
31
- "apps/web": presetRecipes.tanstack,
32
- "apps/server": presetRecipes.backend,
33
- "packages/*": presetRecipes.library,
34
- scripts: presetRecipes.cli
35
- }
36
- };
16
+ // src/preset/preset-effect.ts
17
+ import { existsSync as existsSync2, readdirSync } from "node:fs";
18
+ import { posix as posix2, resolve as resolve2 } from "node:path";
37
19
 
38
20
  // src/preset/preset.constants.ts
39
21
  var presetExtensions = "*.{ts,tsx,mts,cts}";
@@ -347,20 +329,22 @@ var presetGeneratedFiles = [
347
329
  "**/routeTree.gen.ts",
348
330
  "**/paraglide/**"
349
331
  ];
350
- var presetBuiltinFileRoles = [
351
- "service",
352
- "client",
353
- "server",
354
- "rsc",
355
- "utils",
356
- "action",
357
- "query",
358
- "types",
359
- "constants",
360
- "handler",
361
- "test",
362
- "spec"
332
+ var presetLayoutFiles = [
333
+ "*.service.ts",
334
+ "*.client.ts",
335
+ "*.server.ts",
336
+ "*.rsc.ts",
337
+ "*.rsc.tsx",
338
+ "*.utils.ts",
339
+ "*.types.ts",
340
+ "*.constants.ts",
341
+ "*.handler.ts",
342
+ "*.test.ts",
343
+ "*.spec.ts",
344
+ "*-action.*.ts",
345
+ "*-query.*.ts"
363
346
  ];
347
+ var presetLayoutFileName = /^!?[\w.*-]+$/;
364
348
  var presetShorthandOptions = [["tests", "mocking", "preset"], ["backend", "registry", "file"]];
365
349
  var presetOutputFolders = ["dist", "build", "coverage", ".output"];
366
350
  var presetLiteralSegment = /^(?!\.{1,2}$)[a-zA-Z0-9._][a-zA-Z0-9._-]*$/;
@@ -444,7 +428,7 @@ var presetExternals = [
444
428
  "playwright-core"
445
429
  ];
446
430
  var presetGroupOptions = {
447
- layout: { level: "error", services: undefined, hooks: undefined, components: undefined, suffixes: [], strict: false, operations: "flat", service: ["make{Domain}Service", "{Domain}Service"] },
431
+ layout: { level: "error", services: undefined, hooks: undefined, components: undefined, files: presetLayoutFiles, strict: false, operations: "flat", service: ["make{Domain}Service", "{Domain}Service"] },
448
432
  limits: { level: "error", lines: 400, types: 1000, complexity: 32 },
449
433
  style: { level: "error", quotes: "single", semicolons: true, multiline: true, padding: true, dashes: true },
450
434
  basics: { level: "error", trivial: true, state: true, switch: true, negation: true },
@@ -460,17 +444,20 @@ var presetGroupOptions = {
460
444
  tailwind: { level: "error", css: undefined, rem: 16 },
461
445
  shadcn: { level: "error", ui: "@/components/ui", components: ["^@/components/"] },
462
446
  kit: { level: "error" },
463
- backend: { level: "error", exports: undefined, registry: undefined, writes: ["insertInto", "updateTable", "deleteFrom", "mergeInto", "replaceInto", "dispatch", "send"] },
464
- orpc: { level: "error", public: [], composers: [], client: undefined },
447
+ backend: { level: "error", exports: undefined, registry: undefined },
448
+ orpc: { level: "error", public: [], client: undefined },
465
449
  schemas: { level: "error" },
466
- forms: { level: "error", resolver: "standardSchemaResolver" },
467
- sql: { level: "error", sanitizers: ["escapeLikeWildcards"] },
468
- effect: { level: "error", width: 160, exports: undefined, skip: undefined, entries: [] },
450
+ forms: { level: "error" },
451
+ sql: { level: "error" },
452
+ effect: { level: "error", code: true, width: 140, exports: undefined, skip: undefined, entries: [] },
469
453
  console: { level: "error" },
470
454
  deny: { level: "error", imports: {}, calls: {} },
471
- externals: { level: "error", packages: presetExternals }
455
+ externals: { level: "error" }
472
456
  };
457
+ var presetQueryWrites = ["insertInto", "updateTable", "deleteFrom", "mergeInto", "replaceInto", "dispatch", "send"];
473
458
  var presetFolderKeys = ["severity", "source", "services", "rules"];
459
+ var presetFolderOnlyKeys = ["source", "services", "tanstack", "next", "expo", "backend", "console", "kit"];
460
+ var presetFileSegment = /^(?!\.{1,2}$)[\w.@$[\]*-]+$/;
474
461
  var presetA11yRules = [
475
462
  "alt-text",
476
463
  "anchor-has-content",
@@ -510,6 +497,7 @@ var presetA11yRules = [
510
497
  ].map((rule) => `jsx-a11y/${rule}`);
511
498
  var presetExpoRouteExports = ["ErrorBoundary", "unstable_settings", "generateStaticParams"];
512
499
  var presetTestFiles = ["**/{test,tests,e2e}/**/*.{ts,tsx,mts,cts}", "**/*.{test,spec}.{ts,tsx,mts,cts}"];
500
+ var presetTanstackRsc = { name: "@tanstack/react-start/rsc", message: "Render server components only in services/*.rsc.tsx files." };
513
501
  var presetBaseImports = [{ group: ["zod/v4", "zod/v4/**"], message: "Import from zod, not its transition entry point." }];
514
502
  var presetDetectFrameworks = [
515
503
  ["next", ["next", "vinext"]],
@@ -517,6 +505,13 @@ var presetDetectFrameworks = [
517
505
  ["expo", ["expo"]]
518
506
  ];
519
507
  var presetDetectDesktop = ["electrobun", "electron", "@tauri-apps/api", "@tauri-apps/cli"];
508
+ var presetEffectImports = {
509
+ effect: "Import the module subpath, such as effect/Effect.",
510
+ zod: "Use effect/Schema.",
511
+ "ts-pattern": "Use effect/Match.",
512
+ dayjs: "Use effect/DateTime."
513
+ };
514
+ var presetRecipeMark = Symbol("recipe");
520
515
  var presetDetectBackend = ["@orpc/server", "hono", "kysely", "drizzle-orm", "express", "fastify", "elysia"];
521
516
  var presetDetectSkip = ["node_modules", "dist", "build", "coverage", "public", ".output", ".next", ".vinext", ".turbo", ".wrangler", "target"];
522
517
  var presetStylesheetNames = ["styles", "globals", "global", "app", "index", "main", "style", "tailwind"];
@@ -528,12 +523,73 @@ var presetKitImports = ["**/kits", "**/kits/index", "**/kits/index.ts"];
528
523
  var presetKitFiles = "^(?:kit|[a-z0-9]+(?:-[a-z0-9]+)*\\.kit)\\.ts$";
529
524
  var presetCliFiles = "^cli(?:-[a-z0-9]+)*(?:\\.[a-z0-9-]+)*\\.tsx?$";
530
525
  var presetMigrationFiles = "^(?:\\d{3,14}[-_][a-z0-9]+(?:[-_][a-z0-9]+)*|index)\\.ts$";
531
- var presetMigrationNumber = "^(\\d{3,14})[-_]";
526
+ var presetMigrationNumber = "^(\\d{4}-\\d{2}-\\d{2}(?:-\\d{2})*|\\d{3,14})[-_]";
532
527
  var presetRootImports = { "framer-motion": "Use motion/react." };
533
528
  var presetPackageImports = { "../../apps/**": "Packages stay app-agnostic; do not import app internals." };
534
529
 
530
+ // src/preset/preset-recipes.ts
531
+ var presetUi = { react: true, kit: true, tailwind: true };
532
+ function presetRecipe(name, recipe) {
533
+ return { ...recipe, [presetRecipeMark]: name };
534
+ }
535
+ var presetRecipes = {
536
+ tanstack: presetRecipe("tanstack", {
537
+ react: true,
538
+ tanstack: true,
539
+ tailwind: true,
540
+ shadcn: true,
541
+ schemas: true,
542
+ forms: true
543
+ }),
544
+ next: presetRecipe("next", {
545
+ react: true,
546
+ next: true,
547
+ tailwind: true,
548
+ shadcn: true,
549
+ schemas: true,
550
+ forms: true
551
+ }),
552
+ expo: presetRecipe("expo", {
553
+ react: true,
554
+ expo: true,
555
+ schemas: true,
556
+ forms: true
557
+ }),
558
+ backend: presetRecipe("backend", {
559
+ backend: true,
560
+ schemas: true,
561
+ sql: true,
562
+ orpc: true,
563
+ layout: { strict: true }
564
+ }),
565
+ react: presetRecipe("react", {
566
+ react: true,
567
+ tailwind: true,
568
+ schemas: true
569
+ }),
570
+ desktop: presetRecipe("desktop", { console: true }),
571
+ library: presetRecipe("library", { schemas: true }),
572
+ effect: presetRecipe("effect", {
573
+ schemas: true,
574
+ effect: true,
575
+ deny: { imports: presetEffectImports }
576
+ }),
577
+ ui: presetRecipe("ui", presetUi),
578
+ kit: presetRecipe("kit", presetUi),
579
+ cli: presetRecipe("cli", { console: true })
580
+ };
581
+ var presetTemplates = {
582
+ monorepo: {
583
+ "apps/web": presetRecipes.tanstack,
584
+ "apps/server": presetRecipes.backend,
585
+ "packages/*": presetRecipes.library,
586
+ scripts: presetRecipes.cli
587
+ }
588
+ };
589
+
535
590
  // src/preset/preset.utils.ts
536
- import { readFileSync, statSync } from "node:fs";
591
+ import { existsSync, readFileSync, statSync } from "node:fs";
592
+ import { createRequire as createRequire2 } from "node:module";
537
593
  import { posix, resolve } from "node:path";
538
594
  class PresetError extends Error {
539
595
  }
@@ -549,6 +605,33 @@ async function presetEffectImport(load) {
549
605
  }
550
606
  var presetEffectTsgo = await presetEffectImport(import("@effect/tsgo/oxlint-presets"));
551
607
  var presetEffectLinteffect = await presetEffectImport(import("@opsydyn/oxlint-effect"));
608
+ var presetEffectRegexp = await presetEffectImport(import("eslint-plugin-regexp"));
609
+ var presetEffectDeMorgan = await presetEffectImport(import("eslint-plugin-de-morgan"));
610
+ var presetRequire = createRequire2(import.meta.url);
611
+ function presetResolve(name) {
612
+ try {
613
+ return presetRequire.resolve(name);
614
+ } catch {
615
+ return;
616
+ }
617
+ }
618
+ function presetEffectStyle() {
619
+ const sonarjs = presetResolve("eslint-plugin-sonarjs");
620
+ const regexp = presetEffectRegexp ? presetResolve("eslint-plugin-regexp") : undefined;
621
+ const deMorgan = presetEffectDeMorgan ? presetResolve("eslint-plugin-de-morgan") : undefined;
622
+ const rules = { ...presetEffectRegexp?.configs["flat/recommended"].rules, ...presetEffectDeMorgan?.configs.recommended.rules };
623
+ if (sonarjs) {
624
+ rules["sonarjs/super-linear-regex"] = "error";
625
+ }
626
+ return {
627
+ rules,
628
+ jsPlugins: [
629
+ ...regexp ? [{ name: "regexp", specifier: regexp }] : [],
630
+ ...deMorgan ? [{ name: "de-morgan", specifier: deMorgan }] : [],
631
+ ...sonarjs ? [{ name: "sonarjs", specifier: sonarjs }] : []
632
+ ]
633
+ };
634
+ }
552
635
  function presetEffectPackages() {
553
636
  if (!presetEffectTsgo || !presetEffectLinteffect) {
554
637
  throw new PresetError("The effect group needs @effect/tsgo and @opsydyn/oxlint-effect: bun add -d @effect/tsgo @opsydyn/oxlint-effect");
@@ -617,6 +700,9 @@ function presetIntersect(glob, scope) {
617
700
  const matches = presetIntersectSegments(glob.split("/"), scope.split("/")).map((segments) => segments.join("/"));
618
701
  return [...new Set(matches)];
619
702
  }
703
+ function presetGlobEscape(path) {
704
+ return path.replaceAll(/[[\]]/g, (bracket) => `[${bracket}]`);
705
+ }
620
706
  function presetAliases(root, aliases) {
621
707
  return Object.fromEntries(Object.entries(aliases ?? {}).map(([alias, path]) => [alias.replace(/\/$/, ""), resolve(root, path)]));
622
708
  }
@@ -675,77 +761,328 @@ function presetIsFile(path) {
675
761
  return false;
676
762
  }
677
763
  }
764
+ function presetSource(root, directory, configured) {
765
+ if (configured !== undefined) {
766
+ return posix.join(directory, configured);
767
+ }
768
+ return existsSync(resolve(root, directory, "src")) ? posix.join(directory, "src") : directory;
769
+ }
770
+ function presetFilled(conventions, exists) {
771
+ return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
772
+ const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
773
+ return exists(path) && !empty;
774
+ }));
775
+ }
776
+ function presetMergeObject(base, top, depth) {
777
+ const merged = { ...base };
778
+ for (const [key, value] of Object.entries(top)) {
779
+ const previous = Reflect.get(merged, key);
780
+ const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
781
+ if (nested && depth > 0) {
782
+ Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
783
+ } else if (value !== true || !(previous instanceof Object)) {
784
+ Reflect.set(merged, key, value);
785
+ }
786
+ }
787
+ return merged;
788
+ }
789
+ function presetMerge(base, top) {
790
+ if (!(base instanceof Object) || !(top instanceof Object)) {
791
+ return top === true && base instanceof Object ? base : top;
792
+ }
793
+ const merged = presetMergeObject(base, top, 2);
794
+ const before = base.layout instanceof Object ? base.layout.files : undefined;
795
+ const after = top.layout instanceof Object ? top.layout.files : undefined;
796
+ return before && after && merged.layout instanceof Object ? { ...merged, layout: { ...merged.layout, files: [...before, ...after] } } : merged;
797
+ }
798
+
799
+ // src/preset/preset-effect.ts
800
+ var presetEffectQueryWrites = Object.fromEntries(presetQueryWrites.map((name) => [name, "A queries file only reads; write in an actions file."]));
801
+ var presetEffectWorkflowCalls = Object.fromEntries(["selectFrom", ...presetQueryWrites].map((name) => [name, "A workflow reaches data through its domain service, not the database."]));
802
+ var presetEffectFrameworks = Object.fromEntries(["@tanstack/*", "react-dom", "react-dom/**", "next", "next/**", "vite"].map((name) => [name, "The server imports no web framework."]));
803
+ var presetEffectRouters = ["@tanstack/react-router", "next/link", "next/navigation", "next/router", "expo-router"];
804
+ var presetEffectErrors = ["Schema.TaggedError", "Schema.ErrorClass", "Data.TaggedError", "Data.Error"];
805
+ var presetEffectContractNames = "^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.]schemas|[.]admin[.]schemas|[.]contract|[.]admin[.]contract|[.]errors|[.]layer[.]types)[.]ts$";
806
+ var presetEffectContractMessage = "Contract files use <d>.schemas.ts, <d>-<part>.schemas.ts, <d>.admin.schemas.ts, <d>.contract.ts, <d>.admin.contract.ts, <d>.errors.ts or <d>.layer.types.ts.";
807
+ var presetEffectSchemaUses = {
808
+ namespaces: ["Schema"],
809
+ methods: ["Array", "Union", "Record", "Tuple"],
810
+ structural: ["Struct", "Array", "Union", "Record", "Tuple", "NullOr", "UndefinedOr", "optional", "TaggedStruct"]
811
+ };
812
+ function presetEffectExists(root, key) {
813
+ return (path) => {
814
+ const segments = path.split("/");
815
+ const wildcard = segments.findIndex((segment) => segment.includes("*"));
816
+ return existsSync2(resolve2(root, key, ...wildcard === -1 ? segments : segments.slice(0, wildcard)));
817
+ };
818
+ }
819
+ function presetEffectAt(root, key, value) {
820
+ const source = posix2.relative(key, presetSource(root, key, value.source)) || ".";
821
+ return (path) => source === "." ? path : posix2.join(source, path);
822
+ }
823
+ function presetEffectCode(value) {
824
+ return value instanceof Object && Boolean(value.effect) && !(value.effect instanceof Object && value.effect.code === false);
825
+ }
826
+ function presetEffectContract(root, key, value) {
827
+ if (!presetEffectCode(value) || presetEffectDomains(root, key, value)) {
828
+ return false;
829
+ }
830
+ const source = resolve2(root, presetSource(root, key, value.source));
831
+ return presetIsDirectory(source) && readdirSync(source).some((name) => presetIsFile(resolve2(source, name, `${name}.contract.ts`)));
832
+ }
833
+ function presetEffectDomains(root, key, value) {
834
+ return presetEffectCode(value) && presetIsDirectory(resolve2(root, presetSource(root, key, value.source), "domains"));
835
+ }
836
+ function presetEffectServers(root, merged, units) {
837
+ for (const unit of units) {
838
+ if (presetEffectDomains(root, unit[0], unit[1])) {
839
+ unit[1] = presetMerge({ ...presetRecipes.backend, services: "domains" }, unit[1]);
840
+ merged[unit[0]] = unit[1];
841
+ }
842
+ }
843
+ }
844
+ function presetEffectServerKeys(at) {
845
+ return {
846
+ [at(".")]: {
847
+ deny: { imports: { ...presetEffectFrameworks, "effect/Config": "Read config only in application/app.config.ts." } },
848
+ rules: {
849
+ "arch/types-declarations": ["error", { modules: true }],
850
+ "arch/types-inline": ["error", { variables: true, assertions: true, arguments: true }],
851
+ "arch/comments-code": ["error", { why: false, jsdoc: true, allow: ["^\\s*SAFETY: "] }]
852
+ }
853
+ },
854
+ [at("domains")]: {
855
+ layout: {
856
+ files: [
857
+ "*.actions.ts",
858
+ "*.queries.ts",
859
+ "*.layer.ts",
860
+ "*.workflows.ts",
861
+ "*.prompts.ts",
862
+ "*-*.driver.ts",
863
+ "!*.service.ts",
864
+ "!*.client.ts",
865
+ "!*.server.ts",
866
+ "!*.rsc.ts",
867
+ "!*.rsc.tsx",
868
+ "!*.utils.ts",
869
+ "!*.types.ts",
870
+ "!*.handler.ts",
871
+ "!*-action.*.ts",
872
+ "!*-query.*.ts"
873
+ ]
874
+ },
875
+ deny: { imports: { "../*/*.constants{,.ts}": "Another domain's constants stay in that domain." } }
876
+ },
877
+ [at("domains/*/*.actions.ts")]: {
878
+ limits: { lines: 1000 },
879
+ rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Action[A-Z]\\w*"], types: true }] }
880
+ },
881
+ [at("domains/*/*.queries.ts")]: {
882
+ limits: { lines: 1000 },
883
+ deny: { calls: presetEffectQueryWrites, imports: { "./*.actions{,.ts}": "A queries file imports no actions." } },
884
+ rules: { "arch/files-exports": ["error", { stem: "folder", patterns: ["{domain}Query[A-Z]\\w*"], types: true }] }
885
+ },
886
+ [at("domains/*/*.constants.ts")]: { rules: { "arch/files-no-functions": ["error", { reexports: false }] } },
887
+ [at("domains/*/*.layer.ts")]: { rules: { "arch/effect-layer-file": "error" } },
888
+ [at("domains/*/*.driver.ts")]: { rules: { "arch/effect-layer-file": "error" } },
889
+ [at("domains/*/*.workflows.ts")]: {
890
+ deny: {
891
+ calls: presetEffectWorkflowCalls,
892
+ imports: {
893
+ "**/database": "A workflow reaches data through its domain service, not the database.",
894
+ "./*.{actions,queries}{,.ts}": "A workflow calls its domain service, not its own actions or queries."
895
+ }
896
+ }
897
+ },
898
+ [at("application/app.config.ts")]: { deny: { imports: { "effect/Config": false } } },
899
+ [at("application/app.ts")]: {
900
+ deny: { imports: { "**/domains/*/!(*.layer|*.layer.ts|*.workflows|*.workflows.ts)": "The application imports only layer and workflows files." } },
901
+ rules: { "arch/boundaries-imports": "off" }
902
+ },
903
+ [at("**/*.tsx")]: { rules: { "arch/files-restricted": ["error", { message: "Mail templates in mails/ are the only JSX on a server." }] } },
904
+ [at("mails/*.mail.tsx")]: { rules: { "arch/files-restricted": "off" } },
905
+ [at("rpc")]: { rules: { "arch/code-delegate": "error" } }
906
+ };
907
+ }
908
+ function presetEffectContractKeys(at) {
909
+ const schemas = {
910
+ rules: {
911
+ "arch/files-constants-only": ["error", { types: true, functions: true }],
912
+ "arch/files-no-functions": ["error", { reexports: false }],
913
+ "arch/types-imported-alias": "error",
914
+ "arch/schemas-single-use": ["error", presetEffectSchemaUses]
915
+ }
916
+ };
917
+ const noFunctions = { rules: { "arch/files-no-functions": "error" } };
918
+ return {
919
+ [at("*/*.ts")]: {
920
+ rules: {
921
+ "arch/naming-file": ["error", { pattern: presetEffectContractNames, message: presetEffectContractMessage }],
922
+ "arch/naming-folder": ["error", { singularize: "none", separators: [".", "-"] }],
923
+ "arch/files-exports": ["error", { stem: "folder", singularize: "trailing-s", patterns: ["{Domain}\\w*"] }]
924
+ }
925
+ },
926
+ [at("*/*.admin.*.ts")]: { rules: { "arch/files-exports": ["error", { stem: "folder", singularize: "trailing-s", patterns: ["Admin{Domain}\\w*"] }] } },
927
+ [at("*/*.schemas.ts")]: schemas,
928
+ [at("*/*.contract.ts")]: schemas,
929
+ [at("*/*.errors.ts")]: noFunctions,
930
+ [at("*/*.layer.types.ts")]: noFunctions,
931
+ [at("shared/*.ts")]: {
932
+ rules: {
933
+ "arch/naming-file": "off",
934
+ "arch/naming-folder": "off",
935
+ "arch/files-exports": "off",
936
+ "arch/files-no-functions": "off",
937
+ "arch/files-constants-only": ["error", { types: true, functions: true }]
938
+ }
939
+ },
940
+ [at("contract*.ts")]: {
941
+ rules: {
942
+ "arch/files-constants-only": ["error", { types: true, functions: true }],
943
+ "arch/types-imported-alias": "error",
944
+ "arch/schemas-single-use": ["error", presetEffectSchemaUses]
945
+ }
946
+ }
947
+ };
948
+ }
949
+ function presetEffectUnit(root, key, value) {
950
+ if (!value.effect) {
951
+ return {};
952
+ }
953
+ const at = presetEffectAt(root, key, value);
954
+ const contract = presetEffectContract(root, key, value);
955
+ const server = presetEffectDomains(root, key, value);
956
+ const keys = server || contract ? presetFilled(server ? presetEffectServerKeys(at) : presetEffectContractKeys(at), presetEffectExists(root, key)) : {};
957
+ const errors = Object.fromEntries(presetEffectErrors.map((name) => [name, "Tagged errors live in <d>.errors.ts."]));
958
+ const placement = server || contract ? { types: { placement: false } } : {};
959
+ return {
960
+ ...keys,
961
+ ".": presetMerge({ ...placement, deny: { imports: presetEffectImports, calls: errors } }, keys["."] ?? {}),
962
+ [at("**/*.errors.ts")]: { deny: { calls: Object.fromEntries(presetEffectErrors.map((name) => [name, false])) } }
963
+ };
964
+ }
965
+ function presetEffectFrontend(root, key, value) {
966
+ const at = presetEffectAt(root, key, value);
967
+ const exists = presetEffectExists(root, key);
968
+ const framework = [value.tanstack, value.next, value.expo].some(Boolean);
969
+ if (!value.effect || !value.react || framework || !["pages", "link.tsx", "link.ts"].some((path) => exists(at(path)))) {
970
+ return {};
971
+ }
972
+ const routers = presetEffectRouters.map((name) => ({ name, message: "Import the router through link.tsx." }));
973
+ const linkRule = { "no-restricted-imports": ["error", { patterns: presetBaseImports }] };
974
+ return presetFilled({
975
+ ".": { forms: true },
976
+ [at(".")]: {
977
+ deny: { imports: { "@orpc/*": "A frontend package takes its data functions as props, not from an rpc client." } },
978
+ rules: {
979
+ "arch/react-query-props": "error",
980
+ "no-restricted-imports": [
981
+ "error",
982
+ { paths: [{ ...routers[0], allowImportNames: ["ClientOnly"] }, ...routers.slice(1)], patterns: presetBaseImports }
983
+ ]
984
+ }
985
+ },
986
+ [at("link.tsx")]: { rules: linkRule },
987
+ [at("link.ts")]: { rules: linkRule },
988
+ [at("services/**/*.client.ts")]: { rules: { "arch/backend-service-functions": ["error", { frontend: true }] } }
989
+ }, exists);
990
+ }
991
+ function presetEffectWeb(root, key, value, contracts) {
992
+ const at = presetEffectAt(root, key, value);
993
+ if (contracts.length === 0 || !(value.tanstack || value.next)) {
994
+ return {};
995
+ }
996
+ const types = contracts.map((name) => ({ name, allowTypeImports: true, message: "The web imports the contract package as types only." }));
997
+ const rsc = { rules: { "no-restricted-imports": ["error", { paths: types, patterns: presetBaseImports }] } };
998
+ if (!value.tanstack) {
999
+ return { [at(".")]: rsc };
1000
+ }
1001
+ const services = value.services ?? "services";
1002
+ return {
1003
+ [at(".")]: { rules: { "no-restricted-imports": ["error", { paths: [...types, presetTanstackRsc], patterns: presetBaseImports }] } },
1004
+ [at(`${services}/**/*.rsc.ts`)]: rsc,
1005
+ [at(`${services}/**/*.rsc.tsx`)]: rsc
1006
+ };
1007
+ }
1008
+ function presetEffectKit(root, key, value, units) {
1009
+ if (!value.kit || !value.effect) {
1010
+ return {};
1011
+ }
1012
+ const apps = units.filter(([other, folder]) => other !== key && !(folder instanceof Object && folder.kit));
1013
+ const names = apps.flatMap(([other]) => presetManifest(root, other).name ?? []);
1014
+ const message = "A UI kit imports no app package.";
1015
+ return { ".": { deny: { imports: Object.fromEntries(names.flatMap((name) => [[name, message], [`${name}/**`, message]])) } } };
1016
+ }
1017
+ function presetEffectConventions(root, key, value, units) {
1018
+ const contracts = units.flatMap(([other, folder]) => presetEffectContract(root, other, folder) ? presetManifest(root, other).name ?? [] : []);
1019
+ return [
1020
+ ...Object.entries(presetEffectUnit(root, key, value)),
1021
+ ...Object.entries(presetEffectFrontend(root, key, value)),
1022
+ ...Object.entries(presetEffectWeb(root, key, value, contracts)),
1023
+ ...Object.entries(presetEffectKit(root, key, value, units))
1024
+ ];
1025
+ }
678
1026
 
679
1027
  // src/preset/preset-detect.ts
680
1028
  function presetDirectories(root, base, skip = presetDetectSkip) {
681
- if (!presetIsDirectory(resolve2(root, base))) {
1029
+ if (!presetIsDirectory(resolve3(root, base))) {
682
1030
  return [];
683
1031
  }
684
- return readdirSync(resolve2(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve2(root, base, name))).map((name) => posix2.join(base, name));
1032
+ return readdirSync2(resolve3(root, base)).filter((name) => !name.startsWith(".") && !skip.includes(name) && presetIsDirectory(resolve3(root, base, name))).map((name) => posix3.join(base, name));
685
1033
  }
686
1034
  var presetNamedSkip = ["node_modules"];
687
- function presetSource(root, directory, configured) {
688
- if (configured !== undefined) {
689
- return posix2.join(directory, configured);
690
- }
691
- return existsSync(resolve2(root, directory, "src")) ? posix2.join(directory, "src") : directory;
692
- }
693
1035
  function presetRegistryFile(root, source, configured, services) {
694
1036
  if (configured === false) {
695
1037
  return;
696
1038
  }
697
1039
  const file = typeof configured === "string" ? configured : configured?.file;
698
1040
  if (file !== undefined) {
699
- return posix2.normalize(file);
1041
+ return posix3.normalize(file);
700
1042
  }
701
- return [posix2.join(services, "service.ts"), ...presetRegistryFiles.map((path) => posix2.join(source, path))].find((path) => existsSync(resolve2(root, path)));
1043
+ return [posix3.join(services, "service.ts"), ...presetRegistryFiles.map((path) => posix3.join(source, path))].find((path) => existsSync3(resolve3(root, path)));
702
1044
  }
703
1045
  function presetDomains(root, services) {
704
- return presetDirectories(root, services, presetNamedSkip).map((path) => posix2.basename(path)).filter((name) => existsSync(resolve2(root, services, name, `${name}.service.ts`)));
1046
+ return presetDirectories(root, services, presetNamedSkip).map((path) => posix3.basename(path)).filter((name) => existsSync3(resolve3(root, services, name, `${name}.service.ts`)));
705
1047
  }
706
- function presetRecipe(root, directory, app, folder) {
1048
+ function presetRecipe2(root, directory, app, folder) {
707
1049
  const manifest = presetManifest(root, directory);
708
1050
  const dependencies = new Set([manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].flatMap((listed) => Object.keys(listed ?? {})));
709
1051
  const has = (names) => names.some((name) => dependencies.has(name));
710
1052
  const source = presetSource(root, directory, folder.source);
711
1053
  const framework = presetDetectFrameworks.find(([, names]) => has(names))?.[0];
1054
+ const frontend = presetIsDirectory(resolve3(root, source, "pages")) || ["link.tsx", "link.ts"].some((name) => existsSync3(resolve3(root, source, name)));
1055
+ const react = has(["react"]) && (frontend || presetIsDirectory(resolve3(root, source, "components")));
1056
+ const reactRecipe = app || frontend ? presetRecipes.react : presetRecipes.ui;
712
1057
  const servicesFolder = presetServicesFolder(root, source, folder.services, app);
713
- const services = resolve2(root, servicesFolder);
714
- const entries = presetIsDirectory(services) ? readdirSync(services, { withFileTypes: true }) : [];
715
- const folders = entries.filter((entry) => presetIsDirectory(resolve2(services, entry.name))).length;
1058
+ const services = resolve3(root, servicesFolder);
1059
+ const entries = presetIsDirectory(services) ? readdirSync2(services, { withFileTypes: true }) : [];
1060
+ const folders = entries.filter((entry) => presetIsDirectory(resolve3(services, entry.name))).length;
716
1061
  const flat = entries.filter((entry) => entry.isFile() && /\.tsx?$/.test(entry.name)).length > folders;
1062
+ const backend = presetDomains(root, servicesFolder).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services));
1063
+ const layout = flat ? { layout: { services: "flat" } } : {};
1064
+ const style = has(["effect"]) ? { effect: { code: false } } : {};
1065
+ const { deny: _bans, ...effect } = presetRecipes.effect;
717
1066
  if (framework) {
718
- return flat ? { ...presetRecipes[framework], layout: { services: "flat" } } : presetRecipes[framework];
1067
+ return { ...presetRecipes[framework], ...layout, ...style };
719
1068
  }
720
1069
  if (has(presetDetectDesktop)) {
721
1070
  return presetRecipes.desktop;
722
1071
  }
723
- if (presetDomains(root, servicesFolder).length > 0 || has(presetDetectBackend) && (app || presetIsDirectory(services))) {
724
- return presetRecipes.backend;
1072
+ const reactLayout = frontend ? { ...reactRecipe, ...layout } : reactRecipe;
1073
+ if (has(["effect"]) && react) {
1074
+ return { ...reactLayout, ...style };
725
1075
  }
726
- if (has(["react"]) && presetIsDirectory(resolve2(root, source, "components"))) {
727
- return app ? presetRecipes.react : presetRecipes.ui;
1076
+ if (has(["effect"])) {
1077
+ return backend || presetIsDirectory(resolve3(root, source, "domains")) ? { ...presetRecipes.backend, ...effect } : effect;
728
1078
  }
729
- return presetRecipes.library;
730
- }
731
- function presetMergeObject(base, top, depth) {
732
- const merged = { ...base };
733
- for (const [key, value] of Object.entries(top)) {
734
- const previous = Reflect.get(merged, key);
735
- const nested = previous instanceof Object && value instanceof Object && !Array.isArray(previous) && !Array.isArray(value);
736
- if (nested && depth > 0) {
737
- Reflect.set(merged, key, presetMergeObject(previous, value, depth - 1));
738
- } else if (value !== true || !(previous instanceof Object)) {
739
- Reflect.set(merged, key, value);
740
- }
1079
+ if (backend) {
1080
+ return presetRecipes.backend;
741
1081
  }
742
- return merged;
743
- }
744
- function presetMerge(base, top) {
745
- if (!(base instanceof Object) || !(top instanceof Object)) {
746
- return top === true && base instanceof Object ? base : top;
1082
+ if (react) {
1083
+ return reactLayout;
747
1084
  }
748
- return presetMergeObject(base, top, 2);
1085
+ return presetRecipes.library;
749
1086
  }
750
1087
  function presetImportTargets(value) {
751
1088
  if (typeof value === "string") {
@@ -758,7 +1095,7 @@ function presetImportTargets(value) {
758
1095
  }
759
1096
  function presetSpecifiers(manifest, target, file = false) {
760
1097
  const specifiers = Object.entries(manifest.imports ?? {}).flatMap(([key, value]) => presetImportTargets(value).flatMap((mapped) => {
761
- const path = posix2.normalize(mapped);
1098
+ const path = posix3.normalize(mapped);
762
1099
  const parts = path.split("*");
763
1100
  const keyParts = key.split("*");
764
1101
  if (parts.length === 1 && keyParts.length === 1) {
@@ -787,23 +1124,17 @@ function presetOverlaps(left, right) {
787
1124
  function presetDeny(imports) {
788
1125
  return { deny: { imports } };
789
1126
  }
790
- function presetFilled(conventions, exists) {
791
- return Object.fromEntries(Object.entries(conventions).filter(([path, folder]) => {
792
- const empty = folder instanceof Object && folder.deny instanceof Object && Object.keys(folder.deny.imports ?? {}).length === 0;
793
- return exists(path) && !empty;
794
- }));
795
- }
796
1127
  function presetBackendConventions(root, key, value, denied) {
797
1128
  const manifest = presetManifest(root, key);
798
1129
  const source = presetSource(root, key, value.source);
799
- const local = posix2.relative(key, source) || ".";
800
- const at = (path) => local === "." ? path : posix2.join(local, path);
801
- const exists = (path) => existsSync(resolve2(root, key, path));
1130
+ const local = posix3.relative(key, source) || ".";
1131
+ const at = (path) => local === "." ? path : posix3.join(local, path);
1132
+ const exists = (path) => existsSync3(resolve3(root, key, path));
802
1133
  const configured = value.backend instanceof Object ? value.backend.registry : undefined;
803
1134
  const servicesFolder = presetServicesFolder(root, source, value.services);
804
- const servicesKey = posix2.relative(key, servicesFolder);
1135
+ const servicesKey = posix3.relative(key, servicesFolder);
805
1136
  const registryFile = presetRegistryFile(root, source, configured, servicesFolder);
806
- const registry = registryFile === undefined ? undefined : posix2.relative(key, registryFile);
1137
+ const registry = registryFile === undefined ? undefined : posix3.relative(key, registryFile);
807
1138
  const services = presetSpecifiers(manifest, servicesKey);
808
1139
  const message = "Use the service API, not another service’s operations.";
809
1140
  const operations = Object.fromEntries(services.flatMap((prefix) => presetOperationImports.map((pattern) => [`${prefix}/${pattern}`, message])));
@@ -823,27 +1154,27 @@ function presetBackendConventions(root, key, value, denied) {
823
1154
  ...presetDeny({ ...operations, ...Object.fromEntries(services.map((prefix) => [`${prefix}/*/*.service`, "Reach services through the registry."])) })
824
1155
  }
825
1156
  };
826
- for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) => posix2.basename(path))) {
1157
+ for (const domain of presetDirectories(root, servicesFolder, presetNamedSkip).map((path) => posix3.basename(path))) {
827
1158
  const imports = services.flatMap((prefix) => presetOperationImports.filter((pattern) => !denied.has(`${prefix}/${pattern}`)).flatMap((pattern) => [
828
1159
  [`${prefix}/${pattern}`, false],
829
1160
  [`${prefix}/${pattern.replace(/^\*/, `!(${domain})`)}`, message]
830
1161
  ]));
831
- conventions[posix2.join(servicesKey, domain)] = presetDeny(Object.fromEntries(imports));
1162
+ conventions[posix3.join(servicesKey, domain)] = presetDeny(Object.fromEntries(imports));
832
1163
  }
833
1164
  const testFolders = ["tests", "test"];
834
1165
  for (const tests of testFolders) {
835
- if (exists(posix2.join(tests, "kits"))) {
1166
+ if (exists(posix3.join(tests, "kits"))) {
836
1167
  conventions[tests] = presetDeny(Object.fromEntries(presetKitImports.map((pattern) => [pattern, "Import the kit file you need."])));
837
- conventions[posix2.join(tests, "kits")] = { naming: { files: presetKitFiles } };
1168
+ conventions[posix3.join(tests, "kits")] = { naming: { files: presetKitFiles } };
838
1169
  }
839
1170
  }
840
1171
  const queue = ["."];
841
1172
  while (queue.length > 0) {
842
1173
  const folder = queue.shift() ?? ".";
843
- for (const child of presetDirectories(root, posix2.join(key, folder)).map((path) => posix2.relative(key, path))) {
844
- if (posix2.basename(child) === "migrations") {
1174
+ for (const child of presetDirectories(root, posix3.join(key, folder)).map((path) => posix3.relative(key, path))) {
1175
+ if (posix3.basename(child) === "migrations") {
845
1176
  conventions[child] = { layout: false, naming: { files: presetMigrationFiles, unique: presetMigrationNumber } };
846
- } else if (child.split("/").length < 8 && !testFolders.includes(posix2.basename(child))) {
1177
+ } else if (child.split("/").length < 8 && !testFolders.includes(posix3.basename(child))) {
847
1178
  queue.push(child);
848
1179
  }
849
1180
  }
@@ -851,9 +1182,9 @@ function presetBackendConventions(root, key, value, denied) {
851
1182
  return presetFilled(conventions, exists);
852
1183
  }
853
1184
  function presetWebConventions(root, key, value, servers) {
854
- const source = posix2.relative(key, presetSource(root, key, value.source)) || ".";
855
- const at = (path) => source === "." ? path : posix2.join(source, path);
856
- const exists = (path) => existsSync(resolve2(root, key, path));
1185
+ const source = posix3.relative(key, presetSource(root, key, value.source)) || ".";
1186
+ const at = (path) => source === "." ? path : posix3.join(source, path);
1187
+ const exists = (path) => existsSync3(resolve3(root, key, path));
857
1188
  const own = Object.keys(presetManifest(root, key).imports ?? {});
858
1189
  const internals = servers.flatMap((manifest) => Object.keys(manifest.imports ?? {}).filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => specifier.replace(/\*$/, "**")));
859
1190
  const named = servers.flatMap((manifest) => manifest.name ? [manifest.name] : []);
@@ -873,8 +1204,8 @@ function presetWebConventions(root, key, value, servers) {
873
1204
  }
874
1205
  function presetWorkspacePatterns(root, manifest) {
875
1206
  const listed = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
876
- const file = resolve2(root, "pnpm-workspace.yaml");
877
- if (!existsSync(file)) {
1207
+ const file = resolve3(root, "pnpm-workspace.yaml");
1208
+ if (!existsSync3(file)) {
878
1209
  return listed;
879
1210
  }
880
1211
  const patterns = [...listed];
@@ -897,7 +1228,7 @@ function presetWorkspacePatterns(root, manifest) {
897
1228
  return patterns;
898
1229
  }
899
1230
  function presetWorkspaces(root, patterns) {
900
- const clean = (pattern) => posix2.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
1231
+ const clean = (pattern) => posix3.normalize(pattern.replace(/^\.\//, "")).replace(/\/$/, "");
901
1232
  const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(clean);
902
1233
  const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => clean(pattern.slice(1)));
903
1234
  const matches = (path, globs) => globs.some((glob) => path === glob || matchesGlob(path, glob));
@@ -916,7 +1247,7 @@ function presetWorkspaces(root, patterns) {
916
1247
  level.forEach((folder) => candidates.add(folder));
917
1248
  }
918
1249
  }
919
- return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) && existsSync(resolve2(root, path, "package.json"))).toSorted();
1250
+ return [...candidates].filter((path) => matches(path, include) && !matches(path, exclude) && existsSync3(resolve3(root, path, "package.json"))).toSorted();
920
1251
  }
921
1252
  function presetContains(key, path) {
922
1253
  if (key === "." || key === path) {
@@ -942,18 +1273,22 @@ function presetDetect(root, folders) {
942
1273
  };
943
1274
  const patterns = presetWorkspacePatterns(root, manifest);
944
1275
  const directories = presetWorkspaces(root, patterns);
945
- const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe(root, directory, !directory.startsWith("packages/"), configured(directory))]));
946
- const rootRecipe = presetRecipe(root, ".", true, configured("."));
1276
+ const detected = Object.fromEntries(directories.map((directory) => [directory, presetRecipe2(root, directory, !directory.startsWith("packages/"), configured(directory))]));
1277
+ const rootRecipe = presetRecipe2(root, ".", true, configured("."));
947
1278
  const single = Object.keys(manifest).length > 0 || rootRecipe !== presetRecipes.library;
948
1279
  const rootApp = rootRecipe !== presetRecipes.library && presetSource(root, ".", configured(".").source) !== ".";
949
1280
  if (patterns.length === 0 ? single : rootApp) {
950
1281
  detected["."] = rootRecipe;
951
1282
  }
952
- if (existsSync(resolve2(root, "scripts")) && !directories.includes("scripts")) {
1283
+ if (existsSync3(resolve3(root, "scripts")) && !directories.includes("scripts")) {
953
1284
  detected.scripts = presetRecipes.cli;
954
1285
  }
1286
+ if (existsSync3(resolve3(root, ".agents"))) {
1287
+ const { quotes, semicolons } = presetGroupOptions.style;
1288
+ detected[".agents"] = { ...presetRecipes.cli, style: { quotes, semicolons } };
1289
+ }
955
1290
  const keys = Object.keys(folders);
956
- const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix2.dirname(key) === user.slice(0, -2)));
1291
+ const covered = (key) => keys.some((user) => user !== key && user !== "." && (key.startsWith(`${user}/`) || user.endsWith("/*") && posix3.dirname(key) === user.slice(0, -2)));
957
1292
  const merged = { ".": presetDeny(presetRootImports) };
958
1293
  for (const [key, value] of Object.entries(detected)) {
959
1294
  if (!covered(key)) {
@@ -961,7 +1296,9 @@ function presetDetect(root, folders) {
961
1296
  }
962
1297
  }
963
1298
  for (const [key, value] of Object.entries(folders)) {
964
- merged[key] = key in merged ? presetMerge(merged[key], value) : value;
1299
+ const forced = value instanceof Object && presetRecipeMark in value;
1300
+ const kept = forced && key === "." ? presetDeny(presetRootImports) : merged[key];
1301
+ merged[key] = key in merged && (!forced || key === ".") ? presetMerge(kept, value) : value;
965
1302
  }
966
1303
  const length = (key) => key.split("/").length;
967
1304
  const nested = (key) => Object.keys(merged).some((other) => other !== "." && length(other) < length(key) && presetContains(other, key));
@@ -975,6 +1312,7 @@ function presetDetect(root, folders) {
975
1312
  const siblings = Object.entries(merged).filter(([other]) => other !== key && other.endsWith("/*") && length(other) === length(key) && presetContains(other, key));
976
1313
  return [[key, siblings.reduce((inherited, [, sibling]) => presetMerge(sibling, inherited), value)]];
977
1314
  });
1315
+ presetEffectServers(root, merged, units);
978
1316
  const denied = new Set(Object.values(folders).flatMap((folder) => folder instanceof Object && folder.deny instanceof Object ? Object.entries(folder.deny.imports ?? {}).flatMap(([pattern, message]) => typeof message === "string" ? [pattern] : []) : []));
979
1317
  const servers = units.filter(([, value]) => value instanceof Object && Boolean(value.backend)).map(([key]) => presetManifest(root, key));
980
1318
  const internals = servers.flatMap((server) => Object.keys(server.imports ?? {}).map((specifier) => specifier.replace(/\*$/, "**")));
@@ -994,11 +1332,11 @@ function presetDetect(root, folders) {
994
1332
  }
995
1333
  if (value.backend) {
996
1334
  for (const [path, folder] of Object.entries(presetBackendConventions(root, key, value, denied))) {
997
- add(path === "." ? key : posix2.join(key, path), folder);
1335
+ add(path === "." ? key : posix3.join(key, path), folder);
998
1336
  }
999
1337
  } else if (value.tanstack || value.next) {
1000
1338
  for (const [path, folder] of Object.entries(presetWebConventions(root, key, value, servers))) {
1001
- add(path === "." ? key : posix2.join(key, path), folder);
1339
+ add(path === "." ? key : posix3.join(key, path), folder);
1002
1340
  }
1003
1341
  } else if ((value.kit || key.startsWith("packages/")) && !value.expo) {
1004
1342
  const own = Object.keys(presetManifest(root, key).imports ?? {});
@@ -1006,6 +1344,9 @@ function presetDetect(root, folders) {
1006
1344
  const appImports = internals.filter((specifier) => !own.some((mine) => presetOverlaps(mine, specifier))).map((specifier) => [specifier, message]);
1007
1345
  add(key, presetDeny({ ...presetPackageImports, ...Object.fromEntries(appImports) }));
1008
1346
  }
1347
+ for (const [path, folder] of presetEffectConventions(root, key, value, units)) {
1348
+ add(path === "." ? key : posix3.join(key, path), folder);
1349
+ }
1009
1350
  }
1010
1351
  return merged;
1011
1352
  }
@@ -1026,7 +1367,7 @@ var backendGroup = (project, settings) => {
1026
1367
  }
1027
1368
  const configured = typeof settings.registry === "string" ? { file: settings.registry } : settings.registry ?? {};
1028
1369
  const backendUnits = project.units.filter((unit) => unit.kind === "backend");
1029
- const file = configured.file === undefined ? undefined : posix3.normalize(configured.file);
1370
+ const file = configured.file === undefined ? undefined : posix4.normalize(configured.file);
1030
1371
  const registries = file === undefined ? backendUnits.flatMap((unit) => {
1031
1372
  const found = presetRegistryFile(project.root, unit.source, undefined, unit.services);
1032
1373
  return found ? [{ file: found, unit }] : [];
@@ -1036,8 +1377,8 @@ var backendGroup = (project, settings) => {
1036
1377
  throw new PresetError(`backend.registry \`${registry.file}\` must be a project-relative path inside a backend folder.`);
1037
1378
  }
1038
1379
  const options = {
1039
- registry: resolve3(project.root, registry.file),
1040
- services: resolve3(project.root, registry.unit.services),
1380
+ registry: resolve4(project.root, registry.file),
1381
+ services: resolve4(project.root, registry.unit.services),
1041
1382
  skip: configured.skip ?? (file === undefined ? presetRegistrySkip : [])
1042
1383
  };
1043
1384
  if (configured.object !== undefined) {
@@ -1052,9 +1393,11 @@ var backendGroup = (project, settings) => {
1052
1393
  var basicsGroup = (project, settings) => {
1053
1394
  const trivial = { mode: "precise", async: false, callees: [] };
1054
1395
  const { appFiles, safetyFiles, ui, web } = project;
1396
+ const nextEntries = presetScopes(project.nextWeb, "app/**/{layout,page,default}.tsx");
1055
1397
  return [
1056
1398
  ...settings.trivial ? [
1057
- presetOverride(appFiles, { "arch/code-trivial-functions": ["error", trivial] }, ui),
1399
+ presetOverride(appFiles, { "arch/code-trivial-functions": ["error", trivial] }, [...ui, ...nextEntries]),
1400
+ presetOverride(nextEntries, { "arch/code-trivial-functions": ["error", { ...trivial, defaults: false }] }, ui),
1058
1401
  presetOverride([...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")], { "arch/code-trivial-functions": ["error", { ...trivial, services: true }] }, ui)
1059
1402
  ] : [],
1060
1403
  ...settings.state ? [presetOverride(safetyFiles, { "arch/code-mutable-state": ["error", { kinds: ["let", "var"] }] }, ui)] : [],
@@ -1075,22 +1418,20 @@ var consoleGroup = () => [presetOverride([`**/${presetExtensions}`], { "no-conso
1075
1418
 
1076
1419
  // src/preset/groups/deny.ts
1077
1420
  var denyGroup = (project, settings) => {
1078
- const { imports, calls } = settings;
1421
+ const kept = (entries) => Object.fromEntries(Object.entries(entries).filter((entry) => entry[1] !== false));
1422
+ const imports = kept(settings.imports);
1423
+ const calls = kept(settings.calls);
1079
1424
  const overrides = [];
1080
1425
  if (Object.keys(imports).length > 0 || Object.keys(calls).length > 0) {
1081
1426
  overrides.push(presetOverride([`**/${presetExtensions}`], { "arch/boundaries-deny": ["error", { imports, calls }] }));
1082
1427
  }
1083
- for (const unit of project.units) {
1084
- const writes = unit.kind === "backend" ? unit.scope.effective.backend?.writes ?? [] : [];
1085
- if (writes.length === 0) {
1086
- continue;
1087
- }
1428
+ const reads = Object.fromEntries(presetQueryWrites.map((name) => [name, "Query files only read; move writes to an action."]));
1429
+ for (const unit of project.units.filter((item) => item.kind === "backend")) {
1088
1430
  const files = [
1089
1431
  ...presetServiceScopes(project.serviceFolders, [unit.source], "**/*-query.*.ts"),
1090
1432
  ...presetServiceScopes(project.serviceFolders, project.operations.filter((path) => path === unit.source), "*/queries/*.ts")
1091
1433
  ];
1092
- const reads = Object.fromEntries(writes.map((name) => [name, "Query files only read; move writes to an action."]));
1093
- overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: { ...reads, ...calls } }] }));
1434
+ overrides.push(presetOverride(files, { "arch/boundaries-deny": ["error", { imports, calls: kept({ ...reads, ...settings.calls }) }] }));
1094
1435
  }
1095
1436
  return overrides;
1096
1437
  };
@@ -1133,9 +1474,18 @@ var effectTestOff = {
1133
1474
  var effectGroup = (project, settings) => {
1134
1475
  const files = [`**/${presetExtensions}`];
1135
1476
  const max = settings.width;
1477
+ const style = presetOverride(files, {
1478
+ "typescript/consistent-type-assertions": ["error", { assertionStyle: "never" }],
1479
+ "eslint-js/max-len": ["error", { code: max, ignoreUrls: true, ignoreComments: true, ignoreRegExpLiterals: true }],
1480
+ ...presetEffectStyle().rules
1481
+ });
1482
+ if (!settings.code) {
1483
+ return [style];
1484
+ }
1136
1485
  const { tsgo, linteffect } = presetEffectPackages();
1137
1486
  const tsgoRules = [tsgo.correctness, tsgo.antipattern, tsgo.effectNative, tsgo.style].flatMap((config) => Object.keys(config.rules ?? {}));
1138
1487
  return [
1488
+ style,
1139
1489
  presetOverride(files, {
1140
1490
  ...Object.fromEntries(tsgoRules.map((rule) => [rule, "error"])),
1141
1491
  ...linteffect.recommended.rules,
@@ -1146,6 +1496,9 @@ var effectGroup = (project, settings) => {
1146
1496
  presetOverride(files, {
1147
1497
  "arch/code-trivial-functions": "off",
1148
1498
  "arch/code-branch-once": "error",
1499
+ "arch/code-object-params": ["error", { all: true, services: true }],
1500
+ "arch/code-switch": ["error", { message: "Branch with Match." }],
1501
+ "arch/naming-exports": "off",
1149
1502
  "arch/code-function-nesting": ["error", { max: 4 }],
1150
1503
  "arch/code-inline": ["error", { max, nesting: 2, keep: ["^[A-Z]", "^layer"], allow: "^(?:decode|encode)[A-Z]", services: ["of"] }],
1151
1504
  "arch/code-yield-argument": "error",
@@ -1210,17 +1563,17 @@ var expoGroup = (project, settings) => {
1210
1563
  };
1211
1564
 
1212
1565
  // src/preset/groups/externals.ts
1213
- var externalsGroup = (_project, settings) => [
1214
- presetOverride(["**/{vite,next}.config.{ts,mts,cts,js,mjs,cjs}"], { "arch/files-externals": ["error", { packages: settings.packages }] })
1566
+ var externalsGroup = () => [
1567
+ presetOverride(["**/{vite,next}.config.{ts,mts,cts,js,mjs,cjs}"], { "arch/files-externals": ["error", { packages: presetExternals }] })
1215
1568
  ];
1216
1569
 
1217
1570
  // src/preset/groups/forms.ts
1218
- var formsGroup = (project, settings) => [
1219
- presetOverride(project.reactFiles, { "arch/code-paired-call": ["error", { when: "useForm", require: settings.resolver }] }, project.ui)
1571
+ var formsGroup = (project) => [
1572
+ presetOverride(project.reactFiles, { "arch/code-paired-call": ["error", { when: "useForm", require: "standardSchemaResolver" }] }, project.ui)
1220
1573
  ];
1221
1574
 
1222
1575
  // src/preset/groups/imports.ts
1223
- import { resolve as resolve4 } from "node:path";
1576
+ import { resolve as resolve5 } from "node:path";
1224
1577
  function importsPackageNames(project, kinds) {
1225
1578
  return project.units.flatMap((unit) => {
1226
1579
  const name = kinds.includes(unit.kind) ? presetPackageName(project.root, unit.key) : undefined;
@@ -1258,7 +1611,7 @@ var importsGroup = (project, settings) => {
1258
1611
  if (!settings.boundaries) {
1259
1612
  return overrides;
1260
1613
  }
1261
- const absolute = (paths) => paths.map((path) => resolve4(project.root, path));
1614
+ const absolute = (paths) => paths.map((path) => resolve5(project.root, path));
1262
1615
  overrides.push(presetOverride([...project.appFiles, ...presetScopes(project.packages, `**/${presetExtensions}`)], {
1263
1616
  "arch/boundaries-imports": [
1264
1617
  "error",
@@ -1266,7 +1619,7 @@ var importsGroup = (project, settings) => {
1266
1619
  web: absolute(project.web),
1267
1620
  backend: absolute(project.backend),
1268
1621
  packages: absolute(project.packages),
1269
- roles: project.suffixes,
1622
+ roles: project.fileKinds,
1270
1623
  services: absolute([...project.web, ...project.backend].map((source) => project.serviceFolders[source])),
1271
1624
  portable: true,
1272
1625
  aliases: presetAliases(project.root, settings.aliases),
@@ -1280,18 +1633,18 @@ var importsGroup = (project, settings) => {
1280
1633
  };
1281
1634
 
1282
1635
  // src/preset/groups/layout.ts
1283
- import { posix as posix4 } from "node:path";
1636
+ import { posix as posix5 } from "node:path";
1284
1637
  var layoutGroup = (project, settings) => {
1285
1638
  const { web, nextWeb, serviceRoots, app, services, utilities, ui, testFiles, layouts, operations } = project;
1286
1639
  const folderOperations = presetServiceScopes(project.serviceFolders, operations, "*/{actions,queries}/*.ts");
1287
1640
  const operationMessage = "Action/query files export exactly one named operation. Keep helpers used only by this operation private inside its function; do not export them. Move a helper to the module's .utils.ts only when it is genuinely shared, not merely to satisfy lint.";
1288
- const roles = [...presetBuiltinFileRoles.filter((role) => role !== "action" && role !== "query"), ...project.suffixes];
1289
- const conceptFiles = project.suffixes.map((suffix) => `**/*.${suffix}.ts`);
1641
+ const names = settings.files.map((name) => name.split("*").map((part) => part.replaceAll(".", "[.]")).join("[a-z0-9]+(?:-[a-z0-9]+)*"));
1642
+ const kindFiles = project.fileKinds.map((kind) => `**/*.${kind}.ts`);
1290
1643
  const frontendEntries = [...presetServiceScopes(project.serviceFolders, web, "**/*.{client,server,rsc}.ts"), ...presetServiceScopes(project.serviceFolders, web, "**/*.rsc.tsx")];
1291
- const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix4.join(path, "components")));
1644
+ const ungroupedWeb = web.filter((path) => !layouts.some((scope) => scope.path === posix5.join(path, "components")));
1292
1645
  return [
1293
1646
  ...layouts.flatMap(({ path, folder, mode }) => {
1294
- const folders = folder === "services" && operations.some((source) => posix4.join(source, "services") === path);
1647
+ const folders = folder === "services" && operations.some((source) => posix5.join(source, "services") === path);
1295
1648
  const registries = folder === "services" ? project.registries : [];
1296
1649
  const restricted = presetOverride(mode === "flat" ? [`${path}/*/**/${presetExtensions}`] : [`${path}/${presetExtensions}`, `${path}/*/*/**/${presetExtensions}`], {
1297
1650
  "arch/files-restricted": [
@@ -1314,11 +1667,16 @@ var layoutGroup = (project, settings) => {
1314
1667
  "arch/naming-file": [
1315
1668
  "error",
1316
1669
  {
1317
- pattern: `^[a-z0-9]+(?:-[a-z0-9]+)*(?:[.](?:${roles.join("|")})[.]ts|[.]rsc[.]tsx)$`,
1318
- message: `Domain files use domain[-part].<role>.ts with a role from: ${roles.join(", ")}. Add new roles to layout.suffixes.`
1670
+ pattern: `^(?:${names.join("|")})$`,
1671
+ message: `Domain files use one of: ${settings.files.join(", ")}. Add a name to layout.files.`
1319
1672
  }
1320
1673
  ]
1321
- }, ["**/*-{action,query}.*.ts", ...testFiles, ...registries]);
1674
+ }, [
1675
+ ...settings.files.includes("*-action.*.ts") ? ["**/*-action.*.ts"] : [],
1676
+ ...settings.files.includes("*-query.*.ts") ? ["**/*-query.*.ts"] : [],
1677
+ ...testFiles,
1678
+ ...registries
1679
+ ]);
1322
1680
  return [...domain, strict];
1323
1681
  }),
1324
1682
  presetOverride(presetScopes(ungroupedWeb, `components/*/*/**/${presetExtensions}`), { "arch/files-restricted": ["error", { message: "Components allow one group folder." }] }, ui),
@@ -1335,16 +1693,16 @@ var layoutGroup = (project, settings) => {
1335
1693
  "**/*.{client,server,rsc,utils,constants}.ts",
1336
1694
  "**/*.rsc.tsx",
1337
1695
  ...presetServiceScopes(project.serviceFolders, nextWeb, "**/*.action.ts"),
1338
- ...conceptFiles,
1696
+ ...kindFiles,
1339
1697
  ...project.colocated,
1340
1698
  ...testFiles
1341
1699
  ]),
1342
1700
  presetOverride(services, {
1343
1701
  "arch/backend-service-functions": "error",
1344
1702
  "arch/types-inline": ["error", { parameters: true, returns: true, functions: true, min: 1 }]
1345
- }, [...utilities, ...testFiles, ...conceptFiles]),
1703
+ }, [...utilities, ...testFiles, ...kindFiles]),
1346
1704
  presetOverride(frontendEntries, { "arch/backend-service-functions": ["error", { frontend: true }] }, testFiles),
1347
- ...conceptFiles.map((pattern) => presetOverride(presetServiceScopes(project.serviceFolders, app, pattern), {
1705
+ ...kindFiles.map((pattern) => presetOverride(presetServiceScopes(project.serviceFolders, app, pattern), {
1348
1706
  "arch/backend-service-functions": "off",
1349
1707
  "arch/naming-exports": ["error", { stem: "full-basename", scope: "functions" }]
1350
1708
  }, testFiles)),
@@ -1522,7 +1880,7 @@ var nextGroup = (project, settings) => {
1522
1880
  // src/preset/groups/orpc.ts
1523
1881
  var orpcGroup = (project) => project.orpc.public.length > 0 ? [
1524
1882
  presetOverride(project.orpc.public, {
1525
- "arch/backend-orpc-output": ["error", { handler: "handler", output: "output", composers: project.orpc.composers }]
1883
+ "arch/backend-orpc-output": ["error", { handler: "handler", output: "output" }]
1526
1884
  })
1527
1885
  ] : [];
1528
1886
 
@@ -1619,13 +1977,8 @@ var shadcnGroup = (project) => {
1619
1977
  };
1620
1978
 
1621
1979
  // src/preset/groups/sql.ts
1622
- var sqlGroup = (project, settings) => [
1623
- presetOverride(presetServiceScopes(project.serviceFolders, project.serviceRoots, "**/*.ts"), {
1624
- "arch/backend-sql-like": [
1625
- "error",
1626
- { methods: ["like", "ilike"], operators: ["where"], sanitizers: settings.sanitizers, bindings: true }
1627
- ]
1628
- })
1980
+ var sqlGroup = (project) => [
1981
+ presetOverride(presetServiceScopes(project.serviceFolders, project.serviceRoots, "**/*.ts"), { "arch/backend-sql-like": "error" })
1629
1982
  ];
1630
1983
 
1631
1984
  // src/preset/groups/style.ts
@@ -1677,7 +2030,7 @@ var tanstackGroup = (project, settings) => {
1677
2030
  "no-restricted-imports": [
1678
2031
  "error",
1679
2032
  {
1680
- paths: [{ name: "@tanstack/react-start/rsc", message: "Render server components only in services/*.rsc.tsx files." }],
2033
+ paths: [presetTanstackRsc],
1681
2034
  patterns: project.baseImports
1682
2035
  }
1683
2036
  ]
@@ -1713,7 +2066,7 @@ function optionsPatternsTest(patterns, value) {
1713
2066
  function optionsGlob(pattern) {
1714
2067
  const negated = [];
1715
2068
  const source = pattern.replaceAll(/!\(([^()]*)\)/g, (_, options) => {
1716
- negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\*?{}]/g, "\\$&")).join("|"));
2069
+ negated.push(options.split("|").map((option) => option.replaceAll(/[.+^$()|[\]\\?{}]/g, "\\$&").replaceAll("*", "[^/]*")).join("|"));
1717
2070
  return `\x02${negated.length - 1}\x03`;
1718
2071
  }).replaceAll(/[.+^$()|[\]\\]/g, "\\$&").replaceAll("?", "[^/]").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*").replaceAll(/\{([^}]*)\}/g, (_, options) => `(?:${options.split(",").join("|")})`);
1719
2072
  return new RegExp(`^${negated.reduce((text, names, index) => text.replace(`\x02${index}\x03`, `(?!(?:${names})(?:/|$))[^/]*`), source)}$`);
@@ -1829,12 +2182,12 @@ var typesGroup = (project, settings) => {
1829
2182
  };
1830
2183
 
1831
2184
  // src/preset/preset-folders.ts
1832
- import { existsSync as existsSync3, readdirSync as readdirSync4, realpathSync as realpathSync2 } from "node:fs";
1833
- import { posix as posix7, resolve as resolve7 } from "node:path";
2185
+ import { existsSync as existsSync5, readdirSync as readdirSync5, realpathSync as realpathSync2 } from "node:fs";
2186
+ import { posix as posix8, resolve as resolve8 } from "node:path";
1834
2187
 
1835
2188
  // src/preset/preset-stylesheet.ts
1836
- import { readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync } from "node:fs";
1837
- import { posix as posix5, resolve as resolve5 } from "node:path";
2189
+ import { readdirSync as readdirSync3, readFileSync as readFileSync3, realpathSync } from "node:fs";
2190
+ import { posix as posix6, resolve as resolve6 } from "node:path";
1838
2191
  function presetTailwindEntry(css) {
1839
2192
  const code = css.replaceAll(/\/\*[\s\S]*?\*\//g, "");
1840
2193
  return /(?:^|[;{}])\s*@(?:import\s+(?:url\(\s*(["']?)tailwindcss(?:\/[^"')\s]*)?\1\s*\)|(["'])tailwindcss(?:\/[^"']*)?\2)|tailwind\s)/.test(code);
@@ -1845,15 +2198,15 @@ function presetStylesheet(root, directory) {
1845
2198
  const visited = new Set;
1846
2199
  while (queue.length > 0) {
1847
2200
  const folder = queue.shift() ?? directory;
1848
- const real = realpathSync(resolve5(root, folder));
2201
+ const real = realpathSync(resolve6(root, folder));
1849
2202
  if (visited.has(real)) {
1850
2203
  continue;
1851
2204
  }
1852
2205
  visited.add(real);
1853
- for (const name of readdirSync2(resolve5(root, folder))) {
1854
- const path = posix5.join(folder, name);
1855
- const full = resolve5(root, path);
1856
- const depth = posix5.relative(directory, path).split("/").length;
2206
+ for (const name of readdirSync3(resolve6(root, folder))) {
2207
+ const path = posix6.join(folder, name);
2208
+ const full = resolve6(root, path);
2209
+ const depth = posix6.relative(directory, path).split("/").length;
1857
2210
  if (presetIsDirectory(full)) {
1858
2211
  if (!name.startsWith(".") && !presetDetectSkip.includes(name) && depth < 8) {
1859
2212
  queue.push(path);
@@ -1864,15 +2217,15 @@ function presetStylesheet(root, directory) {
1864
2217
  }
1865
2218
  }
1866
2219
  const rank = (path) => {
1867
- const index = presetStylesheetNames.indexOf(posix5.basename(path, ".css"));
2220
+ const index = presetStylesheetNames.indexOf(posix6.basename(path, ".css"));
1868
2221
  return index === -1 ? presetStylesheetNames.length : index;
1869
2222
  };
1870
2223
  return found.toSorted((left, right) => rank(left) - rank(right) || left.length - right.length).at(0);
1871
2224
  }
1872
2225
 
1873
2226
  // src/preset/preset-units.ts
1874
- import { existsSync as existsSync2, readdirSync as readdirSync3 } from "node:fs";
1875
- import { posix as posix6, resolve as resolve6 } from "node:path";
2227
+ import { existsSync as existsSync4, readdirSync as readdirSync4 } from "node:fs";
2228
+ import { posix as posix7, resolve as resolve7 } from "node:path";
1876
2229
  var presetFrameworks = ["tanstack", "next", "expo"];
1877
2230
  var presetKindGroups = [...presetFrameworks, "backend", "console"];
1878
2231
  function presetUnitSource(root, key, kind, source) {
@@ -1880,9 +2233,9 @@ function presetUnitSource(root, key, kind, source) {
1880
2233
  if (source !== "." && !source.split("/").every((segment) => presetLiteralSegment.test(segment))) {
1881
2234
  throw new PresetError(`Folder \`${key}\` source must be \`.\` or a literal relative path.`);
1882
2235
  }
1883
- return posix6.join(key, source);
2236
+ return posix7.join(key, source);
1884
2237
  }
1885
- return kind === "console" || !existsSync2(resolve6(root, key, "src")) ? key : posix6.join(key, "src");
2238
+ return kind === "console" || !existsSync4(resolve7(root, key, "src")) ? key : posix7.join(key, "src");
1886
2239
  }
1887
2240
  function presetSets(folder, name) {
1888
2241
  return folder[name] !== undefined && folder[name] !== false;
@@ -1911,10 +2264,10 @@ function presetHasManifest(root, key) {
1911
2264
  return false;
1912
2265
  }
1913
2266
  if (!key.endsWith("/*")) {
1914
- return existsSync2(resolve6(root, key, "package.json"));
2267
+ return existsSync4(resolve7(root, key, "package.json"));
1915
2268
  }
1916
- const base = resolve6(root, key.slice(0, -2));
1917
- return presetIsDirectory(base) && readdirSync3(base).some((name) => existsSync2(resolve6(base, name, "package.json")));
2269
+ const base = resolve7(root, key.slice(0, -2));
2270
+ return presetIsDirectory(base) && readdirSync4(base).some((name) => existsSync4(resolve7(base, name, "package.json")));
1918
2271
  }
1919
2272
  function presetUnit(root, key, scope) {
1920
2273
  const { effective } = scope;
@@ -1963,20 +2316,24 @@ function presetUnits(root, scopes, ignored) {
1963
2316
  return [presetUnit(root, scope.key, scope)];
1964
2317
  }
1965
2318
  const base = scope.key.slice(0, -2);
1966
- return readdirSync3(resolve6(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve6(root, base, name))).map((name) => posix6.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
2319
+ return readdirSync4(resolve7(root, base)).filter((name) => !name.startsWith(".") && presetIsDirectory(resolve7(root, base, name))).map((name) => posix7.join(base, name)).filter((key) => !explicit.has(key) && !ignored.has(key)).map((key) => presetUnit(root, key, scope));
1967
2320
  });
1968
2321
  }
1969
2322
 
1970
2323
  // src/preset/preset-folders.ts
1971
- function presetNormalizeKey(key) {
2324
+ function presetPortableKey(key) {
1972
2325
  const portable = key.replaceAll("\\", "/");
1973
- if (!portable.trim() || posix7.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
2326
+ if (!portable.trim() || posix8.isAbsolute(portable) || /^[a-z]:/i.test(portable)) {
1974
2327
  throw new PresetError(`Folder keys must be project-relative paths: ${key}`);
1975
2328
  }
1976
- const normalized = posix7.normalize(portable).replace(/\/$/, "");
2329
+ const normalized = posix8.normalize(portable).replace(/\/$/, "");
1977
2330
  if (normalized === ".." || normalized.startsWith("../")) {
1978
2331
  throw new PresetError(`Folder keys must stay inside the project: ${key}`);
1979
2332
  }
2333
+ return normalized;
2334
+ }
2335
+ function presetNormalizeKey(key) {
2336
+ const normalized = presetPortableKey(key);
1980
2337
  const segments = normalized.split("/");
1981
2338
  if (normalized !== "." && segments.some((segment) => segment !== "*" && !presetLiteralSegment.test(segment))) {
1982
2339
  throw new PresetError(`Folder keys must be literal paths; only a whole \`*\` segment may be a wildcard: ${key}`);
@@ -1986,6 +2343,25 @@ function presetNormalizeKey(key) {
1986
2343
  }
1987
2344
  return normalized;
1988
2345
  }
2346
+ function presetBraces(key) {
2347
+ const match = /\{([^{}]*)\}/.exec(key);
2348
+ if (!match) {
2349
+ return [key];
2350
+ }
2351
+ return match[1].split(",").flatMap((option) => presetBraces(`${key.slice(0, match.index)}${option}${key.slice(match.index + match[0].length)}`));
2352
+ }
2353
+ function presetIsFileKey(root, key) {
2354
+ const name = key.split("/").at(-1) ?? "";
2355
+ const pattern = key.includes("*");
2356
+ if (!pattern && !existsSync5(resolve8(root, key)) && /[^.]\.[^.]+$/.test(name)) {
2357
+ throw new PresetError(`File \`${key}\` does not exist.`);
2358
+ }
2359
+ const file = pattern ? name !== "*" && name.includes(".") : presetIsFile(resolve8(root, key));
2360
+ if (file && !key.split("/").every((segment) => presetFileSegment.test(segment))) {
2361
+ throw new PresetError(`File keys hold letters, digits, \`._-@$[]\` and \`*\` wildcards: ${key}`);
2362
+ }
2363
+ return file;
2364
+ }
1989
2365
  function presetMatches(pattern, path) {
1990
2366
  return pattern.every((segment, index) => segment === "*" || segment === path[index]);
1991
2367
  }
@@ -2021,13 +2397,28 @@ function presetGroupValue(name, value, inherited, severity) {
2021
2397
  return { ...base, ...merged, level: severity ?? base.level };
2022
2398
  }
2023
2399
  function presetDenyEntries(inherited, changes) {
2024
- return Object.fromEntries(Object.entries({ ...inherited, ...changes }).flatMap(([key, message]) => {
2400
+ return Object.fromEntries(Object.entries({ ...inherited, ...changes }).map(([key, message]) => {
2025
2401
  if (message !== false && typeof message !== "string") {
2026
2402
  throw new PresetError(`Deny entry \`${key}\` takes a message string or false.`);
2027
2403
  }
2028
- return message === false ? [] : [[key, message]];
2404
+ return [key, message];
2029
2405
  }));
2030
2406
  }
2407
+ function presetLayoutNames(inherited, entries) {
2408
+ if (!Array.isArray(entries) || !entries.every((entry) => typeof entry === "string" && presetLayoutFileName.test(entry))) {
2409
+ throw new PresetError("layout.files takes file names such as `*.prompts.ts`; `!*.utils.ts` removes one.");
2410
+ }
2411
+ const names = [...inherited];
2412
+ for (const entry of entries) {
2413
+ const index = names.indexOf(entry.startsWith("!") ? entry.slice(1) : entry);
2414
+ if (entry.startsWith("!") && index !== -1) {
2415
+ names.splice(index, 1);
2416
+ } else if (!entry.startsWith("!") && index === -1) {
2417
+ names.push(entry);
2418
+ }
2419
+ }
2420
+ return names;
2421
+ }
2031
2422
  function presetEffective(folder, parent) {
2032
2423
  const resolveGroup = (name) => {
2033
2424
  const defaults = presetDefaultGroups.includes(name) ? presetGroupOptions[name] : null;
@@ -2043,6 +2434,11 @@ function presetEffective(folder, parent) {
2043
2434
  calls: presetDenyEntries(inherited.calls, deny.calls)
2044
2435
  };
2045
2436
  }
2437
+ const layout = folder.layout;
2438
+ if (effective.layout && layout instanceof Object && layout.files !== undefined) {
2439
+ const inherited = (parent ? parent.layout?.files : null) ?? presetGroupOptions.layout.files;
2440
+ effective.layout = { ...effective.layout, files: presetLayoutNames(inherited, layout.files) };
2441
+ }
2046
2442
  return effective;
2047
2443
  }
2048
2444
  function presetFolder(key, value) {
@@ -2073,7 +2469,7 @@ function presetScopesTree(root, folders) {
2073
2469
  }
2074
2470
  for (const key of keys) {
2075
2471
  const base = key.endsWith("/*") ? key.slice(0, -2) : key;
2076
- if (!existsSync3(resolve7(root, base))) {
2472
+ if (!existsSync5(resolve8(root, base))) {
2077
2473
  throw new PresetError(`Folder \`${key}\` does not exist.`);
2078
2474
  }
2079
2475
  }
@@ -2105,14 +2501,26 @@ function presetScopesTree(root, folders) {
2105
2501
  return scopes;
2106
2502
  }
2107
2503
  function presetProject(config) {
2108
- const given = resolve7(config.root ?? process.cwd());
2109
- const root = existsSync3(given) ? realpathSync2(given) : given;
2110
- const listed = Object.entries(config.folders ?? {}).map(([key, value]) => [presetNormalizeKey(key), value]);
2111
- const twice = listed.find(([key], index) => listed.findIndex(([other]) => other === key) !== index);
2504
+ const given = resolve8(config.root ?? process.cwd());
2505
+ const root = existsSync5(given) ? realpathSync2(given) : given;
2506
+ const expanded = Object.entries(config.folders ?? {}).flatMap(([key, value]) => presetBraces(key).map((path) => [presetPortableKey(path), value, key.includes("{")]));
2507
+ const files = expanded.filter(([key]) => presetIsFileKey(root, key));
2508
+ const listed = expanded.filter((entry) => !files.includes(entry)).map(([key, value, fromList]) => [presetNormalizeKey(key), value, fromList]);
2509
+ const twice = listed.find(([key, , fromList], index) => listed.some(([other, , otherFromList], at) => at < index && other === key && !fromList && !otherFromList));
2112
2510
  if (twice) {
2113
2511
  throw new PresetError(`Folder \`${twice[0]}\` is listed twice.`);
2114
2512
  }
2115
- const folders = config.detect === false ? Object.fromEntries(listed) : presetDetect(root, Object.fromEntries(listed));
2513
+ const configured = {};
2514
+ for (const [key, value] of listed) {
2515
+ configured[key] = key in configured ? presetMerge(configured[key], value) : value;
2516
+ }
2517
+ const detected = config.detect === false ? configured : presetDetect(root, configured);
2518
+ const conventions = Object.entries(detected).filter(([key]) => !(key in configured) && presetIsFileKey(root, key));
2519
+ const folders = Object.fromEntries(Object.entries(detected).filter(([key]) => !conventions.some(([other]) => other === key)));
2520
+ const fileEntries = [
2521
+ ...conventions.filter(([key]) => !files.some(([other]) => other === key)),
2522
+ ...files.map(([key, value]) => [key, key in detected ? presetMerge(detected[key], value) : value])
2523
+ ];
2116
2524
  const scopes = presetScopesTree(root, folders);
2117
2525
  const ignored = new Set(Object.entries(folders).flatMap(([key, value]) => value === false ? [presetNormalizeKey(key)] : []));
2118
2526
  const live = scopes.filter((scope) => !ignored.has(scope.key));
@@ -2120,6 +2528,22 @@ function presetProject(config) {
2120
2528
  if (dead) {
2121
2529
  throw new PresetError(`Folder \`${dead.key}\` sits inside the skipped folder \`${dead.parent?.key}\`.`);
2122
2530
  }
2531
+ const fileKeys = fileEntries.flatMap(([key, value]) => {
2532
+ if (value === false) {
2533
+ return [];
2534
+ }
2535
+ const folder = presetFolder(key, value);
2536
+ const option = Object.keys(folder).find((name) => presetFolderOnlyKeys.includes(name));
2537
+ if (option) {
2538
+ throw new PresetError(`Set \`${option}\` on a folder, not on \`${key}\`.`);
2539
+ }
2540
+ if (Object(folder.layout).operations !== undefined) {
2541
+ throw new PresetError(`Set layout.operations on an app or package folder, not on \`${key}\`.`);
2542
+ }
2543
+ const pattern = presetGlobEscape(key);
2544
+ const reached = live.filter((scope) => presetIntersect(pattern, scope.key).length > 0).map((scope) => ({ key: scope.key, parent: scope, depth: scope.depth, folder, effective: presetEffective(folder, scope.effective) }));
2545
+ return [{ pattern, folder, scopes: reached }];
2546
+ });
2123
2547
  const units = presetUnits(root, scopes, ignored);
2124
2548
  const unitKeys = new Set(units.map((unit) => unit.scope.key));
2125
2549
  const misplaced = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.layout).operations !== undefined);
@@ -2130,10 +2554,6 @@ function presetProject(config) {
2130
2554
  if (misplaced) {
2131
2555
  throw new PresetError(`Set layout.operations on an app or package folder, not on \`${misplaced.key}\`.`);
2132
2556
  }
2133
- const writes = live.find((scope) => !unitKeys.has(scope.key) && Object(scope.folder.backend).writes !== undefined);
2134
- if (writes) {
2135
- throw new PresetError(`Set backend.writes on a backend folder, not on \`${writes.key}\`.`);
2136
- }
2137
2557
  const of = (kind) => units.filter((unit) => unit.kind === kind);
2138
2558
  const webUnits = of("web");
2139
2559
  const web = webUnits.map((unit) => unit.source);
@@ -2147,19 +2567,15 @@ function presetProject(config) {
2147
2567
  const appFiles = presetScopes(app);
2148
2568
  const webFiles = presetScopes(web);
2149
2569
  const effective = live.map((scope) => scope.effective);
2150
- const suffixes = [...new Set(effective.flatMap((groups) => groups.layout?.suffixes ?? []))];
2151
- if (suffixes.some((suffix) => !/^[a-z][a-z0-9-]*$/.test(suffix))) {
2152
- throw new PresetError("Layout suffixes must be lowercase kebab-case names.");
2153
- }
2154
- if (suffixes.some((suffix) => presetBuiltinFileRoles.includes(suffix))) {
2155
- throw new PresetError("Layout suffixes must not replace built-in file roles.");
2156
- }
2570
+ const fileKinds = [
2571
+ ...new Set([...effective, ...fileKeys.flatMap((fileKey) => fileKey.scopes.map((scope) => scope.effective))].flatMap((groups) => (groups.layout?.files ?? []).flatMap((name) => presetLayoutFiles.includes(name) ? [] : /^\*\.([a-z][a-z0-9-]*)\.ts$/.exec(name)?.[1] ?? [])))
2572
+ ];
2157
2573
  const orpcScopes = effective.flatMap((groups) => groups.orpc ? [groups.orpc] : []);
2158
2574
  const listOf = (values) => [...new Set(values.flatMap((value) => value ?? []))];
2159
2575
  const client = orpcScopes.find((settings) => settings.client !== undefined)?.client;
2160
- const rpcClients = client === undefined ? webUnits.map((unit) => posix7.join(unit.services, unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
2576
+ const rpcClients = client === undefined ? webUnits.map((unit) => posix8.join(unit.services, unit.layout.services === "domain" ? "rpc/rpc.client.ts" : "rpc.client.ts")) : [client];
2161
2577
  const kitUnits = units.filter((unit) => unit.scope.effective.kit);
2162
- const uiRoots = [...new Set([...web.map((path) => posix7.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
2578
+ const uiRoots = [...new Set([...web.map((path) => posix8.join(path, "components/ui")), ...kitUnits.map((unit) => unit.source)])];
2163
2579
  const webUiRoots = uiRoots.filter((path) => web.some((webRoot) => webRoot === "." || path === webRoot || path.startsWith(`${webRoot}/`)));
2164
2580
  const stylesheet = (unit) => {
2165
2581
  const tailwind = unit.scope.effective.tailwind;
@@ -2167,7 +2583,7 @@ function presetProject(config) {
2167
2583
  return;
2168
2584
  }
2169
2585
  const css = tailwind.css ?? presetStylesheet(root, unit.key);
2170
- return css === undefined ? undefined : resolve7(root, css);
2586
+ return css === undefined ? undefined : resolve8(root, css);
2171
2587
  };
2172
2588
  const webCss = Object.fromEntries(webUnits.flatMap((unit) => {
2173
2589
  const css = stylesheet(unit);
@@ -2201,21 +2617,21 @@ function presetProject(config) {
2201
2617
  return [key];
2202
2618
  }
2203
2619
  const base = key.slice(0, -2);
2204
- return readdirSync4(resolve7(root, base)).filter((name) => presetIsDirectory(resolve7(root, base, name))).map((name) => posix7.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
2620
+ return readdirSync5(resolve8(root, base)).filter((name) => presetIsDirectory(resolve8(root, base, name))).map((name) => posix8.join(base, name)).filter((path) => !live.some((scope) => scope.key === path));
2205
2621
  });
2206
2622
  return {
2207
2623
  root,
2208
2624
  serviceFolders,
2209
2625
  scopes: live,
2626
+ fileKeys,
2210
2627
  units,
2211
- suffixes,
2628
+ fileKinds,
2212
2629
  colocated: listOf(effective.map((groups) => groups.types?.colocated)),
2213
2630
  orpc: {
2214
- public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix7.join(unit.source, "rpc/api")).filter((path) => existsSync3(resolve7(root, path))).map((path) => `${path}/**`),
2215
- composers: listOf(orpcScopes.map((settings) => settings.composers)),
2631
+ public: publicProcedures.length > 0 ? publicProcedures : backendUnits.map((unit) => posix8.join(unit.source, "rpc/api")).filter((path) => existsSync5(resolve8(root, path))).map((path) => `${path}/**`),
2216
2632
  client: client ?? ""
2217
2633
  },
2218
- layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: folder === "services" ? unit.services : posix7.join(unit.source, folder), folder, mode }))),
2634
+ layouts: units.flatMap((unit) => Object.entries(unit.layout).map(([folder, mode]) => ({ path: folder === "services" ? unit.services : posix8.join(unit.source, folder), folder, mode }))),
2219
2635
  rpcClients,
2220
2636
  operations: units.flatMap((unit) => unit.scope.effective.layout?.operations === "folders" ? [unit.source] : []),
2221
2637
  packages: libraryUnits.map((unit) => unit.key),
@@ -2253,7 +2669,7 @@ function presetProject(config) {
2253
2669
  baseImports: presetBaseImports,
2254
2670
  css: { ...webCss, ...kitCss },
2255
2671
  registries: backendUnits.flatMap((unit) => presetRegistryFile(root, unit.source, unit.scope.effective.backend?.registry, unit.services) ?? []),
2256
- ignores: skipped.map((key) => `${key}/**`),
2672
+ ignores: [...skipped.map((key) => `${key}/**`), ...fileEntries.flatMap(([key, value]) => value === false ? [presetGlobEscape(key)] : [])],
2257
2673
  outputs: [".", ...units.map((unit) => unit.key)].flatMap((key) => presetOutputFolders.map((folder) => key === "." ? `${folder}/**` : `${key}/${folder}/**`))
2258
2674
  };
2259
2675
  }
@@ -2262,7 +2678,7 @@ function presetProject(config) {
2262
2678
  var recipes = presetRecipes;
2263
2679
  var templates = presetTemplates;
2264
2680
  var externals = presetExternals;
2265
- var require2 = createRequire2(import.meta.url);
2681
+ var require2 = createRequire3(import.meta.url);
2266
2682
  function presetOptional(name, group) {
2267
2683
  try {
2268
2684
  return require2.resolve(name);
@@ -2311,41 +2727,70 @@ function presetSettings(scope, name) {
2311
2727
  }
2312
2728
  return settings ? null : { level: presetLevel(scope) };
2313
2729
  }
2314
- function presetEmit(project, name, builder) {
2730
+ function presetKeyFiles(fileKey, scope, globs, expand) {
2731
+ const scoped = globs.flatMap((glob) => presetIntersect(glob, scope.key));
2732
+ const within = (glob) => {
2733
+ const prefix = glob.slice(0, -`**/${presetExtensions}`.length).replace(/\/$/, "");
2734
+ return glob.endsWith(`**/${presetExtensions}`) && !/[*?{[]/.test(prefix) && (prefix === "" || fileKey.pattern.startsWith(`${prefix}/`)) && /\.(?:ts|tsx|mts|cts)$/.test(fileKey.pattern);
2735
+ };
2736
+ return scoped.some(within) ? presetIntersect(fileKey.pattern, scope.key) : expand(fileKey).filter((path) => scoped.some((glob) => matchesGlob2(path, glob))).map(presetGlobEscape);
2737
+ }
2738
+ function presetEmit(project, name, builder, expand) {
2315
2739
  const key = (scope) => JSON.stringify(presetSettings(scope, name));
2316
2740
  const breaks = project.scopes.filter((scope) => scope.parent === null || key(scope) !== key(scope.parent));
2317
- const nearest = (owner) => breaks.filter((scope) => {
2318
- let current = scope.parent;
2319
- while (current && current !== owner && !breaks.includes(current)) {
2741
+ const region = (scope) => {
2742
+ let current = scope;
2743
+ while (current && !breaks.includes(current)) {
2320
2744
  current = current.parent;
2321
2745
  }
2322
- return scope !== owner && current === owner;
2323
- });
2746
+ return current;
2747
+ };
2748
+ const keyed = project.fileKeys.flatMap((fileKey) => fileKey.scopes.filter((scope) => scope.parent && key(scope) !== key(scope.parent)).map((scope) => ({ owner: region(scope.parent), fileKey, scope })));
2324
2749
  return breaks.flatMap((owner) => {
2325
2750
  const settings = presetSettings(owner, name);
2326
2751
  if (!settings) {
2327
2752
  return [];
2328
2753
  }
2329
- const excluded = nearest(owner).map((scope) => `${scope.key}/**`);
2754
+ const nested = breaks.filter((scope) => scope !== owner && region(scope.parent) === owner).map((scope) => `${scope.key}/**`);
2755
+ const owned = keyed.filter((entry) => entry.owner === owner);
2330
2756
  return builder(project, settings, owner).flatMap((override) => {
2331
2757
  const files = override.files.flatMap((glob) => presetIntersect(glob, owner.key));
2332
2758
  if (files.length === 0 || !override.rules) {
2333
2759
  return [];
2334
2760
  }
2761
+ const replaced = owned.flatMap((entry) => presetKeyFiles(entry.fileKey, entry.scope, override.files, expand));
2335
2762
  return [
2336
2763
  {
2337
2764
  ...override,
2338
2765
  files,
2339
- excludeFiles: [...override.excludeFiles ?? [], ...excluded],
2766
+ excludeFiles: [...override.excludeFiles ?? [], ...nested, ...replaced],
2340
2767
  rules: presetSeverity(override.rules, settings.level)
2341
2768
  }
2342
2769
  ];
2343
2770
  });
2344
2771
  });
2345
2772
  }
2773
+ function presetFileEmit(project, name, builder, expand) {
2774
+ return project.fileKeys.flatMap((fileKey) => fileKey.scopes.flatMap((scope) => {
2775
+ const owner = scope.parent;
2776
+ const settings = presetSettings(scope, name);
2777
+ if (!owner || !settings || JSON.stringify(presetSettings(owner, name)) === JSON.stringify(settings)) {
2778
+ return [];
2779
+ }
2780
+ const nested = project.scopes.filter((other) => other.parent === owner).map((other) => `${other.key}/**`);
2781
+ return builder(project, settings, scope).flatMap((override) => {
2782
+ const files = presetKeyFiles(fileKey, scope, override.files, expand);
2783
+ if (files.length === 0 || !override.rules) {
2784
+ return [];
2785
+ }
2786
+ return [{ ...override, files, excludeFiles: [...override.excludeFiles ?? [], ...nested], rules: presetSeverity(override.rules, settings.level) }];
2787
+ });
2788
+ }));
2789
+ }
2346
2790
  function presetPlugins(project) {
2347
2791
  const react = project.scopes.some((scope) => scope.effective.react);
2348
2792
  const effect = project.scopes.some((scope) => scope.effective.effect);
2793
+ const effectCode = project.scopes.some((scope) => scope.effective.effect?.code);
2349
2794
  const tailwinds = project.scopes.flatMap((scope) => scope.effective.tailwind ? [scope.effective.tailwind] : []);
2350
2795
  const shadcns = project.scopes.flatMap((scope) => scope.effective.shadcn ? [scope.effective.shadcn] : []);
2351
2796
  const tailwind = tailwinds.at(0);
@@ -2368,11 +2813,14 @@ function presetPlugins(project) {
2368
2813
  { name: "dillon-anti-slop", specifier: require2.resolve("oxlint-plugin-arch/dillon-anti-slop") }
2369
2814
  ];
2370
2815
  const plugins = ["typescript", "react", "react-perf", "jsx-a11y", "import", "unicorn", "promise", "vitest", "node", "oxc"];
2371
- if (effect) {
2816
+ if (effectCode) {
2372
2817
  const { tsgo, linteffect } = presetEffectPackages();
2373
2818
  plugins.push(...tsgo.correctness.plugins ?? []);
2374
2819
  jsPlugins.push(...linteffect.recommended.jsPlugins);
2375
2820
  }
2821
+ if (effect) {
2822
+ jsPlugins.push(...presetEffectStyle().jsPlugins);
2823
+ }
2376
2824
  const settings = {
2377
2825
  react: { version: "19" },
2378
2826
  "import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx", ".mts", ".cts"] } }
@@ -2395,17 +2843,29 @@ function preset(config) {
2395
2843
  const project = presetProject(config);
2396
2844
  const [root] = project.scopes;
2397
2845
  const level = presetLevel(root);
2846
+ const expanded = new Map;
2847
+ const expand = (fileKey) => {
2848
+ const paths = expanded.get(fileKey.pattern) ?? globSync(fileKey.pattern, { cwd: project.root, exclude: (path) => posix9.basename(path) === "node_modules" });
2849
+ expanded.set(fileKey.pattern, paths);
2850
+ return paths;
2851
+ };
2398
2852
  const emitGroup = (name) => {
2399
2853
  const builder = presetBuilders[name];
2400
- return builder ? presetEmit(project, name, builder) : [];
2854
+ return builder ? [...presetEmit(project, name, builder, expand), ...presetFileEmit(project, name, builder, expand)] : [];
2401
2855
  };
2402
2856
  const severities = project.scopes.filter((scope) => scope.parent && presetLevel(scope) !== presetLevel(scope.parent)).toSorted((left, right) => left.depth - right.depth).map((scope) => ({ files: [`${scope.key}/**`], rules: presetSeverity(presetCommonRules, presetLevel(scope)) }));
2403
- const overrides = [...severities, ...presetGroupOrder.flatMap(emitGroup)];
2857
+ const fileSeverities = project.fileKeys.flatMap((fileKey) => fileKey.folder.severity ? [{ files: [fileKey.pattern], rules: presetSeverity(presetCommonRules, fileKey.folder.severity) }] : []);
2858
+ const overrides = [...severities, ...fileSeverities, ...presetGroupOrder.flatMap(emitGroup)];
2404
2859
  for (const scope of project.scopes.toSorted((left, right) => left.depth - right.depth)) {
2405
2860
  if (scope.folder.rules) {
2406
2861
  overrides.push({ files: [scope.key === "." ? "**" : `${scope.key}/**`], rules: scope.folder.rules });
2407
2862
  }
2408
2863
  }
2864
+ for (const fileKey of project.fileKeys) {
2865
+ if (fileKey.folder.rules) {
2866
+ overrides.push({ files: [fileKey.pattern], rules: fileKey.folder.rules });
2867
+ }
2868
+ }
2409
2869
  return {
2410
2870
  options: { typeAware: true },
2411
2871
  categories: { correctness: level },