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
@@ -1,7 +1,12 @@
1
- """Module that contains the classes for actions and workflows.
1
+ """Module that contains the classes for defining and executing task workflows.
2
2
 
3
- This module defines the Action and WorkFlow classes, which are used for
4
- creating and executing sequences of actions in a task-based context.
3
+ This module provides the Action and WorkFlow classes for creating structured
4
+ task execution pipelines. Actions represent atomic operations, while WorkFlows
5
+ orchestrate sequences of actions with shared context and error handling.
6
+
7
+ Classes:
8
+ Action: Base class for defining executable actions with context management.
9
+ WorkFlow: Manages action sequences, context propagation, and task lifecycle.
5
10
  """
6
11
 
7
12
  import traceback
@@ -15,6 +20,10 @@ from fabricatio.models.task import Task
15
20
  from fabricatio.models.usages import LLMUsage, ToolBoxUsage
16
21
  from pydantic import Field, PrivateAttr
17
22
 
23
+ OUTPUT_KEY = "task_output"
24
+
25
+ INPUT_KEY = "task_input"
26
+
18
27
 
19
28
  class Action(WithBriefing, LLMUsage):
20
29
  """Class that represents an action to be executed in a workflow.
@@ -46,28 +55,26 @@ class Action(WithBriefing, LLMUsage):
46
55
  self.description = self.description or self.__class__.__doc__ or ""
47
56
 
48
57
  @abstractmethod
49
- async def _execute(self, *_, **cxt) -> Any: # noqa: ANN002
50
- """Execute the action logic with the provided context arguments.
51
-
52
- This method must be implemented by subclasses to define the actual behavior.
58
+ async def _execute(self, *_:Any, **cxt) -> Any:
59
+ """Implement the core logic of the action.
53
60
 
54
61
  Args:
55
- **cxt: The context dictionary containing input and output data.
62
+ **cxt: Context dictionary containing input/output data.
56
63
 
57
64
  Returns:
58
- Any: The result of the action execution.
65
+ Result of the action execution to be stored in context.
59
66
  """
60
67
  pass
61
68
 
62
69
  @final
63
70
  async def act(self, cxt: Dict[str, Any]) -> Dict[str, Any]:
64
- """Perform the action and update the context with results.
71
+ """Execute action and update context.
65
72
 
66
73
  Args:
67
- cxt: The context dictionary containing input and output data.
74
+ cxt (Dict[str, Any]): Shared context dictionary.
68
75
 
69
76
  Returns:
70
- Dict[str, Any]: The updated context dictionary.
77
+ Updated context dictionary with new/modified entries.
71
78
  """
72
79
  ret = await self._execute(**cxt)
73
80
 
@@ -79,21 +86,30 @@ class Action(WithBriefing, LLMUsage):
79
86
 
80
87
  @property
81
88
  def briefing(self) -> str:
82
- """Return a formatted description of the action including personality context if available.
89
+ """Generate formatted action description with personality context.
83
90
 
84
91
  Returns:
85
- str: Formatted briefing text with personality and action description.
92
+ Briefing text combining personality and action description.
86
93
  """
87
94
  if self.personality:
88
95
  return f"## Your personality: \n{self.personality}\n# The action you are going to perform: \n{super().briefing}"
89
96
  return f"# The action you are going to perform: \n{super().briefing}"
90
97
 
98
+ def to_task_output(self)->Self:
99
+ """Set the output key to OUTPUT_KEY and return the action instance."""
100
+ self.output_key=OUTPUT_KEY
101
+ return self
91
102
 
92
103
  class WorkFlow(WithBriefing, ToolBoxUsage):
93
- """Class that represents a sequence of actions to be executed for a task.
104
+ """Manages sequences of actions to fulfill tasks.
94
105
 
95
- A workflow manages the execution of multiple actions in sequence, passing
96
- a shared context between them and handling task lifecycle events.
106
+ Handles context propagation between actions, error handling, and task lifecycle
107
+ events like cancellation and completion.
108
+
109
+ Attributes:
110
+ steps (Tuple): Sequence of Action instances or classes to execute.
111
+ task_input_key (str): Key for storing task instance in context.
112
+ task_output_key (str): Key to retrieve final result from context.
97
113
  """
