Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## [Unreleased]

### Added

- **Registry: dataset mart-only**: i compose senza `clean:` non restano più fuori dal catalogo con i mart orfani. `build_clean_catalog` emette una entry dataset con metadati e `mart_refs`, senza `columns`/`location`; `registry.schema.json` rende i due campi opzionali. Così ACB/MCP vedono il dataset (`find`/`explore`) e risolvono i suoi mart (`gcs_query`).

### Changed

- **Layering ripristinato** (PR #504): rimosse 6 inversioni di dipendenza `core → clean/domain/scout/scaffold`. `sql_validation` spostato in `domain/`, orchestration support in `domain/support.py`, `ProbePool` in `scout/pool.py`, scaffold clean.sql spostato da `raw/run.py` a `cmd_run` (comportamento invariato). Contratto condiviso `dataset_yml_slug`/`iter_workspace_dataset_ymls` in `registry/layout.py`.
Expand Down
27 changes: 24 additions & 3 deletions tests/test_registry_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
build_clean_catalog,
build_entity_graph,
build_mart_catalog,
build_registry,
build_signals,
)
from toolkit.registry.layout import RepoLayout
Expand Down Expand Up @@ -64,6 +65,8 @@
tags: [test]
category: demo
years: [2024]
clean:
sql: "sql/clean.sql"
"""

RUN_RECORD = {
Expand Down Expand Up @@ -184,8 +187,8 @@ def test_clean_catalog_dirname_not_identity(tmp_path: Path) -> None:

@pytest.mark.pure_unit
def test_clean_catalog_missing_parquet_reported(tmp_path: Path) -> None:
"""Dataset dichiarato senza parquet locale → errore, non catalogo finto."""
layout = _make_repo(tmp_path)
"""Dataset con clean.sql ma senza parquet locale → errore, non catalogo finto."""
layout = _make_repo(tmp_path, with_mart=False)
import shutil

shutil.rmtree(layout.repo_root / "out" / "data" / "clean")
Expand All @@ -194,6 +197,24 @@ def test_clean_catalog_missing_parquet_reported(tmp_path: Path) -> None:
assert catalog["datasets"] == []


@pytest.mark.contract
def test_clean_catalog_registers_mart_only_dataset(tmp_path: Path) -> None:
"""Mart-only (no clean.sql né parquet): dataset con mart_refs, senza columns/location."""
layout = _make_repo(tmp_path) # MART_YML: nessun clean.sql
import shutil

shutil.rmtree(layout.repo_root / "out" / "data" / "clean")
built = build_registry(layout)

assert built["errors"]["registry"]["validation"] == []
ds = built["registry"]["datasets"][0]
assert ds["slug"] == "my_dataset"
assert ds["period"] == {"start": 2000, "end": 2024}
assert ds["mart_refs"] == ["my_dataset__mart_trend"]
assert "columns" not in ds
assert "location" not in ds


@pytest.mark.contract
def test_clean_catalog_slims_editorial_entry_without_parquet(tmp_path: Path) -> None:
"""Entry editoriale (no parquet locale) preservata MA filtrata dalle chiavi morte.
Expand All @@ -204,7 +225,7 @@ def test_clean_catalog_slims_editorial_entry_without_parquet(tmp_path: Path) ->
(_slim_dataset_entry) deve comunque applicarsi, così come il fix role
(year → dimension).
"""
layout = _make_repo(tmp_path)
layout = _make_repo(tmp_path, with_mart=False)
import shutil

shutil.rmtree(layout.repo_root / "out" / "data" / "clean")
Expand Down
19 changes: 13 additions & 6 deletions toolkit/registry/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def build_clean_catalog(
continue

columns, latest_year = latest_clean_columns(manifest, alias_map)
if columns is None:
if columns is None and not manifest.is_mart_only:
# Nessun parquet locale: possibile solo se l'entry è editoriale
# (preservata) — altrimenti errore e skip.
if manifest.slug in editorial:
Expand All @@ -155,12 +155,16 @@ def build_clean_catalog(
f"per anni {manifest.years} (runnare la pipeline per generarlo)"
)
continue
if latest_year is None:
if latest_year is None and not manifest.is_mart_only:
derive_errors.append(f"{manifest.slug}: schema letto ma anno non risolvibile")
continue

years_present = sorted(y for y in manifest.years if clean_parquet_path(manifest.cfg, y))
years_eff = years_present or [latest_year]
# mart-only: nessun clean locale → period dagli years dichiarati
years_eff = years_present or ([latest_year] if latest_year else list(manifest.years))
if not years_eff:
derive_errors.append(f"{manifest.slug}: years non dichiarati — period non risolvibile")
continue

entry: dict[str, Any] = {
"slug": manifest.slug,
Expand All @@ -171,10 +175,13 @@ def build_clean_catalog(
"period": manifest.period or {"start": years_eff[0], "end": years_eff[-1]},
"tags": _dedup_tags(manifest.tags),
"category": manifest.category,
"columns": columns,
"location": contract.clean_location(manifest.slug, years_eff),
"stage": "incubating",
}
# Un dataset mart-only non ha clean: niente columns/location. La card
# resta comunque visibile e i mart sono linkati da mart_refs.
if columns is not None:
entry["columns"] = columns
entry["location"] = contract.clean_location(manifest.slug, years_eff)

# Merge editoriale: i campi umani sovrascrivono. role è SEMPRE
# derivato (semantic_type/type), mai dall'editoriale: l'existing
Expand All @@ -194,7 +201,7 @@ def build_clean_catalog(
if not manifest.period and old.get("period"):
entry["period"] = old["period"]
old_cols = {c.get("name"): c for c in old.get("columns", [])}
for col in entry["columns"]:
for col in entry.get("columns", []):
oc = old_cols.get(col["name"])
if oc:
if oc.get("description"):
Expand Down
10 changes: 4 additions & 6 deletions toolkit/registry/schemas/registry.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"items": {
"$ref": "#/$defs/dataset"
},
"description": "Inventario queryabile dei clean parquet (ex clean_catalog)."
"description": "Dataset del repo (ex clean_catalog). I dataset mart-only non hanno clean: columns e location sono opzionali."
},
"marts": {
"type": "array",
Expand Down Expand Up @@ -73,9 +73,7 @@
"slug",
"name",
"description",
"period",
"columns",
"location"
"period"
],
"properties": {
"slug": {
Expand Down Expand Up @@ -141,10 +139,10 @@
},
"columns": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/column"
}
},
"description": "Colonne del clean. Assente/vuoto per i dataset mart-only."
},
"location": {
"$ref": "#/$defs/location"
Expand Down
Loading