@alveolus/arch 0.2.0 → 0.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 (60) hide show
  1. package/README.md +6 -0
  2. package/dist/bin.mjs +4 -2
  3. package/dist/bin.mjs.map +1 -1
  4. package/dist/{cli-P5PwH9OE.mjs → docs-DsQHpTtV.mjs} +190 -5
  5. package/dist/docs-DsQHpTtV.mjs.map +1 -0
  6. package/dist/index.d.mts +44 -2
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +2 -2
  9. package/docs/core/application/command-handlers.md +617 -0
  10. package/docs/core/application/event-publishers.md +234 -0
  11. package/docs/core/application/event-translators.md +329 -0
  12. package/docs/core/application/index.md +99 -0
  13. package/docs/core/application/integration-events.md +277 -0
  14. package/docs/core/application/outbox.md +416 -0
  15. package/docs/core/application/query-handlers.md +292 -0
  16. package/docs/core/application/unit-of-work.md +352 -0
  17. package/docs/core/domain/aggregates.md +822 -0
  18. package/docs/core/domain/domain-errors.md +251 -0
  19. package/docs/core/domain/domain-events.md +292 -0
  20. package/docs/core/domain/domain-services.md +249 -0
  21. package/docs/core/domain/entities.md +431 -0
  22. package/docs/core/domain/index.md +93 -0
  23. package/docs/core/domain/ports.md +284 -0
  24. package/docs/core/domain/repositories.md +335 -0
  25. package/docs/core/domain/value-objects.md +425 -0
  26. package/docs/core/domain/views.md +265 -0
  27. package/docs/core/index.md +108 -0
  28. package/docs/core/strategic/anti-corruption-layers.md +349 -0
  29. package/docs/core/strategic/index.md +83 -0
  30. package/docs/core/strategic/open-host-services.md +287 -0
  31. package/docs/core/strategic/published-language.md +265 -0
  32. package/docs/core/utilities/result.md +413 -0
  33. package/docs/guide/agents.md +68 -0
  34. package/docs/guide/existing-project.md +105 -0
  35. package/docs/guide/getting-started.md +275 -0
  36. package/docs/guide/learning-path.md +123 -0
  37. package/docs/guide/project-layout.md +324 -0
  38. package/docs/guide/versioning.md +42 -0
  39. package/docs/integrations/index.md +112 -0
  40. package/docs/integrations/nestjs.md +169 -0
  41. package/docs/rules/index.md +183 -0
  42. package/docs/rules/layers/no-driving-shortcut.md +119 -0
  43. package/docs/rules/layers/no-impure-domain.md +189 -0
  44. package/docs/rules/layers/no-outward-import.md +184 -0
  45. package/docs/rules/layers/no-portless-adapter.md +123 -0
  46. package/docs/rules/strategic/no-cross-context-import.md +140 -0
  47. package/docs/rules/strategic/no-fat-shared-kernel.md +81 -0
  48. package/docs/rules/strategic/no-leaky-host-service.md +107 -0
  49. package/docs/rules/strategic/no-unmapped-context.md +111 -0
  50. package/docs/rules/tactical/no-aggregate-reference.md +139 -0
  51. package/docs/rules/tactical/no-foreign-command-dependency.md +119 -0
  52. package/docs/rules/tactical/no-foreign-query-dependency.md +106 -0
  53. package/docs/rules/tactical/no-loose-code.md +171 -0
  54. package/docs/rules/tactical/no-misplaced-class.md +146 -0
  55. package/docs/rules/tactical/no-public-field.md +113 -0
  56. package/docs/rules/tactical/no-stateful-service.md +102 -0
  57. package/docs/rules/tactical/no-thrown-failure.md +162 -0
  58. package/docs/rules/tooling/no-loose-disable.md +98 -0
  59. package/package.json +4 -3
  60. package/dist/cli-P5PwH9OE.mjs.map +0 -1