98
114
 
99
115
  description: str = ""
@@ -110,10 +126,10 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
110
126
  )
111
127
  """The sequence of actions to be executed, can be action classes or instances."""
112
128
 
113
- task_input_key: str = Field(default="task_input")
129
+ task_input_key: str = Field(default=INPUT_KEY)
114
130
  """Key used to store the input task in the context dictionary."""
115
131
 
116
- task_output_key: str = Field(default="task_output")
132
+ task_output_key: str = Field(default=OUTPUT_KEY)
117
133
  """Key used to extract the final result from the context dictionary."""
118
134
 
119
135
  extra_init_context: Dict[str, Any] = Field(default_factory=dict, frozen=True)
@@ -129,26 +145,29 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
129
145
  self._instances = tuple(step if isinstance(step, Action) else step() for step in self.steps)
130
146
 
131
147
  def inject_personality(self, personality: str) -> Self:
132
- """Set the personality for all actions that don't have one defined.
148
+ """Set personality for actions without existing personality.
133
149
 
134
150
  Args:
135
- personality: The personality text to inject.
151
+ personality (str): Shared personality context
136
152
 
137
153
  Returns:
138
- Self: The workflow instance for method chaining.
154
+ Workflow instance with updated actions
139
155
  """
140
156
  for action in filter(lambda a: not a.personality, self._instances):
141
157
  action.personality = personality
142
158
  return self
143
159
 
144
160
  async def serve(self, task: Task) -> None:
145
- """Execute the workflow to fulfill the given task.
146
-
147
- This method manages the complete lifecycle of processing a task through
148
- the workflow's sequence of actions.
161
+ """Execute workflow to complete given task.
149
162
 
150
163
  Args:
151
- task: The task to be processed.
164
+ task (Task): Task instance to be processed.
165
+
166
+ Steps:
167
+ 1. Initialize context with task instance and extra data
168
+ 2. Execute each action sequentially
169
+ 3. Handle task cancellation and exceptions
170
+ 4. Extract final result from context
152
171
  """
153
172
  logger.info(f"Start execute workflow: {self.name}")
154
173
 
@@ -158,27 +177,27 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
158
177
  current_action = None
159
178
  try:
160
179
  # Process each action in sequence
161
- for step in self._instances:
180
+ for i,step in enumerate(self._instances):
162
181
  current_action = step.name
163
- logger.info(f"Executing step >> {current_action}")
182
+ logger.info(f"Executing step [{i}] >> {current_action}")
164
183
 
165
184
  # Get current context and execute action
166
185
  context = await self._context.get()
167
186
  act_task = create_task(step.act(context))
168
187
  # Handle task cancellation
169
188
  if task.is_cancelled():
170
- logger.warning(f"Task cancelled by task: {task.name}")
189
+ logger.warning(f"Workflow cancelled by task: {task.name}")
171
190
  act_task.cancel(f"Cancelled by task: {task.name}")
172
191
  break
173
192
 
174
193
  # Update context with modified values
175
194
  modified_ctx = await act_task
176
- logger.success(f"Step execution finished: {current_action}")
195
+ logger.success(f"Step [{i}] `{current_action}` execution finished.")
177
196
  if step.output_key:
178
- logger.success(f"Setting output to `{step.output_key}`")
197
+ logger.success(f"Setting action `{current_action}` output to `{step.output_key}`")
179
198
  await self._context.put(modified_ctx)
180
199
 
181
- logger.success(f"Workflow execution finished: {self.name}")
200
+ logger.success(f"Workflow `{self.name}` execution finished.")
182
201
 
183
202
  # Get final context and extract result
184
203
  final_ctx = await self._context.get()
@@ -198,10 +217,14 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
198
217
  await task.fail()
199
218
 
200
219
  async def _init_context[T](self, task: Task[T]) -> None:
201
- """Initialize the context dictionary for workflow execution.
220
+ """Initialize workflow execution context.
202
221
 
203
222
  Args:
