fabricatio 0.2.8.dev4__cp312-cp312-win_amd64.whl → 0.2.9__cp312-cp312-win_amd64.whl

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 (46) hide show
  1. fabricatio/__init__.py +4 -11
  2. fabricatio/actions/__init__.py +1 -0
  3. fabricatio/actions/article.py +98 -110
  4. fabricatio/actions/article_rag.py +15 -10
  5. fabricatio/actions/output.py +60 -4
  6. fabricatio/actions/rag.py +2 -1
  7. fabricatio/actions/rules.py +72 -0
  8. fabricatio/capabilities/__init__.py +1 -0
  9. fabricatio/capabilities/censor.py +23 -6
  10. fabricatio/capabilities/check.py +46 -27
  11. fabricatio/capabilities/correct.py +35 -16
  12. fabricatio/capabilities/rag.py +5 -4
  13. fabricatio/capabilities/rating.py +56 -49
  14. fabricatio/capabilities/review.py +1 -1
  15. fabricatio/capabilities/task.py +2 -1
  16. fabricatio/config.py +5 -3
  17. fabricatio/fs/readers.py +20 -1
  18. fabricatio/models/action.py +59 -36
  19. fabricatio/models/extra/__init__.py +1 -0
  20. fabricatio/models/extra/advanced_judge.py +4 -4
  21. fabricatio/models/extra/article_base.py +124 -61
  22. fabricatio/models/extra/article_main.py +100 -17
  23. fabricatio/models/extra/article_outline.py +2 -3
  24. fabricatio/models/extra/article_proposal.py +15 -14
  25. fabricatio/models/extra/patches.py +17 -4
  26. fabricatio/models/extra/problem.py +31 -23
  27. fabricatio/models/extra/rule.py +39 -8
  28. fabricatio/models/generic.py +369 -78
  29. fabricatio/models/task.py +1 -1
  30. fabricatio/models/tool.py +149 -14
  31. fabricatio/models/usages.py +46 -42
  32. fabricatio/parser.py +5 -5
  33. fabricatio/rust.cp312-win_amd64.pyd +0 -0
  34. fabricatio/{_rust.pyi → rust.pyi} +42 -4
  35. fabricatio/{_rust_instances.py → rust_instances.py} +1 -1
  36. fabricatio/utils.py +5 -5
  37. fabricatio/workflows/__init__.py +1 -0
  38. fabricatio/workflows/articles.py +3 -5
  39. fabricatio-0.2.9.data/scripts/tdown.exe +0 -0
  40. {fabricatio-0.2.8.dev4.dist-info → fabricatio-0.2.9.dist-info}/METADATA +1 -1
  41. fabricatio-0.2.9.dist-info/RECORD +61 -0
  42. fabricatio/_rust.cp312-win_amd64.pyd +0 -0
  43. fabricatio-0.2.8.dev4.data/scripts/tdown.exe +0 -0
  44. fabricatio-0.2.8.dev4.dist-info/RECORD +0 -56
  45. {fabricatio-0.2.8.dev4.dist-info → fabricatio-0.2.9.dist-info}/WHEEL +0 -0
  46. {fabricatio-0.2.8.dev4.dist-info → fabricatio-0.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -3,6 +3,7 @@
3
3
  from itertools import chain
4
4
  from typing import Dict, Generator, List, Self, Tuple, override
5
5
 
6
+ from fabricatio.fs.readers import extract_sections
6
7
  from fabricatio.journal import logger
7
8
  from fabricatio.models.extra.article_base import (
8
9
  ArticleBase,
@@ -14,22 +15,33 @@ from fabricatio.models.extra.article_base import (
14
15
  from fabricatio.models.extra.article_outline import (
15
16
  ArticleOutline,
16
17
  )
17
- from fabricatio.models.generic import CensoredAble, Display, PersistentAble, SequencePatch, WithRef
18
+ from fabricatio.models.generic import Described, PersistentAble, SequencePatch, SketchedAble, WithRef, WordCount
19
+ from fabricatio.rust import word_count
18
20
  from fabricatio.utils import ok
21
+ from pydantic import Field
19
22
 
23
+ PARAGRAPH_SEP = "// - - -"
20
24
 
21
- class Paragraph(CensoredAble):
25
+
26
+ class Paragraph(SketchedAble, WordCount, Described):
22
27
  """Structured academic paragraph blueprint for controlled content generation."""
23
28
 
24
- description: str
25
- """Functional summary of the paragraph's role in document structure."""
29
+ description: str = Field(
30
+ alias="elaboration",
31
+ description=Described.model_fields["description"].description,
32
+ )
26
33
 
27
- writing_aim: List[str]
34
+ aims: List[str]
28
35
  """Specific communicative objectives for this paragraph's content."""
29
36
 
30
37
  content: str
31
38
  """The actual content of the paragraph, represented as a string."""
32
39
 
40
+ @classmethod
41
+ def from_content(cls, content: str) -> Self:
42
+ """Create a Paragraph object from the given content."""
43
+ return cls(elaboration="", aims=[], expected_word_count=word_count(content), content=content)
44
+
33
45
 
34
46
  class ArticleParagraphSequencePatch(SequencePatch[Paragraph]):
35
47
  """Patch for `Paragraph` list of `ArticleSubsection`."""
@@ -41,11 +53,28 @@ class ArticleSubsection(SubSectionBase):
41
53
  paragraphs: List[Paragraph]
42
54
  """List of Paragraph objects containing the content of the subsection."""
43
55
 
56
+ _max_word_count_deviation: float = 0.3
57
+ """Maximum allowed deviation from the expected word count, as a percentage."""
58
+
59
+ @property
60
+ def word_count(self) -> int:
61
+ """Calculates the total word count of all paragraphs in the subsection."""
62
+ return sum(word_count(p.content) for p in self.paragraphs)
63
+
44
64
  def introspect(self) -> str:
45
- """Introspects the subsection and returns a message if it has no paragraphs."""
65
+ """Introspects the subsection and returns a summary of its state."""
66
+ summary = ""
46
67
  if len(self.paragraphs) == 0:
47
- return f"`{self.__class__.__name__}` titled `{self.title}` have no paragraphs, to achieve the goal of `{self.writing_aim}`."
48
- return ""
68
+ summary += f"`{self.__class__.__name__}` titled `{self.title}` have no paragraphs, You should add some!\n"
69
+ if (
70
+ abs((wc := self.word_count) - self.expected_word_count) / self.expected_word_count
71
+ > self._max_word_count_deviation
72
+ ):
73
+ summary += (
74
+ f"`{self.__class__.__name__}` titled `{self.title}` have {wc} words, expected {self.expected_word_count} words!"
75
+ )
76
+
77
+ return summary
49
78
 
50
79
  def update_from_inner(self, other: Self) -> Self:
51
80
  """Updates the current instance with the attributes of another instance."""
@@ -61,20 +90,62 @@ class ArticleSubsection(SubSectionBase):
61
90
  Returns:
62
91
  str: Typst code snippet for rendering.
63
92
  """
64
- return f"=== {self.title}\n" + "\n\n".join(p.content for p in self.paragraphs)
93
+ return f"=== {self.title}\n" + f"\n{PARAGRAPH_SEP}\n".join(p.content for p in self.paragraphs)
94
+
95
+ @classmethod
96
+ def from_typst_code(cls, title: str, body: str) -> Self:
97
+ """Creates an Article object from the given Typst code."""
98
+ return cls(
99
+ heading=title,
100
+ elaboration="",
101
+ paragraphs=[Paragraph.from_content(p) for p in body.split(PARAGRAPH_SEP)],
102
+ expected_word_count=word_count(body),
103
+ aims=[],
104
+ support_to=[],
105
+ depend_on=[],
106
+ )
65
107
 
66
108
 
67
109
  class ArticleSection(SectionBase[ArticleSubsection]):
68
110
  """Atomic argumentative unit with high-level specificity."""
69
111
 
112
+ @classmethod
113
+ def from_typst_code(cls, title: str, body: str) -> Self:
114
+ """Creates an Article object from the given Typst code."""
115
+ return cls(
116
+ subsections=[
117
+ ArticleSubsection.from_typst_code(*pack) for pack in extract_sections(body, level=3, section_char="=")
118
+ ],
119
+ heading=title,
120
+ elaboration="",
121
+ expected_word_count=word_count(body),
122
+ aims=[],
123
+ support_to=[],
124
+ depend_on=[],
125
+ )
126
+
70
127
 
71
128
  class ArticleChapter(ChapterBase[ArticleSection]):
72
129
  """Thematic progression implementing research function."""
73
130
 
131
+ @classmethod
132
+ def from_typst_code(cls, title: str, body: str) -> Self:
133
+ """Creates an Article object from the given Typst code."""
134
+ return cls(
135
+ sections=[
136
+ ArticleSection.from_typst_code(*pack) for pack in extract_sections(body, level=2, section_char="=")
137
+ ],
138
+ heading=title,
139
+ elaboration="",
140
+ expected_word_count=word_count(body),
141
+ aims=[],
142
+ support_to=[],
143
+ depend_on=[],
144
+ )
145
+
74
146
 
75
147
  class Article(
76
- Display,
77
- CensoredAble,
148
+ SketchedAble,
78
149
  WithRef[ArticleOutline],
79
150
  PersistentAble,
80
151
  ArticleBase[ArticleChapter],
@@ -95,7 +166,7 @@ class Article(
95
166
 
96
167
  @override
97
168
  def iter_subsections(self) -> Generator[Tuple[ArticleChapter, ArticleSection, ArticleSubsection], None, None]:
98
- return super().iter_subsections()
169
+ return super().iter_subsections() # pyright: ignore [reportReturnType]
99
170
 
100
171
  @classmethod
101
172
  def from_outline(cls, outline: ArticleOutline) -> "Article":
@@ -108,31 +179,43 @@ class Article(
108
179
  Article: The generated article.
109
180
  """
110
181
  # Set the title from the outline
111
- article = Article(**outline.model_dump(exclude={"chapters"}), chapters=[])
182
+ article = Article(**outline.model_dump(exclude={"chapters"}, by_alias=True), chapters=[])
112
183
 
113
184
  for chapter in outline.chapters:
114
185
  # Create a new chapter
115
186
  article_chapter = ArticleChapter(
116
187
  sections=[],
117
- **chapter.model_dump(exclude={"sections"}),
188
+ **chapter.model_dump(exclude={"sections"}, by_alias=True),
118
189
  )
119
190
  for section in chapter.sections:
120
191
  # Create a new section
121
192
  article_section = ArticleSection(
122
193
  subsections=[],
123
- **section.model_dump(exclude={"subsections"}),
194
+ **section.model_dump(exclude={"subsections"}, by_alias=True),
124
195
  )
125
196
  for subsection in section.subsections:
126
197
  # Create a new subsection
127
198
  article_subsection = ArticleSubsection(
128
199
  paragraphs=[],
129
- **subsection.model_dump(),
200
+ **subsection.model_dump(by_alias=True),
130
201
  )
131
202
  article_section.subsections.append(article_subsection)
132
203
  article_chapter.sections.append(article_section)
133
204
  article.chapters.append(article_chapter)
134
205
  return article
135
206
 
207
+ @classmethod
208
+ def from_typst_code(cls, title: str, body: str) -> Self:
209
+ """Generates an article from the given Typst code."""
210
+ return cls(
211
+ chapters=[
212
+ ArticleChapter.from_typst_code(*pack) for pack in extract_sections(body, level=1, section_char="=")
213
+ ],
214
+ heading=title,
215
+ expected_word_count=word_count(body),
216
+ abstract="",
217
+ )
218
+
136
219
  def gather_dependencies(self, article: ArticleOutlineBase) -> List[ArticleOutlineBase]:
137
220
  """Gathers dependencies for all sections and subsections in the article.
138
221
 
@@ -142,7 +225,7 @@ class Article(
142
225
 
143
226
  supports = []
144
227
  for a in self.iter_dfs_rev():
145
- if article in {ok(b.deref(self)) for b in a.support_to}:
228
+ if article in {ok(b.deref(self)) for b in a.support_to}: # pyright: ignore [reportUnhashable]
146
229
  supports.append(a)
147
230
 
148
231
  return list(set(depends + supports))
@@ -9,7 +9,7 @@ from fabricatio.models.extra.article_base import (
9
9
  SubSectionBase,
10
10
  )
11
11
  from fabricatio.models.extra.article_proposal import ArticleProposal
12
- from fabricatio.models.generic import CensoredAble, Display, PersistentAble, WithRef
12
+ from fabricatio.models.generic import PersistentAble, SketchedAble, WithRef
13
13
 
14
14
 
15
15
  class ArticleSubsectionOutline(SubSectionBase):
@@ -25,8 +25,7 @@ class ArticleChapterOutline(ChapterBase[ArticleSectionOutline]):
25
25
 
26
26
 
27
27
  class ArticleOutline(
28
- Display,
29
- CensoredAble,
28
+ SketchedAble,
30
29
  WithRef[ArticleProposal],
31
30
  PersistentAble,
32
31
  ArticleBase[ArticleChapterOutline],
@@ -2,21 +2,25 @@
2
2
 
3
3
  from typing import Dict, List
4
4
 
5
- from fabricatio.models.generic import AsPrompt, CensoredAble, Display, PersistentAble, WithRef
6
-
7
-
8
- class ArticleProposal(CensoredAble, Display, WithRef[str], AsPrompt, PersistentAble):
5
+ from fabricatio.models.generic import (
6
+ AsPrompt,
7
+ Described,
8
+ Language,
9
+ PersistentAble,
10
+ SketchedAble,
11
+ Titled,
12
+ WithRef,
13
+ WordCount,
14
+ )
15
+ from pydantic import Field
16
+
17
+
18
+ class ArticleProposal(SketchedAble, WithRef[str], AsPrompt, PersistentAble, WordCount, Described, Titled, Language):
9
19
  """Structured proposal for academic paper development with core research elements.
10
20
 
11
21
  Guides LLM in generating comprehensive research proposals with clearly defined components.
12
22
  """
13
23
 
14
- language: str
15
- """The language in which the article is written. This should align with the language specified in the article briefing."""
16
-
17
- title: str
18
- """The title of the academic paper, formatted in Title Case."""
19
-
20
24
  focused_problem: List[str]
21
25
  """A list of specific research problems or questions that the paper aims to address."""
22
26
 
@@ -38,12 +42,9 @@ class ArticleProposal(CensoredAble, Display, WithRef[str], AsPrompt, PersistentA
38
42
  keywords: List[str]
39
43
  """A list of keywords that represent the main topics and focus areas of the research."""
40
44
 
41
- abstract: str
45
+ description: str = Field(alias="abstract")
42
46
  """A concise summary of the research proposal, outlining the main points and objectives."""
43
47
 
44
- min_word_count: int
45
- """The minimum number of words required for the research proposal."""
46
-
47
48
  def _as_prompt_inner(self) -> Dict[str, str]:
48
49
  return {
49
50
  "ArticleBriefing": self.referenced,
@@ -1,7 +1,20 @@
1
- """A patch class for updating the description and name of a `WithBriefing` object."""
1
+ """A patch class for updating the description and name of a `WithBriefing` object, all fields within this instance will be directly copied onto the target model's field."""
2
2
 
3
- from fabricatio.models.generic import Patch, WithBriefing
3
+ from typing import Optional, Type
4
4
 
5
+ from fabricatio.models.extra.rule import RuleSet
6
+ from fabricatio.models.generic import Language, Patch, WithBriefing
7
+ from pydantic import BaseModel
5
8
 
6
- class BriefingPatch[T:WithBriefing](Patch[T], WithBriefing):
7
- """Patch class for updating the description and name of a `WithBriefing` object."""
9
+
10
+ class BriefingMetadata[T: WithBriefing](Patch[T], WithBriefing):
11
+ """A patch class for updating the description and name of a `WithBriefing` object, all fields within this instance will be directly copied onto the target model's field."""
12
+
13
+
14
+ class RuleSetMetadata(BriefingMetadata[RuleSet], Language):
15
+ """A patch class for updating the description and name of a `RuleSet` object, all fields within this instance will be directly copied onto the target model's field."""
16
+
17
+ @staticmethod
18
+ def ref_cls() -> Optional[Type[BaseModel]]:
19
+ """Get the reference class of the model."""
20
+ return RuleSet
@@ -1,11 +1,12 @@
1
1
  """A class representing a problem-solution pair identified during a review process."""
2
2
 
3
3
  from itertools import chain
4
- from typing import List, Literal, Optional, Self
4
+ from typing import Any, List, Optional, Self, Tuple, Unpack
5
5
 
6
6
  from fabricatio.journal import logger
7
7
  from fabricatio.models.generic import SketchedAble, WithBriefing
8
8
  from fabricatio.utils import ask_edit
9
+ from pydantic import Field
9
10
  from questionary import Choice, checkbox, text
10
11
  from rich import print as r_print
11
12
 
@@ -13,36 +14,30 @@ from rich import print as r_print
13
14
  class Problem(SketchedAble, WithBriefing):
14
15
  """Represents a problem identified during review."""
15
16
 
16
- description: str
17
- """Description of the problem, The """
17
+ description: str = Field(alias="cause")
18
+ """The cause of the problem, including the root cause, the context, and the impact, make detailed enough for engineer to understand the problem and its impact."""
18
19
 
19
- severity: Literal["low", "medium", "high"]
20
- """Severity level of the problem."""
21
-
22
- category: str
23
- """Category of the problem."""
20
+ severity_level: int = Field(ge=0, le=10)
21
+ """Severity level of the problem, which is a number between 0 and 10, 0 means the problem is not severe, 10 means the problem is extremely severe."""
24
22
 
25
23
  location: str
26
24
  """Location where the problem was identified."""
27
25
 
28
- recommendation: str
29
- """Recommended solution or action."""
30
-
31
26
 
32
27
  class Solution(SketchedAble, WithBriefing):
33
28
  """Represents a proposed solution to a problem."""
34
29
 
35
- description: str
30
+ description: str = Field(alias="mechanism")
36
31
  """Description of the solution, including a detailed description of the execution steps, and the mechanics, principle or fact."""
37
32
 
38
33
  execute_steps: List[str]
39
- """A list of steps to execute to implement the solution, which is expected to be able to finally solve the corresponding problem."""
34
+ """A list of steps to execute to implement the solution, which is expected to be able to finally solve the corresponding problem, and which should be an Idiot-proof tutorial."""
40
35
 
41
- feasibility: Literal["low", "medium", "high"]
42
- """Feasibility level of the solution."""
36
+ feasibility_level: int = Field(ge=0, le=10)
37
+ """Feasibility level of the solution, which is a number between 0 and 10, 0 means the solution is not feasible, 10 means the solution is complete feasible."""
43
38
 
44
- impact: Literal["low", "medium", "high"]
45
- """Impact level of the solution."""
39
+ impact_level: int = Field(ge=0, le=10)
40
+ """Impact level of the solution, which is a number between 0 and 10, 0 means the solution is not impactful, 10 means the solution is extremely impactful."""
46
41
 
47
42
 
48
43
  class ProblemSolutions(SketchedAble):
@@ -51,7 +46,12 @@ class ProblemSolutions(SketchedAble):
51
46
  problem: Problem
52
47
  """The problem identified in the review."""
53
48
  solutions: List[Solution]
54
- """A collection of potential solutions."""
49
+ """A collection of potential solutions, spread the thought, add more solution as possible.Do not leave this as blank"""
50
+
51
+ def model_post_init(self, context: Any, /) -> None:
52
+ """Initialize the problem-solution pair with a problem and a list of solutions."""
53
+ if len(self.solutions) == 0:
54
+ logger.warning(f"No solution found for problem {self.problem.name}, please add more solutions manually.")
55
55
 
56
56
  def update_from_inner(self, other: Self) -> Self:
57
57
  """Update the current instance with another instance's attributes."""
@@ -69,6 +69,10 @@ class ProblemSolutions(SketchedAble):
69
69
  self.solutions = solutions
70
70
  return self
71
71
 
72
+ def has_solutions(self) -> bool:
73
+ """Check if the problem-solution pair has any solutions."""
74
+ return len(self.solutions) > 0
75
+
72
76
  async def edit_problem(self) -> Self:
73
77
  """Interactively edit the problem description."""
74
78
  self.problem = Problem.model_validate_strings(
@@ -87,11 +91,11 @@ class ProblemSolutions(SketchedAble):
87
91
  """Check if the improvement is decided."""
88
92
  return len(self.solutions) == 1
89
93
 
90
- def final_solution(self) -> Optional[Solution]:
94
+ def final_solution(self, always_use_first: bool = False) -> Optional[Solution]:
91
95
  """Get the final solution."""
92
- if not self.decided():
96
+ if not always_use_first and not self.decided():
93
97
  logger.error(
94
- f"There is more than one solution for problem {self.problem.name}, please decide which solution is eventually adopted."
98
+ f"There is {len(self.solutions)} solutions for problem {self.problem.name}, please decide which solution is eventually adopted."
95
99
  )
96
100
  return None
97
101
  return self.solutions[0]
@@ -106,6 +110,10 @@ class Improvement(SketchedAble):
106
110
  problem_solutions: List[ProblemSolutions]
107
111
  """Collection of problems identified during review along with their potential solutions."""
108
112
 
113
+ def all_problems_have_solutions(self) -> bool:
114
+ """Check if all problems have solutions."""
115
+ return all(ps.has_solutions() for ps in self.problem_solutions)
116
+
109
117
  async def supervisor_check(self, check_solutions: bool = True) -> Self:
110
118
  """Perform an interactive review session to filter problems and solutions.
111
119
 
@@ -145,9 +153,9 @@ class Improvement(SketchedAble):
145
153
  return all(ps.decided() for ps in self.problem_solutions)
146
154
 
147
155
  @classmethod
148
- def gather(cls, *improvements: Self) -> Self:
156
+ def gather(cls, *improvements: Unpack[Tuple["Improvement", ...]]) -> Self:
149
157
  """Gather multiple improvements into a single instance."""
150
158
  return cls(
151
- focused_on="\n".join(imp.focused_on for imp in improvements),
159
+ focused_on=";".join(imp.focused_on for imp in improvements),
152
160
  problem_solutions=list(chain(*(imp.problem_solutions for imp in improvements))),
153
161
  )
@@ -1,21 +1,52 @@
1
- """A module containing classes related to rule sets and rules."""
1
+ """A module containing classes related to rule sets and rules.
2
2
 
3
- from typing import List
3
+ This module provides the `Rule` and `RuleSet` classes, which are used to define and manage
4
+ individual rules and collections of rules, respectively. These classes are designed to
5
+ facilitate the creation, organization, and application of rules in various contexts,
6
+ ensuring clarity, consistency, and enforceability. The module supports detailed
7
+ descriptions, examples, and metadata for each rule and rule set, making it suitable for
8
+ complex rule management systems.
9
+ """
4
10
 
5
- from fabricatio.models.generic import Display, PersistentAble, ProposedAble, WithBriefing
11
+ from typing import List, Self, Tuple, Unpack
6
12
 
13
+ from fabricatio.models.generic import Language, PersistentAble, SketchedAble, WithBriefing
14
+ from more_itertools import flatten
7
15
 
8
- class Rule(WithBriefing,ProposedAble,Display):
16
+
17
+ class Rule(WithBriefing, Language, SketchedAble, PersistentAble):
9
18
  """Represents a rule or guideline for a specific topic."""
10
19
 
11
20
  violation_examples: List[str]
12
- """Examples of violations of the rule."""
21
+ """A list of concrete examples demonstrating violations of this rule. Each example should
22
+ be a clear scenario or case that illustrates how the rule can be broken, including the
23
+ context, actions, and consequences of the violation. These examples should help in
24
+ understanding the boundaries of the rule."""
25
+
13
26
  compliance_examples: List[str]
14
- """Examples of how to comply with the rule."""
27
+ """A list of concrete examples demonstrating proper compliance with this rule. Each example
28
+ should be a clear scenario or case that illustrates how to correctly follow the rule,
29
+ including the context, actions, and positive outcomes of compliance. These examples should
30
+ serve as practical guidance for implementing the rule correctly."""
15
31
 
16
32
 
17
- class RuleSet(ProposedAble, Display, PersistentAble, WithBriefing):
33
+ class RuleSet(SketchedAble, PersistentAble, WithBriefing, Language):
18
34
  """Represents a collection of rules and guidelines for a particular topic."""
19
35
 
20
36
  rules: List[Rule]
21
- """The rules and guidelines contained in the rule set."""
37
+ """The collection of rules and guidelines contained in this rule set. Each rule should be
38
+ a well-defined, specific guideline that contributes to the overall purpose of the rule set.
39
+ The rules should be logically organized and consistent with each other, forming a coherent
40
+ framework for the topic or domain covered by the rule set."""
41
+
42
+ @classmethod
43
+ def gather(cls, *rulesets: Unpack[Tuple["RuleSet",...]]) -> Self:
44
+ """Gathers multiple rule sets into a single rule set."""
45
+ if not rulesets:
46
+ raise ValueError("No rulesets provided")
47
+ return cls(
48
+ language=rulesets[0].language,
49
+ name=";".join(ruleset.name for ruleset in rulesets),
50
+ description=";".join(ruleset.description for ruleset in rulesets),
51
+ rules=list(flatten(r.rules for r in rulesets)),
52
+ )