package/README.md CHANGED
@@ -35,8 +35,14 @@ Seventeen rules, each with a page that says what it reports, why, how to fix it
35
35
  cannot see. A baseline for existing projects, `error` / `warn` / `info` levels, disable comments
36
36
  with a reason, JSON and SARIF output for the pull request.
37
37
 
38
+ The documentation is installed with the package, for you and for a coding agent:
39
+ `npx alveolus explain layers/no-impure-domain` prints a rule, `npx alveolus explain aggregates` a
40
+ building block, and `npx alveolus init` writes the configuration and the instructions that tell
41
+ Claude Code, Cursor or Codex to read them there.
42
+
38
43
  - [Rules](https://alveolus.dev/rules/)
39
44
  - [Getting started](https://alveolus.dev/guide/getting-started)
40
45
  - [Adopt it on an existing project](https://alveolus.dev/guide/existing-project)
46
+ - [Coding agents](https://alveolus.dev/guide/agents)
41
47
 
42
48
  Node.js 24 or later. MIT.
package/dist/bin.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { t as Cli } from "./cli-P5PwH9OE.mjs";
2
+ import { r as Cli, t as Docs } from "./docs-DsQHpTtV.mjs";
3
+ import { fileURLToPath } from "node:url";
3
4
  //#region src/bin.ts
4
- const cli = new Cli(process.stdout, process.stderr, process.cwd(), process.stdout.isTTY);
5
+ const docs = new Docs(fileURLToPath(new URL("../docs/", import.meta.url)));
6
+ const cli = new Cli(process.stdout, process.stderr, process.cwd(), docs, process.stdout.isTTY);
5
7
  process.exitCode = await cli.run(process.argv.slice(2));
6
8
  //#endregion
7
9
  export {};
package/dist/bin.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.mjs","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Cli } from \"./cli/index.ts\";\n\nconst cli = new Cli(process.stdout, process.stderr, process.cwd(), process.stdout.isTTY);\nprocess.exitCode = await cli.run(process.argv.slice(2));\n"],"mappings":";;;AAGA,MAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,GAAG,QAAQ,OAAO,KAAK;AACvF,QAAQ,WAAW,MAAM,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"bin.mjs","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { fileURLToPath } from \"node:url\";\n\nimport { Cli } from \"./cli/index.ts\";\nimport { Docs } from \"./docs/index.ts\";\n\nconst docs = new Docs(fileURLToPath(new URL(\"../docs/\", import.meta.url)));\nconst cli = new Cli(process.stdout, process.stderr, process.cwd(), docs, process.stdout.isTTY);\nprocess.exitCode = await cli.run(process.argv.slice(2));\n"],"mappings":";;;;AAMA,MAAM,OAAO,IAAI,KAAK,cAAc,IAAI,IAAI,YAAY,YAAY,GAAG,CAAC,CAAC;AACzE,MAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,GAAG,MAAM,QAAQ,OAAO,KAAK;AAC7F,QAAQ,WAAW,MAAM,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { Command, CommanderError, Option } from "commander";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, globSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, join, matchesGlob, normalize, relative, resolve, sep } from "node:path";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import { createHash } from "node:crypto";
@@ -1997,7 +1997,9 @@ var Report = class {
1997
1997
  text() {
1998
1998
  const blocks = [];
1999
1999
  for (const [file, violations] of this.byFile()) blocks.push(this.block(file, violations));
2000
- return `${[...blocks, this.summary()].join("\n\n")}\n`;
2000
+ const parts = [...blocks, this.summary()];
2001
+ if (blocks.length > 0) parts.push(this.colors.dim("Why, and how to fix it: npx alveolus explain <rule>"));
2002
+ return `${parts.join("\n\n")}\n`;
2001
2003
  }
2002
2004
  json() {
2003
2005
  const { baselined, files, stale, suppressed, violations } = this.input;
@@ -2773,18 +2775,91 @@ var TsMorphImporter = class TsMorphImporter extends Importer {
2773
2775
  return offset !== "" && !offset.startsWith("..") && !isAbsolute(offset) && !offset.split(/[\\/]/).includes("node_modules");
2774
2776
  }
2775
2777
  };
2778
+ const scaffolds = [
2779
+ {
2780
+ content: `import { defineConfig } from "@alveolus/arch";
2781
+
2782
+ export default defineConfig({
2783
+ boundedContexts: {},
2784
+ root: "src",
2785
+ });
2786
+ `,
2787
+ path: "alveolus.config.ts"
2788
+ },
2789
+ {
2790
+ content: `---
2791
+ name: alveolus
2792
+ description: Domain-Driven Design with @alveolus/core and @alveolus/arch. Use when writing or changing a class of the domain, the application or an adapter, when a class extends an Alveolus building block, or when alveolus arch check reports a violation.
2793
+ ---
2794
+
2795
+ # Alveolus
2796
+
2797
+ The documentation is installed with the package: read it with \`npx alveolus explain <topic>\`, not on the web. \`npx alveolus explain\` lists every topic.
2798
+
2799
+ 1. Before writing a class, read its building block: \`npx alveolus explain aggregates\`, \`entities\`, \`value-objects\`, \`domain-events\`, \`domain-errors\`, \`ports\`, \`repositories\`, \`command-handlers\`, \`query-handlers\`, \`result\`.
2800
+ 2. Where a file goes and what each layer may import: \`npx alveolus explain project-layout\`.
2801
+ 3. After each change, run \`npx alveolus arch check\`.
2802
+ 4. On a violation, read the rule before changing the code: \`npx alveolus explain <rule>\`, with the rule id of the report, such as \`layers/no-impure-domain\`. Fix the cause: never turn a rule off, add a disable comment or edit \`alveolus.baseline.json\` by hand without asking.
2803
+ `,
2804
+ path: ".claude/skills/alveolus/SKILL.md"
2805
+ },
2806
+ {
2807
+ content: `## Alveolus
2808
+
2809
+ This project uses Alveolus for Domain-Driven Design: the building blocks of \`@alveolus/core\` and the architecture checks of \`@alveolus/arch\`. The documentation is installed with the package: \`npx alveolus explain\` lists the topics and \`npx alveolus explain <topic>\` prints one, so do not look for it on the web. Run \`npx alveolus arch check\` after each change, and read the rule reported with \`npx alveolus explain <rule>\` before fixing. See \`.claude/skills/alveolus/SKILL.md\`.
2810
+ `,
2811
+ marker: "npx alveolus explain",
2812
+ path: "AGENTS.md"
2813
+ }
2814
+ ];
2815
+ //#endregion
2816
+ //#region src/init/init.ts
2817
+ var Init = class {
2818
+ projectDir;
2819
+ constructor(projectDir) {
2820
+ this.projectDir = projectDir;
2821
+ }
2822
+ run() {
2823
+ const written = [];
2824
+ for (const scaffold of scaffolds) written.push({
2825
+ outcome: this.write(scaffold),
2826
+ path: scaffold.path
2827
+ });
2828
+ return written;
2829
+ }
2830
+ get hint() {
2831
+ const claude = join(this.projectDir, "CLAUDE.md");
2832
+ if (!existsSync(claude)) return;
2833
+ return "CLAUDE.md exists: Claude Code reads it instead of AGENTS.md, so add a line with @AGENTS.md to it.";
2834
+ }
2835
+ write(scaffold) {
2836
+ const path = join(this.projectDir, scaffold.path);
2837
+ if (!existsSync(path)) {
2838
+ mkdirSync(dirname(path), { recursive: true });
2839
+ writeFileSync(path, scaffold.content);
2840
+ return "created";
2841
+ }
2842
+ if (scaffold.marker === void 0) return "kept";
2843
+ const existing = readFileSync(path, "utf8");
2844
+ if (existing.includes(scaffold.marker)) return "kept";
2845
+ writeFileSync(path, `${existing.trimEnd()}\n\n${scaffold.content}`);
2846
+ return "appended";
2847
+ }
2848
+ };
2776
2849
  //#endregion
2777
2850
  //#region src/cli/cli.ts
2778
2851
  var Cli = class {
2779
2852
  stdout;
2780
2853
  stderr;
2781
2854
  cwd;
2855
+ docs;
2782
2856
  colored;
2783
2857
  exitCode = 0;
2784
- constructor(stdout, stderr, cwd, colored = false) {
2858
+ constructor(stdout, stderr, cwd, docs, colored = false) {
2785
2859
  this.stdout = stdout;
2786
2860
  this.stderr = stderr;
2787
2861
  this.cwd = cwd;
2862
+ this.docs = docs;
2788
2863
  this.colored = colored;
2789
2864
  }
2790
2865
  async run(args) {
@@ -2804,8 +2879,30 @@ var Cli = class {
2804
2879
  const arch = program.command("arch").description("Check the architecture of a Domain-Driven Design project");
2805
2880
  this.withOptions(arch.command("check").description("Report the violations that are not in the baseline")).action((options) => this.check(options));
2806
2881
  this.withOptions(arch.command("baseline").description(`Write the current violations to ${Baseline.fileName}`)).option("--allow-growth", "write the baseline even when it holds more entries than before").action((options) => this.baseline(options));
2882
+ program.command("explain").description("Print a page of the documentation: a rule, a building block or a guide").argument("[topic]", "a rule id, a building block or a guide; without it, the list of topics").action((topic) => this.explain(topic));
2883
+ program.command("init").description(`Write ${ConfigLoader.fileName}, and the instructions that tell a coding agent to read the documentation installed with the package`).option("--project <dir>", "project directory", ".").action((options) => this.init(options));
2807
2884
  return program;
2808
2885
  }
2886
+ explain(topic) {
2887
+ if (topic === void 0) {
2888
+ this.stdout.write(`${this.docs.topics().join("\n")}\n`);
2889
+ return;
2890
+ }
2891
+ const match = this.docs.find(topic);
2892
+ if ("page" in match) {
2893
+ this.stdout.write(match.page.text());
2894
+ return;
2895
+ }
2896
+ const list = match.candidates.length === 0 ? "alveolus explain lists the topics." : `Did you mean ${match.candidates.join(", ")}?`;
2897
+ this.stderr.write(`No page for ${topic}: ${list}\n`);
2898
+ this.exitCode = 1;
2899
+ }
2900
+ init(options) {
2901
+ const init = new Init(resolve(this.cwd, options.project));
2902
+ for (const { outcome, path } of init.run()) this.stdout.write(`${outcome} ${path}\n`);
2903
+ this.stdout.write(`Name your bounded contexts in ${ConfigLoader.fileName}, then run alveolus arch check.\n`);
2904
+ if (init.hint !== void 0) this.stderr.write(`${init.hint}\n`);
2905
+ }
2809
2906
  withOptions(command) {
2810
2907
  return command.option("--project <dir>", "project directory", ".").option("--config <file>", "configuration file", ConfigLoader.fileName).option("--tsconfig <file>", "TypeScript configuration, tsconfig.json or the one set in the configuration file").addOption(new Option("--format <format>", "how violations are printed").choices([
2811
2908
  "text",
@@ -2869,6 +2966,94 @@ var Cli = class {
2869
2966
  }
2870
2967
  };
2871
2968
  //#endregion
2872
- export { Config as a, RuleRegistry as c, AllowedPackages as d, Baseline as f, ConfigLoader as i, Rule as l, TsMorphImporter as n, Report as o, Importer as r, Checker as s, Cli as t, Architecture as u };
2969
+ //#region src/docs/page.ts
2970
+ const html = [
2971
+ {
2972
+ pattern: /<dt>/g,
2973
+ text: "- "
2974
+ },
2975
+ {
2976
+ pattern: /<\/dt>\s*<dd>/g,
2977
+ text: ": "
2978
+ },
2979
+ {
2980
+ pattern: /<[^>\n]+>/g,
2981
+ text: ""
2982
+ },
2983
+ {
2984
+ pattern: /^\t+- /gm,
2985
+ text: "- "
2986
+ },
2987
+ {
2988
+ pattern: /&lt;/g,
2989
+ text: "<"
2990
+ },
2991
+ {
2992
+ pattern: /&gt;/g,
2993
+ text: ">"
2994
+ },
2995
+ {
2996
+ pattern: /&amp;/g,
2997
+ text: "&"
2998
+ }
2999
+ ];
3000
+ const frontmatter = /^---\n([\s\S]*?)\n---\n/;
3001
+ var Page = class {
3002
+ topic;
3003
+ source;
3004
+ constructor(topic, source) {
3005
+ this.topic = topic;
3006
+ this.source = source;
3007
+ }
3008
+ get description() {
3009
+ const header = frontmatter.exec(this.source)?.[1] ?? "";
3010
+ return /^description: "?(.*?)"?$/m.exec(header)?.[1] ?? "";
3011
+ }
3012
+ text() {
3013
+ let text = this.source.replace(frontmatter, "");
3014
+ for (const { pattern, text: replacement } of html) text = text.replace(pattern, replacement);
3015
+ return `${text.replace(/\n{3,}/g, "\n\n").trim()}\n`;
3016
+ }
3017
+ };
3018
+ //#endregion
3019
+ //#region src/docs/docs.ts
3020
+ var Docs = class Docs {
3021
+ dir;
3022
+ static sections = [
3023
+ "guide",
3024
+ "integrations",
3025
+ "core",
3026
+ "rules"
3027
+ ];
3028
+ constructor(dir) {
3029
+ this.dir = dir;
3030
+ }
3031
+ topics() {
3032
+ const topics = [];
3033
+ for (const section of Docs.sections) for (const file of globSync("**/*.md", { cwd: join(this.dir, section) }).sort()) topics.push(this.topicOf(join(section, file)));
3034
+ return topics.sort();
3035
+ }
3036
+ find(name) {
3037
+ const wanted = name.replace(/\.md$/, "").replace(/\/$/, "").replace(/\\/g, "/");
3038
+ const candidates = this.topics().filter((topic) => topic === wanted || topic.endsWith(`/${wanted}`));
3039
+ const exact = candidates.find((topic) => topic === wanted);
3040
+ if (exact !== void 0) return { page: this.read(exact) };
3041
+ if (candidates.length === 1 && candidates[0] !== void 0) return { page: this.read(candidates[0]) };
3042
+ return { candidates };
3043
+ }
3044
+ read(topic) {
3045
+ const path = join(this.dir, ...topic.split("/"));
3046
+ try {
3047
+ return new Page(topic, readFileSync(`${path}.md`, "utf8"));
3048
+ } catch {
3049
+ return new Page(topic, readFileSync(join(path, "index.md"), "utf8"));
3050
+ }
3051
+ }
3052
+ topicOf(file) {
3053
+ return file.split(sep).join("/").replace(/\.md$/, "").replace(/\/index$/, "");
3054
+ }
3055
+ };
3056
+ //#endregion
3057
+ export { TsMorphImporter as a, Config as c, RuleRegistry as d, Rule as f, Baseline as h, Init as i, Report as l, AllowedPackages as m, Page as n, Importer as o, Architecture as p, Cli as r, ConfigLoader as s, Docs as t, Checker as u };
2873
3058
 
2874
- //# sourceMappingURL=cli-P5PwH9OE.mjs.map
3059
+ //# sourceMappingURL=docs-DsQHpTtV.mjs.map