204
- task: The task being served by this workflow.
223
+ task (Task[T]): Task being processed
224
+
225
+ Context includes:
226
+ - Task instance stored under task_input_key
227
+ - Any extra_init_context values
205
228
  """
206
229
  logger.debug(f"Initializing context for workflow: {self.name}")
207
230
  initial_context = {self.task_input_key: task, **dict(self.extra_init_context)}
@@ -222,7 +245,7 @@ class WorkFlow(WithBriefing, ToolBoxUsage):
222
245
  Returns:
223
246
  Self: The workflow instance for method chaining.
224
247
  """
225
- self.provide_tools_to(self._instances)
248
+ self.provide_tools_to(i for i in self._instances if isinstance(i,ToolBoxUsage))
226
249
  return self
227
250
 
228
251
  def update_init_context(self, /, **kwargs) -> Self:
@@ -0,0 +1 @@
1
+ """A module contains extra models for fabricatio."""
@@ -2,17 +2,17 @@
2
2
 
3
3
  from typing import List
4
4
 
5
- from fabricatio.models.generic import Display, ProposedAble
5
+ from fabricatio.models.generic import SketchedAble
6
6
 
7
7
 
8
- class JudgeMent(ProposedAble, Display):
8
+ class JudgeMent(SketchedAble):
9
9
  """Represents a judgment result containing supporting/denying evidence and final verdict.
10
10
 
11
11
  The class stores both affirmative and denies evidence, truth and reasons lists along with the final boolean judgment.
12
12
  """
13
13
 
14
14
  issue_to_judge: str
15
- """The issue to be judged"""
15
+ """The issue to be judged, including the original question and context"""
16
16
 
17
17
  deny_evidence: List[str]
18
18
  """List of clues supporting the denial."""
@@ -21,7 +21,7 @@ class JudgeMent(ProposedAble, Display):
21
21
  """List of clues supporting the affirmation."""
22
22
 
23
23
  final_judgement: bool
24
- """The final judgment made according to all extracted clues."""
24
+ """The final judgment made according to all extracted clues. true for the `issue_to_judge` is correct and false for incorrect."""
25
25
 
26
26
  def __bool__(self) -> bool:
27
27
  """Return the final judgment value.
@@ -7,16 +7,20 @@ from typing import Generator, List, Optional, Self, Tuple, overload
7
7
 
8
8
  from fabricatio.models.generic import (
9
9
  AsPrompt,
10
- CensoredAble,
11
- Display,
10
+ Described,
12
11
  FinalizedDumpAble,
13
12
  Introspect,
13
+ Language,
14
14
  ModelHash,
15
15
  PersistentAble,
16
16
  ProposedUpdateAble,
17
17
  ResolveUpdateConflict,
18
18
  SequencePatch,
19
+ SketchedAble,
20
+ Titled,
21
+ WordCount,
19
22
  )
23
+ from pydantic import Field
20
24
 
21
25
 
22
26
  class ReferringType(StrEnum):
@@ -30,51 +34,51 @@ class ReferringType(StrEnum):
30
34
  type RefKey = Tuple[str, Optional[str], Optional[str]]
31
35
 
32
36
 
33
- class ArticleRef(CensoredAble, ProposedUpdateAble):
37
+ class ArticleRef(ProposedUpdateAble):
34
38
  """Reference to a specific chapter, section or subsection within the article. You SHALL not refer to an article component that is external and not present within our own article.
35
39
 
36
40
  Examples:
37
41
  - Referring to a chapter titled `Introduction`:
38
42
  Using Python
39
43
  ```python
40
- ArticleRef(referred_chapter_title="Introduction")
44
+ ArticleRef(chap="Introduction")
41
45
  ```
42
46
  Using JSON
43
47
  ```json
44
- {referred_chapter_title="Introduction"}
48
+ {chap="Introduction"}
45
49
  ```
46
50
  - Referring to a section titled `Background` under the `Introduction` chapter:
47
51
  Using Python
48
52
  ```python
49
- ArticleRef(referred_chapter_title="Introduction", referred_section_title="Background")
53
+ ArticleRef(chap="Introduction", sec="Background")
50
54
  ```
51
55
  Using JSON
