Skip to content

Commit 562f687

Browse files
Gabrymi93claude
andauthored
feat: consuma discovered_portals_summary.json (portal scout) (#22)
* feat: consuma discovered_portals_summary.json in session_bootstrap e workspace_triage * docs: aggiorna tabella artifact consumati con radar e portal-scout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: session_bootstrap più compatto — rimuovi dettaglio errori e GCS path --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 20ed538 commit 562f687

4 files changed

Lines changed: 121 additions & 15 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ frontmatter o README manuali. Oggi consuma:
2020

2121
| Repo | Path | Uso |
2222
|---|---|---|
23-
| `source-observatory` | `data/catalog/catalog_signals.json` | health e regressioni sorgenti |
24-
| `dataset-incubator` | `registry/pipeline_signals.json` | stato operativo candidate |
23+
| `source-observatory` | `data/radar/radar_summary.json` | health complessivo delle fonti nel registry |
24+
| `source-observatory` | `data/catalog/catalog_signals.json` | segnali di regressione per singola fonte |
25+
| `source-observatory` | `data/portal_scout/discovered_portals_summary.json` | nuovi portali PA scoperti da portal-scout |
26+
| `dataset-incubator` | `registry/pipeline_signals.json` | stato operativo dei dataset candidate |
2527
| `dataset-incubator` | `registry/clean_catalog.json` | dataset clean/queryable disponibili |
2628

2729
URL raw:

src/agent_context_builder/render.py

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
from .git_local import GitLocalCollector, GitState
1010
from .signals import (
1111
DICleanCatalog,
12+
PortalScoutSummary,
1213
RadarSummary,
1314
RepoSignals,
1415
SourceObservatorySignals,
1516
parse_di_clean_catalog,
17+
parse_portal_scout_summary,
1618
parse_radar_summary,
1719
parse_repo_signals,
1820
parse_source_observatory_signals,
@@ -51,6 +53,7 @@ def __init__(
5153
# Cache for remote signal fetches — avoids double requests within one build
5254
self._so_signals_cache: SourceObservatorySignals | None | type[_UNSET] = _UNSET
5355
self._radar_cache: RadarSummary | None | type[_UNSET] = _UNSET
56+
self._portal_scout_cache: PortalScoutSummary | None | type[_UNSET] = _UNSET
5457
self._di_signals_cache: RepoSignals | None | type[_UNSET] = _UNSET
5558
self._di_clean_catalog_cache: DICleanCatalog | None | type[_UNSET] = _UNSET
5659

@@ -155,6 +158,10 @@ def render_session_bootstrap(self) -> str:
155158
catalog = self._fetch_di_clean_catalog()
156159
lines += self._render_dataset_catalog_section(catalog)
157160

161+
# Portal scout (nuovi candidati strutturati)
162+
scout = self._fetch_portal_scout()
163+
lines += self._render_portal_scout_section(scout)
164+
158165
return "\n".join(lines)
159166

160167
def _fetch_radar_summary(self) -> RadarSummary | None:
@@ -192,6 +199,42 @@ def _render_radar_section(self, radar: RadarSummary | None) -> list[str]:
192199
lines.append("")
193200
return lines
194201

202+
def _fetch_portal_scout(self) -> PortalScoutSummary | None:
203+
if self._portal_scout_cache is not _UNSET:
204+
return self._portal_scout_cache # type: ignore[return-value]
205+
raw = self.github_collector.get_raw_file(
206+
"source-observatory", "data/portal_scout/discovered_portals_summary.json"
207+
)
208+
if raw is None:
209+
self._portal_scout_cache = None
210+
return None
211+
try:
212+
result = parse_portal_scout_summary(raw)
213+
except ValueError as exc:
214+
self.github_collector.fetch_errors["source-observatory:portal_scout"] = str(exc)
215+
result = None
216+
self._portal_scout_cache = result
217+
return result
218+
219+
def _render_portal_scout_section(self, scout: PortalScoutSummary | None) -> list[str]:
220+
lines = ["## Portal Scout", ""]
221+
if scout is None:
222+
lines.append("> *discovered_portals_summary unavailable*")
223+
lines.append("")
224+
return lines
225+
lines.append(
226+
f"Portali rilevati: {scout.total_portals} — "
227+
f"nuovi candidati: {scout.new_candidates} — "
228+
f"strutturati confermati: {scout.new_confirmed_protocol}"
229+
)
230+
if scout.new_structured:
231+
lines.append("")
232+
lines.append("**Nuovi candidati strutturati:**")
233+
for c in scout.new_structured:
234+
lines.append(f"- `{c.domain}` — {c.protocol.upper()}")
235+
lines.append("")
236+
return lines
237+
195238
def _fetch_source_observatory_signals(self) -> SourceObservatorySignals | None:
196239
if self._so_signals_cache is not _UNSET:
197240
return self._so_signals_cache # type: ignore[return-value]
@@ -232,7 +275,7 @@ def _render_source_health_section(
232275
issues = so.regressions + so.alerts
233276
if issues:
234277
for s in issues:
235-
lines.append(f"- **{s.source}** ({s.protocol}): {s.result}{s.detail}")
278+
lines.append(f"- **{s.source}** ({s.protocol}): {s.result}")
236279
if s.suggested_action and s.suggested_action != "nessuna":
237280
lines.append(f" - azione: {s.suggested_action}")
238281
else:
@@ -316,6 +359,7 @@ def render_workspace_triage(self) -> dict[str, Any]:
316359
"source_health": self._build_source_health_dict(),
317360
"pipeline_state": self._build_pipeline_state_dict(),
318361
"dataset_catalog": self._build_dataset_catalog_dict(),
362+
"portal_scout": self._build_portal_scout_dict(),
319363
}
320364
return triage
321365

@@ -469,17 +513,9 @@ def _render_dataset_catalog_section(
469513
)
470514
for dataset in clean_ready[:8]:
471515
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
516+
line = f"- **{dataset.slug}** ({dataset.visibility}): {dataset.name}"
475517
if period:
476518
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}`"
483519
lines.append(line)
484520
if len(clean_ready) > 8:
485521
lines.append(f"- *...and {len(clean_ready) - 8} more clean_ready datasets*")
@@ -532,6 +568,23 @@ def _format_period(period: dict[str, Any]) -> str:
532568
return str(start)
533569
return f"{start or '?'}-{end or '?'}"
534570

571+
def _build_portal_scout_dict(self) -> dict[str, Any]:
572+
scout = self._fetch_portal_scout()
573+
if scout is None:
574+
return {"available": False}
575+
return {
576+
"available": True,
577+
"generated_at": scout.generated_at,
578+
"total_portals": scout.total_portals,
579+
"new_candidates": scout.new_candidates,
580+
"new_confirmed_protocol": scout.new_confirmed_protocol,
581+
"by_protocol": scout.by_protocol,
582+
"new_structured": [
583+
{"domain": c.domain, "protocol": c.protocol}
584+
for c in scout.new_structured
585+
],
586+
}
587+
535588
def _build_pipeline_state_dict(self) -> dict[str, Any]:
536589
di = self._fetch_di_pipeline_signals()
537590
if di is None:

src/agent_context_builder/signals.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,54 @@ def parse_radar_summary(raw: str) -> RadarSummary:
285285
red=counts.get("RED", 0),
286286
sources=sources,
287287
)
288+
289+
290+
@dataclass
291+
class PortalCandidate:
292+
"""Single portal candidate from discovered_portals_summary.json."""
293+
294+
domain: str
295+
protocol: str
296+
probe_url: str
297+
298+
299+
@dataclass
300+
class PortalScoutSummary:
301+
"""Portal discovery summary from source-observatory discovered_portals_summary.json."""
302+
303+
generated_at: str
304+
total_portals: int
305+
new_candidates: int
306+
new_confirmed_protocol: int
307+
known_registry_seen: int
308+
by_protocol: dict[str, int]
309+
new_structured: list[PortalCandidate] = field(default_factory=list)
310+
known_registry_healthcheck: list[PortalCandidate] = field(default_factory=list)
311+
312+
313+
def parse_portal_scout_summary(raw: str) -> PortalScoutSummary:
314+
try:
315+
data: dict[str, Any] = json.loads(raw)
316+
except json.JSONDecodeError as exc:
317+
raise ValueError(f"Invalid JSON: {exc}") from exc
318+
319+
def _parse_candidates(items: list[dict[str, Any]]) -> list[PortalCandidate]:
320+
return [
321+
PortalCandidate(
322+
domain=c.get("domain", ""),
323+
protocol=c.get("protocol", ""),
324+
probe_url=c.get("probe_url", ""),
325+
)
326+
for c in items
327+
]
328+
329+
return PortalScoutSummary(
330+
generated_at=data.get("generated_at", "unknown"),
331+
total_portals=data.get("total_portals", 0),
332+
new_candidates=data.get("new_candidates", 0),
333+
new_confirmed_protocol=data.get("new_confirmed_protocol", 0),
334+
known_registry_seen=data.get("known_registry_seen", 0),
335+
by_protocol=data.get("by_protocol", {}),
336+
new_structured=_parse_candidates(data.get("new_structured", [])),
337+
known_registry_healthcheck=_parse_candidates(data.get("known_registry_healthcheck", [])),
338+
)

tests/test_render.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,14 +391,14 @@ def _raw_file_side_effect(repo, path, ref="main"):
391391
renderer.render_session_bootstrap()
392392
renderer.render_workspace_triage()
393393

394-
# Four distinct files (radar_summary + catalog_signals + DI pipeline_signals + DI clean catalog), each fetched once
395-
assert gh.get_raw_file.call_count == 4
394+
# Five distinct files fetched once each
395+
assert gh.get_raw_file.call_count == 5
396396
paths_fetched = [call.args[1] for call in gh.get_raw_file.call_args_list]
397397
assert "data/radar/radar_summary.json" in paths_fetched
398-
assert "data/radar/radar_summary.json" in paths_fetched
399398
assert "data/catalog/catalog_signals.json" in paths_fetched
400399
assert "registry/pipeline_signals.json" in paths_fetched
401400
assert "registry/clean_catalog.json" in paths_fetched
401+
assert "data/portal_scout/discovered_portals_summary.json" in paths_fetched
402402

403403

404404
def test_render_topic_index():

0 commit comments

Comments
 (0)