kostria 0.1.0__tar.gz
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.
- kostria-0.1.0/ARCHITECTURE.md +274 -0
- kostria-0.1.0/CHANGELOG.md +80 -0
- kostria-0.1.0/LICENSE +21 -0
- kostria-0.1.0/MANIFEST.in +16 -0
- kostria-0.1.0/PKG-INFO +198 -0
- kostria-0.1.0/README.md +167 -0
- kostria-0.1.0/kostria/__init__.py +3 -0
- kostria-0.1.0/kostria/analysis.py +312 -0
- kostria-0.1.0/kostria/attribution.py +359 -0
- kostria-0.1.0/kostria/badge.py +56 -0
- kostria-0.1.0/kostria/cache.py +112 -0
- kostria-0.1.0/kostria/cli.py +616 -0
- kostria-0.1.0/kostria/collectors/__init__.py +62 -0
- kostria-0.1.0/kostria/collectors/claude_code.py +233 -0
- kostria-0.1.0/kostria/collectors/codex.py +82 -0
- kostria-0.1.0/kostria/collectors/opencode.py +88 -0
- kostria-0.1.0/kostria/compare.py +89 -0
- kostria-0.1.0/kostria/config.py +159 -0
- kostria-0.1.0/kostria/doctor.py +209 -0
- kostria-0.1.0/kostria/landing/index.html +648 -0
- kostria-0.1.0/kostria/landing/vendor/ScrollTrigger.min.js +11 -0
- kostria-0.1.0/kostria/landing/vendor/fonts/Inter.woff2 +0 -0
- kostria-0.1.0/kostria/landing/vendor/fonts/SourceSerif4.woff2 +0 -0
- kostria-0.1.0/kostria/landing/vendor/gsap.min.js +11 -0
- kostria-0.1.0/kostria/landing/vendor/three.core.js +60007 -0
- kostria-0.1.0/kostria/landing/vendor/three.module.js +19610 -0
- kostria-0.1.0/kostria/markdown.py +146 -0
- kostria-0.1.0/kostria/period.py +103 -0
- kostria-0.1.0/kostria/pricing.py +275 -0
- kostria-0.1.0/kostria/render.py +678 -0
- kostria-0.1.0/kostria/report.py +307 -0
- kostria-0.1.0/kostria/sync.py +226 -0
- kostria-0.1.0/kostria/telemetry.py +101 -0
- kostria-0.1.0/kostria/watch.py +73 -0
- kostria-0.1.0/kostria/web/dashboard.html +1496 -0
- kostria-0.1.0/kostria/web/vendor/fonts/Inter.woff2 +0 -0
- kostria-0.1.0/kostria/web/vendor/fonts/SourceSerif4.woff2 +0 -0
- kostria-0.1.0/kostria/web/vendor/gsap.min.js +11 -0
- kostria-0.1.0/kostria.egg-info/PKG-INFO +198 -0
- kostria-0.1.0/kostria.egg-info/SOURCES.txt +71 -0
- kostria-0.1.0/kostria.egg-info/dependency_links.txt +1 -0
- kostria-0.1.0/kostria.egg-info/entry_points.txt +2 -0
- kostria-0.1.0/kostria.egg-info/requires.txt +4 -0
- kostria-0.1.0/kostria.egg-info/top_level.txt +1 -0
- kostria-0.1.0/pyproject.toml +77 -0
- kostria-0.1.0/setup.cfg +4 -0
- kostria-0.1.0/tests/test_analysis.py +178 -0
- kostria-0.1.0/tests/test_attribution.py +255 -0
- kostria-0.1.0/tests/test_badge.py +56 -0
- kostria-0.1.0/tests/test_cache.py +136 -0
- kostria-0.1.0/tests/test_claude_code_collector.py +114 -0
- kostria-0.1.0/tests/test_cli.py +172 -0
- kostria-0.1.0/tests/test_codex_collector.py +107 -0
- kostria-0.1.0/tests/test_compare.py +89 -0
- kostria-0.1.0/tests/test_config.py +127 -0
- kostria-0.1.0/tests/test_contrast.py +130 -0
- kostria-0.1.0/tests/test_doctor.py +97 -0
- kostria-0.1.0/tests/test_e2e.py +260 -0
- kostria-0.1.0/tests/test_init.py +121 -0
- kostria-0.1.0/tests/test_markdown.py +75 -0
- kostria-0.1.0/tests/test_opencode_collector.py +102 -0
- kostria-0.1.0/tests/test_period.py +93 -0
- kostria-0.1.0/tests/test_pricing.py +212 -0
- kostria-0.1.0/tests/test_pricing_openai.py +81 -0
- kostria-0.1.0/tests/test_pricing_values.py +130 -0
- kostria-0.1.0/tests/test_project_budgets.py +257 -0
- kostria-0.1.0/tests/test_render.py +151 -0
- kostria-0.1.0/tests/test_report.py +252 -0
- kostria-0.1.0/tests/test_robustness.py +141 -0
- kostria-0.1.0/tests/test_serve_cache.py +77 -0
- kostria-0.1.0/tests/test_signals.py +84 -0
- kostria-0.1.0/tests/test_sync_cli.py +325 -0
- kostria-0.1.0/tests/test_watch.py +108 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Arquitetura do kostria
|
|
2
|
+
|
|
3
|
+
Documento para quem for mexer no código. Baseado no que o código faz de
|
|
4
|
+
fato; se algo aqui divergir da implementação, a implementação manda.
|
|
5
|
+
|
|
6
|
+
## 1. Visão geral
|
|
7
|
+
|
|
8
|
+
O `kostria` é um pacote Python de auditoria **local** de custo de IA. Ele lê
|
|
9
|
+
os registros que as ferramentas já gravam na máquina (Claude Code, OpenCode,
|
|
10
|
+
Codex CLI), calcula ou aceita o custo de cada sessão e atribui esse gasto a
|
|
11
|
+
projeto, conta, modelo e — quando há histórico git — a área/feature.
|
|
12
|
+
|
|
13
|
+
Existe porque o total sozinho não decide nada: o valor do produto está na
|
|
14
|
+
**distribuição** (qual projeto, qual conta, com que evidência). Tudo roda
|
|
15
|
+
offline, sem dependência externa e sem rede (`pyproject.toml` declara
|
|
16
|
+
`dependencies = []`).
|
|
17
|
+
|
|
18
|
+
## 2. Fluxo de dados
|
|
19
|
+
|
|
20
|
+
A unidade de troca entre camadas é a `Session` (`collectors/__init__.py`).
|
|
21
|
+
A sessão é o menor recorte em que as ferramentas concordam: Claude Code
|
|
22
|
+
registra por mensagem; OpenCode e Codex agregam por sessão.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
logs locais Session[] dict (relatório)
|
|
26
|
+
(~/.claude, sqlite, + meta por agregado em
|
|
27
|
+
~/.codex/...) coletor várias dimensões
|
|
28
|
+
| | |
|
|
29
|
+
v v v
|
|
30
|
+
+-----------+ +-------------+ +-------------+ +----------+
|
|
31
|
+
| coletores |-->| atribuição |-->| report.build|-->| render |
|
|
32
|
+
| collect() | | split_ | | + analysis | | html/md |
|
|
33
|
+
| | | session_cost| | + features | | csv/json |
|
|
34
|
+
+-----------+ +-------------+ +-------------+ +----------+
|
|
35
|
+
^ ^
|
|
36
|
+
| |
|
|
37
|
+
cache.py (só cli.py (scan,
|
|
38
|
+
claude_code hoje) report, serve,
|
|
39
|
+
doctor, ...)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Passos concretos:
|
|
43
|
+
|
|
44
|
+
1. **Coleta** — `collect_all()` chama cada coletor. Falha de um não derruba
|
|
45
|
+
os outros; o erro vira entrada em `meta[nome]`.
|
|
46
|
+
2. **Período** (opcional) — `period.filter_sessions` recorta por
|
|
47
|
+
`started_at` e pela fração de edições dentro da janela (sessão longa
|
|
48
|
+
que atravessa o mês é rateada e o relatório declara a aproximação).
|
|
49
|
+
3. **Atribuição** — para cada sessão, `split_session_cost` rateia custo e
|
|
50
|
+
tokens entre projetos e devolve o método usado (`edited` / `read` /
|
|
51
|
+
`cwd`).
|
|
52
|
+
4. **Agregação** — `report.build` monta um único dict: totais, buckets
|
|
53
|
+
(`by_project`, `by_tool`, `by_vendor`, `by_account`, `by_model`,
|
|
54
|
+
`by_provenance`, `by_attribution`), sinais de `analysis.py`,
|
|
55
|
+
opcionalmente custo por feature via `features_for_repo` e status de
|
|
56
|
+
orçamento. Comparação entre períodos (`compare.compare_reports`) é
|
|
57
|
+
anexada depois pela CLI quando `--compare` está ativo.
|
|
58
|
+
5. **Render** — a CLI escolhe o formato. O dict é a fonte única para
|
|
59
|
+
terminal, HTML (`render.py`), Markdown, CSV, JSON e o dashboard
|
|
60
|
+
(`serve` + `web/dashboard.html`).
|
|
61
|
+
|
|
62
|
+
Separar montagem (`report`) de apresentação (`render` / `markdown` / CSV)
|
|
63
|
+
existe de propósito: CLI, dashboard e testes consomem a mesma estrutura.
|
|
64
|
+
|
|
65
|
+
## 3. Por que a atribuição segue arquivos, não o `cwd`
|
|
66
|
+
|
|
67
|
+
Documentado no próprio `attribution.py`:
|
|
68
|
+
|
|
69
|
+
> O diretório da sessão mente. O agente edita vários repositórios a partir
|
|
70
|
+
> de um cwd só (working dirs adicionais, worktrees, pastas temporárias).
|
|
71
|
+
> Medir por cwd joga o custo no projeto errado. A atribuição segue os
|
|
72
|
+
> ARQUIVOS efetivamente editados.
|
|
73
|
+
|
|
74
|
+
Cascata de evidência em `split_session_cost`, do mais forte ao mais fraco
|
|
75
|
+
(`ATTRIBUTION`):
|
|
76
|
+
|
|
77
|
+
| método | evidência |
|
|
78
|
+
|----------|------------------------------------------------|
|
|
79
|
+
| `edited` | paths em `session.edits` (escrita = entrega) |
|
|
80
|
+
| `read` | paths em `session.reads` (leitura = contexto) |
|
|
81
|
+
| `cwd` | diretório declarado, sem arquivo tocado |
|
|
82
|
+
|
|
83
|
+
O rateio é uniforme entre os projetos encontrados nos paths (custo/N,
|
|
84
|
+
tokens//N). O Claude Code popula `edits` e `reads` a partir das tool calls
|
|
85
|
+
(`Edit`/`Write`/… e `Read`/`Grep`/`Glob`/…). O comentário no coletor é
|
|
86
|
+
explícito: sem leituras, toda sessão de investigação cairia no diretório
|
|
87
|
+
declarado — que mente.
|
|
88
|
+
|
|
89
|
+
`ProjectResolver` resolve path → nome de projeto com, nesta ordem:
|
|
90
|
+
índice de worktrees (`git worktree list` + convenção `<repo>-wt`),
|
|
91
|
+
projeto codificado em scratch de sessão (`-home-user-code-app`),
|
|
92
|
+
`git rev-parse --git-common-dir`, primeiro nível do workspace, ou basename.
|
|
93
|
+
Worktree não é outro projeto; scratch/tmp/UUID viram rótulos genéricos.
|
|
94
|
+
|
|
95
|
+
Dentro de um repo, a unidade fina de feature é o **commit** (não o PR):
|
|
96
|
+
`features_for_repo` liga cada edição ao primeiro commit posterior que tocou
|
|
97
|
+
o arquivo e agrega por escopo convencional (`feat(escopo): …`) e por ticket
|
|
98
|
+
no subject/body.
|
|
99
|
+
|
|
100
|
+
## 4. Proveniência: nenhum número sem classificação
|
|
101
|
+
|
|
102
|
+
Regra em `pricing.py`:
|
|
103
|
+
|
|
104
|
+
> Nenhum número sai daqui sem `provenance`. Preço que não pode ser
|
|
105
|
+
> verificado devolve custo `None`, nunca uma estimativa silenciosa.
|
|
106
|
+
|
|
107
|
+
Camadas em `PROVENANCE`:
|
|
108
|
+
|
|
109
|
+
| valor | significado |
|
|
110
|
+
|-------------------|----------------------------------------------------------|
|
|
111
|
+
| `measured` | tokens do log + tabela de preço pública |
|
|
112
|
+
| `self_reported` | custo calculado pela própria ferramenta |
|
|
113
|
+
| `tokens_only` | tokens medidos, sem tabela de preço verificada |
|
|
114
|
+
| `assumed` | preço de um modelo **assumido pelo operador**, não no log|
|
|
115
|
+
| `uninstrumented` | a ferramenta não registra consumo (legenda; uso futuro) |
|
|
116
|
+
|
|
117
|
+
Como cada coletor preenche isso hoje:
|
|
118
|
+
|
|
119
|
+
- **Claude Code** — cada sessão com tokens sai com `provenance="measured"`.
|
|
120
|
+
Por evento, `anthropic_usage_cost` precifica se o modelo está na tabela;
|
|
121
|
+
modelo desconhecido incrementa `meta["unpriced"]` e não soma USD (os
|
|
122
|
+
tokens do evento ainda entram no total da sessão).
|
|
123
|
+
- **OpenCode** — lê o custo do SQLite → `self_reported` (vários
|
|
124
|
+
fornecedores; não reaplicamos tabela própria).
|
|
125
|
+
- **Codex** — `openai_usage_cost`. Sem breakdown input/output, ou modelo
|
|
126
|
+
desconhecido → `Cost(None, tokens, "tokens_only")`. Com
|
|
127
|
+
`--assume-codex-model`, se precificar, a sessão vira `assumed`.
|
|
128
|
+
|
|
129
|
+
No relatório, `priced_cost` soma só `measured` + `self_reported`. A
|
|
130
|
+
**cobertura** é sobre tokens (`priced_tokens / total_tokens`), não sobre
|
|
131
|
+
custo: custo não apurado entra como 0 e faria cobertura por USD parecer
|
|
132
|
+
sempre 100%. O doctor ainda exige que `tokens_only` tenha custo ~0.
|
|
133
|
+
|
|
134
|
+
## 5. Cache incremental (`cache.py`)
|
|
135
|
+
|
|
136
|
+
Varrer o histórico inteiro a cada execução é desperdício: sessão
|
|
137
|
+
encerrada não muda. O cache guarda o resultado por **arquivo de origem**,
|
|
138
|
+
com chave `mtime_ns:size`. Fica em `~/.cache/kostria/` (ou `$XDG_CACHE_HOME`).
|
|
139
|
+
Apagar a pasta é sempre seguro. `VERSION` no módulo invalida o formato
|
|
140
|
+
inteiro quando o cálculo muda. Gravação é atômica (`.tmp` + `os.replace`).
|
|
141
|
+
|
|
142
|
+
Hoje só o coletor do Claude Code usa `SessionCache` (é quem varre a árvore
|
|
143
|
+
de `*.jsonl` sob cada config dir). Dedupe por id de mensagem continua
|
|
144
|
+
valendo mesmo em hit de cache (retomada/compactação copia mensagens
|
|
145
|
+
entre arquivos).
|
|
146
|
+
|
|
147
|
+
### A armadilha mutation-on-read
|
|
148
|
+
|
|
149
|
+
`_from_dict` precisa extrair `edits`/`reads` do dict serializado. A versão
|
|
150
|
+
bugada fazia `d.pop("edits")` **no dicionário ainda referenciado por
|
|
151
|
+
`self._data`**. Consequência, documentada no próprio código:
|
|
152
|
+
|
|
153
|
+
> Ler o cache destruía o cache, e a atribuição por arquivo morria em
|
|
154
|
+
> silêncio a partir da terceira execução (o custo total continuava certo).
|
|
155
|
+
|
|
156
|
+
A correção é copiar antes de mutar: `d = dict(d)` e só então `pop`.
|
|
157
|
+
|
|
158
|
+
### Por que há teste de regressão específico
|
|
159
|
+
|
|
160
|
+
`tests/test_cache.py`, classe `TestCacheSurvivesReadWriteCycles`:
|
|
161
|
+
|
|
162
|
+
- `test_edits_survive_three_cycles` — put → save → get/save × 3; as
|
|
163
|
+
edições têm que continuar lá.
|
|
164
|
+
- `test_read_does_not_mutate_internal_state` — após `get`, a chave
|
|
165
|
+
`"edits"` ainda existe em `c._data[...]["sessions"][0]`.
|
|
166
|
+
|
|
167
|
+
O bug era invisível para qualquer asserção de total: o USD batia, a
|
|
168
|
+
distribuição por projeto não. Por isso o teste ataca o estado interno e o
|
|
169
|
+
ciclo de regravação, não só o round-trip feliz.
|
|
170
|
+
|
|
171
|
+
## 6. `doctor.py`
|
|
172
|
+
|
|
173
|
+
Nasceu de um incidente real (descrito no docstring do módulo): um bug de
|
|
174
|
+
cache apagou as edições de todas as sessões e derrubou a atribuição por
|
|
175
|
+
arquivo, enquanto o **custo total continuou correto**. Conferir o total
|
|
176
|
+
não prova distribuição.
|
|
177
|
+
|
|
178
|
+
A verificação compara **rotas independentes** e invariantes, por exemplo:
|
|
179
|
+
|
|
180
|
+
- coleta sem cache × com cache (total e `by_project`);
|
|
181
|
+
- contagem de edições preservada;
|
|
182
|
+
- soma de `by_project` / `by_account` / `by_attribution` fecha com o total;
|
|
183
|
+
- toda chave de proveniência tem legenda; `tokens_only` não inventa USD;
|
|
184
|
+
- o relatório serializa como JSON estrito (`allow_nan=False`);
|
|
185
|
+
- modo anônimo não vaza e-mail nem `/home/...`;
|
|
186
|
+
- avisos (não críticos): fração com evidência de arquivo, cobertura de
|
|
187
|
+
preço, custo só-cwd, dedupe ativa.
|
|
188
|
+
|
|
189
|
+
### Disco em movimento
|
|
190
|
+
|
|
191
|
+
`_log_fingerprint` captura `(mtime, size)` de todo jsonl do Claude Code
|
|
192
|
+
antes e depois das coletas. Se os logs mudaram durante o doctor — típico
|
|
193
|
+
quando a sessão que roda o doctor está gravando no próprio projeto
|
|
194
|
+
auditado — divergência entre fresco e cache é **inconclusive**, não
|
|
195
|
+
corrupção. Nesses casos o check é marcado `skipped` em vez de falha, para
|
|
196
|
+
não gritar lobo.
|
|
197
|
+
|
|
198
|
+
## 7. Convenções do projeto
|
|
199
|
+
|
|
200
|
+
- **Sem dependência de terceiros** — só stdlib. Python `>=3.10`
|
|
201
|
+
(`tomllib` para config exige 3.11+; abaixo disso a config é ignorada com
|
|
202
|
+
aviso).
|
|
203
|
+
- **Sem rede na apuração** — lê disco local. `serve` escuta só em
|
|
204
|
+
`127.0.0.1` de propósito (relatório tem dado sensível).
|
|
205
|
+
- **Config** — `./.kostria.toml` ou `~/.config/kostria/config.toml`; projeto
|
|
206
|
+
sobrepõe usuário; CLI tem a última palavra (`config.apply` só preenche
|
|
207
|
+
buracos).
|
|
208
|
+
- **Testes** — `tests/`, `unittest`.
|
|
209
|
+
- **Rodar a suite:**
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
python3 -m unittest discover -s tests
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- **Tom do código** — comentários explicam *por que*, não o óbvio; falha
|
|
216
|
+
de dado de terceiro não pode derrubar a apuração inteira (`_int`
|
|
217
|
+
tolerante, coletor que devolve `available: False`, etc.).
|
|
218
|
+
- **JSON estrito** — nunca emitir `NaN`/`Infinity` (já quebrou o dashboard
|
|
219
|
+
em silêncio; ver `compare._delta` e `cli._json_dump`).
|
|
220
|
+
|
|
221
|
+
## 8. Como adicionar um coletor
|
|
222
|
+
|
|
223
|
+
Referência: `collectors/__init__.py` e os três coletores existentes.
|
|
224
|
+
|
|
225
|
+
1. Crie `kostria/collectors/<ferramenta>.py` com:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
def collect(... ) -> tuple[list[Session], dict]:
|
|
229
|
+
...
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- Devolve `(lista_de_Session, meta)`.
|
|
233
|
+
- Se a ferramenta não estiver instalada / sem dados: `([], {"available": False})`
|
|
234
|
+
(ou equivalente), sem levantar.
|
|
235
|
+
- Exceção não tratada ainda é engolida por `collect_all` e vira
|
|
236
|
+
`meta[nome] = {"error": ...}`, mas o ideal é falhar de forma controlada.
|
|
237
|
+
|
|
238
|
+
2. Preencha `Session` com o que for confiável:
|
|
239
|
+
|
|
240
|
+
- `tool`, `account`, `vendor`, `tokens`, `provenance` (obrigatório no
|
|
241
|
+
sentido de contrato — ver `PROVENANCE`);
|
|
242
|
+
- `cost` como `float` ou `None` (nunca chute silencioso);
|
|
243
|
+
- `edits` / `reads` como `list[Edit]` se a ferramenta expuser paths —
|
|
244
|
+
isso alimenta atribuição forte; se não expuser, deixe vazio e a
|
|
245
|
+
sessão cairá em `cwd` (como OpenCode/Codex hoje);
|
|
246
|
+
- `models`, `token_mix`, `started_at`, `source`, `cwd` conforme existir.
|
|
247
|
+
|
|
248
|
+
3. Registre em `collect_all`:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
from . import claude_code, codex, opencode, minha_ferramenta
|
|
252
|
+
...
|
|
253
|
+
for name, mod in (..., ("minha_ferramenta", minha_ferramenta)):
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Passe kwargs específicos só quando o coletor os aceitar (padrão atual:
|
|
257
|
+
`use_cache` só no Claude Code, `assume_model` só no Codex).
|
|
258
|
+
|
|
259
|
+
4. Precificação:
|
|
260
|
+
|
|
261
|
+
- Se houver tabela pública estável, coloque em `pricing.py` e devolva
|
|
262
|
+
`Cost` com `measured` ou `tokens_only`.
|
|
263
|
+
- Se a ferramenta já calcula o custo, use `self_reported` e documente
|
|
264
|
+
no docstring do coletor (caso OpenCode).
|
|
265
|
+
- Se o operador puder forçar premissa, use `assumed` e exponha flag na
|
|
266
|
+
CLI/config (caso Codex).
|
|
267
|
+
|
|
268
|
+
5. Testes em `tests/` com fixtures mínimas (arquivo/sqlite temporário),
|
|
269
|
+
cobrindo: ausência da ferramenta, parse tolerante, proveniência
|
|
270
|
+
correta, e — se houver cache — ciclo get/save sem mutar estado.
|
|
271
|
+
|
|
272
|
+
Não é necessário implementar cache no coletor novo; só vale a pena se a
|
|
273
|
+
varredura for cara e o artefato for imutável por `mtime`/tamanho como no
|
|
274
|
+
Claude Code.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Tudo o que mudou no Kostria, agrupado por sessão de trabalho e na ordem em que foi construído.
|
|
4
|
+
|
|
5
|
+
## [2026-07-25]
|
|
6
|
+
|
|
7
|
+
### Núcleo do produto
|
|
8
|
+
|
|
9
|
+
- Coletores multi-conta para Claude Code, OpenCode (SQLite) e Codex.
|
|
10
|
+
- Custo por feature: arquivo → commit → área/item do board.
|
|
11
|
+
- Atribuição pelos arquivos editados, não pelo diretório de trabalho atual.
|
|
12
|
+
- Worktree resolvido para o repositório principal via `git-common-dir`.
|
|
13
|
+
- Sinais iniciais: reprocessamento, retrabalho, eficiência por modelo e projeção.
|
|
14
|
+
- CLI `scan`, `report` e `serve` com relatório HTML.
|
|
15
|
+
- Exportação CSV para o financeiro (152 linhas por dimensão).
|
|
16
|
+
- README, landing e testes iniciais; `pyproject` com descoberta explícita de pacotes para evitar falha no `pip install -e`.
|
|
17
|
+
- Recuperação do projeto de origem a partir do scratch de sessão do Claude Code, decodificando o caminho codificado em `/tmp/claude-<uid>/<cwd-codificado>/<sessão>/` e casando contra os projetos existentes.
|
|
18
|
+
- `--repo` repetível e descoberta automática de repositórios git entre os projetos que mais gastaram.
|
|
19
|
+
- Arquivo de configuração `.kostria.toml` para guardar padrões de workspace, repo, budget, período e modelo assumido; chave desconhecida ou tipo errado viram aviso no stderr, e TOML quebrado não derruba o comando.
|
|
20
|
+
- `--format md` para resumo colável em e-mail ou ticket.
|
|
21
|
+
- `kostria init` detecta pastas de trabalho plausíveis e já escreve o `.kostria.toml` inicial.
|
|
22
|
+
- CSV mais completo incluindo as dimensões `by_attribution` e `project_budgets`.
|
|
23
|
+
- Atribuição em cascata de evidência: escreveu > leu > diretório. Read, NotebookRead, Grep e Glob passam a trazer o caminho como evidência secundária.
|
|
24
|
+
- Sinais novos: dias fora do padrão, sessões mais caras e perfil semanal.
|
|
25
|
+
- `kostria watch` para vigia ao vivo no terminal.
|
|
26
|
+
- Selo SVG local (`kostria badge`) sem depender de serviço externo.
|
|
27
|
+
|
|
28
|
+
### Recorte temporal e comparação
|
|
29
|
+
|
|
30
|
+
- `--since`/`--until` aceitam AAAA-MM-DD, `30d`, `2w`, `6m`, `1y`, `hoje` e `ontem`.
|
|
31
|
+
- Sessão que atravessa a fronteira do período entra rateada pela fração de edições dentro da janela, sem mutar a sessão original.
|
|
32
|
+
- `--budget` compara a projeção mensal com o limite e alerta se estoura.
|
|
33
|
+
- `--compare` mede a janela imediatamente anterior, de mesma duração, e mostra as maiores variações por projeto, conta, modelo e fornecedor.
|
|
34
|
+
- Corrigido: a duração da janela passou a contar os dois extremos (por exemplo, 08 a 14 vira 7 dias, não 6).
|
|
35
|
+
|
|
36
|
+
### Integridade e cache
|
|
37
|
+
|
|
38
|
+
- Cache incremental da coleta: validação por mtime e tamanho, passando de 3,6s para 0,6s; `--no-cache` para ignorar.
|
|
39
|
+
- **Corrigido**: "ler o cache destruía o cache". `_from_dict` fazia `d.pop('edits')` sobre o dicionário guardado em `self._data`, então a leitura apagava a chave e o `save()` seguinte regravava o cache sem as edições. A partir do terceiro ciclo sobravam 17 edições de 1264 e toda a atribuição por arquivo colapsava no cwd. A correção copia o dict antes do `pop`; caches corrompidos são invalidados pela `VERSION` 4.
|
|
40
|
+
- `kostria doctor` para verificação de integridade dos números, comparando rotas independentes (cache × leitura direta, soma das partes, proveniência, custo negativo, custo não atribuído, deduplicação etc.).
|
|
41
|
+
- Tabela de preços da OpenAI completa a lacuna do Codex ($413,20): input, cached input e output separados; `--assume-codex-model` permite declarar o modelo ausente com proveniência `assumed`.
|
|
42
|
+
- Tratamento de dados malformados: JSONL corrompido, arquivo binário, token não numérico, valor negativo, timestamp inválido, período invertido, caminho com acento/quebra de linha/aspas e SQLite corrompido. Um único valor estranho deixava de derrubar a coleta inteira.
|
|
43
|
+
- **Corrigido**: `float('inf')` na variação percentual era serializado como `Infinity`, que não é JSON válido, e o dashboard ficava vazio sem logar erro no servidor. Sem base anterior, `pct` passou a ser `None` e o consumidor usa o campo `new`.
|
|
44
|
+
- **Corrigido**: modo anônimo vazava e-mail real e caminho absoluto da casa em `collectors.config_dirs` e em `signals`. A limpeza agora é recursiva e cega ao formato, varrendo o payload por padrões de e-mail e caminho absoluto.
|
|
45
|
+
- **Corrigido**: `kostria doctor` falhava intermitentemente quando o próprio Claude Code gravava logs durante a leitura. Agora ele tira uma "fotografia" (mtime + tamanho) dos logs antes do ciclo e marca as checagens vulneráveis como "pulado" quando o disco está em movimento.
|
|
46
|
+
- Serialização JSON com `allow_nan=False` para falhar alto em vez de emitir payload inválido.
|
|
47
|
+
- Servidor passou a atender em paralelo (`ThreadingHTTPServer`) e a reaproveitar a apuração com lock; falhas do processo viram HTTP 500 em vez de derrubar o servidor.
|
|
48
|
+
|
|
49
|
+
### UX e UI do painel
|
|
50
|
+
|
|
51
|
+
- Dashboard interativo local servido por `kostria serve`.
|
|
52
|
+
- Dashboard reescrito com direção visual própria: régua proporcional de atribuição no topo, tipografia monoespaçada para números, paleta fria (teal para valor medido, âmbar para lacuna declarada), navegação lateral com deep-link por hash e acessibilidade.
|
|
53
|
+
- Relatório estático com a mesma identidade visual do dashboard; regras de impressão para não cortar blocos no PDF.
|
|
54
|
+
- Landing com a tese como demonstração; depois com seção de objeções e comparação honesta.
|
|
55
|
+
- Dashboard com busca global, atalhos de teclado, cópia de valor em dólar com um clique, exportação client-side (JSON/CSV), skeleton de carregamento, estados vazios específicos, tooltips de ajuda, microcopy revisado e indicador de atualidade relativa.
|
|
56
|
+
- **Corrigido**: h1 da landing quebrava em 4 linhas em tela larga; ajustado para 3 linhas.
|
|
57
|
+
- **Corrigido**: vazamento horizontal no painel em tela estreita (item flex não encolhia e a página ganhava largura extra).
|
|
58
|
+
- **Corrigido**: cinza de apoio estava abaixo do mínimo de contraste WCAG AA; trocado por `#5a6c7a`.
|
|
59
|
+
- **Corrigido**: entidades HTML escapavam como literais na tela (`Sessões`); texto acentuado vai direto em UTF-8.
|
|
60
|
+
- Corrigido: mockups de terminal perdiam quebra de linha por falta de `white-space: pre-wrap`.
|
|
61
|
+
|
|
62
|
+
### Orçamento por projeto e previsão de fim de mês
|
|
63
|
+
|
|
64
|
+
- `--project-budget NOME=VALOR` (repetível) e tabela `[project_budgets]` no `.kostria.toml` para alertar quando um projeto específico estoura, não só o total da organização.
|
|
65
|
+
- `end_of_month_forecast()` responde quanto já foi gasto desde o dia 1 do mês corrente mais a média recente extrapolada pelos dias que faltam no calendário (não numa janela móvel genérica).
|
|
66
|
+
- Exposto em `scan`, no relatório HTML, em `--format md` e no dashboard.
|
|
67
|
+
|
|
68
|
+
### Testes e qualidade
|
|
69
|
+
|
|
70
|
+
- **Bug de teste revelado por mutação**: os testes de preço anteriores importavam constantes do módulo e calculavam `expected = 3.0 * CACHE_READ`, o que fazia a expectativa mudar junto com o bug. Foram travados em literais calculados à mão, e as 5 mutações passaram a falhar.
|
|
71
|
+
- Testes adversariais para dados corrompidos e bordas de calendário (dia 1, último dia do mês, fevereiro bissexto e não-bissexto).
|
|
72
|
+
- Testes de contraste travando onze pares cor/fundo com cálculo de razão, não com impressão visual.
|
|
73
|
+
- Teste de ponta a ponta que roda a CLI de verdade sobre um `HOME` sintético, em vez de substituir o coletor por um duplo.
|
|
74
|
+
- Total final: 287 testes.
|
|
75
|
+
|
|
76
|
+
### Outras correções
|
|
77
|
+
|
|
78
|
+
- Corrigido: `--anonymize` antes do subcomando era ignorado silenciosamente por causa do `argparse` com `parents=`; as flags comuns passaram a ser lidas do `argv` inteiro e reaplicadas.
|
|
79
|
+
- Corrigido: `main()` devolvia o relatório como código de saída depois que `doctor` foi ligado; apenas `doctor` devolve código de erro.
|
|
80
|
+
- Corrigido: `json.load(open(cand))` sem fechar o arquivo em `_account_of`, achado pelo `ResourceWarning` nos testes.
|
kostria-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pedro Henrique Quadro
|
|
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.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Inclui os assets web embarcados no pacote (dashboard, landing e vendor
|
|
2
|
+
# vendorizado) tanto na sdist quanto na wheel. Sao necessarios para que
|
|
3
|
+
# `kostria serve` funcione quando instalado via pip.
|
|
4
|
+
recursive-include kostria/web *.html
|
|
5
|
+
recursive-include kostria/web/vendor *.js
|
|
6
|
+
recursive-include kostria/web/vendor/fonts *
|
|
7
|
+
recursive-include kostria/landing *.html
|
|
8
|
+
recursive-include kostria/landing/vendor *.js
|
|
9
|
+
recursive-include kostria/landing/vendor *.mjs
|
|
10
|
+
recursive-include kostria/landing/vendor/fonts *
|
|
11
|
+
|
|
12
|
+
# Metadados e docs da raiz.
|
|
13
|
+
include README.md
|
|
14
|
+
include CHANGELOG.md
|
|
15
|
+
include ARCHITECTURE.md
|
|
16
|
+
include LICENSE
|
kostria-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kostria
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Auditoria local de custo de IA: atribui gasto a projeto, feature e conta
|
|
5
|
+
Author-email: Pedro Henrique Quadro <184245414+PedroHenrique0713@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PedroHenrique0713/kostria
|
|
8
|
+
Project-URL: Repository, https://github.com/PedroHenrique0713/kostria
|
|
9
|
+
Project-URL: Issues, https://github.com/PedroHenrique0713/kostria/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/PedroHenrique0713/kostria/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: ai,llm,cost,audit,attribution,claude-code,opencode,codex,token-usage,developer-tools,observability
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: POSIX
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Kostria
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="https://kostria.hypermind.space/static/brand/og-default.png" alt="Kostria" width="480">
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<strong>Pasta nao e projeto.</strong> O Kostria segue o arquivo que a IA
|
|
40
|
+
editou de verdade, resolve o repositorio certo e extrai dali feature e
|
|
41
|
+
ticket do Jira — em 30 segundos, sem proxy, sem SDK, sem mudar codigo.
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<a href="https://pypi.org/project/kostria/"><img src="https://img.shields.io/pypi/v/kostria" alt="PyPI"></a>
|
|
46
|
+
<a href="LICENSE"><img src="https://img.shields.io/pypi/l/kostria" alt="MIT"></a>
|
|
47
|
+
<a href="#"><img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="zero dependencies"></a>
|
|
48
|
+
<a href="#"><img src="https://img.shields.io/badge/python-3.10+-blue" alt="Python 3.10+"></a>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## O que e
|
|
54
|
+
|
|
55
|
+
Ferramentas de IA (Claude Code, OpenCode, Codex CLI) custam caro e crescem
|
|
56
|
+
rapido. O painel do fornecedor mostra custo por conta — nao por projeto, nao
|
|
57
|
+
por feature, nao por ticket. Planilha juntada na mao e lenda com erro.
|
|
58
|
+
|
|
59
|
+
O **Kostria** le os logs que essas ferramentas ja deixam no disco, segue o
|
|
60
|
+
arquivo editado ate o commit que o fechou, e extrai dali o escopo da feature e
|
|
61
|
+
o ticket. O board do time agrega isso pra todo mundo, sem a sessao bruta sair
|
|
62
|
+
do disco de ninguem.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
$ pip install kostria
|
|
66
|
+
$ kostria scan
|
|
67
|
+
|
|
68
|
+
custo total: $2,480.00 tokens: 1.2B sessoes: 89
|
|
69
|
+
|
|
70
|
+
by feature:
|
|
71
|
+
checkout-pix (PROJ-118) $942.40 38%
|
|
72
|
+
onboarding (PROJ-204) $719.20 29%
|
|
73
|
+
busca (PROJ-091) $496.00 20%
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Por que atribuir por cwd mente:** 65% das edicoes do Claude Code acontecem
|
|
77
|
+
em repos que NAO sao o diretorio de trabalho. Worktree, monorepo, sessao de
|
|
78
|
+
terminal — o `cwd` joga o custo no projeto errado. O Kostria segue o arquivo.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Quickstart
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install kostria
|
|
86
|
+
kostria init # detecta suas pastas de trabalho
|
|
87
|
+
kostria scan # 30 segundos ate o primeiro resultado
|
|
88
|
+
kostria serve # dashboard em http://127.0.0.1:8787
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Zero dependencias. So Python 3.10+ e stdlib.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## O que o Kostria responde
|
|
96
|
+
|
|
97
|
+
- **Quanto cada projeto consumiu?** Por arquivo editado, nao por diretorio.
|
|
98
|
+
- **Quanto cada feature custou?** Arquivo → commit → escopo + ticket.
|
|
99
|
+
- **Qual modelo e mais eficiente?** Custo por edicao entregue, nao por token.
|
|
100
|
+
- **Quanto da assinatura foi usado?** Separa consumo real do fixo (Claude
|
|
101
|
+
Pro/Max).
|
|
102
|
+
- **Tem conta ociosa?** Sinaliza contas sem atividade nos ultimos 14 dias.
|
|
103
|
+
- **O orcamento vai estourar?** Projecao mensal + previsao de fechamento do mes.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Suporta
|
|
108
|
+
|
|
109
|
+
| Ferramenta | Coletor | Preco |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| Claude Code | `~/.claude*/projects/**/*.jsonl` | Tabela Anthropic publica (medido) |
|
|
112
|
+
| OpenCode | `~/.local/share/opencode/opencode.db` | Auto-reportado (confiavel) |
|
|
113
|
+
| Codex CLI | `~/.codex/sessions/rollout-*.jsonl` | Tokens (sem tabela OpenAI) |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Planos
|
|
118
|
+
|
|
119
|
+
| Plano | Preco | Inclui |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| **Local** | Gratis | Motor de atribuicao completo, painel local, retencao 90d |
|
|
122
|
+
| **Pro** | $9/mes | Integracao Jira/Linear/GitHub Issues, sync cross-machine |
|
|
123
|
+
| **Team** | $14/assento/mes | Board agregado do time, budget alerts, org-scoped sync |
|
|
124
|
+
|
|
125
|
+
O motor e open source (MIT). O board do time e opcional e hospedado.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Comandos
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
kostria scan # resumo no terminal
|
|
133
|
+
kostria scan --since 30d --compare # contra mes anterior
|
|
134
|
+
kostria scan --budget 3000 # alerta de orcamento
|
|
135
|
+
kostria scan --anonymize # mascara nomes (compartilhavel)
|
|
136
|
+
|
|
137
|
+
kostria report # HTML (abre no navegador)
|
|
138
|
+
kostria report --format json -o dados.json
|
|
139
|
+
kostria report --format csv -o custos.csv
|
|
140
|
+
|
|
141
|
+
kostria serve # dashboard local em 127.0.0.1:8787
|
|
142
|
+
kostria watch # vigia ao vivo no terminal
|
|
143
|
+
kostria doctor # 11 checagens de integridade
|
|
144
|
+
kostria badge -o badge.svg # selo shields.io local
|
|
145
|
+
|
|
146
|
+
kostria sync # envia agregado pro board do time
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Privacidade
|
|
152
|
+
|
|
153
|
+
- Roda 100% local. Nada sai da maquina sem voce pedir.
|
|
154
|
+
- O `kostria sync` envia so o agregado: projeto, dia, custo, tokens. Nunca
|
|
155
|
+
caminho de arquivo, nunca conteudo de sessao, nunca e-mail.
|
|
156
|
+
- `--anonymize` troca nomes por hash estavel para relatorios compartilhaveis.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Por que o Kostria e diferente
|
|
161
|
+
|
|
162
|
+
Nao e um proxy (Helicone). Nao e tracing de qualidade (Langfuse). Nao e FinOps
|
|
163
|
+
de cloud (Vantage). E o unico que le os logs que ja estao no disco, segue o
|
|
164
|
+
arquivo ate o commit e extrai dali feature e ticket — sem SDK, sem proxy, sem
|
|
165
|
+
mudar uma linha de codigo.
|
|
166
|
+
|
|
167
|
+
[Mapa completo de concorrentes →](COMPETITORS.md)
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Docs
|
|
172
|
+
|
|
173
|
+
- [Estrategia de lancamento](GTM.md)
|
|
174
|
+
- [Mapa de concorrentes](COMPETITORS.md)
|
|
175
|
+
- [One-pager / pitch](PITCH.md)
|
|
176
|
+
- [Arquitetura](ARCHITECTURE.md)
|
|
177
|
+
- [Guia de contribuicao](CONTRIBUTING.md)
|
|
178
|
+
- [Saas blueprint](SAAS-BLUEPRINT.md)
|
|
179
|
+
- [Checklist de lancamento](LAUNCH.md)
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Testes
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
python3 -m unittest discover -s tests # CLI: 388 testes
|
|
187
|
+
cd saas && python3 -m pytest -q # SaaS: 123 testes
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
O SaaS nao exige Postgres: a suite injeta um pool em memoria.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Licenca
|
|
195
|
+
|
|
196
|
+
MIT. O CLI e livre. O board hospedado e opcional.
|
|
197
|
+
|
|
198
|
+
Feito em Vicosa, MG por [Pedro Henrique](https://github.com/PedroHenrique0713).
|