52
56
  ```json
53
- {referred_chapter_title="Introduction", referred_section_title="Background"}
57
+ {chap="Introduction", sec="Background"}
54
58
  ```
55
59
  - Referring to a subsection titled `Related Work` under the `Background` section of the `Introduction` chapter:
56
60
  Using Python
57
61
  ```python
58
- ArticleRef(referred_chapter_title="Introduction", referred_section_title="Background", referred_subsection_title="Related Work")
62
+ ArticleRef(chap="Introduction", sec="Background", subsec="Related Work")
59
63
  ```
60
64
  Using JSON
61
65
  ```json
62
- {referred_chapter_title="Introduction", referred_section_title="Background", referred_subsection_title="Related Work"}
66
+ {chap="Introduction", sec="Background", subsec="Related Work"}
63
67
  ```
64
68
  """
65
69
 
66
- referred_chapter_title: str
70
+ chap: str
67
71
  """`title` Field of the referenced chapter"""
68
- referred_section_title: Optional[str] = None
72
+ sec: Optional[str] = None
69
73
  """`title` Field of the referenced section."""
70
- referred_subsection_title: Optional[str] = None
74
+ subsec: Optional[str] = None
71
75
  """`title` Field of the referenced subsection."""
72
76
 
73
77
  def update_from_inner(self, other: Self) -> Self:
74
78
  """Updates the current instance with the attributes of another instance."""
75
- self.referred_chapter_title = other.referred_chapter_title
76
- self.referred_section_title = other.referred_section_title
77
- self.referred_subsection_title = other.referred_subsection_title
79
+ self.chap = other.chap
80
+ self.sec = other.sec
81
+ self.subsec = other.subsec
78
82
  return self
79
83
 
80
84
  def deref(self, article: "ArticleBase") -> Optional["ArticleOutlineBase"]:
@@ -86,39 +90,41 @@ class ArticleRef(CensoredAble, ProposedUpdateAble):
86
90
  Returns:
87
91
  ArticleMainBase | ArticleOutline | None: The dereferenced section or subsection, or None if not found.
88
92
  """
89
- chap = next((chap for chap in article.chapters if chap.title == self.referred_chapter_title), None)
90
- if self.referred_section_title is None or chap is None:
93
+ chap = next((chap for chap in article.chapters if chap.title == self.chap), None)
94
+ if self.sec is None or chap is None:
91
95
  return chap
92
- sec = next((sec for sec in chap.sections if sec.title == self.referred_section_title), None)
93
- if self.referred_subsection_title is None or sec is None:
96
+ sec = next((sec for sec in chap.sections if sec.title == self.sec), None)
97
+ if self.subsec is None or sec is None:
94
98
  return sec
95
- return next((subsec for subsec in sec.subsections if subsec.title == self.referred_subsection_title), None)
99
+ return next((subsec for subsec in sec.subsections if subsec.title == self.subsec), None)
96
100
 
97
101
  @property
98
102
  def referring_type(self) -> ReferringType:
99
103
  """Determine the type of reference based on the presence of specific attributes."""
100
- if self.referred_subsection_title is not None:
104
+ if self.subsec is not None:
101
105
  return ReferringType.SUBSECTION
102
- if self.referred_section_title is not None:
106
+ if self.sec is not None:
103
107
  return ReferringType.SECTION
104
108
  return ReferringType.CHAPTER
105
109
 
106
110
 
107
- class ArticleMetaData(CensoredAble, Display):
111
+ class ArticleMetaData(SketchedAble, Described, WordCount, Titled, Language):
108
112
  """Metadata for an article component."""
109
113
 
110
- description: str
111
- """Description of the research component in academic style."""
114
+ description: str = Field(
115
+ alias="elaboration",
116
+ description=Described.model_fields["description"].description,
117
+ )
112
118
 
113
- support_to: List[ArticleRef]
114
- """List of references to other component of this articles that this component supports."""
115
- depend_on: List[ArticleRef]
116
- """List of references to other component of this articles that this component depends on."""
119
+ title: str = Field(alias="heading", description=Titled.model_fields["title"].description)
117
120
 
118
- writing_aim: List[str]
121
+ aims: List[str]
119
122
  """List of writing aims of the research component in academic style."""
