@orchestrator-claude/definitions 3.32.2 → 3.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +145 -0
  2. package/agents/adversarial-reviewer.md +2 -2
  3. package/agents/azure-devops-engineer.md +9 -1
  4. package/agents/cicd-platform-engineer.md +477 -0
  5. package/agents/debug-sidecar.md +6 -1
  6. package/agents/doc-sidecar.md +6 -1
  7. package/agents/frontend-engineer.md +173 -0
  8. package/agents/implementer.md +39 -23
  9. package/agents/methodology-researcher.md +2 -1
  10. package/agents/operational-artifact-builder.md +2 -1
  11. package/agents/orchestrator.md +47 -76
  12. package/agents/planner.md +2 -2
  13. package/agents/presentation-engineer.md +1 -1
  14. package/agents/python-data-engineer.md +214 -0
  15. package/agents/reviewer.md +16 -8
  16. package/agents/specifier.md +1 -1
  17. package/agents/task-generator.md +1 -1
  18. package/agents/test-sidecar.md +15 -4
  19. package/dist/cli.js +78 -0
  20. package/dist/cli.js.map +1 -1
  21. package/dist/commands/DiffCommand.js +1 -1
  22. package/dist/commands/DiffCommand.js.map +1 -1
  23. package/dist/commands/InstallCommand.d.ts +41 -0
  24. package/dist/commands/InstallCommand.d.ts.map +1 -0
  25. package/dist/commands/InstallCommand.js +253 -0
  26. package/dist/commands/InstallCommand.js.map +1 -0
  27. package/dist/http/ApiClient.d.ts +5 -0
  28. package/dist/http/ApiClient.d.ts.map +1 -1
  29. package/dist/http/ApiClient.js.map +1 -1
  30. package/dist/install/DevCheckoutDetector.d.ts +66 -0
  31. package/dist/install/DevCheckoutDetector.d.ts.map +1 -0
  32. package/dist/install/DevCheckoutDetector.js +150 -0
  33. package/dist/install/DevCheckoutDetector.js.map +1 -0
  34. package/dist/install/HooksWirer.d.ts +29 -0
  35. package/dist/install/HooksWirer.d.ts.map +1 -0
  36. package/dist/install/HooksWirer.js +80 -0
  37. package/dist/install/HooksWirer.js.map +1 -0
  38. package/dist/install/LocalDiffReporter.d.ts +25 -0
  39. package/dist/install/LocalDiffReporter.d.ts.map +1 -0
  40. package/dist/install/LocalDiffReporter.js +50 -0
  41. package/dist/install/LocalDiffReporter.js.map +1 -0
  42. package/dist/install/Materializer.d.ts +67 -0
  43. package/dist/install/Materializer.d.ts.map +1 -0
  44. package/dist/install/Materializer.js +196 -0
  45. package/dist/install/Materializer.js.map +1 -0
  46. package/dist/install/McpRegistrar.d.ts +63 -0
  47. package/dist/install/McpRegistrar.d.ts.map +1 -0
  48. package/dist/install/McpRegistrar.js +114 -0
  49. package/dist/install/McpRegistrar.js.map +1 -0
  50. package/dist/install/ProcessRunner.d.ts +25 -0
  51. package/dist/install/ProcessRunner.d.ts.map +1 -0
  52. package/dist/install/ProcessRunner.js +41 -0
  53. package/dist/install/ProcessRunner.js.map +1 -0
  54. package/dist/install/ProjectSettingsNeutralizer.d.ts +39 -0
  55. package/dist/install/ProjectSettingsNeutralizer.d.ts.map +1 -0
  56. package/dist/install/ProjectSettingsNeutralizer.js +100 -0
  57. package/dist/install/ProjectSettingsNeutralizer.js.map +1 -0
  58. package/dist/install/types.d.ts +52 -0
  59. package/dist/install/types.d.ts.map +1 -0
  60. package/dist/install/types.js +8 -0
  61. package/dist/install/types.js.map +1 -0
  62. package/dist/manifest/ManifestLoader.d.ts.map +1 -1
  63. package/dist/manifest/ManifestLoader.js +3 -2
  64. package/dist/manifest/ManifestLoader.js.map +1 -1
  65. package/dist/manifest/types.d.ts +3 -0
  66. package/dist/manifest/types.d.ts.map +1 -1
  67. package/dist/scripts/bundle-hooks.d.ts +39 -0
  68. package/dist/scripts/bundle-hooks.d.ts.map +1 -0
  69. package/dist/scripts/bundle-hooks.js +165 -0
  70. package/dist/scripts/bundle-hooks.js.map +1 -0
  71. package/dist/scripts/generate-manifest.d.ts +22 -3
  72. package/dist/scripts/generate-manifest.d.ts.map +1 -1
  73. package/dist/scripts/generate-manifest.js +78 -35
  74. package/dist/scripts/generate-manifest.js.map +1 -1
  75. package/hooks/dangling-guard.mjs +7426 -0
  76. package/hooks/dangling-guard.ts +223 -6
  77. package/hooks/gate-guardian.mjs +463 -0
  78. package/hooks/hooks.json +63 -0
  79. package/hooks/lib/additional-directories-sync.ts +295 -0
  80. package/hooks/lib/api-client.ts +388 -7
  81. package/hooks/lib/session-selection.ts +81 -0
  82. package/hooks/mailbox-listener.mjs +259 -0
  83. package/hooks/post-compact.mjs +7931 -0
  84. package/hooks/post-implement.mjs +238 -0
  85. package/hooks/session-start.mjs +8234 -0
  86. package/hooks/session-start.ts +264 -23
  87. package/hooks/sprint-registry.mjs +59 -0
  88. package/hooks/subagent-start.mjs +289 -0
  89. package/hooks/subagent-stop.mjs +7793 -0
  90. package/hooks/subagent-stop.ts +4 -2
  91. package/hooks/user-prompt.mjs +329 -0
  92. package/hooks/workflow-guard.mjs +7805 -0
  93. package/hooks/workflow-guard.ts +360 -119
  94. package/hooks-settings-template.json +63 -0
  95. package/manifest.json +124 -90
  96. package/mcp-servers.json +48 -0
  97. package/package.json +10 -4
  98. package/personas/auditor-compliance.md +104 -0
  99. package/personas/cfo-conservador.md +88 -0
  100. package/personas/designer-perfeccionista.md +104 -0
  101. package/personas/diretor-cetico-fmcg.md +80 -0
  102. package/personas/engenheiro-cetico-construcao.md +107 -0
  103. package/skills/artifact-generator/SKILL.md +1 -1
  104. package/skills/artifact-production/SKILL.md +28 -0
  105. package/skills/backlog-synthesis/SKILL.md +8 -3
  106. package/skills/checkpoint-protocol/SKILL.md +7 -0
  107. package/skills/infographic-engine/SKILL.md +116 -0
  108. package/skills/orchestrator/SKILL.md +93 -7
  109. package/skills/persona-library/SKILL.md +1 -1
  110. package/skills/project-conventions/SKILL.md +53 -30
  111. package/skills/project-management-enterprise/SKILL.md +1 -1
  112. package/skills/python-data-engineering/SKILL.md +792 -0
  113. package/skills/release/SKILL.md +29 -18
  114. package/skills/release/release.sh +65 -4
  115. package/skills/storybook-validated-loop/SKILL.md +110 -0
  116. package/skills/tdd-discipline/SKILL.md +16 -0
  117. package/skills/workflow-coordination/SKILL.md +117 -16
