abnt-mcp 0.2.0__py3-none-any.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.
- abnt_mcp/__init__.py +6 -0
- abnt_mcp/__main__.py +6 -0
- abnt_mcp/config.py +266 -0
- abnt_mcp/models.py +344 -0
- abnt_mcp/parsers/__init__.py +32 -0
- abnt_mcp/parsers/bibtex.py +183 -0
- abnt_mcp/parsers/citations.py +139 -0
- abnt_mcp/parsers/pdf_reader.py +425 -0
- abnt_mcp/pillars/__init__.py +45 -0
- abnt_mcp/pillars/advisory.py +366 -0
- abnt_mcp/pillars/compiler.py +458 -0
- abnt_mcp/pillars/diagnostics.py +814 -0
- abnt_mcp/pillars/spec.py +1171 -0
- abnt_mcp/pillars/writing.py +518 -0
- abnt_mcp/resources/__init__.py +13 -0
- abnt_mcp/resources/guides.py +106 -0
- abnt_mcp/server.py +597 -0
- abnt_mcp/templates/__init__.py +11 -0
- abnt_mcp/templates/latex.py +142 -0
- abnt_mcp/templates/markdown.py +85 -0
- abnt_mcp/templates/typst.py +129 -0
- abnt_mcp-0.2.0.dist-info/METADATA +324 -0
- abnt_mcp-0.2.0.dist-info/RECORD +26 -0
- abnt_mcp-0.2.0.dist-info/WHEEL +4 -0
- abnt_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- abnt_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
abnt_mcp/__init__.py
ADDED
abnt_mcp/__main__.py
ADDED
abnt_mcp/config.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Configurações, padrões de regex e constantes para o ABNT Copilot MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
SERVER_NAME = "abnt-mcp"
|
|
9
|
+
SERVER_VERSION = "0.1.0"
|
|
10
|
+
DEFAULT_USER_AGENT = f"{SERVER_NAME}/{SERVER_VERSION} (mailto:abnt-copilot@example.com)"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_current_year() -> int:
|
|
14
|
+
"""Retorna o ano corrente dinamicamente (evita datas hardcoded)."""
|
|
15
|
+
return datetime.now().year
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_default_author_source() -> str:
|
|
19
|
+
"""Retorna a indicação de fonte autoral padrão com o ano corrente."""
|
|
20
|
+
return f"Elaborado pelo autor ({get_current_year()})."
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_safe_path(target_path: str | Path, base_dir: str | Path | None = None) -> Path:
|
|
24
|
+
"""
|
|
25
|
+
Valida caminhos de arquivo contra caracteres nulos e path traversal fora de base_dir.
|
|
26
|
+
"""
|
|
27
|
+
raw_str = str(target_path)
|
|
28
|
+
if "\x00" in raw_str or not raw_str.strip():
|
|
29
|
+
raise ValueError(f"Caminho inválido ou malformado: '{target_path}'")
|
|
30
|
+
|
|
31
|
+
resolved = Path(target_path).expanduser().resolve()
|
|
32
|
+
if base_dir is not None:
|
|
33
|
+
base_resolved = Path(base_dir).expanduser().resolve()
|
|
34
|
+
try:
|
|
35
|
+
resolved.relative_to(base_resolved)
|
|
36
|
+
except ValueError as exc:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Operação bloqueada por segurança (Path Traversal): '{resolved}' está fora do diretório base '{base_resolved}'."
|
|
39
|
+
) from exc
|
|
40
|
+
return resolved
|
|
41
|
+
|
|
42
|
+
# Catálogo de Regras Normativas Canônicas (Spec-Driven Development)
|
|
43
|
+
# Sincronizado com specs/01-normative-catalog.spec.md
|
|
44
|
+
NORMATIVE_RULES: dict[str, dict[str, str]] = {
|
|
45
|
+
"NBR14724-R01": {
|
|
46
|
+
"standard": "ABNT NBR 14724:2011",
|
|
47
|
+
"clause": "Seção 4.2",
|
|
48
|
+
"title": "Elementos pré-textuais, textuais e pós-textuais obrigatórios de monografias",
|
|
49
|
+
"severity": "error",
|
|
50
|
+
},
|
|
51
|
+
"NBR14724-R02": {
|
|
52
|
+
"standard": "ABNT NBR 14724:2011",
|
|
53
|
+
"clause": "Seção 5.3",
|
|
54
|
+
"title": "Paginação física: contagem na Folha de Rosto e impressão arábica a partir da Introdução",
|
|
55
|
+
"severity": "critical",
|
|
56
|
+
},
|
|
57
|
+
"NBR14724-R03": {
|
|
58
|
+
"standard": "ABNT NBR 14724:2011",
|
|
59
|
+
"clause": "Seção 5.8",
|
|
60
|
+
"title": "Ilustrações com título superior, chamada textual prévia e fonte inferior",
|
|
61
|
+
"severity": "warning",
|
|
62
|
+
},
|
|
63
|
+
"NBR6022-R01": {
|
|
64
|
+
"standard": "ABNT NBR 6022:2018",
|
|
65
|
+
"clause": "Seção 5",
|
|
66
|
+
"title": "Estrutura de artigo científico periódico (título, resumo, introdução, desenvolvimento, conclusão e referências)",
|
|
67
|
+
"severity": "error",
|
|
68
|
+
},
|
|
69
|
+
"NBR10520-R01": {
|
|
70
|
+
"standard": "ABNT NBR 10520:2023",
|
|
71
|
+
"clause": "Seção 4.2",
|
|
72
|
+
"title": "Sistema autor-data entre parênteses em caixa mista (Silva, 2023), vedada CAIXA ALTA",
|
|
73
|
+
"severity": "error",
|
|
74
|
+
},
|
|
75
|
+
"NBR10520-R02": {
|
|
76
|
+
"standard": "ABNT NBR 10520:2023",
|
|
77
|
+
"clause": "Seção 7.1",
|
|
78
|
+
"title": "Consistência cruzada estrita entre citações no texto e referências no arquivo .bib",
|
|
79
|
+
"severity": "error",
|
|
80
|
+
},
|
|
81
|
+
"NBR10520-R03": {
|
|
82
|
+
"standard": "ABNT NBR 10520:2023",
|
|
83
|
+
"clause": "Seções 5.2 / 5.3",
|
|
84
|
+
"title": "Formatação de citações diretas curtas (<=3 linhas, aspas) e longas (>3 linhas, recuo de 4 cm, fonte 10)",
|
|
85
|
+
"severity": "error",
|
|
86
|
+
},
|
|
87
|
+
"NBR6023-R01": {
|
|
88
|
+
"standard": "ABNT NBR 6023:2018",
|
|
89
|
+
"clause": "Anexo A (A.1)",
|
|
90
|
+
"title": "Abreviatura padronizada de meses (três caracteres com ponto, exceto 'maio' por extenso sem ponto)",
|
|
91
|
+
"severity": "warning",
|
|
92
|
+
},
|
|
93
|
+
"NBR6023-R02": {
|
|
94
|
+
"standard": "ABNT NBR 6023:2018",
|
|
95
|
+
"clause": "Seção 6.2",
|
|
96
|
+
"title": "Alinhamento à margem esquerda e destaque tipográfico exclusivo no título principal",
|
|
97
|
+
"severity": "error",
|
|
98
|
+
},
|
|
99
|
+
"NBR6024-R01": {
|
|
100
|
+
"standard": "ABNT NBR 6024:2012",
|
|
101
|
+
"clause": "Seção 4.1",
|
|
102
|
+
"title": "Numeração progressiva das seções sem ponto final após o último algarismo",
|
|
103
|
+
"severity": "warning",
|
|
104
|
+
},
|
|
105
|
+
"NBR6027-R01": {
|
|
106
|
+
"standard": "ABNT NBR 6027:2012",
|
|
107
|
+
"clause": "Seção 4",
|
|
108
|
+
"title": "Sumário restrito aos elementos textuais e pós-textuais (vedada inclusão de elementos pré-textuais)",
|
|
109
|
+
"severity": "error",
|
|
110
|
+
},
|
|
111
|
+
"NBR6028-R01": {
|
|
112
|
+
"standard": "ABNT NBR 6028:2021",
|
|
113
|
+
"clause": "Seção 3.3",
|
|
114
|
+
"title": "Resumo em parágrafo único contínuo na 3ª pessoa e palavras-chave separadas por ponto e vírgula",
|
|
115
|
+
"severity": "warning",
|
|
116
|
+
},
|
|
117
|
+
"IBGE1993-R01": {
|
|
118
|
+
"standard": "Normas Tabulares IBGE (1993)",
|
|
119
|
+
"clause": "Seção 3.1",
|
|
120
|
+
"title": "Distinção entre Tabela (dados quantitativos, laterais abertas) e Quadro (dados qualitativos, bordas fechadas)",
|
|
121
|
+
"severity": "error",
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Abreviatura oficial de meses segundo a NBR 6023:2018 (atenção ao 'maio' sem ponto!)
|
|
126
|
+
ABNT_MONTHS: dict[str, str] = {
|
|
127
|
+
"1": "jan.",
|
|
128
|
+
"01": "jan.",
|
|
129
|
+
"jan": "jan.",
|
|
130
|
+
"january": "jan.",
|
|
131
|
+
"janeiro": "jan.",
|
|
132
|
+
"2": "fev.",
|
|
133
|
+
"02": "fev.",
|
|
134
|
+
"feb": "fev.",
|
|
135
|
+
"february": "fev.",
|
|
136
|
+
"fevereiro": "fev.",
|
|
137
|
+
"3": "mar.",
|
|
138
|
+
"03": "mar.",
|
|
139
|
+
"mar": "mar.",
|
|
140
|
+
"march": "mar.",
|
|
141
|
+
"março": "mar.",
|
|
142
|
+
"4": "abr.",
|
|
143
|
+
"04": "abr.",
|
|
144
|
+
"apr": "abr.",
|
|
145
|
+
"april": "abr.",
|
|
146
|
+
"abril": "abr.",
|
|
147
|
+
"5": "maio",
|
|
148
|
+
"05": "maio",
|
|
149
|
+
"may": "maio",
|
|
150
|
+
"mai": "maio",
|
|
151
|
+
"maio": "maio",
|
|
152
|
+
"6": "jun.",
|
|
153
|
+
"06": "jun.",
|
|
154
|
+
"jun": "jun.",
|
|
155
|
+
"june": "jun.",
|
|
156
|
+
"junho": "jun.",
|
|
157
|
+
"7": "jul.",
|
|
158
|
+
"07": "jul.",
|
|
159
|
+
"jul": "jul.",
|
|
160
|
+
"july": "jul.",
|
|
161
|
+
"julho": "jul.",
|
|
162
|
+
"8": "ago.",
|
|
163
|
+
"08": "ago.",
|
|
164
|
+
"aug": "ago.",
|
|
165
|
+
"august": "ago.",
|
|
166
|
+
"agosto": "ago.",
|
|
167
|
+
"9": "set.",
|
|
168
|
+
"09": "set.",
|
|
169
|
+
"sep": "set.",
|
|
170
|
+
"september": "set.",
|
|
171
|
+
"setembro": "set.",
|
|
172
|
+
"10": "out.",
|
|
173
|
+
"oct": "out.",
|
|
174
|
+
"october": "out.",
|
|
175
|
+
"outubro": "out.",
|
|
176
|
+
"11": "nov.",
|
|
177
|
+
"november": "nov.",
|
|
178
|
+
"novembro": "nov.",
|
|
179
|
+
"12": "dez.",
|
|
180
|
+
"dec": "dez.",
|
|
181
|
+
"december": "dez.",
|
|
182
|
+
"dezembro": "dez.",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Regex para detectar autores em CAIXA ALTA entre parênteses (norma antiga NBR 10520:2002)
|
|
186
|
+
# Na norma 2023, deve ser (Silva, 2021) e não (SILVA, 2021)
|
|
187
|
+
RE_UPPERCASE_CITATION = re.compile(
|
|
188
|
+
r"\(([A-ZÁÉÍÓÚÂÊÔÃÕÇ]{2,}(?:\s+[A-ZÁÉÍÓÚÂÊÔÃÕÇ]{2,})*(?:;\s*[A-ZÁÉÍÓÚÂÊÔÃÕÇ]{2,}(?:\s+[A-ZÁÉÍÓÚÂÊÔÃÕÇ]{2,})*)*),\s*(\d{4}[^\)]*)\)"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Regex para ponto final indevido no indicativo numérico de seções (NBR 6024:2012)
|
|
192
|
+
# Incorreto: "1.1. Introdução" ou "# 1.1. Introdução"
|
|
193
|
+
# Correto: "1.1 Introdução" ou "# 1.1 Introdução"
|
|
194
|
+
RE_INVALID_SECTION_DOT = re.compile(
|
|
195
|
+
r"^[ \t]*(?:#+\s*)?(\d+(?:\.\d+)*)\.\s+([A-ZÁÉÍÓÚÂÊÔÃÕÇ].*)$", re.MULTILINE
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Regex para maio com ponto (incorreto pela NBR 6023)
|
|
199
|
+
RE_MAI_WITH_DOT = re.compile(r"\bmai\.(?!\w)", re.IGNORECASE)
|
|
200
|
+
|
|
201
|
+
# Lista de termos e pronomes em 1ª pessoa
|
|
202
|
+
FIRST_PERSON_PRONOUNS: set[str] = {
|
|
203
|
+
"eu", "me", "mim", "comigo", "meu", "minha", "meus", "minhas",
|
|
204
|
+
"nós", "nos", "conosco", "nosso", "nossa", "nossos", "nossas"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Verbos comuns em 1ª pessoa do singular e plural (passado, presente, futuro)
|
|
208
|
+
FIRST_PERSON_VERBS: set[str] = {
|
|
209
|
+
# Singular
|
|
210
|
+
"fiz", "faço", "farei", "analisei", "analiso", "analisarei",
|
|
211
|
+
"observei", "observo", "observarei", "concluo", "concluí", "concluirei",
|
|
212
|
+
"acredito", "acreditei", "acreditarei", "percebo", "percebi", "perceberei",
|
|
213
|
+
"verifico", "verifiquei", "verificarei", "identifico", "identifiquei", "identificarei",
|
|
214
|
+
"utilizei", "utilizo", "utilizarei", "propus", "proponho", "proporei",
|
|
215
|
+
"desenvolvi", "desenvolvo", "desenvolverei", "notei", "noto", "notarei",
|
|
216
|
+
"considerei", "considero", "considerarei", "estudei", "estudo", "estudarei",
|
|
217
|
+
"pretendo", "pretendi", "pretenderei", "apresento", "apresentei",
|
|
218
|
+
# Plural
|
|
219
|
+
"fizemos", "faremos", "analisamos", "analisaremos",
|
|
220
|
+
"observamos", "observaremos", "concluímos", "concluiremos",
|
|
221
|
+
"acreditamos", "acreditaremos", "percebemos", "perceberemos",
|
|
222
|
+
"verificamos", "verificaremos", "identificamos", "identificaremos",
|
|
223
|
+
"utilizamos", "utilizaremos", "propusemos", "proporemos",
|
|
224
|
+
"desenvolvemos", "desenvolveremos", "notamos", "notaremos",
|
|
225
|
+
"consideramos", "consideraremos", "estudamos", "estudaremos",
|
|
226
|
+
"pretendemos", "pretenderemos", "apresentamos", "apresentaremos",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Juízos de valor sem fundamentação e termos dogmáticos
|
|
230
|
+
VALUE_JUDGMENTS: list[tuple[str, str]] = [
|
|
231
|
+
("obviamente", "Substitua por dados empíricos ou referência; o que é 'óbvio' requer demonstração na ciência."),
|
|
232
|
+
("claramente", "Evite advérbios opinativos. Deixe que os dados demonstrem a conclusão."),
|
|
233
|
+
("sem dúvida", "Na ciência impera a dúvida metódica; utilize 'evidências apontam' ou cite a fonte."),
|
|
234
|
+
("sem sombra de dúvida", "Afirmação categórica sem rigor científico."),
|
|
235
|
+
("indubitavelmente", "Evite certezas absolutas sem citação ou prova matemática."),
|
|
236
|
+
("com certeza", "Linguagem informal e assertiva sem respaldo."),
|
|
237
|
+
("é evidente", "Se fosse evidente não necessitaria de fundamentação; apresente a fonte."),
|
|
238
|
+
("notoriamente", "Apelo ao senso comum; fundamente com citação bibliográfica."),
|
|
239
|
+
("comprovadamente", "Só utilize se acompanhar a citação comprobatória direta."),
|
|
240
|
+
("perfeito", "Juízo de valor qualitativo excessivo."),
|
|
241
|
+
("maravilhoso", "Termo emotivo/qualificativo inadequado ao texto científico."),
|
|
242
|
+
("fantástico", "Tom hiperbólico desprovido de rigor técnico."),
|
|
243
|
+
("inquestionável", "No método científico, hipóteses são sempre falseáveis."),
|
|
244
|
+
("na minha opinião", "Trabalho científico fundamenta-se em fatos e literatura, não em opinião pessoal."),
|
|
245
|
+
("no meu ponto de vista", "Substitua pela constatação dos dados ou fundamentação teórica."),
|
|
246
|
+
("a meu ver", "Substitua por constatações impessoais."),
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
# Expressões coloquiais
|
|
250
|
+
COLLOQUIALISMS: list[tuple[str, str]] = [
|
|
251
|
+
("a gente", "Substitua pelo pronome impessoal 'se' ou terceira pessoa ('observa-se')."),
|
|
252
|
+
("pra caramba", "Expressão de gíria/informal inadequada."),
|
|
253
|
+
("bater o martelo", "Metáfora informal; use 'decidir', 'definir' ou 'concluir'."),
|
|
254
|
+
("dar um jeito", "Expressão informal; use 'solucionar' ou 'contornar a limitação'."),
|
|
255
|
+
("rolar", "Gíria informal; utilize 'ocorrer', 'acontecer' ou 'suceder'."),
|
|
256
|
+
("rolou", "Gíria informal; utilize 'ocorreu' ou 'aconteceu'."),
|
|
257
|
+
("ficar de olho", "Linguagem coloquial; utilize 'monitorar' ou 'acompanhar atentamente'."),
|
|
258
|
+
("por aí vai", "Expressão vaga; use 'entre outros' ou cite os itens pertinentes."),
|
|
259
|
+
("mais ou menos", "Imprecisão; forneça valores, faixas de incerteza ou margem de erro."),
|
|
260
|
+
("coisa", "Substantivo genérico vago; especifique o objeto, variável, fenômeno ou elemento."),
|
|
261
|
+
("coisas", "Termo genérico vago; especifique os elementos ou variáveis."),
|
|
262
|
+
("tipo assim", "Cacoete de linguagem coloquial."),
|
|
263
|
+
("troço", "Gíria genérica inadequada."),
|
|
264
|
+
("pegar pesado", "Metáfora coloquial; use 'ser rigoroso'."),
|
|
265
|
+
("tirar de letra", "Expressão coloquial inadequada."),
|
|
266
|
+
]
|
abnt_mcp/models.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Modelos de dados Pydantic para validações e tipagem do ABNT Copilot MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Author(BaseModel):
|
|
9
|
+
"""Representação estruturada de autor."""
|
|
10
|
+
first_name: str = Field(default="", description="Prenome(s) do autor (ex: 'Maria Aparecida')")
|
|
11
|
+
last_name: str = Field(..., description="Sobrenome do autor (ex: 'Silva')")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CitationInput(BaseModel):
|
|
15
|
+
"""Parâmetros de formatação de citação conforme a NBR 10520:2023."""
|
|
16
|
+
authors: list[str] = Field(
|
|
17
|
+
...,
|
|
18
|
+
description="Lista de sobrenomes dos autores (ex: ['Silva', 'Santos'])",
|
|
19
|
+
min_length=1
|
|
20
|
+
)
|
|
21
|
+
year: str | int = Field(..., description="Ano da publicação (ex: 2023)")
|
|
22
|
+
page: str | None = Field(
|
|
23
|
+
default=None,
|
|
24
|
+
description="Página ou localizador (ex: '45', '12-15', 'local. 340')"
|
|
25
|
+
)
|
|
26
|
+
style: Literal["in_parentheses", "in_text"] = Field(
|
|
27
|
+
default="in_parentheses",
|
|
28
|
+
description="in_parentheses: (Silva, 2023, p. 10) | in_text: Silva (2023, p. 10)"
|
|
29
|
+
)
|
|
30
|
+
quote_type: Literal["direct_short", "direct_long", "indirect"] = Field(
|
|
31
|
+
default="indirect",
|
|
32
|
+
description="Tipo de citação: direta curta (<=3 linhas), direta longa (>3 linhas) ou indireta"
|
|
33
|
+
)
|
|
34
|
+
quote_text: str | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description="Texto citado na íntegra (necessário para citações diretas)"
|
|
37
|
+
)
|
|
38
|
+
target_format: Literal["markdown", "latex", "typst", "text"] = Field(
|
|
39
|
+
default="markdown",
|
|
40
|
+
description="Formato de saída gerado"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ReferenceInput(BaseModel):
|
|
45
|
+
"""Parâmetros para referência bibliográfica conforme a NBR 6023:2018."""
|
|
46
|
+
entry_type: Literal[
|
|
47
|
+
"book", "book_chapter", "journal_article", "website",
|
|
48
|
+
"thesis", "legislation", "conference", "software"
|
|
49
|
+
] = Field(..., description="Tipo de documento referenciado")
|
|
50
|
+
title: str = Field(..., description="Título principal da obra (receberá destaque)")
|
|
51
|
+
subtitle: str | None = Field(default=None, description="Subtítulo (não recebe destaque)")
|
|
52
|
+
authors: list[Author] | None = Field(default=None, description="Lista de autores")
|
|
53
|
+
edition: str | None = Field(default=None, description="Ex: '3. ed.'")
|
|
54
|
+
city: str | None = Field(default=None, description="Cidade de publicação")
|
|
55
|
+
publisher: str | None = Field(default=None, description="Editora ou instituição")
|
|
56
|
+
year: str | int = Field(..., description="Ano de publicação")
|
|
57
|
+
journal_name: str | None = Field(default=None, description="Nome da revista ou periódico")
|
|
58
|
+
volume: str | None = Field(default=None, description="Volume do periódico")
|
|
59
|
+
number: str | None = Field(default=None, description="Número/fascículo do periódico")
|
|
60
|
+
pages: str | None = Field(default=None, description="Páginas (ex: '45-60')")
|
|
61
|
+
url: str | None = Field(default=None, description="Endereço eletrônico")
|
|
62
|
+
access_date: str | None = Field(default=None, description="Data de acesso (ex: '24 out. 2026')")
|
|
63
|
+
degree: str | None = Field(default=None, description="Grau acadêmico para teses/dissertações")
|
|
64
|
+
institution: str | None = Field(default=None, description="Instituição de ensino para teses")
|
|
65
|
+
emphasis_style: Literal["bold", "italic"] = Field(
|
|
66
|
+
default="bold",
|
|
67
|
+
description="Estilo de destaque do título (negrito ou itálico)"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DiagnosticItem(BaseModel):
|
|
72
|
+
"""Modelo unificado de diagnóstico (diferencia regra normativa estrita de heurística editorial)."""
|
|
73
|
+
rule_id: str = Field(..., description="ID canônico (ex: 'NBR10520-R01') ou código heurístico ('HEURISTIC-TONE-01')")
|
|
74
|
+
nature: Literal["normative", "heuristic", "institutional"] = Field(
|
|
75
|
+
default="normative",
|
|
76
|
+
description="Natureza do diagnóstico: normative (ABNT/IBGE), heuristic (editorial/IA) ou institutional"
|
|
77
|
+
)
|
|
78
|
+
severity: Literal["info", "warning", "error", "critical"] = "warning"
|
|
79
|
+
confidence: Literal["high", "medium", "low"] = "high"
|
|
80
|
+
message: str
|
|
81
|
+
suggestion: str = ""
|
|
82
|
+
file: str | None = None
|
|
83
|
+
line: int | None = None
|
|
84
|
+
human_review_required: bool = False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ToneIssue(BaseModel):
|
|
88
|
+
"""Registro de inadequação de tom científico."""
|
|
89
|
+
category: Literal["first_person", "value_judgment", "colloquialism", "ungrounded_paragraph"]
|
|
90
|
+
term: str
|
|
91
|
+
context: str
|
|
92
|
+
line_number: int | None = None
|
|
93
|
+
suggestion: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ToneAnalysisResult(BaseModel):
|
|
97
|
+
"""Resultado da análise de tom e rigor científico."""
|
|
98
|
+
score: int = Field(..., ge=0, le=100, description="Nota de rigor científico (0 a 100)")
|
|
99
|
+
is_acceptable: bool = Field(..., description="Se o texto atende ao padrão científico")
|
|
100
|
+
total_words: int
|
|
101
|
+
issues_count: int
|
|
102
|
+
issues: list[ToneIssue]
|
|
103
|
+
diagnostics: list[DiagnosticItem] = Field(
|
|
104
|
+
default_factory=list,
|
|
105
|
+
description="Diagnósticos unificados marcados como heuristic (requerem revisão humana)"
|
|
106
|
+
)
|
|
107
|
+
summary: str
|
|
108
|
+
suggested_rewrites: list[dict[str, str]]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class StructureCheckResult(BaseModel):
|
|
112
|
+
"""Resultado da validação estrutural da NBR 14724 / NBR 6022."""
|
|
113
|
+
standard: str = "NBR 14724:2011"
|
|
114
|
+
is_valid: bool
|
|
115
|
+
compliance_score: int = Field(..., ge=0, le=100)
|
|
116
|
+
document_type: str = "TCC/Monografia/Dissertação"
|
|
117
|
+
missing_mandatory: list[str]
|
|
118
|
+
optional_elements_present: list[str]
|
|
119
|
+
elements_found: list[str]
|
|
120
|
+
section_warnings: list[str]
|
|
121
|
+
rule_violations: list[dict[str, str]] = Field(
|
|
122
|
+
default_factory=list,
|
|
123
|
+
description="Lista de violações rastreáveis por ID canônico (ex: NBR14724-R01)"
|
|
124
|
+
)
|
|
125
|
+
diagnostics: list[DiagnosticItem] = Field(
|
|
126
|
+
default_factory=list,
|
|
127
|
+
description="Lista de diagnósticos unificados"
|
|
128
|
+
)
|
|
129
|
+
recommendations: list[str]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CitationValidationResult(BaseModel):
|
|
133
|
+
"""Resultado da validação de citações vs referências."""
|
|
134
|
+
total_citations_in_text: int
|
|
135
|
+
total_references_in_bib: int
|
|
136
|
+
orphan_citations: list[dict[str, str | int]] = Field(
|
|
137
|
+
description="Citações presentes no texto mas ausentes no .bib"
|
|
138
|
+
)
|
|
139
|
+
unused_references: list[str] = Field(
|
|
140
|
+
description="Chaves do .bib que nunca foram citadas no texto"
|
|
141
|
+
)
|
|
142
|
+
uppercase_violations: list[dict[str, str | int]] = Field(
|
|
143
|
+
description="Citações em caixa alta no texto violando a NBR 10520:2023"
|
|
144
|
+
)
|
|
145
|
+
diagnostics: list[DiagnosticItem] = Field(
|
|
146
|
+
default_factory=list,
|
|
147
|
+
description="Lista de diagnósticos normativos de citações e referências"
|
|
148
|
+
)
|
|
149
|
+
is_consistent: bool
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class IllustrationAuditResult(BaseModel):
|
|
153
|
+
"""Resultado da auditoria de figuras, tabelas e quadros (NBR 14724 item 5.8 e IBGE 1993)."""
|
|
154
|
+
is_valid: bool
|
|
155
|
+
total_figures: int = 0
|
|
156
|
+
total_tables: int = 0
|
|
157
|
+
total_quadros: int = 0
|
|
158
|
+
unreferenced_items: list[dict[str, Any]] = Field(
|
|
159
|
+
default_factory=list,
|
|
160
|
+
description="Ilustrações, tabelas ou quadros declarados sem chamada prévia no texto"
|
|
161
|
+
)
|
|
162
|
+
missing_sources: list[dict[str, Any]] = Field(
|
|
163
|
+
default_factory=list,
|
|
164
|
+
description="Itens sem indicação obrigatória de 'Fonte:' na parte inferior"
|
|
165
|
+
)
|
|
166
|
+
broken_image_paths: list[dict[str, Any]] = Field(
|
|
167
|
+
default_factory=list,
|
|
168
|
+
description="Caminhos de arquivos de imagem referenciados que não existem no disco"
|
|
169
|
+
)
|
|
170
|
+
numbering_issues: list[dict[str, Any]] = Field(
|
|
171
|
+
default_factory=list,
|
|
172
|
+
description="Numeração fora de ordem sequencial ou duplicada"
|
|
173
|
+
)
|
|
174
|
+
quadro_vs_tabela_warnings: list[dict[str, Any]] = Field(
|
|
175
|
+
default_factory=list,
|
|
176
|
+
description="Alertas de confusão conceitual entre Tabela (quantitativo) e Quadro (qualitativo)"
|
|
177
|
+
)
|
|
178
|
+
diagnostics: list[DiagnosticItem] = Field(
|
|
179
|
+
default_factory=list,
|
|
180
|
+
description="Lista de diagnósticos unificados"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class AcademicPaper(BaseModel):
|
|
185
|
+
"""Metadados de um artigo científico retornado por APIs de busca."""
|
|
186
|
+
title: str
|
|
187
|
+
authors: list[str]
|
|
188
|
+
year: str | int | None = None
|
|
189
|
+
journal: str | None = None
|
|
190
|
+
doi: str | None = None
|
|
191
|
+
url: str | None = None
|
|
192
|
+
abstract: str | None = None
|
|
193
|
+
source: str
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ==============================================================================
|
|
197
|
+
# MODELOS DE SPEC-DRIVEN ACADEMIC WRITING (SDD)
|
|
198
|
+
# ==============================================================================
|
|
199
|
+
|
|
200
|
+
class NormativeProfile(BaseModel):
|
|
201
|
+
"""Camada 1: Perfil normativo e institucional da especificação acadêmica."""
|
|
202
|
+
document_type: Literal["tcc", "dissertacao", "tese", "artigo"] = "tcc"
|
|
203
|
+
standard: Literal["ABNT_14724", "ABNT_6022"] = "ABNT_14724"
|
|
204
|
+
institution: str | None = None
|
|
205
|
+
course: str | None = None
|
|
206
|
+
department: str | None = None
|
|
207
|
+
advisor: str | None = None
|
|
208
|
+
cover_header_lines: list[str] = Field(
|
|
209
|
+
default_factory=list,
|
|
210
|
+
description="Linhas superiores do cabeçalho da capa na ordem institucional"
|
|
211
|
+
)
|
|
212
|
+
has_cover_logo: bool = Field(
|
|
213
|
+
default=False,
|
|
214
|
+
description="Se a capa institucional utiliza logotipo/brasão"
|
|
215
|
+
)
|
|
216
|
+
cover_logo_path: str | None = Field(
|
|
217
|
+
default=None,
|
|
218
|
+
description="Caminho relativo da imagem da logo da instituição no projeto"
|
|
219
|
+
)
|
|
220
|
+
preamble_text: str | None = Field(
|
|
221
|
+
default=None,
|
|
222
|
+
description="Preâmbulo institucional completo da Folha de Rosto"
|
|
223
|
+
)
|
|
224
|
+
required_pre_textual: list[str] = Field(
|
|
225
|
+
default_factory=list,
|
|
226
|
+
description="Elementos pré-textuais adicionais exigidos pela instituição"
|
|
227
|
+
)
|
|
228
|
+
conclusion_label: str = Field(
|
|
229
|
+
default="Conclusão",
|
|
230
|
+
description="Rótulo adotado para a seção conclusiva ('Conclusão' ou 'Considerações Finais')"
|
|
231
|
+
)
|
|
232
|
+
inferred_from_pdf: bool = Field(
|
|
233
|
+
default=False,
|
|
234
|
+
description="Se os padrões institucionais foram extraídos automaticamente de um PDF modelo"
|
|
235
|
+
)
|
|
236
|
+
emphasis_style: Literal["bold", "italic"] = "bold"
|
|
237
|
+
min_references: int = Field(default=15, ge=1)
|
|
238
|
+
min_scientific_tone_score: int = Field(default=80, ge=0, le=100)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class SpecificObjective(BaseModel):
|
|
242
|
+
"""Camada 2: Objetivo específico rastreável na matriz científica."""
|
|
243
|
+
id: str = Field(..., description="Identificador canônico, ex: 'OBJ-01'")
|
|
244
|
+
description: str = Field(..., description="Enunciado formal do objetivo específico")
|
|
245
|
+
keywords: list[str] = Field(
|
|
246
|
+
default_factory=list,
|
|
247
|
+
description="Termos/conceitos-chave que comprovam discussão e resolução do objetivo"
|
|
248
|
+
)
|
|
249
|
+
target_section: str | None = Field(
|
|
250
|
+
default=None,
|
|
251
|
+
description="Seção ou capítulo responsável por desenvolver este objetivo"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class PlannedChapter(BaseModel):
|
|
256
|
+
"""Camada 3: Capítulo ou seção planejada na estrutura do manuscrito."""
|
|
257
|
+
number: str = Field(..., description="Indicativo numérico (ex: '1', '2', '2.1')")
|
|
258
|
+
title: str = Field(..., description="Título planejado do capítulo/seção")
|
|
259
|
+
min_words: int = Field(default=200, ge=0)
|
|
260
|
+
min_citations: int = Field(default=1, ge=0)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class ResearchSpec(BaseModel):
|
|
264
|
+
"""Manifesto de Especificação Científica (abnt.spec.json)."""
|
|
265
|
+
spec_version: str = "1.0"
|
|
266
|
+
title: str
|
|
267
|
+
author: str | None = None
|
|
268
|
+
research_problem: str = Field(..., description="Pergunta de partida / problema de pesquisa")
|
|
269
|
+
general_objective: str = Field(..., description="Objetivo geral do estudo")
|
|
270
|
+
methodology_summary: str = Field(..., description="Abordagem, método e procedimentos")
|
|
271
|
+
specific_objectives: list[SpecificObjective] = Field(..., min_length=1)
|
|
272
|
+
profile: NormativeProfile = Field(default_factory=NormativeProfile)
|
|
273
|
+
planned_chapters: list[PlannedChapter] = Field(default_factory=list)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class ObjectiveAuditItem(BaseModel):
|
|
277
|
+
"""Resultado de auditoria individual de um objetivo específico (OBJ-XX)."""
|
|
278
|
+
id: str
|
|
279
|
+
description: str
|
|
280
|
+
status: Literal["fulfilled", "partial", "missing"]
|
|
281
|
+
covered_in_body: bool
|
|
282
|
+
answered_in_conclusion: bool
|
|
283
|
+
matched_keywords: list[str] = Field(default_factory=list)
|
|
284
|
+
missing_keywords: list[str] = Field(default_factory=list)
|
|
285
|
+
feedback: str
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ChapterAuditItem(BaseModel):
|
|
289
|
+
"""Resultado de auditoria de um capítulo planejado."""
|
|
290
|
+
number: str
|
|
291
|
+
title: str
|
|
292
|
+
found_in_document: bool
|
|
293
|
+
feedback: str
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class SpecAuditResult(BaseModel):
|
|
297
|
+
"""Relatório consolidado de auditoria orientada a especificação (SDD)."""
|
|
298
|
+
spec_path: str
|
|
299
|
+
title: str
|
|
300
|
+
document_type: str
|
|
301
|
+
standard_applied: str
|
|
302
|
+
is_compliant: bool
|
|
303
|
+
submission_readiness: Literal["READY", "READY_WITH_WARNINGS", "NOT_READY"] = "NOT_READY"
|
|
304
|
+
spec_compliance_score: int = Field(..., ge=0, le=100)
|
|
305
|
+
structure_score: int = Field(..., ge=0, le=100)
|
|
306
|
+
objectives_fulfillment_rate: int = Field(..., ge=0, le=100)
|
|
307
|
+
tone_score: int = Field(..., ge=0, le=100)
|
|
308
|
+
references_count: int
|
|
309
|
+
min_references_required: int
|
|
310
|
+
institutional_compliance: dict[str, Any] = Field(
|
|
311
|
+
default_factory=dict,
|
|
312
|
+
description="Status de conformidade com as exigências específicas da universidade/periódico"
|
|
313
|
+
)
|
|
314
|
+
illustrations_summary: dict[str, Any] = Field(
|
|
315
|
+
default_factory=dict,
|
|
316
|
+
description="Resumo da auditoria de figuras, tabelas e quadros"
|
|
317
|
+
)
|
|
318
|
+
objectives_audit: list[ObjectiveAuditItem]
|
|
319
|
+
chapters_audit: list[ChapterAuditItem]
|
|
320
|
+
normative_issues: list[dict[str, str]]
|
|
321
|
+
diagnostics: list[DiagnosticItem] = Field(
|
|
322
|
+
default_factory=list,
|
|
323
|
+
description="Lista consolidada de diagnósticos (normativos, institucionais e heurísticos)"
|
|
324
|
+
)
|
|
325
|
+
recommendations: list[str]
|
|
326
|
+
saved_report_paths: dict[str, str] = Field(
|
|
327
|
+
default_factory=dict,
|
|
328
|
+
description="Caminhos dos relatórios persistidos em .abnt-mcp/ (JSON e Markdown)"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class ProjectScaffoldResult(BaseModel):
|
|
333
|
+
"""Resultado do scaffolding estrutural de um projeto acadêmico SDD."""
|
|
334
|
+
success: bool
|
|
335
|
+
project_path: str
|
|
336
|
+
format: Literal["typst", "latex", "markdown"]
|
|
337
|
+
main_file: str
|
|
338
|
+
bib_file: str
|
|
339
|
+
spec_json_path: str
|
|
340
|
+
spec_md_path: str
|
|
341
|
+
cover_logo_path: str | None = None
|
|
342
|
+
created_files: list[str] = Field(default_factory=list)
|
|
343
|
+
inferred_institutional_profile: dict[str, Any] | None = None
|
|
344
|
+
message: str
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Módulos de parsing de documentos, citações, BibTeX e PDFs."""
|
|
2
|
+
|
|
3
|
+
from abnt_mcp.parsers.citations import (
|
|
4
|
+
extract_citations_from_latex,
|
|
5
|
+
extract_citations_from_markdown,
|
|
6
|
+
extract_citations_from_typst,
|
|
7
|
+
extract_citations_from_project,
|
|
8
|
+
detect_uppercase_citations,
|
|
9
|
+
)
|
|
10
|
+
from abnt_mcp.parsers.bibtex import (
|
|
11
|
+
clean_latex_accents,
|
|
12
|
+
extract_bib_keys_and_dois,
|
|
13
|
+
sanitize_bibtex_string,
|
|
14
|
+
append_entry_if_absent,
|
|
15
|
+
normalize_abnt_month,
|
|
16
|
+
)
|
|
17
|
+
from abnt_mcp.parsers.pdf_reader import extract_pdf_section, audit_pdf_pagination
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"extract_citations_from_latex",
|
|
21
|
+
"extract_citations_from_markdown",
|
|
22
|
+
"extract_citations_from_typst",
|
|
23
|
+
"extract_citations_from_project",
|
|
24
|
+
"detect_uppercase_citations",
|
|
25
|
+
"clean_latex_accents",
|
|
26
|
+
"extract_bib_keys_and_dois",
|
|
27
|
+
"sanitize_bibtex_string",
|
|
28
|
+
"append_entry_if_absent",
|
|
29
|
+
"normalize_abnt_month",
|
|
30
|
+
"extract_pdf_section",
|
|
31
|
+
"audit_pdf_pagination",
|
|
32
|
+
]
|