120
- title: str
121
- """Do not add any prefix or suffix to the title. should not contain special characters."""
123
+
124
+ support_to: List[ArticleRef]
125
+ """List of references to other future components in this article that this component supports to."""
126
+ depend_on: List[ArticleRef]
127
+ """List of references to other previous components in this article that this component depends on."""
122
128
 
123
129
 
124
130
  class ArticleRefSequencePatch(SequencePatch[ArticleRef]):
@@ -146,8 +152,8 @@ class ArticleOutlineBase(
146
152
  self.support_to.extend(other.support_to)
147
153
  self.depend_on.clear()
148
154
  self.depend_on.extend(other.depend_on)
149
- self.writing_aim.clear()
150
- self.writing_aim.extend(other.writing_aim)
155
+ self.aims.clear()
156
+ self.aims.extend(other.aims)
151
157
  self.description = other.description
152
158
  return self
153
159
 
@@ -271,25 +277,19 @@ class ChapterBase[T: SectionBase](ArticleOutlineBase):
271
277
  return ""
272
278
 
273
279
 
274
- class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
280
+ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, WordCount, Described, Titled, Language, ABC):
275
281
  """Base class for article outlines."""
276
282
 
277
- language: str
278
- """Written language of the article. SHALL be aligned to the language of the article proposal provided."""
283
+ title: str = Field(alias="heading", description=Titled.model_fields["title"].description)
284
+ description: str = Field(alias="abstract")
285
+ """The abstract serves as a concise summary of an academic article, encapsulating its core purpose, methodologies, key results,
286
+ and conclusions while enabling readers to rapidly assess the relevance and significance of the study.
287
+ Functioning as the article's distilled essence, it succinctly articulates the research problem, objectives,
288
+ and scope, providing a roadmap for the full text while also facilitating database indexing, literature reviews,
289
+ and citation tracking through standardized metadata. Additionally, it acts as an accessibility gateway,
290
+ allowing scholars to gauge the study's contribution to existing knowledge, its methodological rigor,
291
+ and its broader implications without engaging with the entire manuscript, thereby optimizing scholarly communication efficiency."""
279
292
 
280
- title: str
281
- """Title of the academic paper."""
282
-
283
- prospect: str
284
- """Consolidated research statement with four pillars:
285
- 1. Problem Identification: Current limitations
286
- 2. Methodological Response: Technical approach
287
- 3. Empirical Validation: Evaluation strategy
288
- 4. Scholarly Impact: Field contributions
289
- """
290
-
291
- abstract: str
292
- """The abstract is a concise summary of the academic paper's main findings."""
293
293
  chapters: List[T]
294
294
  """Chapters of the article. Contains at least one chapter. You can also add more as needed."""
295
295
 
@@ -376,34 +376,97 @@ class ArticleBase[T: ChapterBase](FinalizedDumpAble, AsPrompt, ABC):
376
376
  return component, summary
377
377
  return None
378
378
 
379
- @overload
380
- def find_illegal_ref(self, gather_identical: bool) -> Optional[Tuple[ArticleRef | List[ArticleRef], str]]: ...
379
+ def gather_introspected(self) -> Optional[str]:
380
+ """Gathers all introspected components in the article structure."""
381
+ return "\n".join([i for component in self.chapters if (i := component.introspect())])
382
+
383
+
384
+ def iter_chap_title(self) -> Generator[str, None, None]:
385
+ """Iterates through all chapter titles in the article."""
386
+ for chap in self.chapters:
387
+ yield chap.title
388
+
389
+ def iter_section_title(self) -> Generator[str, None, None]:
390
+ """Iterates through all section titles in the article."""
391
+ for _, sec in self.iter_sections():
392
+ yield sec.title
393
+
394
+ def iter_subsection_title(self) -> Generator[str, None, None]:
395
+ """Iterates through all subsection titles in the article."""
396
+ for _, _, subsec in self.iter_subsections():
397
+ yield subsec.title
381
398
 
382
399
  @overload
