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
38 changes: 36 additions & 2 deletions config/portfolio-catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1297,4 +1331,4 @@ repos:
owner: d
lifecycle_state: archived
review_cadence: quarterly
intended_disposition: archive
intended_disposition: archive
17 changes: 16 additions & 1 deletion src/portfolio_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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

Expand Down Expand Up @@ -601,4 +616,4 @@ def build_intent_alignment_summary(audits: list[Any]) -> dict[str, Any]:
return {
"counts": dict(counts),
"summary": summary,
}
}
34 changes: 33 additions & 1 deletion tests/test_portfolio_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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