diff --git a/config/portfolio-catalog.yaml b/config/portfolio-catalog.yaml index 659dfc5..7003ca6 100644 --- a/config/portfolio-catalog.yaml +++ b/config/portfolio-catalog.yaml @@ -554,10 +554,18 @@ repos: tool_provenance: claude-code Signal & Noise: owner: d + purpose: public Signal & Noise portfolio surface lifecycle_state: active - review_cadence: weekly + criticality: medium + review_cadence: monthly intended_disposition: maintain + category: vanity tool_provenance: claude-code + maturity_program: maintain + target_maturity: operating + automation_eligible: false + aliases: + - signal-noise SpecCompanion: owner: d lifecycle_state: active @@ -954,6 +962,32 @@ repos: target_maturity: functional automation_eligible: false notes: Active through the June 10-22, 2026 Fable window; staged harness/config changes are operator-installed only, then archive or park the campaign workspace after final index review. + fable-os-divergence: + owner: d + purpose: Fable campaign workspace for agent-OS divergence analysis and follow-up checklists + lifecycle_state: experimental + criticality: medium + review_cadence: ad-hoc + intended_disposition: experiment + category: infrastructure + tool_provenance: claude-ai + maturity_program: experiment + target_maturity: functional + automation_eligible: false + notes: Campaign evidence and next-step planning workspace; keep out of default portfolio attention unless a publish/park/archive decision is active. + fable-site-rebuild: + owner: d + purpose: control room for rebuilding the public portfolio site around the Fable site-rebuild campaign + lifecycle_state: experimental + criticality: medium + review_cadence: ad-hoc + intended_disposition: experiment + category: infrastructure + tool_provenance: claude-ai + maturity_program: experiment + target_maturity: functional + automation_eligible: false + notes: Not a product repo; the product surface is portfolio-index. Keep this workspace out of default attention except when the site-rebuild campaign is actively being dispatched. portfolio-index: owner: d purpose: public-safe static portfolio index for hiring-manager review @@ -1297,4 +1331,4 @@ repos: owner: d lifecycle_state: archived review_cadence: quarterly - intended_disposition: archive + intended_disposition: archive \ No newline at end of file diff --git a/src/portfolio_catalog.py b/src/portfolio_catalog.py index 53cfdfc..3ddf2a8 100644 --- a/src/portfolio_catalog.py +++ b/src/portfolio_catalog.py @@ -293,6 +293,10 @@ def _normalize_repo_entries( "matched_by": "full-name" if "/" in key else "bare-name", "has_explicit_entry": True, } + aliases = raw_value.get("aliases") or [] + if aliases and not isinstance(aliases, list): + errors.append(f"Portfolio catalog entry '{key}' aliases must be a list.") + aliases = [] for field_name, allowed in ( ("lifecycle_state", VALID_LIFECYCLE_STATES), ("criticality", VALID_CRITICALITY), @@ -316,6 +320,17 @@ def _normalize_repo_entries( seen_bare_names.add(bare_key) normalized_entries[_normalize_key(key)] = normalized + for raw_alias in aliases: + alias = _safe_text(raw_alias) + if not alias: + continue + normalized_alias = _normalize_key(alias) + if normalized_alias in normalized_entries: + warnings.append( + f"Portfolio catalog alias '{alias}' for '{key}' duplicates an existing repo key." + ) + continue + normalized_entries[normalized_alias] = normalized return normalized_entries @@ -601,4 +616,4 @@ def build_intent_alignment_summary(audits: list[Any]) -> dict[str, Any]: return { "counts": dict(counts), "summary": summary, - } + } \ No newline at end of file diff --git a/tests/test_portfolio_catalog.py b/tests/test_portfolio_catalog.py index f6b3554..376f9e6 100644 --- a/tests/test_portfolio_catalog.py +++ b/tests/test_portfolio_catalog.py @@ -62,6 +62,38 @@ def test_load_portfolio_catalog_accepts_defaults_and_repo_entries(tmp_path: Path assert catalog["repos"]["repob"]["doctor_standard"] == "" +def test_load_portfolio_catalog_indexes_repo_aliases(tmp_path: Path): + path = tmp_path / "portfolio-catalog.yaml" + path.write_text( + """ +repos: + Signal & Noise: + owner: d + purpose: public portfolio surface + lifecycle_state: active + criticality: medium + review_cadence: monthly + intended_disposition: maintain + category: vanity + aliases: + - signal-noise +""", + encoding="utf-8", + ) + + catalog = load_portfolio_catalog(path) + entry = catalog_entry_for_repo( + {"name": "signal-noise", "full_name": "saagpatel/signal-noise", "path": "signal-noise"}, + catalog, + ) + + assert catalog["errors"] == [] + assert entry["has_explicit_entry"] is True + assert entry["catalog_key"] == "Signal & Noise" + assert entry["owner"] == "d" + assert entry["matched_by"] == "path" + + def test_load_portfolio_catalog_normalizes_doctor_standard(tmp_path: Path): path = tmp_path / "portfolio-catalog.yaml" path.write_text( @@ -356,4 +388,4 @@ def test_catalog_line_and_summaries_cover_missing_contracts(): assert catalog_summary["cataloged_repo_count"] == 1 assert catalog_summary["missing_contract_count"] == 1 assert alignment_summary["counts"]["aligned"] == 1 - assert alignment_summary["counts"]["missing-contract"] == 1 + assert alignment_summary["counts"]["missing-contract"] == 1 \ No newline at end of file