Skip to content

Commit 20ed538

Browse files
authored
feat: consume DI clean catalog (#20)
1 parent 8ceaed0 commit 20ed538

5 files changed

Lines changed: 348 additions & 2 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ da GitHub e, se disponibile, dai checkout locali dei repo Lab.
1313

1414
La CI aggiorna gli artifact GitHub-only ogni 6 ore sul branch `context`.
1515

16+
## Artifact Consumati
17+
18+
ACB preferisce artifact JSON generati e versionati dai repo Lab rispetto a
19+
frontmatter o README manuali. Oggi consuma:
20+
21+
| Repo | Path | Uso |
22+
|---|---|---|
23+
| `source-observatory` | `data/catalog/catalog_signals.json` | health e regressioni sorgenti |
24+
| `dataset-incubator` | `registry/pipeline_signals.json` | stato operativo candidate |
25+
| `dataset-incubator` | `registry/clean_catalog.json` | dataset clean/queryable disponibili |
26+
1627
URL raw:
1728

1829
```text

src/agent_context_builder/render.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
from .github import GitHubCollector, PR
99
from .git_local import GitLocalCollector, GitState
1010
from .signals import (
11+
DICleanCatalog,
1112
RadarSummary,
1213
RepoSignals,
1314
SourceObservatorySignals,
15+
parse_di_clean_catalog,
1416
parse_radar_summary,
1517
parse_repo_signals,
1618
parse_source_observatory_signals,
@@ -50,6 +52,7 @@ def __init__(
5052
self._so_signals_cache: SourceObservatorySignals | None | type[_UNSET] = _UNSET
5153
self._radar_cache: RadarSummary | None | type[_UNSET] = _UNSET
5254
self._di_signals_cache: RepoSignals | None | type[_UNSET] = _UNSET
55+
self._di_clean_catalog_cache: DICleanCatalog | None | type[_UNSET] = _UNSET
5356

5457
def render_session_bootstrap(self) -> str:
5558
"""Render session_bootstrap.md.
@@ -148,6 +151,10 @@ def render_session_bootstrap(self) -> str:
148151
di = self._fetch_di_pipeline_signals()
149152
lines += self._render_pipeline_state_section(di)
150153

154+
# Dataset catalog (clean/queryable datasets)
155+
catalog = self._fetch_di_clean_catalog()
156+
lines += self._render_dataset_catalog_section(catalog)
157+
151158
return "\n".join(lines)
152159

153160
def _fetch_radar_summary(self) -> RadarSummary | None:
@@ -308,6 +315,7 @@ def render_workspace_triage(self) -> dict[str, Any]:
308315
"radar": self._build_radar_dict(),
309316
"source_health": self._build_source_health_dict(),
310317
"pipeline_state": self._build_pipeline_state_dict(),
318+
"dataset_catalog": self._build_dataset_catalog_dict(),
311319
}
312320
return triage
313321

@@ -381,6 +389,23 @@ def _fetch_di_pipeline_signals(self) -> RepoSignals | None:
381389
self._di_signals_cache = result
382390
return result
383391

392+
def _fetch_di_clean_catalog(self) -> DICleanCatalog | None:
393+
if self._di_clean_catalog_cache is not _UNSET:
394+
return self._di_clean_catalog_cache # type: ignore[return-value]
395+
raw = self.github_collector.get_raw_file(
396+
"dataset-incubator", "registry/clean_catalog.json"
397+
)
398+
if raw is None:
399+
self._di_clean_catalog_cache = None
400+
return None
401+
try:
402+
result = parse_di_clean_catalog(raw)
403+
except ValueError as exc:
404+
self.github_collector.fetch_errors["dataset-incubator:clean_catalog"] = str(exc)
405+
result = None
406+
self._di_clean_catalog_cache = result
407+
return result
408+
384409
def _render_pipeline_state_section(self, di: RepoSignals | None) -> list[str]:
385410
lines = []
386411
lines.append("## Pipeline State")
@@ -417,6 +442,96 @@ def _render_pipeline_state_section(self, di: RepoSignals | None) -> list[str]:
417442
lines.append("")
418443
return lines
419444

445+
def _render_dataset_catalog_section(
446+
self, catalog: DICleanCatalog | None
447+
) -> list[str]:
448+
lines = []
449+
lines.append("## Dataset Catalog")
450+
lines.append("")
451+
if catalog is None:
452+
err = self.github_collector.fetch_errors.get(
453+
"dataset-incubator:clean_catalog"
454+
) or self.github_collector.fetch_errors.get(
455+
"dataset-incubator:registry/clean_catalog.json"
456+
)
457+
if err:
458+
lines.append(f"> *clean_catalog unavailable — {err}*")
459+
else:
460+
lines.append("> *clean_catalog unavailable*")
461+
lines.append("")
462+
return lines
463+
464+
clean_ready = catalog.clean_ready
465+
public_count = sum(1 for d in clean_ready if d.visibility == "public")
466+
lines.append(
467+
f"*{len(clean_ready)} clean_ready dataset(s), "
468+
f"{public_count} public* (updated {catalog.updated_at})"
469+
)
470+
for dataset in clean_ready[:8]:
471+
period = self._format_period(dataset.period)
472+
location = dataset.location.get("path", "")
473+
line = f"- **{dataset.slug}** ({dataset.status}, {dataset.visibility}): "
474+
line += dataset.name
475+
if period:
476+
line += f" [{period}]"
477+
line += (
478+
f" - {dataset.metric_columns} metric, "
479+
f"{dataset.dimension_columns} dimension columns"
480+
)
481+
if location:
482+
line += f" - `{location}`"
483+
lines.append(line)
484+
if len(clean_ready) > 8:
485+
lines.append(f"- *...and {len(clean_ready) - 8} more clean_ready datasets*")
486+
lines.append("")
487+
return lines
488+
489+
def _build_dataset_catalog_dict(self) -> dict[str, Any]:
490+
catalog = self._fetch_di_clean_catalog()
491+
if catalog is None:
492+
return {
493+
"available": False,
494+
"errors": {
495+
k: v for k, v in self.github_collector.fetch_errors.items()
496+
if "clean_catalog" in k
497+
},
498+
}
499+
return {
500+
"available": True,
501+
"schema_version": catalog.schema_version,
502+
"name": catalog.name,
503+
"updated_at": catalog.updated_at,
504+
"summary": {
505+
"total": len(catalog.datasets),
506+
"clean_ready": len(catalog.clean_ready),
507+
"public": sum(1 for d in catalog.clean_ready if d.visibility == "public"),
508+
},
509+
"datasets": [
510+
{
511+
"slug": d.slug,
512+
"name": d.name,
513+
"status": d.status,
514+
"visibility": d.visibility,
515+
"period": d.period,
516+
"location": d.location,
517+
"metric_columns": d.metric_columns,
518+
"dimension_columns": d.dimension_columns,
519+
"column_count": d.column_count,
520+
}
521+
for d in catalog.datasets
522+
],
523+
}
524+
525+
@staticmethod
526+
def _format_period(period: dict[str, Any]) -> str:
527+
start = period.get("start")
528+
end = period.get("end")
529+
if start is None and end is None:
530+
return ""
531+
if start == end:
532+
return str(start)
533+
return f"{start or '?'}-{end or '?'}"
534+
420535
def _build_pipeline_state_dict(self) -> dict[str, Any]:
421536
di = self._fetch_di_pipeline_signals()
422537
if di is None:

src/agent_context_builder/signals.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,36 @@ def actionable(self) -> list[RepoSignal]:
7474
return [s for s in self.signals if s.status in ("warn", "error")]
7575

7676

77+
@dataclass
78+
class DICleanDataset:
79+
"""Single clean dataset entry from dataset-incubator clean_catalog.json."""
80+
81+
slug: str
82+
name: str
83+
status: str
84+
visibility: str
85+
period: dict[str, Any] = field(default_factory=dict)
86+
location: dict[str, Any] = field(default_factory=dict)
87+
metric_columns: int = 0
88+
dimension_columns: int = 0
89+
column_count: int = 0
90+
91+
92+
@dataclass
93+
class DICleanCatalog:
94+
"""Clean dataset catalog from dataset-incubator registry."""
95+
96+
schema_version: str
97+
name: str
98+
updated_at: str
99+
datasets: list[DICleanDataset] = field(default_factory=list)
100+
101+
@property
102+
def clean_ready(self) -> list[DICleanDataset]:
103+
"""Datasets with status clean_ready."""
104+
return [d for d in self.datasets if d.status == "clean_ready"]
105+
106+
77107
def parse_repo_signals(raw: str) -> RepoSignals:
78108
"""Parse a repo-signals standard v1 JSON string.
79109
@@ -112,6 +142,54 @@ def parse_repo_signals(raw: str) -> RepoSignals:
112142
)
113143

114144

145+
def parse_di_clean_catalog(raw: str) -> DICleanCatalog:
146+
"""Parse dataset-incubator registry/clean_catalog.json.
147+
148+
ACB keeps the fields needed for agent orientation and triage. Descriptive
149+
metadata such as description, source, and registry_source remains in the
150+
upstream catalog and is intentionally omitted from this compact model.
151+
152+
Args:
153+
raw: Raw JSON content of clean_catalog.json
154+
155+
Returns:
156+
Parsed DICleanCatalog instance
157+
158+
Raises:
159+
ValueError: If the JSON is invalid
160+
"""
161+
try:
162+
data: dict[str, Any] = json.loads(raw)
163+
except json.JSONDecodeError as exc:
164+
raise ValueError(f"Invalid JSON: {exc}") from exc
165+
166+
datasets = []
167+
for item in data.get("datasets", []):
168+
columns = item.get("columns", [])
169+
metric_columns = sum(1 for c in columns if c.get("role") == "metric")
170+
dimension_columns = sum(1 for c in columns if c.get("role") == "dimension")
171+
datasets.append(
172+
DICleanDataset(
173+
slug=item.get("slug", ""),
174+
name=item.get("name", item.get("slug", "")),
175+
status=item.get("status", ""),
176+
visibility=item.get("visibility", ""),
177+
period=item.get("period", {}),
178+
location=item.get("location", {}),
179+
metric_columns=metric_columns,
180+
dimension_columns=dimension_columns,
181+
column_count=len(columns),
182+
)
183+
)
184+
185+
return DICleanCatalog(
186+
schema_version=str(data.get("schema_version", "1")),
187+
name=data.get("name", ""),
188+
updated_at=data.get("updated_at", "unknown"),
189+
datasets=datasets,
190+
)
191+
192+
115193
def parse_source_observatory_signals(raw: str) -> SourceObservatorySignals:
116194
"""Parse raw JSON string into SourceObservatorySignals.
117195

0 commit comments

Comments
 (0)