archdrift 0.1.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.
- archdrift/__init__.py +17 -0
- archdrift/__main__.py +6 -0
- archdrift/cli.py +650 -0
- archdrift-0.1.0.dist-info/METADATA +114 -0
- archdrift-0.1.0.dist-info/RECORD +8 -0
- archdrift-0.1.0.dist-info/WHEEL +4 -0
- archdrift-0.1.0.dist-info/entry_points.txt +2 -0
- archdrift-0.1.0.dist-info/licenses/LICENSE +21 -0
archdrift/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
archdrift — Contrato de arquitetura para agentes de código.
|
|
3
|
+
|
|
4
|
+
API pública mínima: quem quiser usar como biblioteca (em vez de CLI)
|
|
5
|
+
importa daqui. Tudo o mais em cli.py é considerado interno.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from archdrift.cli import (
|
|
9
|
+
main,
|
|
10
|
+
cmd_generate,
|
|
11
|
+
cmd_check,
|
|
12
|
+
scan,
|
|
13
|
+
detect_stack,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = ["main", "cmd_generate", "cmd_check", "scan", "detect_stack"]
|
archdrift/__main__.py
ADDED
archdrift/cli.py
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
archdrift.py — Contrato de arquitetura para agentes de código.
|
|
4
|
+
|
|
5
|
+
Gera e mantém um ARCHITECTURE.md que é ao mesmo tempo:
|
|
6
|
+
1. MAPA -> árvore curada + stack detectada (regenerado pelo script)
|
|
7
|
+
2. CONTRATO -> tabela de responsabilidades editada por HUMANOS
|
|
8
|
+
e validada em CI (drift detection)
|
|
9
|
+
|
|
10
|
+
COMANDOS:
|
|
11
|
+
python archdrift.py generate <path> # gera/atualiza preservando curadoria
|
|
12
|
+
python archdrift.py check <path> # valida drift; exit 1 se divergiu (CI)
|
|
13
|
+
python archdrift.py <path> # atalho para generate
|
|
14
|
+
|
|
15
|
+
COMO FUNCIONA O MERGE:
|
|
16
|
+
O ARCHITECTURE.md tem seções GERENCIADAS, delimitadas por marcadores:
|
|
17
|
+
|
|
18
|
+
<!-- map:begin <nome> -->
|
|
19
|
+
...conteúdo regenerado pelo script...
|
|
20
|
+
<!-- map:end <nome> -->
|
|
21
|
+
|
|
22
|
+
Tudo FORA dos marcadores é seu: notas, ADRs, diagramas, o que quiser.
|
|
23
|
+
O script nunca toca nesse texto.
|
|
24
|
+
|
|
25
|
+
A tabela de responsabilidades é caso especial: fica dentro de marcadores,
|
|
26
|
+
mas o MERGE preserva as descrições que você editou à mão. Prioridade:
|
|
27
|
+
descrição humana > heurística por nome (ROLE_HINTS) > "(preencher)"
|
|
28
|
+
|
|
29
|
+
COMO FUNCIONA O CHECK (para CI):
|
|
30
|
+
Falha (exit 1) se qualquer uma destas condições for verdadeira:
|
|
31
|
+
a) a estrutura em disco mudou desde a última geração (hash divergente)
|
|
32
|
+
b) existe pasta relevante em disco SEM responsabilidade declarada
|
|
33
|
+
c) existe pasta documentada que NÃO existe mais em disco
|
|
34
|
+
Ou seja: o time é obrigado a manter o contrato vivo — exatamente o que
|
|
35
|
+
agentes de código precisam para confiar no arquivo.
|
|
36
|
+
|
|
37
|
+
FILOSOFIA:
|
|
38
|
+
Tools como Repomix/CodeGraph são DESCRITIVOS: leem o código e dizem o que
|
|
39
|
+
existe. Este script é NORMATIVO: captura a INTENÇÃO do arquiteto e cobra
|
|
40
|
+
que o repo continue obedecendo. O agente lê a intenção; o CI valida.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import sys
|
|
44
|
+
import json
|
|
45
|
+
import re
|
|
46
|
+
import hashlib
|
|
47
|
+
import argparse
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from collections import Counter
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# CONFIGURAÇÃO
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
# Pastas que NÃO interessam ao agente (ruído). Ajuste à sua stack.
|
|
56
|
+
IGNORE_DIRS = {
|
|
57
|
+
".git", "node_modules", "__pycache__", ".venv", "venv", "env",
|
|
58
|
+
"dist", "build", ".next", ".nuxt", "target", "vendor",
|
|
59
|
+
".idea", ".vscode", "coverage", ".pytest_cache", ".mypy_cache",
|
|
60
|
+
"bin", "obj", ".terraform", "tmp", ".turbo", ".cache",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Arquivos ignorados na listagem (não mudam a arquitetura).
|
|
64
|
+
# Inclui as próprias saídas do script, para não poluir execuções seguintes.
|
|
65
|
+
IGNORE_FILES = {".DS_Store", "Thumbs.db", "desktop.ini",
|
|
66
|
+
"ARCHITECTURE.md", "structure.json"}
|
|
67
|
+
|
|
68
|
+
# Dotfiles/dotdirs que SÃO relevantes para o agente entender o projeto.
|
|
69
|
+
DOTFILE_WHITELIST = {
|
|
70
|
+
".github", ".gitlab-ci.yml", ".env.example", ".env.sample",
|
|
71
|
+
".gitignore", ".dockerignore", ".editorconfig", ".nvmrc",
|
|
72
|
+
".eslintrc.json", ".eslintrc.js", ".prettierrc", ".babelrc",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Profundidade máxima da ÁRVORE exibida. Além disso vira ruído.
|
|
76
|
+
MAX_DEPTH = 4
|
|
77
|
+
|
|
78
|
+
# Profundidade máxima do CONTRATO: pastas até este nível PRECISAM ter
|
|
79
|
+
# responsabilidade declarada (o check cobra). Mais fundo que isso é detalhe
|
|
80
|
+
# de implementação e não entra na tabela.
|
|
81
|
+
DOC_DEPTH = 2
|
|
82
|
+
|
|
83
|
+
# Máximo de arquivos de CÓDIGO listados individualmente por diretório.
|
|
84
|
+
MAX_CODE_FILES_PER_DIR = 30
|
|
85
|
+
|
|
86
|
+
# Placeholder para responsabilidade pendente. O check falha enquanto existir.
|
|
87
|
+
PENDING = "_(preencher)_"
|
|
88
|
+
|
|
89
|
+
# Extensões consideradas CÓDIGO/CONFIG -> listadas individualmente na árvore.
|
|
90
|
+
CODE_EXTS = {
|
|
91
|
+
".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
92
|
+
".go", ".rs", ".java", ".kt", ".rb", ".php", ".cs", ".swift",
|
|
93
|
+
".c", ".h", ".cpp", ".hpp", ".sql", ".sh", ".ps1",
|
|
94
|
+
".vue", ".svelte", ".astro",
|
|
95
|
+
".yml", ".yaml", ".toml", ".ini", ".env", ".tf",
|
|
96
|
+
".html", ".css", ".scss", ".sass", ".graphql", ".proto", ".prisma",
|
|
97
|
+
".md", ".mdx",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Arquivos SEM extensão que importam (não são colapsados em grupo).
|
|
101
|
+
IMPORTANT_BARE_NAMES = {
|
|
102
|
+
"Dockerfile", "Makefile", "Procfile", "Rakefile", "Gemfile",
|
|
103
|
+
"LICENSE", "CODEOWNERS",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# JSONs de config que importam; os demais .json são tratados como dados.
|
|
107
|
+
IMPORTANT_JSON_NAMES = {
|
|
108
|
+
"package.json", "tsconfig.json", "composer.json", "deno.json",
|
|
109
|
+
"jsconfig.json", "lerna.json", "nx.json", "turbo.json",
|
|
110
|
+
"vercel.json", "firebase.json", "app.json", "manifest.json",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Marcadores de stack: arquivo na raiz -> (stack, descrição).
|
|
114
|
+
STACK_MARKERS = {
|
|
115
|
+
"package.json": ("Node.js", "manifesto de dependências npm"),
|
|
116
|
+
"tsconfig.json": ("TypeScript", "configuração do compilador TS"),
|
|
117
|
+
"pyproject.toml": ("Python", "manifesto do projeto Python"),
|
|
118
|
+
"requirements.txt": ("Python", "dependências pip"),
|
|
119
|
+
"Pipfile": ("Python", "dependências pipenv"),
|
|
120
|
+
"go.mod": ("Go", "módulo Go"),
|
|
121
|
+
"Cargo.toml": ("Rust", "manifesto Cargo"),
|
|
122
|
+
"pom.xml": ("Java (Maven)", "build Maven"),
|
|
123
|
+
"build.gradle": ("Java/Kotlin (Gradle)", "build Gradle"),
|
|
124
|
+
"build.gradle.kts": ("Kotlin (Gradle)", "build Gradle KTS"),
|
|
125
|
+
"composer.json": ("PHP", "dependências Composer"),
|
|
126
|
+
"Gemfile": ("Ruby", "dependências Bundler"),
|
|
127
|
+
"mix.exs": ("Elixir", "projeto Mix"),
|
|
128
|
+
"Dockerfile": ("Docker", "imagem de container"),
|
|
129
|
+
"docker-compose.yml": ("Docker Compose","orquestração local"),
|
|
130
|
+
"docker-compose.yaml": ("Docker Compose","orquestração local"),
|
|
131
|
+
"serverless.yml": ("Serverless", "config Serverless Framework"),
|
|
132
|
+
"next.config.js": ("Next.js", "config Next.js"),
|
|
133
|
+
"next.config.mjs": ("Next.js", "config Next.js"),
|
|
134
|
+
"next.config.ts": ("Next.js", "config Next.js"),
|
|
135
|
+
"nuxt.config.ts": ("Nuxt", "config Nuxt"),
|
|
136
|
+
"vite.config.ts": ("Vite", "bundler Vite"),
|
|
137
|
+
"vite.config.js": ("Vite", "bundler Vite"),
|
|
138
|
+
"angular.json": ("Angular", "workspace Angular"),
|
|
139
|
+
"manage.py": ("Django", "entrypoint Django"),
|
|
140
|
+
"artisan": ("Laravel", "CLI Laravel"),
|
|
141
|
+
"main.tf": ("Terraform", "infraestrutura como código"),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Heurísticas de responsabilidade por nome de pasta.
|
|
145
|
+
# São apenas o CHUTE INICIAL: o merge preserva o que o humano escrever.
|
|
146
|
+
ROLE_HINTS = {
|
|
147
|
+
"src": "Código-fonte principal da aplicação",
|
|
148
|
+
"app": "Ponto de entrada / camada de aplicação",
|
|
149
|
+
"api": "Endpoints e rotas expostas",
|
|
150
|
+
"routes": "Definição de rotas HTTP",
|
|
151
|
+
"controllers": "Camada de controle (recebe request, orquestra)",
|
|
152
|
+
"services": "Regras de negócio / lógica de domínio",
|
|
153
|
+
"models": "Modelos de dados / entidades",
|
|
154
|
+
"entities": "Entidades de domínio",
|
|
155
|
+
"repositories": "Acesso a dados (persistência)",
|
|
156
|
+
"db": "Configuração e migrações de banco",
|
|
157
|
+
"database": "Configuração e migrações de banco",
|
|
158
|
+
"migrations": "Scripts de migração de schema",
|
|
159
|
+
"components": "Componentes de UI reutilizáveis",
|
|
160
|
+
"pages": "Páginas / telas da aplicação",
|
|
161
|
+
"views": "Camada de apresentação",
|
|
162
|
+
"hooks": "React hooks / lógica reutilizável de UI",
|
|
163
|
+
"utils": "Funções utilitárias genéricas",
|
|
164
|
+
"helpers": "Funções auxiliares",
|
|
165
|
+
"lib": "Bibliotecas internas / wrappers",
|
|
166
|
+
"config": "Configuração da aplicação",
|
|
167
|
+
"middleware": "Middlewares (auth, logging, etc.)",
|
|
168
|
+
"middlewares": "Middlewares (auth, logging, etc.)",
|
|
169
|
+
"tests": "Testes automatizados",
|
|
170
|
+
"test": "Testes automatizados",
|
|
171
|
+
"__tests__": "Testes automatizados",
|
|
172
|
+
"e2e": "Testes end-to-end",
|
|
173
|
+
"public": "Assets estáticos servidos diretamente",
|
|
174
|
+
"assets": "Imagens, fontes e mídia",
|
|
175
|
+
"static": "Arquivos estáticos",
|
|
176
|
+
"styles": "Folhas de estilo / temas",
|
|
177
|
+
"types": "Definições de tipos (TS) / interfaces",
|
|
178
|
+
"interfaces": "Contratos / interfaces",
|
|
179
|
+
"schemas": "Schemas de validação",
|
|
180
|
+
"dto": "Data Transfer Objects",
|
|
181
|
+
"docs": "Documentação do projeto",
|
|
182
|
+
"scripts": "Scripts de automação / build",
|
|
183
|
+
"store": "Estado global (Redux/Zustand/etc.)",
|
|
184
|
+
"context": "Contextos de estado (React Context)",
|
|
185
|
+
"guards": "Proteção de rotas / autorização",
|
|
186
|
+
"workers": "Jobs assíncronos / background workers",
|
|
187
|
+
"jobs": "Jobs agendados / filas",
|
|
188
|
+
"events": "Eventos de domínio / pub-sub",
|
|
189
|
+
"adapters": "Adaptadores para serviços externos",
|
|
190
|
+
"infra": "Infraestrutura (deploy, IaC)",
|
|
191
|
+
"shared": "Código compartilhado entre módulos",
|
|
192
|
+
"core": "Núcleo do domínio / kernel da aplicação",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Marcadores das seções gerenciadas dentro do ARCHITECTURE.md.
|
|
196
|
+
MARK_BEGIN = "<!-- map:begin {name} -->"
|
|
197
|
+
MARK_END = "<!-- map:end {name} -->"
|
|
198
|
+
MARK_HASH = "<!-- map:hash {value} -->"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# CLASSIFICAÇÃO
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def infer_role(dirname: str) -> str:
|
|
206
|
+
"""
|
|
207
|
+
Responsabilidade inferida pelo nome da pasta.
|
|
208
|
+
Matching exato primeiro; depois parcial por palavra
|
|
209
|
+
(ex: 'user-services' -> hint de 'services').
|
|
210
|
+
"""
|
|
211
|
+
name = dirname.lower()
|
|
212
|
+
if name in ROLE_HINTS:
|
|
213
|
+
return ROLE_HINTS[name]
|
|
214
|
+
parts = set(name.replace("-", " ").replace("_", " ").replace(".", " ").split())
|
|
215
|
+
for hint, role in ROLE_HINTS.items():
|
|
216
|
+
if hint in parts:
|
|
217
|
+
return role
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def is_code_file(path: Path) -> bool:
|
|
222
|
+
"""Código/config (listado um a um) vs. dado/asset (colapsado em grupo)."""
|
|
223
|
+
if path.name in IMPORTANT_BARE_NAMES:
|
|
224
|
+
return True
|
|
225
|
+
if path.name in DOTFILE_WHITELIST:
|
|
226
|
+
return True
|
|
227
|
+
if path.suffix.lower() == ".json":
|
|
228
|
+
return path.name in IMPORTANT_JSON_NAMES
|
|
229
|
+
return path.suffix.lower() in CODE_EXTS
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
# DETECÇÃO DE STACK
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def detect_stack(root: Path):
|
|
237
|
+
"""Procura arquivos marcadores na raiz. Retorna [(stack, arquivo, desc)]."""
|
|
238
|
+
found = []
|
|
239
|
+
for marker, (stack, desc) in STACK_MARKERS.items():
|
|
240
|
+
if (root / marker).exists():
|
|
241
|
+
found.append((stack, marker, desc))
|
|
242
|
+
|
|
243
|
+
workflows = root / ".github" / "workflows"
|
|
244
|
+
if workflows.is_dir():
|
|
245
|
+
n = sum(1 for f in workflows.iterdir() if f.suffix in (".yml", ".yaml"))
|
|
246
|
+
if n:
|
|
247
|
+
found.append(("GitHub Actions",
|
|
248
|
+
f".github/workflows ({n} workflow(s))",
|
|
249
|
+
"pipelines de CI/CD"))
|
|
250
|
+
return found
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# VARREDURA DO FILESYSTEM
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def scan(root: Path):
|
|
258
|
+
"""
|
|
259
|
+
Percorre o projeto e retorna (nodes, edges, tree_lines).
|
|
260
|
+
IDs dos nós = caminho relativo à raiz ('.' para a raiz).
|
|
261
|
+
Cada nó de diretório carrega 'depth' para o contrato saber
|
|
262
|
+
quais pastas exigem documentação (depth <= DOC_DEPTH).
|
|
263
|
+
"""
|
|
264
|
+
nodes, edges, tree_lines = [], [], []
|
|
265
|
+
root = root.resolve()
|
|
266
|
+
visited = set() # (dev, inode) -> proteção contra loop de symlink
|
|
267
|
+
|
|
268
|
+
nodes.append({"id": ".", "name": root.name, "type": "dir",
|
|
269
|
+
"role": "Raiz do projeto", "depth": 0})
|
|
270
|
+
|
|
271
|
+
def keep(entry: Path) -> bool:
|
|
272
|
+
"""Filtro de ruído."""
|
|
273
|
+
name = entry.name
|
|
274
|
+
if entry.is_dir() and name in IGNORE_DIRS:
|
|
275
|
+
return False
|
|
276
|
+
if entry.is_file() and name in IGNORE_FILES:
|
|
277
|
+
return False
|
|
278
|
+
if name.startswith(".") and name not in DOTFILE_WHITELIST:
|
|
279
|
+
return False
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
def walk(current: Path, parent_id: str, depth: int, prefix: str):
|
|
283
|
+
if depth > MAX_DEPTH:
|
|
284
|
+
return
|
|
285
|
+
try:
|
|
286
|
+
st = current.resolve().stat()
|
|
287
|
+
key = (st.st_dev, st.st_ino)
|
|
288
|
+
except OSError:
|
|
289
|
+
return
|
|
290
|
+
if key in visited:
|
|
291
|
+
return
|
|
292
|
+
visited.add(key)
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
entries = sorted((e for e in current.iterdir() if keep(e)),
|
|
296
|
+
key=lambda p: (p.is_file(), p.name.lower()))
|
|
297
|
+
except PermissionError:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
dirs = [e for e in entries if e.is_dir()]
|
|
301
|
+
files = [e for e in entries if e.is_file()]
|
|
302
|
+
|
|
303
|
+
# Truncamento por tipo: código listado, dados colapsados por extensão
|
|
304
|
+
code_files = [f for f in files if is_code_file(f)]
|
|
305
|
+
data_files = [f for f in files if not is_code_file(f)]
|
|
306
|
+
shown_code = code_files[:MAX_CODE_FILES_PER_DIR]
|
|
307
|
+
hidden_code = len(code_files) - len(shown_code)
|
|
308
|
+
data_summary = Counter(
|
|
309
|
+
(f.suffix.lower() or "(sem extensão)") for f in data_files
|
|
310
|
+
).most_common()
|
|
311
|
+
|
|
312
|
+
display = ([("dir", d) for d in dirs]
|
|
313
|
+
+ [("file", f) for f in shown_code]
|
|
314
|
+
+ ([("more", hidden_code)] if hidden_code else [])
|
|
315
|
+
+ [("summary", s) for s in data_summary])
|
|
316
|
+
|
|
317
|
+
for i, (kind, item) in enumerate(display):
|
|
318
|
+
last = (i == len(display) - 1)
|
|
319
|
+
connector = "└── " if last else "├── "
|
|
320
|
+
child_prefix = prefix + (" " if last else "│ ")
|
|
321
|
+
|
|
322
|
+
if kind == "dir":
|
|
323
|
+
node_id = item.name if parent_id == "." else f"{parent_id}/{item.name}"
|
|
324
|
+
role = infer_role(item.name)
|
|
325
|
+
comment = f" # {role}" if role else ""
|
|
326
|
+
tree_lines.append(f"{prefix}{connector}{item.name}/{comment}")
|
|
327
|
+
nodes.append({"id": node_id, "name": item.name, "type": "dir",
|
|
328
|
+
"role": role, "depth": depth})
|
|
329
|
+
edges.append({"from": parent_id, "to": node_id, "rel": "contains"})
|
|
330
|
+
walk(item, node_id, depth + 1, child_prefix)
|
|
331
|
+
|
|
332
|
+
elif kind == "file":
|
|
333
|
+
node_id = item.name if parent_id == "." else f"{parent_id}/{item.name}"
|
|
334
|
+
tree_lines.append(f"{prefix}{connector}{item.name}")
|
|
335
|
+
nodes.append({"id": node_id, "name": item.name, "type": "file",
|
|
336
|
+
"role": "", "depth": depth})
|
|
337
|
+
edges.append({"from": parent_id, "to": node_id, "rel": "contains"})
|
|
338
|
+
|
|
339
|
+
elif kind == "more":
|
|
340
|
+
tree_lines.append(f"{prefix}{connector}… (+{item} arquivos de código)")
|
|
341
|
+
|
|
342
|
+
elif kind == "summary":
|
|
343
|
+
ext, count = item
|
|
344
|
+
tree_lines.append(f"{prefix}{connector}({count} arquivo(s) {ext})")
|
|
345
|
+
node_id = f"*{ext}" if parent_id == "." else f"{parent_id}/*{ext}"
|
|
346
|
+
nodes.append({"id": node_id, "name": f"*{ext}", "type": "file_group",
|
|
347
|
+
"role": "", "depth": depth, "count": count})
|
|
348
|
+
edges.append({"from": parent_id, "to": node_id, "rel": "contains"})
|
|
349
|
+
|
|
350
|
+
tree_lines.append(f"{root.name}/")
|
|
351
|
+
walk(root, ".", 1, "")
|
|
352
|
+
return nodes, edges, tree_lines
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def contract_dirs(nodes):
|
|
356
|
+
"""Pastas que participam do CONTRATO (exigem responsabilidade declarada)."""
|
|
357
|
+
return [n for n in nodes
|
|
358
|
+
if n["type"] == "dir" and n["id"] != "." and n["depth"] <= DOC_DEPTH]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def structure_hash(tree_lines, stack_info) -> str:
|
|
362
|
+
"""
|
|
363
|
+
Hash da estrutura atual. Gravado no MD na geração; o check compara
|
|
364
|
+
com o recalculado para saber se o mapa está desatualizado.
|
|
365
|
+
"""
|
|
366
|
+
payload = "\n".join(tree_lines) + "|" + json.dumps(sorted(stack_info))
|
|
367
|
+
return hashlib.sha1(payload.encode("utf-8")).hexdigest()[:12]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
# PARSE DO ARCHITECTURE.md EXISTENTE (para merge e check)
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
def parse_existing(md_text: str):
|
|
375
|
+
"""
|
|
376
|
+
Extrai do MD existente:
|
|
377
|
+
- roles: {pasta -> responsabilidade} (a curadoria humana)
|
|
378
|
+
- hash: hash gravado na última geração (ou None)
|
|
379
|
+
Só linhas de tabela no formato `| `pasta` | descrição |` contam.
|
|
380
|
+
"""
|
|
381
|
+
roles = {}
|
|
382
|
+
# Linhas de tabela cujo 1º campo é um caminho entre crases
|
|
383
|
+
for m in re.finditer(r"^\|\s*`([^`]+)`\s*\|\s*(.*?)\s*\|\s*$",
|
|
384
|
+
md_text, re.MULTILINE):
|
|
385
|
+
path, desc = m.group(1), m.group(2)
|
|
386
|
+
# Ignora a linha de cabeçalho/separador acidentalmente casada
|
|
387
|
+
if set(desc) <= {"-", " "}:
|
|
388
|
+
continue
|
|
389
|
+
roles[path] = desc
|
|
390
|
+
|
|
391
|
+
hm = re.search(r"<!-- map:hash (\w+) -->", md_text)
|
|
392
|
+
return roles, (hm.group(1) if hm else None)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def merge_roles(scanned_dirs, existing_roles):
|
|
396
|
+
"""
|
|
397
|
+
O coração do merge: decide a responsabilidade final de cada pasta.
|
|
398
|
+
1. Se o humano escreveu algo (e não é o placeholder) -> preserva.
|
|
399
|
+
2. Senão, se há heurística por nome -> usa.
|
|
400
|
+
3. Senão -> placeholder pendente (o check vai cobrar).
|
|
401
|
+
Retorna ({pasta -> desc}, [pastas documentadas que sumiram do disco]).
|
|
402
|
+
"""
|
|
403
|
+
merged = {}
|
|
404
|
+
scanned_ids = {n["id"] for n in scanned_dirs}
|
|
405
|
+
|
|
406
|
+
for n in scanned_dirs:
|
|
407
|
+
human = existing_roles.get(n["id"], "")
|
|
408
|
+
if human and human != PENDING:
|
|
409
|
+
merged[n["id"]] = human # curadoria humana vence
|
|
410
|
+
elif n["role"]:
|
|
411
|
+
merged[n["id"]] = n["role"] # heurística
|
|
412
|
+
else:
|
|
413
|
+
merged[n["id"]] = PENDING # pendência explícita
|
|
414
|
+
|
|
415
|
+
# Pastas documentadas antes, mas que não existem mais (drift)
|
|
416
|
+
orphans = [p for p in existing_roles
|
|
417
|
+
if p not in scanned_ids and p != "."]
|
|
418
|
+
return merged, orphans
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
# GERAÇÃO DO MARKDOWN (com seções gerenciadas)
|
|
423
|
+
# ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
def render_section(name: str, body_lines) -> str:
|
|
426
|
+
"""Envelopa um bloco de conteúdo nos marcadores de seção gerenciada."""
|
|
427
|
+
return "\n".join([MARK_BEGIN.format(name=name),
|
|
428
|
+
*body_lines,
|
|
429
|
+
MARK_END.format(name=name)])
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def render_stack(stack_info) -> str:
|
|
433
|
+
lines = ["## Stack detectada", ""]
|
|
434
|
+
if stack_info:
|
|
435
|
+
lines += ["| Tecnologia | Evidência | O que é |",
|
|
436
|
+
"|------------|-----------|---------|"]
|
|
437
|
+
lines += [f"| {s} | `{m}` | {d} |" for s, m, d in stack_info]
|
|
438
|
+
else:
|
|
439
|
+
lines.append("_Nenhum marcador de stack encontrado na raiz._")
|
|
440
|
+
return render_section("stack", lines)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def render_tree(tree_lines, hash_value: str) -> str:
|
|
444
|
+
lines = ["## Estrutura de pastas", "",
|
|
445
|
+
MARK_HASH.format(value=hash_value),
|
|
446
|
+
"```", *tree_lines, "```"]
|
|
447
|
+
return render_section("tree", lines)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def render_contract(merged_roles) -> str:
|
|
451
|
+
lines = [
|
|
452
|
+
"## Responsabilidades das pastas",
|
|
453
|
+
"",
|
|
454
|
+
"<!-- Edite as descrições abaixo: elas são PRESERVADAS entre gerações. -->",
|
|
455
|
+
f"<!-- Linhas com {PENDING} são pendências: `check` falha até você preenchê-las. -->",
|
|
456
|
+
"",
|
|
457
|
+
"| Pasta | Responsabilidade |",
|
|
458
|
+
"|-------|------------------|",
|
|
459
|
+
]
|
|
460
|
+
lines += [f"| `{p}` | {d} |" for p, d in sorted(merged_roles.items())]
|
|
461
|
+
return render_section("contract", lines)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def build_new_markdown(root, stack_info, tree_lines, merged_roles, hash_value) -> str:
|
|
465
|
+
"""Template completo — usado só quando o ARCHITECTURE.md ainda não existe."""
|
|
466
|
+
return "\n".join([
|
|
467
|
+
f"# Arquitetura — {root.name}",
|
|
468
|
+
"",
|
|
469
|
+
"> Contrato de arquitetura mantido por `archdrift.py`.",
|
|
470
|
+
"> Seções entre `<!-- map:begin/end -->` são regeneradas; o resto é seu.",
|
|
471
|
+
"",
|
|
472
|
+
render_stack(stack_info),
|
|
473
|
+
"",
|
|
474
|
+
render_tree(tree_lines, hash_value),
|
|
475
|
+
"",
|
|
476
|
+
render_contract(merged_roles),
|
|
477
|
+
"",
|
|
478
|
+
"## Notas",
|
|
479
|
+
"",
|
|
480
|
+
"_Espaço livre: decisões de arquitetura, convenções, ADRs. "
|
|
481
|
+
"O script nunca toca nesta seção._",
|
|
482
|
+
"",
|
|
483
|
+
])
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def replace_section(md_text: str, name: str, new_block: str) -> str:
|
|
487
|
+
"""
|
|
488
|
+
Substitui o conteúdo de uma seção gerenciada existente.
|
|
489
|
+
Se os marcadores não existirem (ex: MD antigo/da v1), o bloco é
|
|
490
|
+
anexado ao final — o usuário reorganiza a posição uma vez, se quiser.
|
|
491
|
+
"""
|
|
492
|
+
begin = re.escape(MARK_BEGIN.format(name=name))
|
|
493
|
+
end = re.escape(MARK_END.format(name=name))
|
|
494
|
+
pattern = re.compile(begin + r".*?" + end, re.DOTALL)
|
|
495
|
+
if pattern.search(md_text):
|
|
496
|
+
# \g<0> não se aplica: substituímos o bloco inteiro pelo novo
|
|
497
|
+
return pattern.sub(lambda _: new_block, md_text)
|
|
498
|
+
return md_text.rstrip() + "\n\n" + new_block + "\n"
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# ---------------------------------------------------------------------------
|
|
502
|
+
# COMANDOS
|
|
503
|
+
# ---------------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
def cmd_generate(root: Path) -> int:
|
|
506
|
+
"""Gera ou atualiza ARCHITECTURE.md + structure.json preservando curadoria."""
|
|
507
|
+
stack_info = detect_stack(root)
|
|
508
|
+
nodes, edges, tree_lines = scan(root)
|
|
509
|
+
cdirs = contract_dirs(nodes)
|
|
510
|
+
h = structure_hash(tree_lines, stack_info)
|
|
511
|
+
|
|
512
|
+
md_path = root / "ARCHITECTURE.md"
|
|
513
|
+
existing_roles, _ = ({}, None)
|
|
514
|
+
if md_path.exists():
|
|
515
|
+
existing_roles, _ = parse_existing(md_path.read_text(encoding="utf-8"))
|
|
516
|
+
|
|
517
|
+
merged, orphans = merge_roles(cdirs, existing_roles)
|
|
518
|
+
|
|
519
|
+
if md_path.exists():
|
|
520
|
+
# MERGE: só as seções gerenciadas mudam; o texto humano fica intacto
|
|
521
|
+
md = md_path.read_text(encoding="utf-8")
|
|
522
|
+
md = replace_section(md, "stack", render_stack(stack_info))
|
|
523
|
+
md = replace_section(md, "tree", render_tree(tree_lines, h))
|
|
524
|
+
md = replace_section(md, "contract", render_contract(merged))
|
|
525
|
+
else:
|
|
526
|
+
md = build_new_markdown(root, stack_info, tree_lines, merged, h)
|
|
527
|
+
|
|
528
|
+
md_path.write_text(md, encoding="utf-8")
|
|
529
|
+
|
|
530
|
+
# Grafo JSON com as responsabilidades FINAIS (pós-merge) aplicadas aos nós
|
|
531
|
+
for n in nodes:
|
|
532
|
+
if n["id"] in merged:
|
|
533
|
+
n["role"] = merged[n["id"]]
|
|
534
|
+
graph = {
|
|
535
|
+
"root": str(root),
|
|
536
|
+
"generated_by": "archdrift.py",
|
|
537
|
+
"hash": h,
|
|
538
|
+
"stack": [{"stack": s, "marker": m, "desc": d} for s, m, d in stack_info],
|
|
539
|
+
"nodes": nodes,
|
|
540
|
+
"edges": edges,
|
|
541
|
+
}
|
|
542
|
+
(root / "structure.json").write_text(
|
|
543
|
+
json.dumps(graph, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
544
|
+
|
|
545
|
+
# Resumo no terminal
|
|
546
|
+
pending = [p for p, d in merged.items() if d == PENDING]
|
|
547
|
+
print(f"✓ {md_path} atualizado (hash {h})")
|
|
548
|
+
print(f"✓ {root / 'structure.json'} gerado ({len(edges)} arestas)")
|
|
549
|
+
if stack_info:
|
|
550
|
+
print(f"✓ Stack: {', '.join(sorted({s for s, _, _ in stack_info}))}")
|
|
551
|
+
if pending:
|
|
552
|
+
print(f"⚠ {len(pending)} pasta(s) sem responsabilidade declarada:")
|
|
553
|
+
for p in pending:
|
|
554
|
+
print(f" - {p}")
|
|
555
|
+
print(" Edite a tabela no ARCHITECTURE.md. (`check` falha até resolver.)")
|
|
556
|
+
if orphans:
|
|
557
|
+
print(f"⚠ {len(orphans)} pasta(s) documentada(s) que não existem mais "
|
|
558
|
+
f"(removidas da tabela): {', '.join(orphans)}")
|
|
559
|
+
return 0
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def cmd_check(root: Path) -> int:
|
|
563
|
+
"""
|
|
564
|
+
Valida o contrato. Pensado para CI:
|
|
565
|
+
python archdrift.py check . # exit 1 em caso de drift
|
|
566
|
+
"""
|
|
567
|
+
md_path = root / "ARCHITECTURE.md"
|
|
568
|
+
if not md_path.exists():
|
|
569
|
+
print("✗ ARCHITECTURE.md não existe. Rode `generate` primeiro.")
|
|
570
|
+
return 1
|
|
571
|
+
|
|
572
|
+
stack_info = detect_stack(root)
|
|
573
|
+
nodes, _, tree_lines = scan(root)
|
|
574
|
+
cdirs = contract_dirs(nodes)
|
|
575
|
+
current_hash = structure_hash(tree_lines, stack_info)
|
|
576
|
+
|
|
577
|
+
roles, saved_hash = parse_existing(md_path.read_text(encoding="utf-8"))
|
|
578
|
+
scanned_ids = {n["id"] for n in cdirs}
|
|
579
|
+
|
|
580
|
+
problems = []
|
|
581
|
+
|
|
582
|
+
# (a) Estrutura mudou e ninguém regenerou o mapa
|
|
583
|
+
if saved_hash is None:
|
|
584
|
+
problems.append("Mapa sem hash (formato antigo?). Rode `generate` para migrar.")
|
|
585
|
+
elif saved_hash != current_hash:
|
|
586
|
+
problems.append(
|
|
587
|
+
f"Estrutura em disco mudou desde a última geração "
|
|
588
|
+
f"(hash {saved_hash} -> {current_hash}). Rode `generate`.")
|
|
589
|
+
|
|
590
|
+
# (b) Pastas do contrato sem responsabilidade declarada
|
|
591
|
+
for n in cdirs:
|
|
592
|
+
desc = roles.get(n["id"], "")
|
|
593
|
+
if not desc or desc == PENDING:
|
|
594
|
+
problems.append(f"Pasta sem responsabilidade declarada: `{n['id']}`")
|
|
595
|
+
|
|
596
|
+
# (c) Pastas documentadas que sumiram do disco
|
|
597
|
+
for p in roles:
|
|
598
|
+
if p != "." and p not in scanned_ids:
|
|
599
|
+
problems.append(f"Pasta documentada mas inexistente em disco: `{p}`")
|
|
600
|
+
|
|
601
|
+
if problems:
|
|
602
|
+
print(f"✗ Drift de arquitetura detectado ({len(problems)} problema(s)):\n")
|
|
603
|
+
for p in problems:
|
|
604
|
+
print(f" - {p}")
|
|
605
|
+
print("\nCorrija editando o ARCHITECTURE.md ou rode `generate` e revise o diff.")
|
|
606
|
+
return 1
|
|
607
|
+
|
|
608
|
+
print(f"✓ Arquitetura em conformidade (hash {current_hash}, "
|
|
609
|
+
f"{len(cdirs)} pastas documentadas).")
|
|
610
|
+
return 0
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# ---------------------------------------------------------------------------
|
|
614
|
+
# CLI
|
|
615
|
+
# ---------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
def main():
|
|
618
|
+
parser = argparse.ArgumentParser(
|
|
619
|
+
description="Contrato de arquitetura para agentes de código.")
|
|
620
|
+
sub = parser.add_subparsers(dest="command")
|
|
621
|
+
|
|
622
|
+
p_gen = sub.add_parser("generate", help="gera/atualiza preservando curadoria")
|
|
623
|
+
p_gen.add_argument("path", nargs="?", default=".")
|
|
624
|
+
|
|
625
|
+
p_chk = sub.add_parser("check", help="valida drift (exit 1 se divergiu)")
|
|
626
|
+
p_chk.add_argument("path", nargs="?", default=".")
|
|
627
|
+
|
|
628
|
+
# Retrocompatibilidade: `python archdrift.py <path>` == generate
|
|
629
|
+
argv = sys.argv[1:]
|
|
630
|
+
if argv and argv[0] not in ("generate", "check", "-h", "--help"):
|
|
631
|
+
argv = ["generate", *argv]
|
|
632
|
+
|
|
633
|
+
args = parser.parse_args(argv)
|
|
634
|
+
if not args.command:
|
|
635
|
+
parser.print_help()
|
|
636
|
+
sys.exit(1)
|
|
637
|
+
|
|
638
|
+
root = Path(args.path)
|
|
639
|
+
if not root.is_dir():
|
|
640
|
+
print(f"Caminho inválido ou não é diretório: {root}")
|
|
641
|
+
sys.exit(1)
|
|
642
|
+
|
|
643
|
+
if args.command == "generate":
|
|
644
|
+
sys.exit(cmd_generate(root.resolve()))
|
|
645
|
+
else:
|
|
646
|
+
sys.exit(cmd_check(root.resolve()))
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
if __name__ == "__main__":
|
|
650
|
+
main()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: archdrift
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Architecture contract for AI coding agents: a curated, human-edited ARCHITECTURE.md with drift detection in CI.
|
|
5
|
+
Project-URL: Homepage, https://github.com/EvertDeveloper/archdrift
|
|
6
|
+
Project-URL: Issues, https://github.com/EvertDeveloper/archdrift/issues
|
|
7
|
+
Author: Everton Godoi
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents-md,ai-agents,architecture,claude-code,context-engineering,cursor,documentation,drift-detection
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: build; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: twine; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# archdrift
|
|
25
|
+
|
|
26
|
+
**Architecture contract for AI coding agents.** A curated `ARCHITECTURE.md` that agents read, humans edit, and CI enforces — so your architecture docs never rot.
|
|
27
|
+
|
|
28
|
+
[](https://github.com/SEU_USUARIO/archdrift/actions)
|
|
29
|
+
[](https://pypi.org/project/archdrift/)
|
|
30
|
+

|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## The problem
|
|
36
|
+
|
|
37
|
+
Tools like Repomix and CodeGraph are **descriptive**: they read your code and tell agents what exists. But no tool captures what your architecture is *supposed* to be — the intent behind it. So agents guess, `ARCHITECTURE.md` files rot within weeks, and nobody notices until an agent (or a new dev) builds on top of stale assumptions.
|
|
38
|
+
|
|
39
|
+
## The idea
|
|
40
|
+
|
|
41
|
+
`archdrift` makes your `ARCHITECTURE.md` **normative**:
|
|
42
|
+
|
|
43
|
+
1. **Generated map** — folder tree, detected stack, responsibilities table. Curated for token efficiency: code files listed, assets collapsed.
|
|
44
|
+
2. **Human curation that survives** — edit any description in the responsibilities table, add ADRs and notes anywhere outside the managed sections. Regeneration **never** destroys your edits.
|
|
45
|
+
3. **Drift detection in CI** — `archdrift check` exits 1 when reality diverges from the documented contract. The doc can't rot, because rot breaks the build.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
┌─────────────┐ generate ┌─────────────────┐ read ┌────────────┐
|
|
49
|
+
│ Filesystem │ ────────────▶ │ ARCHITECTURE.md │ ────────▶ │ AI agent │
|
|
50
|
+
└─────────────┘ │ (you curate) │ └────────────┘
|
|
51
|
+
▲ └─────────────────┘
|
|
52
|
+
│ │ check (CI)
|
|
53
|
+
└───────── drift? exit 1 ◀──────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uvx archdrift . # or: pipx run archdrift .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This generates `ARCHITECTURE.md` (the contract) and `structure.json` (the graph, for tooling). Folders without a known role get marked `_(preencher)_` — fill them in, they're preserved forever.
|
|
63
|
+
|
|
64
|
+
Then enforce it in CI:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
# .github/workflows/ci.yml
|
|
68
|
+
- name: Architecture drift check
|
|
69
|
+
run: uvx archdrift check .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Now every PR that adds an undocumented folder, deletes a documented one, or changes structure without regenerating the map **fails the build** until someone updates the contract.
|
|
73
|
+
|
|
74
|
+
## How merging works
|
|
75
|
+
|
|
76
|
+
Managed sections are delimited by markers:
|
|
77
|
+
|
|
78
|
+
```markdown
|
|
79
|
+
<!-- map:begin tree -->
|
|
80
|
+
...regenerated by the tool...
|
|
81
|
+
<!-- map:end tree -->
|
|
82
|
+
|
|
83
|
+
## ADR-001 ← yours, never touched
|
|
84
|
+
|
|
85
|
+
<!-- map:begin contract -->
|
|
86
|
+
| Folder | Responsibility |
|
|
87
|
+
| `src/models` | Prisma entities — NEVER import from controllers | ← your edit, preserved
|
|
88
|
+
<!-- map:end contract -->
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Priority for each folder's description: **your edit** > name-based heuristic > pending placeholder.
|
|
92
|
+
|
|
93
|
+
## Why not just use a repo packer / knowledge graph?
|
|
94
|
+
|
|
95
|
+
Use both. Repomix, CodeGraph, aider's repo-map are excellent at telling agents *what the code is*. `archdrift` covers the piece they can't infer: *what the architecture is meant to be* — and keeps that promise honest via CI. It's `dependency-cruiser` energy, rethought for the agent era, in a single zero-dependency Python file.
|
|
96
|
+
|
|
97
|
+
## Roadmap
|
|
98
|
+
|
|
99
|
+
- [x] Managed sections + merge (human curation survives regeneration)
|
|
100
|
+
- [x] `check` mode — drift detection with meaningful exit codes for CI
|
|
101
|
+
- [x] Stack detection, token-efficient tree (type-aware truncation)
|
|
102
|
+
- [ ] `AGENTS.md` integration (pointer section into the emerging convention)
|
|
103
|
+
- [ ] Declarative rules (`src/controllers must-not-import src/repositories`)
|
|
104
|
+
- [ ] MCP server mode
|
|
105
|
+
- [ ] Semantic graph: declared dependencies vs. real imports — the diff is the product
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install -e ".[dev]"
|
|
111
|
+
pytest -v
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
MIT licensed. Issues and PRs welcome.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
archdrift/__init__.py,sha256=nsNQ-gNwMTbCJXQE5hxuAZsi8xB8L3GNNXT077Y8bEQ,401
|
|
2
|
+
archdrift/__main__.py,sha256=0-ZTHY4-siqvXfUQB4gqCctmTfQRRuMp7uGG6nmpoSk,139
|
|
3
|
+
archdrift/cli.py,sha256=wtN4M7eHuqrUpt8Awtje7l4vJZKq_XQLK5yQNtYMlzU,26459
|
|
4
|
+
archdrift-0.1.0.dist-info/METADATA,sha256=JaVW_jTiA9yznZvyZS2SmZ_sDPs1DMkIaJAbKH0bMGk,5179
|
|
5
|
+
archdrift-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
6
|
+
archdrift-0.1.0.dist-info/entry_points.txt,sha256=KOT9EKPWUJt-hgvseVUHKgetqeAy_AZpvRsLcJoIYk8,49
|
|
7
|
+
archdrift-0.1.0.dist-info/licenses/LICENSE,sha256=wk-NfONivtJfZ1aaVCIbkAEC3scE2gJRRnPus141BqA,1065
|
|
8
|
+
archdrift-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SEU_NOME
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|