@@ -0,0 +1,792 @@
1
+ ---
2
+ name: python-data-engineering
3
+ description: Python data engineering patterns para ETL pipelines, schema validators, fuzzy matchers, Excel generators. Use when implementing Python+pandas+rapidfuzz+openpyxl+azure-storage-blob projects. Cobre pytest discipline, mypy --strict, ruff config, Pydantic v2, pandas idioms, fuzzy matching tuning, Azure SDK Python, structured logging, government CSV parsing. Não cobre application backends Python web (FastAPI/Django) — usar /backend skills para isso.
4
+ ---
5
+
6
+ # Python Data Engineering — Patterns and Conventions
7
+
8
+ This skill is the **canonical reference** for Python data engineering work in this org. It is loaded by the `python-data-engineer` agent and is also available to any other agent dispatching Python+data tasks.
9
+
10
+ For deep pandas reference beyond what's here, see also: <https://jeffallan.github.io/claude-skills/skills/data-ml/pandas-pro/> (external skill, not maintained by this org).
11
+
12
+ ## 1. Project Conventions
13
+
14
+ ### Project layout (pyproject.toml-based)
15
+
16
+ ```
17
+ project-name/
18
+ ├── pyproject.toml # PEP 517/518 — single source for deps, tools config
19
+ ├── .gitignore # blocks *.xlsx, .cache/, .env, __pycache__/, .mypy_cache/
20
+ ├── src/
21
+ │ └── package_name/ # snake_case package
22
+ │ ├── __init__.py
23
+ │ └── module.py
24
+ ├── tests/
25
+ │ ├── conftest.py # shared fixtures
26
+ │ ├── unit/
27
+ │ ├── integration/
28
+ │ └── acceptance/
29
+ └── docs/
30
+ ```
31
+
32
+ **Why `src/` layout** (not flat): forces install-then-test pattern, prevents accidental imports from working tree, mypy + ruff treat correctly.
33
+
34
+ ### `pyproject.toml` baseline
35
+
36
+ ```toml
37
+ [project]
38
+ name = "package-name"
39
+ version = "0.1.0"
40
+ requires-python = ">=3.11"
41
+ dependencies = [
42
+ "pandas>=2.1",
43
+ "pydantic>=2.5",
44
+ "pydantic-settings>=2.1",
45
+ ]
46
+
47
+ [project.optional-dependencies]
48
+ dev = [
49
+ "pytest>=7.4",
50
+ "pytest-cov>=4.1",
51
+ "pytest-mock>=3.12",
52
+ "ruff>=0.1",
53
+ "mypy>=1.7",
54
+ ]
55
+
56
+ [tool.ruff]
57
+ line-length = 100
58
+ target-version = "py311"
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "I", "N", "UP", "B", "SIM", "RUF"]
62
+ # E/F: pycodestyle errors + pyflakes
63
+ # I: isort
64
+ # N: pep8-naming
65
+ # UP: pyupgrade
66
+ # B: bugbear (common bug patterns)
67
+ # SIM: simplify
68
+ # RUF: ruff-specific
69
+
70
+ [tool.mypy]
71
+ strict = true
72
+ python_version = "3.11"
73
+ warn_return_any = true
74
+ warn_unused_ignores = true
75
+ disallow_untyped_defs = true
76
+ disallow_any_explicit = false # allow Any explicitly only when annotated
77
+
78
+ [tool.pytest.ini_options]
79
+ markers = [
80
+ "integration: requires external services (Azurite, etc.)",
81
+ "acceptance: requires real data — KPI gates",
82
+ ]
83
+ addopts = "--strict-markers --cov-fail-under=80"
84
+ ```
85
+
86
+ **Rule**: never disable a ruff lint rule globally without comment. If needed, `# noqa: <code> # reason: …` per-line.
87
+
88
+ ## 2. TDD with pytest
89
+
90
+ ### Test layout
91
+
92
+ | Type | Path | Marker | Runs in CI? |
93
+ |---|---|---|---|
94
+ | Unit | `tests/unit/test_<module>.py` | (none) | yes, fast |
95
+ | Integration | `tests/integration/test_<feature>.py` | `@pytest.mark.integration` | yes (with services) |
96
+ | Acceptance | `tests/acceptance/test_kpi_<name>.py` | `@pytest.mark.acceptance` | no — manual only |
97
+
98
+ ### Test function naming
99
+
100
+ ```python
101
+ def test_<unit>_<condition>_<expected_behavior>() -> None:
102
+ """One-line docstring describing intent."""
103
+ ...
104
+ ```
105
+
106
+ Examples:
107
+ - `test_parse_inep_csv_with_valid_2023_schema_returns_dataframe`
108
+ - `test_parse_inep_csv_missing_required_column_raises_schema_error`
109
+ - `test_crossjoin_stage1_exact_match_resolves_70_percent`
110
+
111
+ ### Fixture patterns
112
+
113
+ ```python
114
+ # tests/conftest.py
115
+ import pytest
116
+ from pathlib import Path
117
+
118
+ @pytest.fixture
119
+ def inep_sample_2023(tmp_path: Path) -> Path:
120
+ """Provides a 20-row synthetic INEP 2023 CSV."""
121
+ src = Path("tests/fixtures/inep_sample_2023.csv")
122
+ dst = tmp_path / "inep.csv"
123
+ dst.write_bytes(src.read_bytes())
124
+ return dst
125
+ ```
126
+
127
+ **Rule**: never modify fixture files in tests. Always copy to `tmp_path` first.
128
+
129
+ ### Parametrization for table-driven tests
130
+
131
+ ```python
132
+ @pytest.mark.parametrize(
133
+ "qt_presencial,qt_ead,expected",
134
+ [
135
+ (10000, 0, "PRESENCIAL"),
136
+ (0, 10000, "EAD"),
137
+ (5000, 5000, "HIBRIDO"),
138
+ (8000, 2000, "PRESENCIAL"), # 80% presencial
139
+ (2000, 8000, "EAD"), # 80% ead
140
+ (4000, 4000, "HIBRIDO"), # 50/50 fits 30-70 mix range
141
+ (0, 0, "SEM_DADOS"),
142
+ ],
143
+ )
144
+ def test_derive_modalidade_predominante(qt_presencial: int, qt_ead: int, expected: str) -> None:
145
+ assert derive_modalidade(qt_presencial, qt_ead) == expected
146
+ ```
147
+
148
+ ### Mocking with pytest-mock
149
+
150
+ ```python
151
+ def test_fetch_latest_inep_retries_on_503(mocker: MockerFixture) -> None:
152
+ mock_get = mocker.patch("redbull_sfm.inep_ingest.requests.get")
153
+ mock_get.side_effect = [
154
+ mocker.Mock(status_code=503),
155
+ mocker.Mock(status_code=503),
156
+ mocker.Mock(status_code=200, content=b"..."),
157
+ ]
158
+ df, year = fetch_latest_inep()
159
+ assert mock_get.call_count == 3
160
+ ```
161
+
162
+ **Rule**: prefer `mocker.patch` over `unittest.mock.patch` for cleaner teardown.
163
+
164
+ ### Coverage gate
165
+
166
+ ```bash
167
+ pytest tests/unit/ --cov=src --cov-fail-under=80 --cov-report=term-missing
168
+ ```
169
+
170
+ Required: ≥80% branch coverage on new code. CI fails below threshold.
171
+
172
+ ## 3. Type Hints + mypy --strict
173
+
174
+ ### Essential rules
175
+
176
+ ```python
177
+ # Always type function signatures
178
+ def parse(path: Path, year: int) -> pd.DataFrame:
179
+ ...
180
+
181
+ # Use Protocol for structural typing (duck typing)
182
+ from typing import Protocol
183
+
184
+ class CsvReader(Protocol):
185
+ def read(self, path: Path) -> pd.DataFrame: ...
186
+
187
+ # Use TypedDict for dict-like structured data
188
+ from typing import TypedDict
189
+
190
+ class RunReportDict(TypedDict):
191
+ year_used: int
192
+ kpis: dict[str, float]
193
+ duration_seconds: float
194
+ ```
195
+
196
+ ### dataclass vs Pydantic BaseModel — decision tree
197
+
198
+ | Use case | Choose |
199
+ |---|---|
200
+ | Internal data, no validation needed | `@dataclass(frozen=True)` |
201
+ | External data parsing + validation | `pydantic.BaseModel` |
202
+ | Settings from env | `pydantic_settings.BaseSettings` |
203
+ | Performance-critical hot path | `@dataclass(slots=True, frozen=True)` |
204
+ | Cross-field validation | `BaseModel` with `@model_validator(mode="after")` |
205
+
206
+ ### Avoid `Any`
207
+
208
+ ```python
209
+ # BAD
210
+ def process(data: Any) -> Any: ...
211
+
212
+ # GOOD
213
+ from typing import TypeVar
214
+ T = TypeVar("T", bound=pd.DataFrame)
215
+ def process(data: T) -> T: ...
216
+ ```
217
+
218
+ If `Any` is unavoidable, annotate with comment:
219
+ ```python
220
+ def from_external_lib(x: Any) -> int: # third-party returns Any-typed
221
+ return int(x)
222
+ ```
223
+
224
+ ## 4. Pydantic v2 Patterns
225
+
226
+ ### Schema validation pattern
227
+
228
+ ```python
229
+ from pydantic import BaseModel, Field, field_validator
230
+ from typing import Literal
231
+
232
+ class INEPRecord(BaseModel):
233
+ """Single row of MICRODADOS_CADASTRO_IES_<year>.csv."""
234
+
235
+ CO_IES: int = Field(..., gt=0, description="Código MEC da IES")
236
+ NO_IES: str = Field(..., min_length=3)
237
+ NO_MUNICIPIO_IES: str = Field(..., min_length=2)
238
+ SG_UF_IES: str = Field(..., pattern=r"^[A-Z]{2}$")
239
+ TP_REDE: Literal[1, 2, 3, 4] # 1=pub federal, 2=pub estadual, etc.
240
+ TP_SITUACAO: Literal[1, 2, 3] # 1=ativa, 2=extinta, 3=desativada
241
+ QT_MAT_PRESENCIAL: int = Field(..., ge=0)
242
+ QT_MAT_EAD: int = Field(..., ge=0)
243
+
244
+ @field_validator("NO_IES", "NO_MUNICIPIO_IES")
245
+ @classmethod
246
+ def strip_and_upper(cls, v: str) -> str:
247
+ return v.strip().upper()
248
+ ```
249
+
250
+ ### Cross-field validation
251
+
252
+ ```python
253
+ from pydantic import model_validator
254
+
255
+ class MatchResultMetadata(BaseModel):
256
+ total_input_universities: int
257
+ matched_count: int
258
+ unmatched_count: int
259
+
260
+ @model_validator(mode="after")
261
+ def matched_plus_unmatched_equals_input(self) -> "MatchResultMetadata":
262
+ if self.matched_count + self.unmatched_count != self.total_input_universities:
263
+ raise ValueError("matched + unmatched must equal input total")
264
+ return self
265
+ ```
266
+
267
+ ### Settings management (replaces os.getenv boilerplate)
268
+
269
+ ```python
270
+ from pydantic_settings import BaseSettings, SettingsConfigDict
271
+
272
+ class AzureSettings(BaseSettings):
273
+ """Auto-loads from env vars: AZURE_SAS_INPUT, AZURE_SAS_OUTPUT, AZURE_STORAGE_ACCOUNT."""
274
+
275
+ model_config = SettingsConfigDict(env_prefix="AZURE_", env_file=".env")
276
+
277
+ sas_input: str = Field(..., min_length=20)
278
+ sas_output: str = Field(..., min_length=20)
279
+ storage_account: str = Field(..., pattern=r"^[a-z0-9]{3,24}$")
280
+ input_container: str = "redbull-sfm-input"
281
+ output_container: str = "redbull-sfm-output"
282
+
283
+ # Usage
284
+ settings = AzureSettings() # raises ValidationError if env missing
285
+ ```
286
+
287
+ ### Serialization
288
+
289
+ ```python
290
+ report = RunReport(year_used=2023, kpis={"K-1": 2543}, duration_seconds=47.3)
291
+ json_str = report.model_dump_json(indent=2) # NOT json.dumps(asdict(report))
292
+ dict_form = report.model_dump()
293
+ ```
294
+
295
+ ### Validation error handling
296
+
297
+ ```python
298
+ from pydantic import ValidationError
299
+
300
+ try:
301
+ record = INEPRecord.model_validate(row_dict)
302
+ except ValidationError as e:
303
+ # Log structured error — each field with its own error
304
+ log.error("inep_row_invalid", row_idx=idx, errors=e.errors())
305
+ continue # skip malformed row
306
+ ```
307
+
308
+ **Rule**: `e.errors()` returns list of dicts — preserve via structured logging, do NOT just `str(e)`.
309
+
310
+ ## 5. Pandas Idioms
311
+
312
+ For deep pandas patterns beyond this section, see jeffallan claude-skills `pandas-pro` (external reference).
313
+
314
+ ### Method chaining (readability)
315
+
316
+ ```python
317
+ # BAD — temporary variables, harder to follow
318
+ df_clean = df.dropna(subset=["CO_IES"])
319
+ df_filtered = df_clean[df_clean["TP_SITUACAO"] == 1]
320
+ df_typed = df_filtered.astype({"TP_REDE": "category"})
321
+
322
+ # GOOD — chained
323
+ df_typed = (
324
+ df
325
+ .dropna(subset=["CO_IES"])
326
+ .query("TP_SITUACAO == 1")
327
+ .astype({"TP_REDE": "category"})
328
+ )
329
+ ```
330
+
331
+ ### dtype management (memory + correctness)
332
+
333
+ ```python
334
+ # After read_csv, immediately enforce dtypes
335
+ df = pd.read_csv(
336
+ path,
337
+ encoding="latin-1",
338
+ dtype={
339
+ "CO_IES": "int64",
340
+ "TP_REDE": "Int8", # nullable int8, accepts NaN
341
+ "TP_SITUACAO": "Int8",
342
+ "NO_IES": "string", # not "object"
343
+ "SG_UF_IES": "category", # low-cardinality
344
+ },
345
+ )
346
+ ```
347
+
348
+ **Rule**: never let pandas auto-infer dtype on production code paths. Always explicit.
349
+
350
+ ### Avoiding SettingWithCopyWarning
351
+
352
+ ```python
353
+ # BAD
354
+ df_active = df[df["TP_SITUACAO"] == 1]
355
+ df_active["MODALIDADE"] = "..." # warning
356
+
357
+ # GOOD
358
+ df_active = df[df["TP_SITUACAO"] == 1].copy()
359
+ df_active["MODALIDADE"] = "..."
360
+
361
+ # BETTER
362
+ df_active = df.query("TP_SITUACAO == 1").assign(MODALIDADE="...")
363
+ ```
364
+
365
+ ### Validation after transforms
366
+
367
+ ```python
368
+ # Always check invariants explicitly after non-trivial transforms
369
+ df_agg = df.groupby("UNIVERSITY").agg(...)
370
+ assert df_agg.index.is_unique, "aggregation produced duplicate UNIVERSITY keys"
371
+ assert len(df_agg) == 430, f"expected 430 universities, got {len(df_agg)}"
372
+ ```
373
+
374
+ ### Merge validation
375
+
376
+ ```python
377
+ # Always specify validate when merging to catch unexpected cardinality
378
+ result = pd.merge(
379
+ df_mmm,
380
+ df_inep,
381
+ on="match_key",
382
+ how="left",
383
+ validate="one_to_one", # raises if not 1:1
384
+ indicator=True, # adds _merge column for debugging
385
+ )
386
+ unmatched = result.query("_merge == 'left_only'")
387
+ ```
388
+
389
+ ## 6. Fuzzy Matching com rapidfuzz
390
+
391
+ ### Algorithm selection
392
+
393
+ | Algorithm | Use case | Pitfall |
394
+ |---|---|---|
395
+ | `fuzz.ratio` | Strings same length, char-level diff | Bad for word reordering |
396
+ | `fuzz.token_sort_ratio` | Word reordering OK ("FED ALAGOAS" vs "ALAGOAS FED") | Slow on long strings |
397
+ | `fuzz.partial_ratio` | Prefixes/substrings ("UFAL" matches "UNIVERSIDADE FEDERAL DE ALAGOAS") | False positives in short strings |
398
+ | `fuzz.WRatio` | "Smart" weighted combo — when unsure | Less predictable, harder to tune threshold |
399
+
400
+ ### Threshold tuning
401
+
402
+ Start with **85** as default threshold. Calibrate via known-truth golden set:
403
+
404
+ ```python
405
+ # Calibration script — run once with ~20-30 known pairs
406
+ from rapidfuzz import fuzz
407
+
408
+ golden_pairs = [
409
+ ("UNIVERSIDADE FEDERAL DE ALAGOAS", "UFAL"), # expected: match
410
+ ("USP", "Universidade de Sao Paulo"), # expected: match
411
+ ("Universidade Anhanguera", "UNESP"), # expected: no match
412
+ ...
413
+ ]
414
+
415
+ for s1, s2 in golden_pairs:
416
+ print(f"token_sort: {fuzz.token_sort_ratio(s1, s2):3d} partial: {fuzz.partial_ratio(s1, s2):3d} {s1[:30]} | {s2[:30]}")
417
+ ```
418
+
419
+ Pick threshold where positive-pairs score ≥ T and negative-pairs score < T.
420
+
421
+ ### Performance — process in batches
422
+
423
+ ```python
424
+ from rapidfuzz import process
425
+
426
+ # Instead of N*M loop (slow):
427
+ for mmm_name in mmm_names:
428
+ for inep_name in inep_names:
429
+ score = fuzz.token_sort_ratio(mmm_name, inep_name)
430
+
431
+ # Use vectorized cdist (fast):
432
+ import numpy as np
433
+ matrix = process.cdist(mmm_names, inep_names, scorer=fuzz.token_sort_ratio)
434
+ # matrix[i, j] is score between mmm_names[i] and inep_names[j]
435
+ best_matches = np.argmax(matrix, axis=1)
436
+ best_scores = matrix[np.arange(len(mmm_names)), best_matches]
437
+ ```
438
+
439
+ ### Normalization BEFORE fuzzy
440
+
441
+ ```python
442
+ from unidecode import unidecode
443
+
444
+ def normalize(s: str) -> str:
445
+ """Apply BEFORE feeding to fuzz: removes accents, casing, punctuation."""
446
+ return unidecode(s.lower().strip()).replace("-", " ").replace(",", " ")
447
+
448
+ # Then fuzz on normalized strings
449
+ score = fuzz.token_sort_ratio(normalize(s1), normalize(s2))
450
+ ```
451
+
452
+ ## 7. Schema Evolution Patterns
453
+
454
+ ### Versioned validators
455
+
456
+ ```python
457
+ # schemas.py
458
+ INEP_2023_REQUIRED_COLS = frozenset({
459
+ "CO_IES", "NO_IES", "NO_MUNICIPIO_IES", "SG_UF_IES",
460
+ "TP_REDE", "TP_SITUACAO", "QT_MAT_PRESENCIAL", "QT_MAT_EAD",
461
+ })
462
+
463
+ INEP_2024_REQUIRED_COLS = INEP_2023_REQUIRED_COLS # presumed stable; update when published
464
+
465
+ def validate_schema(df: pd.DataFrame, year: int) -> None:
466
+ expected = {
467
+ 2023: INEP_2023_REQUIRED_COLS,
468
+ 2024: INEP_2024_REQUIRED_COLS,
469
+ }.get(year)
470
+ if expected is None:
471
+ raise SchemaError(f"unknown INEP year {year}")
472
+ missing = expected - set(df.columns)
473
+ if missing:
474
+ raise SchemaError(f"INEP {year} missing required cols: {sorted(missing)}")
475
+ # extras are OK — warn but don't fail
476
+ extras = set(df.columns) - expected
477
+ if extras:
478
+ log.warning("inep_schema_extras_detected", year=year, extras=sorted(extras))
479
+ ```
480
+
481
+ ### Backward-compat strategy
482
+
483
+ When INEP schema changes between years:
484
+ 1. Add `INEP_<NEW_YEAR>_REQUIRED_COLS` to schemas.py (don't modify old)
485
+ 2. If a column was renamed, add a normalization step in `parse_inep_csv` that maps old → new name
486
+ 3. If a column was removed, add a "synthesized" column from other data
487
+ 4. Never drop support for old schemas until you confirm no one needs to re-process old years
488
+
489
+ ## 8. Azure SDK Python (azure-storage-blob)
490
+
491
+ ### Async client (preferred for I/O)
492
+
493
+ ```python
494
+ from azure.storage.blob.aio import BlobServiceClient
495
+ from azure.core.exceptions import ResourceNotFoundError
496
+
497
+ async def download_blob(container: str, blob: str, sas_url: str) -> bytes:
498
+ async with BlobServiceClient(account_url=sas_url) as service:
499
+ async with service.get_blob_client(container=container, blob=blob) as client:
500
+ try:
501
+ stream = await client.download_blob()
502
+ return await stream.readall()
503
+ except ResourceNotFoundError:
504
+ log.error("blob_not_found", container=container, blob=blob)
505
+ raise
506
+ ```
507
+
508
+ ### SAS handling
509
+
510
+ ```python
511
+ # NEVER log full SAS URL — redact the query string
512
+ def redact_sas(url: str) -> str:
513
+ """Remove ?sv=...&sig=... from URL for safe logging."""
514
+ return url.split("?")[0] + "?<redacted>"
515
+ ```
516
+
517
+ ### Retry policy
518
+
519
+ ```python
520
+ from azure.core.pipeline.policies import RetryPolicy
521
+
522
+ # Built-in retry with exp backoff
523
+ service = BlobServiceClient(
524
+ account_url=sas_url,
525
+ retry_total=3,
526
+ retry_backoff_factor=2,
527
+ retry_backoff_max=30,
528
+ )
529
+ ```
530
+
531
+ ### Error types to handle explicitly
532
+
533
+ | Exception | When | Resposta |
534
+ |---|---|---|
535
+ | `ResourceNotFoundError` | Blob doesn't exist | Hard fail, log container/blob |
536
+ | `ResourceExistsError` | Overwrite without flag | Either set `overwrite=True` or rename |
537
+ | `ClientAuthenticationError` | SAS expired/invalid | Hard fail, alert (need new SAS) |
538
+ | `ServiceRequestError` | Network/transient | Retry (already covered by RetryPolicy) |
539
+
540
+ ## 9. openpyxl Deep
541
+
542
+ ### Conditional formatting
543
+
544
+ ```python
545
+ from openpyxl.formatting.rule import CellIsRule
546
+ from openpyxl.styles import PatternFill
547
+
548
+ ws.conditional_formatting.add(
549
+ "F2:F500", # MATCH_SCORE column
550
+ CellIsRule(
551
+ operator="lessThan",
552
+ formula=["85"],
553
+ fill=PatternFill(start_color="FFCDD2", end_color="FFCDD2", fill_type="solid"), # red
554
+ ),
555
+ )
556
+ ws.conditional_formatting.add(
557
+ "F2:F500",
558
+ CellIsRule(
559
+ operator="between",
560
+ formula=["85", "89"],
561
+ fill=PatternFill(start_color="FFF9C4", end_color="FFF9C4", fill_type="solid"), # yellow
562
+ ),
563
+ )
564
+ ```
565
+
566
+ ### AutoFilter
567
+
568
+ ```python
569
+ ws.auto_filter.ref = ws.dimensions # filter buttons on all columns
570
+ ```
571
+
572
+ ### NamedStyle for consistent headers
573
+
574
+ ```python
575
+ from openpyxl.styles import NamedStyle, Font, PatternFill, Alignment
576
+
577
+ header_style = NamedStyle(name="header")
578
+ header_style.font = Font(bold=True, color="FFFFFF")
579
+ header_style.fill = PatternFill(start_color="001E60", end_color="001E60", fill_type="solid") # Red Bull blue
580
+ header_style.alignment = Alignment(horizontal="center", vertical="center")
581
+
582
+ # Register once on workbook
583
+ wb.add_named_style(header_style)
584
+
585
+ # Apply
586
+ for cell in ws[1]: # row 1 = header
587
+ cell.style = "header"
588
+ ```
589
+
590
+ ### Freeze panes
591
+
592
+ ```python
593
+ ws.freeze_panes = "A2" # freeze row 1
594
+ ```
595
+
596
+ ### Performance — write_only mode for large outputs
597
+
598
+ ```python
599
+ from openpyxl import Workbook
600
+ wb = Workbook(write_only=True) # ~10x faster, but read_only=False after save
601
+ ws = wb.create_sheet("IES_FULL")
602
+ ws.append(headers)
603
+ for row in rows_iter:
604
+ ws.append(row)
605
+ wb.save(path)
606
+ ```
607
+
608
+ ## 10. Structured Logging (structlog)
609
+
610
+ ### Config (call once at startup)
611
+
612
+ ```python
613
+ import structlog
614
+ import logging
615
+ import sys
616
+
617
+ def configure_logging(level: str = "INFO") -> None:
618
+ logging.basicConfig(format="%(message)s", stream=sys.stdout, level=level)
619
+ structlog.configure(
620
+ processors=[
621
+ structlog.contextvars.merge_contextvars,
622
+ structlog.processors.add_log_level,
623
+ structlog.processors.TimeStamper(fmt="iso", utc=True),
624
+ structlog.processors.StackInfoRenderer(),
625
+ structlog.processors.format_exc_info,
626
+ structlog.processors.JSONRenderer(),
627
+ ],
628
+ wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
629
+ cache_logger_on_first_use=True,
630
+ )
631
+ ```
632
+
633
+ ### Usage patterns
634
+
635
+ ```python
636
+ log = structlog.get_logger()
637
+
638
+ # Bind context at function entry
639
+ log = log.bind(workflow="annual-update", year=2023)
640
+
641
+ # Log with structured fields (NOT f-strings)
642
+ log.info("inep_download_started", url=url, container=container)
643
+ # BAD: log.info(f"Downloading from {url}")
644
+
645
+ # Use exception() to capture stack
646
+ try:
647
+ ...
648
+ except SchemaError:
649
+ log.exception("inep_schema_validation_failed", year=year)
650
+ raise
651
+ ```
652
+
653
+ ### Required fields in all logs
654
+
655
+ | Field | Always | Why |
656
+ |---|---|---|
657
+ | `event` | yes (first arg) | snake_case action name |
658
+ | `workflow` | yes | enables filtering across runs |
659
+ | `year` | yes | enables filtering across years |
660
+ | Module-specific kwargs | as relevant | structured grep |
661
+
662
+ **Rule**: never log secrets, SAS URLs, or PII. Use `redact_sas()` helper.
663
+
664
+ ## 11. INEP / Government CSV Patterns
665
+
666
+ ### Encoding fallback
667
+
668
+ ```python
669
+ def read_csv_resilient(path: Path) -> pd.DataFrame:
670
+ """Try latin-1 → cp1252 → utf-8. INEP files are usually latin-1."""
671
+ for encoding in ("latin-1", "cp1252", "utf-8"):
672
+ try:
673
+ return pd.read_csv(path, encoding=encoding, sep=";")
674
+ except UnicodeDecodeError:
675
+ continue
676
+ raise ValueError(f"could not decode {path} with any known encoding")
677
+ ```
678
+
679
+ ### Malformed rows
680
+
681
+ ```python
682
+ # Pandas: skip bad rows but log them
683
+ df = pd.read_csv(
684
+ path,
685
+ encoding="latin-1",
686
+ sep=";",
687
+ on_bad_lines=lambda bad_line: (
688
+ log.warning("inep_bad_row_skipped", line=bad_line),
689
+ None, # skip
690
+ )[1],
691
+ engine="python", # required for on_bad_lines callable
692
+ )
693
+ ```
694
+
695
+ ### Year discovery from MEC portal
696
+
697
+ ```python
698
+ import re
699
+ import requests
700
+
701
+ def discover_latest_inep_year() -> int:
702
+ """Scrape MEC portal for the most recent published Censo year.
703
+
704
+ Falls back to (current_calendar_year - 2) if scraping fails — INEP publishes
705
+ with ~1y lag, so 2026 Q1 usually has 2024 latest, but conservatively assume 2023.
706
+ """
707
+ try:
708
+ resp = requests.get(
709
+ "https://www.gov.br/inep/pt-br/acesso-a-informacao/dados-abertos/microdados/censo-da-educacao-superior",
710
+ timeout=30,
711
+ )
712
+ years = sorted(set(int(y) for y in re.findall(r"\b(20\d{2})\b", resp.text)), reverse=True)
713
+ return years[0] if years else 2023
714
+ except requests.RequestException:
715
+ log.warning("inep_year_discovery_failed", fallback=2023)
716
+ return 2023
717
+ ```
718
+
719
+ ### Data quality assertions per row
720
+
721
+ ```python
722
+ def validate_inep_row(row: dict[str, Any]) -> bool:
723
+ """Quick sanity checks per row. Reject if any fail."""
724
+ if row.get("CO_IES", 0) <= 0:
725
+ return False
726
+ if not re.match(r"^[A-Z]{2}$", str(row.get("SG_UF_IES", ""))):
727
+ return False
728
+ if row.get("QT_MAT_PRESENCIAL", -1) < 0 or row.get("QT_MAT_EAD", -1) < 0:
729
+ return False
730
+ return True
731
+ ```
732
+
733
+ ## 12. Quality Gates
734
+
735
+ ### Pre-commit checklist (run in order)
736
+
737
+ ```bash
738
+ ruff format --check src tests # NO uncommitted formatting drift
739
+ ruff check src tests # 0 lint errors
740
+ mypy --strict src # 0 type errors
741
+ pytest tests/unit --cov=src --cov-fail-under=80
742
+ pytest tests/integration # requires Azurite
743
+ # acceptance tests run manually only — never in CI
744
+ ```
745
+
746
+ ### CI pipeline structure (GHA)
747
+
748
+ ```yaml
749
+ jobs:
750
+ lint-and-types:
751
+ steps:
752
+ - ruff format --check src tests
753
+ - ruff check src tests
754
+ - mypy --strict src
755
+ unit:
756
+ needs: lint-and-types
757
+ steps:
758
+ - pytest tests/unit --cov=src --cov-fail-under=80
759
+ integration:
760
+ needs: unit
761
+ services:
762
+ azurite:
763
+ image: mcr.microsoft.com/azure-storage/azurite
764
+ steps:
765
+ - pytest tests/integration
766
+ ```
767
+
768
+ ### Definition of Done per module
769
+
770
+ A module is **DONE** when:
771
+ 1. All unit tests pass (TDD red → green)
772
+ 2. Type-check passes (`mypy --strict <module>`)
773
+ 3. Lint passes (`ruff check <module>`)
774
+ 4. Coverage ≥80% branch
775
+ 5. At least one integration test covers the module if it has I/O
776
+ 6. Module exports documented in `__init__.py`
777
+ 7. Acceptance tests exist for KPI-bearing functionality (if applicable)
778
+
779
+ ## Cross-references
780
+
781
+ - TDD discipline (TS-flavored): `/tdd-discipline` — different stack, **do NOT use for Python projects**
782
+ - Project conventions (stack-agnostic): `/project-conventions`
783
+ - Checkpoint protocol: `/checkpoint-protocol`
784
+ - KB lookup: `/kb-lookup` — for org-specific patterns
785
+ - External pandas deep dive: <https://jeffallan.github.io/claude-skills/skills/data-ml/pandas-pro/>
786
+
787
+ ## When NOT to use this skill
788
+
789
+ - Python web backends (FastAPI/Django) — use backend-specific skills if available
790
+ - Pure scripting (no schemas, no data transformations) — overkill
791
+ - TS/React projects — wrong language entirely
792
+ - Azure infra (Bicep, GHA OIDC) — use `/azure-prepare`, `/azure-deploy` instead