383
- def find_illegal_ref(self) -> Optional[Tuple[ArticleRef, str]]: ...
400
+ def find_illegal_ref(self, gather_identical: bool) -> Optional[Tuple[ArticleOutlineBase,ArticleRef | List[ArticleRef], str]]: ...
384
401
 
385
- def find_illegal_ref(self, gather_identical: bool = False) -> Optional[Tuple[ArticleRef | List[ArticleRef], str]]:
402
+ @overload
403
+ def find_illegal_ref(self) -> Optional[Tuple[ArticleOutlineBase,ArticleRef, str]]: ...
404
+ def find_illegal_ref(
405
+ self, gather_identical: bool = False
406
+ ) -> Optional[Tuple[ArticleOutlineBase, ArticleRef | List[ArticleRef], str]]:
386
407
  """Finds the first illegal component in the outline.
387
408
 
388
409
  Returns:
389
410
  Tuple[ArticleOutlineBase, str]: A tuple containing the illegal component and an error message.
390
411
  """
391
412
  summary = ""
413
+ chap_titles_set = set(self.iter_chap_title())
414
+ sec_titles_set = set(self.iter_section_title())
415
+ subsec_titles_set = set(self.iter_subsection_title())
416
+
392
417
  for component in self.iter_dfs_rev():
393
418
  for ref in chain(component.depend_on, component.support_to):
394
419
  if not ref.deref(self):
395
420
  summary += f"Invalid internal reference in `{component.__class__.__name__}` titled `{component.title}`, because the referred {ref.referring_type} is not exists within the article, see the original obj dump: {ref.model_dump()}\n"
396
- if summary and not gather_identical:
397
- return ref, summary
421
+
422
+ if ref.chap not in (chap_titles_set):
423
+ summary += f"Chapter titled `{ref.chap}` is not any of {chap_titles_set}\n"
424
+ if ref.sec and ref.sec not in (sec_titles_set):
425
+ summary += f"Section Titled `{ref.sec}` is not any of {sec_titles_set}\n"
426
+ if ref.subsec and ref.subsec not in (subsec_titles_set):
427
+ summary += f"Subsection Titled `{ref.subsec}` is not any of {subsec_titles_set}"
428
+
398
429
  if summary and gather_identical:
399
- return [
400
- identical_ref
401
- for identical_ref in chain(self.iter_depend_on(), self.iter_support_on())
402
- if identical_ref == ref
403
- ], summary
430
+ return (
431
+ component,
432
+ [
433
+ identical_ref
434
+ for identical_ref in chain(self.iter_depend_on(), self.iter_support_on())
435
+ if identical_ref == ref
436
+ ],
437
+ summary,
438
+ )
439
+ if summary:
440
+ return component, ref, summary
404
441
 
405
442
  return None
406
443
 
444
+ def gather_illegal_ref(self) -> Tuple[List[ArticleRef], str]:
445
+ """Gathers all illegal references in the article."""
446
+ summary = []
447
+ chap_titles_set = set(self.iter_chap_title())
448
+ sec_titles_set = set(self.iter_section_title())
449
+ subsec_titles_set = set(self.iter_subsection_title())
450
+ res_seq = []
451
+
452
+ for component in self.iter_dfs():
453
+ for ref in (
454
+ r for r in chain(component.depend_on, component.support_to) if not r.deref(self) and r not in res_seq
455
+ ):
456
+ res_seq.append(ref)
457
+ if ref.chap not in chap_titles_set:
458
+ summary.append(
459
+ f"Chapter titled `{ref.chap}` is not exist, since it is not any of {chap_titles_set}."
460
+ )
461
+ if ref.sec and (ref.sec not in sec_titles_set):
462
+ summary.append(f"Section Titled `{ref.sec}` is not exist, since it is not any of {sec_titles_set}")
463
+ if ref.subsec and (ref.subsec not in subsec_titles_set):
464
+ summary.append(
465
+ f"Subsection Titled `{ref.subsec}` is not exist, since it is not any of {subsec_titles_set}"
466
+ )
467
+
468
+ return res_seq, "\n".join(summary)
469
+
407
470
  def finalized_dump(self) -> str:
408
471
  """Generates standardized hierarchical markup for academic publishing systems.
409
472