From 01e697cc433f811b0d8d524852bd787730afd4f5 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 00:36:14 -0700 Subject: [PATCH 01/15] fix(portfolio-truth): reconcile operator attention --- config/portfolio-catalog.yaml | 49 +++++++++++++++-------------- tests/test_portfolio_catalog.py | 56 ++++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/config/portfolio-catalog.yaml b/config/portfolio-catalog.yaml index 5f06fb3..77e344d 100644 --- a/config/portfolio-catalog.yaml +++ b/config/portfolio-catalog.yaml @@ -103,7 +103,7 @@ repos: owner: d purpose: local-first AI governance core with offline enforcement and hash-chained audit exports - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: maintain @@ -132,7 +132,7 @@ repos: ApplyKit: owner: d purpose: deterministic local-first application packet generation - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: maintain category: commercial @@ -227,7 +227,7 @@ repos: MCPAudit: owner: d purpose: local MCP security and drift audit tool - lifecycle_state: manual-only + lifecycle_state: active review_cadence: weekly operating_path: maintain category: infrastructure @@ -278,7 +278,7 @@ repos: operant-public: owner: d purpose: operating-agent calibration benchmark with shipped public MCP access - lifecycle_state: manual-only + lifecycle_state: active criticality: high review_cadence: weekly operating_path: maintain @@ -363,7 +363,7 @@ repos: JobCommandCenter: owner: d purpose: desktop command-center shell for portfolio and operator workflows - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: maintain category: commercial @@ -386,7 +386,7 @@ repos: tool_provenance: claude-code AIWorkFlow: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: monthly operating_path: maintain category: commercial @@ -400,7 +400,7 @@ repos: Phantom Frequencies: owner: d purpose: compact authored supernatural-radio game being finished as an Oddworks release - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: finish @@ -419,7 +419,7 @@ repos: SignalDecay: owner: d purpose: one-room 8-12 minute tactile signal-tuning game for the Oddworks label - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: finish @@ -626,7 +626,7 @@ repos: automation_eligible: true Afterimage: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: maintain category: commercial @@ -648,7 +648,7 @@ repos: Liminal: owner: d purpose: focused atmospheric iOS experience being finished as an Oddworks release - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: finish @@ -710,7 +710,7 @@ repos: category: vanity GPT_RAG: owner: d - lifecycle_state: dormant + lifecycle_state: manual-only review_cadence: monthly operating_path: maintain category: infrastructure @@ -799,7 +799,7 @@ repos: DeepTank: owner: d purpose: authored aquatic simulation game being evaluated and finished for the Oddworks label - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: finish @@ -953,7 +953,7 @@ repos: operating_path: maintain cross-system-smoke: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: maintain category: infrastructure @@ -1079,14 +1079,15 @@ repos: owner: d purpose: public-safe static portfolio index for hiring-manager review lifecycle_state: active + criticality: high review_cadence: monthly operating_path: maintain - category: vanity + category: commercial tool_provenance: claude-code maturity_program: maintain target_maturity: operating automation_eligible: false - notes: Keep manual-only unless code or deployment work is active; resume and custom-domain decisions are mostly external to this repo. + notes: Tier-0 public workshop and canonical saagarpatel.dev product surface. Keep in default attention as the public front door for the core portfolio. rag-knowledge-base: owner: d lifecycle_state: active @@ -1102,7 +1103,7 @@ repos: BattleGrid: owner: d purpose: focused tactical grid game being evaluated and finished for the Oddworks label - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: finish @@ -1114,7 +1115,7 @@ repos: OddworksCabinet: owner: d purpose: evidence-first gallery and release surface for small authored Oddworks experiences - lifecycle_state: active + lifecycle_state: manual-only criticality: high review_cadence: weekly operating_path: finish @@ -1296,7 +1297,7 @@ repos: maturity_program: finish mcpaudit-web: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: finish maturity_program: finish @@ -1484,7 +1485,7 @@ repos: owner: d purpose: private local-first macOS instrument for forecast-backed decision receipts (forecast, deliberate, decide, observe, learn loop) - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: finish category: commercial @@ -1494,7 +1495,7 @@ repos: owner: d purpose: Book Two / Field Manual manuscript workspace holding the complete validation-gated draft - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: finish category: commercial @@ -1518,7 +1519,7 @@ repos: owner: d purpose: upstream-contribution fork of the ccusage CLI (Claude Code usage/cost reporting), kept as an active contribution vehicle - lifecycle_state: active + lifecycle_state: manual-only review_cadence: monthly operating_path: maintain category: infrastructure @@ -1535,7 +1536,7 @@ repos: owner: d purpose: public publishing imprint for difficult systems explained through direct manipulation, simulation, annotation, narrative, and evidence - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: finish category: commercial @@ -1546,10 +1547,12 @@ repos: purpose: public interactive explainer of the personal multi-agent operator OS, deployed at operator-os-explainer.vercel.app lifecycle_state: active + criticality: high review_cadence: monthly operating_path: maintain - category: vanity + category: commercial tool_provenance: claude-code + notes: Tier-0 flagship story for the Operator OS and part of the public workshop product surface. repository-renaissance: owner: d purpose: local-first case system for understanding and planning interventions in diff --git a/tests/test_portfolio_catalog.py b/tests/test_portfolio_catalog.py index b5c2eb0..d5d423e 100644 --- a/tests/test_portfolio_catalog.py +++ b/tests/test_portfolio_catalog.py @@ -176,43 +176,63 @@ def test_live_catalog_reflects_tribunal_reactivation_of_recovery_exclusions() -> assert entry["automation_eligible"] is False -def test_live_catalog_keeps_settled_infrastructure_manual_only() -> None: +def test_live_catalog_matches_operator_attention_reconciliation() -> None: catalog_path = Path(__file__).parents[1] / "config" / "portfolio-catalog.yaml" catalog = load_portfolio_catalog(catalog_path) - # GPT_RAG removed from this cohort by operator ruling 2026-07-17: the tribunal - # found its live-driver claim evidentially false (the JSON trace artifacts its - # README promises exist nowhere in the tree), so it is dormant, not manual-only. + tier_zero = { + "MCPAudit": "infrastructure", + "mcp-trust": "infrastructure", + "bridge-db": "infrastructure", + "GithubRepoAuditor": "infrastructure", + "PortfolioCommandCenter": "infrastructure", + "operant-public": "infrastructure", + "portfolio-index": "commercial", + "operator-os-explainer": "commercial", + } + for repo_name, category in tier_zero.items(): + entry = catalog["repos"][repo_name.lower()] + assert entry["lifecycle_state"] == "active" + assert entry["operating_path"] == "maintain" + assert entry["category"] == category + + # Tier 1, Tier 2, and explicitly unranked projects remain available only when + # the operator asks for them. They do not create default portfolio attention. manual_only = { "_machine/machine-control-tower", "agent-bridge", - "MCPAudit", "knowledgecore", - "operant-public", "mcpforge", + "mcpaudit-web", "notification-hub", + "cross-system-smoke", + "continuity", "cross-provider-egress-guard", "cost-tracker", "portfolio-health", "portfolio-mcp", "Lazarus", - "continuity", "peer-agent-tools", + "AIGCCore", + "ApplyKit", + "JobCommandCenter", + "AIWorkFlow", + "Phantom Frequencies", + "SignalDecay", + "Afterimage", + "Liminal", + "GPT_RAG", + "DeepTank", + "BattleGrid", + "OddworksCabinet", + "Temper", + "book-two-manuscript", + "ccusage", + "manipulable-library", } for repo_name in manual_only: assert catalog["repos"][repo_name.lower()]["lifecycle_state"] == "manual-only" - assert catalog["repos"]["afterimage"]["category"] == "commercial" - for repo_name in ( - "AIGCCore", - "bridge-db", - "GithubRepoAuditor", - "mcp-trust", - "cross-system-smoke", - "PortfolioCommandCenter", - ): - assert catalog["repos"][repo_name.lower()]["lifecycle_state"] == "active" - def test_catalog_entry_matches_full_name_then_bare_name(): catalog = { From 9b20ceec6cf0c6db6fcc45e0b91f863cfd9d0820 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 00:58:18 -0700 Subject: [PATCH 02/15] fix(portfolio-truth): make Tier 0 exclusive --- config/portfolio-catalog.yaml | 2 +- tests/test_portfolio_catalog.py | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/config/portfolio-catalog.yaml b/config/portfolio-catalog.yaml index 77e344d..6497131 100644 --- a/config/portfolio-catalog.yaml +++ b/config/portfolio-catalog.yaml @@ -1090,7 +1090,7 @@ repos: notes: Tier-0 public workshop and canonical saagarpatel.dev product surface. Keep in default attention as the public front door for the core portfolio. rag-knowledge-base: owner: d - lifecycle_state: active + lifecycle_state: manual-only review_cadence: weekly operating_path: maintain category: infrastructure diff --git a/tests/test_portfolio_catalog.py b/tests/test_portfolio_catalog.py index d5d423e..e088d09 100644 --- a/tests/test_portfolio_catalog.py +++ b/tests/test_portfolio_catalog.py @@ -186,7 +186,7 @@ def test_live_catalog_matches_operator_attention_reconciliation() -> None: "bridge-db": "infrastructure", "GithubRepoAuditor": "infrastructure", "PortfolioCommandCenter": "infrastructure", - "operant-public": "infrastructure", + "saagpatel/operant": "infrastructure", "portfolio-index": "commercial", "operator-os-explainer": "commercial", } @@ -196,6 +196,25 @@ def test_live_catalog_matches_operator_attention_reconciliation() -> None: assert entry["operating_path"] == "maintain" assert entry["category"] == category + # OPERANT has one logical identity with two catalog lookup keys: the canonical + # GitHub full name and the local checkout basename. Normalize that alias before + # asserting exclusivity so a duplicate lookup contract is not mistaken for a + # ninth project. + operant_alias = catalog["repos"]["operant-public"] + canonical_operant = catalog["repos"]["saagpatel/operant"] + for field in ("lifecycle_state", "operating_path", "category"): + assert operant_alias[field] == canonical_operant[field] + + attention_aliases = {"operant-public": "saagpatel/operant"} + eligible_catalog_keys = { + attention_aliases.get(repo_name, repo_name) + for repo_name, entry in catalog["repos"].items() + if entry["lifecycle_state"] == "active" + and entry["operating_path"] in {"maintain", "finish"} + and entry["category"] in {"infrastructure", "commercial"} + } + assert eligible_catalog_keys == {name.lower() for name in tier_zero} + # Tier 1, Tier 2, and explicitly unranked projects remain available only when # the operator asks for them. They do not create default portfolio attention. manual_only = { @@ -229,6 +248,7 @@ def test_live_catalog_matches_operator_attention_reconciliation() -> None: "book-two-manuscript", "ccusage", "manipulable-library", + "rag-knowledge-base", } for repo_name in manual_only: assert catalog["repos"][repo_name.lower()]["lifecycle_state"] == "manual-only" From cce9c9e814b665df46c7b273dccac363985d2483 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 01:30:23 -0700 Subject: [PATCH 03/15] fix(portfolio-truth): encode complete Tier 0 policy --- config/portfolio-catalog.yaml | 15 +++- src/portfolio_truth_reconcile.py | 72 +++++++++++++++++-- src/project_registry.py | 4 ++ tests/test_portfolio_catalog.py | 3 +- tests/test_portfolio_truth.py | 118 ++++++++++++++++++++++++++++++- tests/test_project_registry.py | 14 ++++ 6 files changed, 216 insertions(+), 10 deletions(-) diff --git a/config/portfolio-catalog.yaml b/config/portfolio-catalog.yaml index 6497131..acb2734 100644 --- a/config/portfolio-catalog.yaml +++ b/config/portfolio-catalog.yaml @@ -103,7 +103,7 @@ repos: owner: d purpose: local-first AI governance core with offline enforcement and hash-chained audit exports - lifecycle_state: manual-only + lifecycle_state: active criticality: high review_cadence: weekly operating_path: maintain @@ -1480,6 +1480,19 @@ repos: lifecycle_state: archived review_cadence: quarterly operating_path: archive + personal-ops: + owner: d + purpose: local operator control plane for cross-system workflows, approvals, and recovery + lifecycle_state: active + criticality: high + review_cadence: weekly + operating_path: maintain + category: infrastructure + tool_provenance: codex + maturity_program: maintain + target_maturity: operating + automation_eligible: false + notes: Tier-0 Operator OS project. The live checkout is outside the Projects workspace and is represented in generated registry output as supp:personal-ops. # --- 2026-07-17 Portfolio Tribunal: contracts for previously uncataloged KEEP repos --- Temper: owner: d diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index cf6b3c7..101a5ac 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -38,6 +38,7 @@ SecurityFields, display_activity_status, ) +from src.project_registry import DEFAULT_SUPPLEMENTARY from src.registry_parser import _normalize logger = logging.getLogger(__name__) @@ -262,6 +263,9 @@ def build_portfolio_truth_snapshot( now=now, exclusion_counts=exclusion_counts, ) + workspace_projects.extend( + _cataloged_supplementary_projects(catalog_data=catalog_data, now=now) + ) projects = [ _build_truth_project( raw_project, @@ -355,6 +359,54 @@ def build_portfolio_truth_snapshot( ) +def _cataloged_supplementary_projects( + *, catalog_data: dict[str, Any], now: datetime +) -> list[dict[str, Any]]: + """Promote explicitly cataloged repo-less Operator OS identities into truth.""" + + projects: list[dict[str, Any]] = [] + for supplementary in DEFAULT_SUPPLEMENTARY: + name = str(supplementary.get("display_name") or "").strip() + canonical_key = str(supplementary.get("canonical_key") or "").strip() + if not name or not canonical_key: + continue + catalog_entry = catalog_entry_for_repo( + {"name": name, "full_name": name, "path": canonical_key}, + catalog_data, + ) + if not catalog_entry.get("has_explicit_entry"): + continue + projects.append( + { + "name": name, + "project_path": None, + "path": canonical_key, + "top_level_dir": "supplementary", + "group_entry": {}, + "has_git": False, + "repo_full_name": "", + "default_branch": "", + "context_files": [], + "context_quality": "none", + "primary_context_file": "AGENTS.md", + "project_summary_present": False, + "current_state_present": False, + "stack_present": False, + "run_instructions_present": False, + "known_risks_present": False, + "next_recommended_move_present": False, + "missing_context_fields": [], + "supporting_context_files": [], + "stack": ["Unknown"], + "last_meaningful_activity_at": None, + "inferred_tool_provenance": "", + "now": now, + "source": "supplementary-registry", + } + ) + return projects + + def _build_coverage_envelope( *, projects: list[PortfolioTruthProject], @@ -764,10 +816,11 @@ def _build_truth_project( provenance=provenance, readme_char_count=derived_readme_char_count, ) + raw_source = str(raw_project.get("source") or "workspace") provenance["derived.context_quality"] = { - "source": "workspace+catalog" + "source": f"{raw_source}+catalog" if context_quality != raw_context_quality - else "workspace", + else raw_source, "detail": ( f"{raw_context_quality}->{context_quality}" if context_quality != raw_context_quality @@ -896,7 +949,7 @@ def _build_truth_project( automation_eligible=declared_values["automation_eligible"], ) provenance["derived.last_meaningful_activity_at"] = { - "source": "git" if raw_project["has_git"] and last_activity else "workspace", + "source": "git" if raw_project["has_git"] and last_activity else raw_source, "detail": "derived", } provenance["derived.activity_status"] = { @@ -912,15 +965,15 @@ def _build_truth_project( "detail": attention_state, } provenance["derived.stack"] = { - "source": "workspace", + "source": raw_source, "detail": ", ".join(raw_project["stack"]), } provenance["derived.context_files"] = { - "source": "workspace", + "source": raw_source, "detail": str(len(raw_project["context_files"])), } provenance["derived.primary_context_file"] = { - "source": "workspace", + "source": raw_source, "detail": raw_project["primary_context_file"], } for field in ( @@ -932,7 +985,7 @@ def _build_truth_project( "next_recommended_move_present", ): provenance[f"derived.{field}"] = { - "source": "workspace", + "source": raw_source, "detail": str(bool(raw_project[field])).lower(), } @@ -1252,6 +1305,11 @@ def _attention_state_for( return "experiment" if lifecycle_state == "manual-only": return "manual-only" + if lifecycle_state == "active" and operating_path == "maintain": + if category == "infrastructure": + return "active-infra" + if category == "commercial": + return "active-product" if activity_status == "stale": # A declared finish path is itself an unresolved operator decision. It can # remain valid while the default branch is stale (for example, when work is diff --git a/src/project_registry.py b/src/project_registry.py index 0b96f4c..d81fe54 100644 --- a/src/project_registry.py +++ b/src/project_registry.py @@ -437,7 +437,10 @@ def build_project_registry( _Entry(p["identity"], (p.get("declared") or {}).get("lifecycle_state"), "auditor", None) for p in snapshot.get("projects", []) ] + existing_keys = {entry.canonical_key for entry in entries} for supp in supplementary: + if supp["canonical_key"] in existing_keys: + continue entries.append( _Entry( { @@ -451,6 +454,7 @@ def build_project_registry( supp.get("note"), ) ) + existing_keys.add(supp["canonical_key"]) by_key = {e.canonical_key: e for e in entries} index: dict[str, _Entry] = {} diff --git a/tests/test_portfolio_catalog.py b/tests/test_portfolio_catalog.py index e088d09..2bf92e8 100644 --- a/tests/test_portfolio_catalog.py +++ b/tests/test_portfolio_catalog.py @@ -186,7 +186,9 @@ def test_live_catalog_matches_operator_attention_reconciliation() -> None: "bridge-db": "infrastructure", "GithubRepoAuditor": "infrastructure", "PortfolioCommandCenter": "infrastructure", + "personal-ops": "infrastructure", "saagpatel/operant": "infrastructure", + "AIGCCore": "infrastructure", "portfolio-index": "commercial", "operator-os-explainer": "commercial", } @@ -232,7 +234,6 @@ def test_live_catalog_matches_operator_attention_reconciliation() -> None: "portfolio-mcp", "Lazarus", "peer-agent-tools", - "AIGCCore", "ApplyKit", "JobCommandCenter", "AIWorkFlow", diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index b2d930b..121a5d3 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -30,6 +30,7 @@ load_safe_notion_project_context, ) from src.portfolio_truth_validate import validate_portfolio_report_markdown +from src.project_registry import build_project_registry from src.registry_parser import parse_registry @@ -450,6 +451,121 @@ def test_truth_snapshot_respects_declared_and_derived_fields( assert "open_high_critical" in snapshot_dict["projects"][0]["security"] +def test_live_catalog_produces_exact_tier_zero_attention_semantics( + tmp_path: Path, +) -> None: + """Pin the operator's Tier 0 policy at generated-output level. + + personal-ops lives outside the audited Projects workspace, so portfolio truth + carries the nine repo-backed logical identities while the generated canonical + project registry carries personal-ops as its established supplementary identity. + """ + + workspace = tmp_path / "workspace" + workspace.mkdir() + now = datetime(2026, 7, 18, 12, tzinfo=timezone.utc) + repo_projects = { + "MCPAudit": None, + "mcp-trust": None, + "bridge-db": None, + "GithubRepoAuditor": None, + "PortfolioCommandCenter": None, + "operant-public": "saagpatel/operant", + "AIGCCore": None, + "portfolio-index": None, + "operator-os-explainer": None, + } + supporting_or_retired = ( + "mcpaudit-web", + "mcpforge", + "notification-hub", + "cross-system-smoke", + "continuity", + "ApplyKit", + ) + + for name, remote in { + **repo_projects, + **dict.fromkeys(supporting_or_retired), + }.items(): + project = workspace / name + project.mkdir() + readme = project / "README.md" + _write(readme, f"# {name}\n\nTier policy fixture.\n") + _set_mtime(readme, now.timestamp()) + if remote: + subprocess.run( + ["git", "init"], + cwd=project, + capture_output=True, + check=True, + ) + subprocess.run( + [ + "git", + "remote", + "add", + "origin", + f"https://github.com/{remote}.git", + ], + cwd=project, + capture_output=True, + check=True, + ) + + result = build_portfolio_truth_snapshot( + workspace_root=workspace, + catalog_path=Path(__file__).parents[1] / "config" / "portfolio-catalog.yaml", + include_notion=False, + now=now, + ) + active = {} + by_display_name = {} + for project in result.snapshot.projects: + by_display_name[project.identity.display_name] = project + if project.derived.attention_state not in {"active-infra", "active-product"}: + continue + logical_key = ( + project.identity.repo_full_name or project.identity.display_name + ) + active[logical_key] = project.derived.attention_state + + assert active == { + "MCPAudit": "active-infra", + "mcp-trust": "active-infra", + "bridge-db": "active-infra", + "GithubRepoAuditor": "active-infra", + "PortfolioCommandCenter": "active-infra", + "personal-ops": "active-infra", + "saagpatel/operant": "active-infra", + "AIGCCore": "active-infra", + "portfolio-index": "active-product", + "operator-os-explainer": "active-product", + } + for name in supporting_or_retired: + assert by_display_name[name].derived.attention_state == "manual-only" + + registry = build_project_registry( + result.snapshot.to_dict(), + overrides_config_path=None, + ) + registry_by_key = { + entry["canonical_key"]: entry for entry in registry["entries"] + } + assert registry_by_key["supp:personal-ops"]["lifecycle_state"] == "active" + assert ( + result.catalog_data["repos"]["personal-ops"]["lifecycle_state"] == "active" + ) + personal_ops = by_display_name["personal-ops"] + assert personal_ops.identity.project_key == "supp:personal-ops" + assert personal_ops.derived.activity_status == "stale" + assert personal_ops.derived.attention_state == "active-infra" + assert ( + personal_ops.provenance["derived.context_quality"]["source"] + == "supplementary-registry" + ) + + def test_truth_snapshot_operating_path_catalog_field_matches_legacy_disposition( portfolio_workspace: Path, legacy_registry: Path, @@ -639,7 +755,7 @@ def test_attention_state_classifier_separates_activity_from_operator_attention() path_override="", risk_entry={"security_risk": False}, ) - == "parked" + == "active-product" ) diff --git a/tests/test_project_registry.py b/tests/test_project_registry.py index 9fd0a07..dc50ea2 100644 --- a/tests/test_project_registry.py +++ b/tests/test_project_registry.py @@ -76,6 +76,20 @@ def test_build_includes_supplementary_projects_from_defaults(): assert registry["entry_count"] == len(SNAPSHOT["projects"]) + 2 +def test_build_does_not_duplicate_supplementary_project_promoted_into_truth(): + snapshot = _snapshot(_ident("supp:personal-ops", "personal-ops", None)) + + registry = build_project_registry(snapshot, overrides_config_path=None) + personal_ops = [ + entry + for entry in registry["entries"] + if entry["canonical_key"] == "supp:personal-ops" + ] + + assert len(personal_ops) == 1 + assert personal_ops[0]["source"] == "auditor" + + def test_supp_key_is_emitted_for_repo_less_entries_only(): # A repo-backed and a repo-less auditor project side by side. snapshot = _snapshot( From b8fa89bb1bc04f4e4f0e669926ee92b76c19bb1e Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 02:18:43 -0700 Subject: [PATCH 04/15] fix(security): align GitHub cohort with Tier 0 --- src/github_security_coverage.py | 47 +++++++++++++++---- tests/test_github_security_coverage.py | 63 +++++++++++++++++++++----- 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/src/github_security_coverage.py b/src/github_security_coverage.py index 4305780..a4b9043 100644 --- a/src/github_security_coverage.py +++ b/src/github_security_coverage.py @@ -26,6 +26,7 @@ DEFAULT_ATTENTION_STATES = frozenset( {"active-product", "active-infra", "decision-needed"} ) +DEFAULT_EXPECTED_GITHUB_COHORT_COUNT = 9 PROVIDER_NAMES = ("dependabot", "code_scanning", "secret_scanning") ELIGIBILITY_SOURCE = "github-account-repository-preflight-v1" ELIGIBILITY_REASON = "private_user_repo_plan_unavailable" @@ -151,9 +152,9 @@ def _canonical_repo(value: Any) -> str: def derive_default_attention_cohort( portfolio_truth: dict[str, Any], *, - expected_count: int = 16, + expected_count: int = DEFAULT_EXPECTED_GITHUB_COHORT_COUNT, ) -> tuple[str, ...]: - """Return the canonical default-attention GitHub cohort, failing on expansion.""" + """Return the repo-backed default-attention cohort, failing on expansion.""" repos: list[str] = [] for project in portfolio_truth.get("projects") or []: if not isinstance(project, dict): @@ -162,7 +163,14 @@ def derive_default_attention_cohort( if derived.get("attention_state") not in DEFAULT_ATTENTION_STATES: continue identity = _mapping(project.get("identity")) - repos.append(_canonical_repo(identity.get("repo_full_name"))) + repo_full_name = identity.get("repo_full_name") + if not _text(repo_full_name) and _text(identity.get("project_key")).startswith( + "supp:" + ): + # Supplementary projects such as personal-ops are real portfolio + # identities, but they do not have a GitHub repository to query. + continue + repos.append(_canonical_repo(repo_full_name)) if len({repo.lower() for repo in repos}) != len(repos): raise SecurityCoverageError( "default-attention cohort contains duplicate canonical repositories" @@ -729,7 +737,7 @@ def collect_security_coverage( portfolio_truth: dict[str, Any], *, token: str | None, - expected_cohort_count: int = 16, + expected_cohort_count: int = DEFAULT_EXPECTED_GITHUB_COHORT_COUNT, base_request_limit: int = DEFAULT_BASE_REQUEST_LIMIT, total_request_limit: int = DEFAULT_TOTAL_REQUEST_LIMIT, quota_reserve: int = DEFAULT_QUOTA_RESERVE, @@ -754,6 +762,7 @@ def collect_security_coverage( validate_security_coverage_receipt( prior_receipt, max_age_hours=24 * 365, + expected_cohort_count=expected_cohort_count, now=now, ) cohort = derive_default_attention_cohort( @@ -1131,7 +1140,7 @@ def validate_security_coverage_receipt( payload: Any, *, max_age_hours: int = 24, - expected_cohort_count: int = 16, + expected_cohort_count: int = DEFAULT_EXPECTED_GITHUB_COHORT_COUNT, now: datetime | None = None, source_path: str = "", ) -> LoadedSecurityCoverage: @@ -1264,6 +1273,7 @@ def load_security_coverage_receipt( path: Path, *, max_age_hours: int = 24, + expected_cohort_count: int = DEFAULT_EXPECTED_GITHUB_COHORT_COUNT, now: datetime | None = None, ) -> LoadedSecurityCoverage: try: @@ -1279,14 +1289,24 @@ def load_security_coverage_receipt( return validate_security_coverage_receipt( payload, max_age_hours=max_age_hours, + expected_cohort_count=expected_cohort_count, now=now, source_path=str(path), ) -def write_security_coverage_receipt(payload: dict[str, Any], path: Path) -> None: +def write_security_coverage_receipt( + payload: dict[str, Any], + path: Path, + *, + expected_cohort_count: int = DEFAULT_EXPECTED_GITHUB_COHORT_COUNT, +) -> None: """Atomically write a validated receipt payload.""" - validate_security_coverage_receipt(payload, max_age_hours=24 * 365) + validate_security_coverage_receipt( + payload, + max_age_hours=24 * 365, + expected_cohort_count=expected_cohort_count, + ) path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_name(f".{path.name}.tmp") temporary.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") @@ -1330,7 +1350,11 @@ def main() -> None: default=24, help="Freshness window used by --validate-only (default: 24)", ) - parser.add_argument("--expected-cohort-count", type=int, default=16) + parser.add_argument( + "--expected-cohort-count", + type=int, + default=DEFAULT_EXPECTED_GITHUB_COHORT_COUNT, + ) parser.add_argument( "--base-request-limit", type=int, default=DEFAULT_BASE_REQUEST_LIMIT ) @@ -1345,6 +1369,7 @@ def main() -> None: loaded = load_security_coverage_receipt( args.output, max_age_hours=args.max_age_hours, + expected_cohort_count=args.expected_cohort_count, ) print( json.dumps( @@ -1370,7 +1395,11 @@ def main() -> None: quota_reserve=args.quota_reserve, prior_receipt=prior, ) - write_security_coverage_receipt(receipt, args.output) + write_security_coverage_receipt( + receipt, + args.output, + expected_cohort_count=args.expected_cohort_count, + ) except SecurityCoverageError as exc: raise SystemExit(str(exc)) from exc print( diff --git a/tests/test_github_security_coverage.py b/tests/test_github_security_coverage.py index a562361..3ac7ffc 100644 --- a/tests/test_github_security_coverage.py +++ b/tests/test_github_security_coverage.py @@ -10,6 +10,7 @@ from src.github_security_coverage import ( DEFAULT_BASE_REQUEST_LIMIT, + DEFAULT_EXPECTED_GITHUB_COHORT_COUNT, GITHUB_SECURITY_RECEIPT_FILENAME, SecurityCoverageError, collect_security_coverage, @@ -83,10 +84,12 @@ def _collect( *, session: _Session | None = None, prior: dict[str, Any] | None = None, + cohort_count: int = 16, ) -> dict[str, Any]: return collect_security_coverage( - _truth(), + _truth(cohort_count), token="opaque-test-token", + expected_cohort_count=cohort_count, session=session or _Session(), prior_receipt=prior, now=NOW, @@ -142,12 +145,37 @@ def _eligibility_responses(count: int = 6) -> list[_Response]: def test_default_attention_cohort_is_exact_and_fail_closed() -> None: - cohort = derive_default_attention_cohort(_truth()) + truth = _truth(DEFAULT_EXPECTED_GITHUB_COHORT_COUNT) + truth["projects"].append( + { + "identity": { + "project_key": "supp:personal-ops", + "repo_full_name": "", + }, + "derived": {"attention_state": "active-infra"}, + } + ) + cohort = derive_default_attention_cohort(truth) - assert len(cohort) == 16 + assert len(cohort) == DEFAULT_EXPECTED_GITHUB_COHORT_COUNT assert "owner/parked" not in cohort - with pytest.raises(SecurityCoverageError, match="expected 16, observed 17"): - derive_default_attention_cohort(_truth(17)) + with pytest.raises(SecurityCoverageError, match="expected 9, observed 10"): + derive_default_attention_cohort(_truth(10)) + + +def test_repo_less_non_supplementary_attention_identity_fails_closed() -> None: + truth = _truth(DEFAULT_EXPECTED_GITHUB_COHORT_COUNT) + truth["projects"].append( + { + "identity": {"project_key": "repo:missing", "repo_full_name": ""}, + "derived": {"attention_state": "active-infra"}, + } + ) + + with pytest.raises( + SecurityCoverageError, match="invalid canonical repository name" + ): + derive_default_attention_cohort(truth) def test_no_token_never_attempts_collection() -> None: @@ -178,6 +206,8 @@ def test_validate_only_requires_no_token_or_network( str(path), "--max-age-hours", "24", + "--expected-cohort-count", + "16", ], ) @@ -238,7 +268,9 @@ def test_incremental_eligibility_preflight_skips_plan_blocked_providers() -> Non assert providers[provider]["http_status"] == 200 assert providers[provider]["http_classification"] == "eligibility" assert providers[provider]["counts"] is None - validate_security_coverage_receipt(receipt, now=NOW) + validate_security_coverage_receipt( + receipt, expected_cohort_count=16, now=NOW + ) def test_eligibility_claim_requires_matching_embedded_provenance() -> None: @@ -253,7 +285,9 @@ def test_eligibility_claim_requires_matching_embedded_provenance() -> None: SecurityCoverageError, match="eligibility-based unavailability is unproven", ): - validate_security_coverage_receipt(receipt, now=NOW) + validate_security_coverage_receipt( + receipt, expected_cohort_count=16, now=NOW + ) def test_malformed_eligibility_preflight_falls_back_without_exceeding_budget() -> None: @@ -437,7 +471,9 @@ def test_stale_provider_observation_becomes_unknown_count() -> None: provider = receipt["repositories"]["owner/repo-00"]["providers"]["dependabot"] provider["observed_at"] = (NOW - timedelta(hours=25)).isoformat() - loaded = validate_security_coverage_receipt(receipt, now=NOW) + loaded = validate_security_coverage_receipt( + receipt, expected_cohort_count=16, now=NOW + ) normalized = loaded.entries_by_full_name["owner/repo-00"]["providers"]["dependabot"] assert normalized["state"] == "stale" @@ -447,7 +483,7 @@ def test_stale_provider_observation_becomes_unknown_count() -> None: def test_receipt_loader_uses_embedded_provenance_not_newer_mtime( tmp_path: Path, ) -> None: - receipt = _collect() + receipt = _collect(cohort_count=DEFAULT_EXPECTED_GITHUB_COHORT_COUNT) canonical = tmp_path / GITHUB_SECURITY_RECEIPT_FILENAME canonical.write_text(json.dumps(receipt)) decoy = tmp_path / "github-security-coverage-newer.json" @@ -458,7 +494,10 @@ def test_receipt_loader_uses_embedded_provenance_not_newer_mtime( assert loaded is not None assert loaded.source_path == str(canonical) - assert len(loaded.entries_by_full_name) == 16 + assert ( + len(loaded.entries_by_full_name) + == DEFAULT_EXPECTED_GITHUB_COHORT_COUNT + ) def test_receipt_provenance_and_provider_timestamps_fail_closed(tmp_path: Path) -> None: @@ -474,7 +513,9 @@ def test_receipt_provenance_and_provider_timestamps_fail_closed(tmp_path: Path) provider = receipt["repositories"]["owner/repo-00"]["providers"]["dependabot"] provider["observed_at"] = (NOW + timedelta(minutes=1)).isoformat() with pytest.raises(SecurityCoverageError, match="later than receipt produced_at"): - validate_security_coverage_receipt(receipt, now=NOW) + validate_security_coverage_receipt( + receipt, expected_cohort_count=16, now=NOW + ) def test_canonical_receipt_join_does_not_fall_back_to_repo_basename() -> None: From 1ea946c603c943b6adf3581507219d6d98999b7f Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 02:28:07 -0700 Subject: [PATCH 05/15] fix(security): handle repo-less and prior cohort transitions --- src/github_security_coverage.py | 14 ++++++++++++- src/portfolio_truth_reconcile.py | 2 +- tests/test_github_security_coverage.py | 29 ++++++++++++++++++++++++++ tests/test_portfolio_truth.py | 5 +++-- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/github_security_coverage.py b/src/github_security_coverage.py index a4b9043..9bb31dc 100644 --- a/src/github_security_coverage.py +++ b/src/github_security_coverage.py @@ -759,12 +759,24 @@ def collect_security_coverage( ): raise SecurityCoverageError("request budget limits exceed the bounded contract") if prior_receipt is not None: + prior_expected_count = _mapping(prior_receipt.get("cohort")).get( + "expected_count" + ) + if not isinstance(prior_expected_count, int): + raise SecurityCoverageError( + "prior receipt cohort expected_count is invalid" + ) validate_security_coverage_receipt( prior_receipt, max_age_hours=24 * 365, - expected_cohort_count=expected_cohort_count, + expected_cohort_count=prior_expected_count, now=now, ) + if prior_expected_count != expected_cohort_count: + # A valid receipt for the previous bounded cohort cannot safely + # supply conditional-request or eligibility hints for the new one. + # Ignore it so the policy transition can produce fresh evidence. + prior_receipt = None cohort = derive_default_attention_cohort( portfolio_truth, expected_count=expected_cohort_count ) diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index 101a5ac..fe7efba 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -924,7 +924,7 @@ def _build_truth_project( "active-product", "active-infra", "decision-needed", - }: + } and str(raw_project.get("repo_full_name") or "").strip(): security = replace( security, cohort_member=True, diff --git a/tests/test_github_security_coverage.py b/tests/test_github_security_coverage.py index 3ac7ffc..1c02761 100644 --- a/tests/test_github_security_coverage.py +++ b/tests/test_github_security_coverage.py @@ -187,6 +187,35 @@ def test_no_token_never_attempts_collection() -> None: assert session.calls == [] +def test_valid_prior_for_old_cohort_is_ignored_during_contraction() -> None: + old_prior = _collect(cohort_count=16) + session = _Session() + + receipt = _collect( + session=session, + prior=old_prior, + cohort_count=DEFAULT_EXPECTED_GITHUB_COHORT_COUNT, + ) + + assert receipt["cohort"]["repository_count"] == 9 + assert len(session.calls) == 27 + assert all( + kwargs.get("headers") == {} + for _, kwargs in session.calls + ) + + +def test_invalid_prior_for_old_cohort_still_fails_closed() -> None: + old_prior = _collect(cohort_count=16) + old_prior["producer"]["commit"] = "invalid" + + with pytest.raises(SecurityCoverageError, match="producer commit"): + _collect( + prior=old_prior, + cohort_count=DEFAULT_EXPECTED_GITHUB_COHORT_COUNT, + ) + + def test_validate_only_requires_no_token_or_network( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 121a5d3..6e716ce 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -436,8 +436,8 @@ def test_truth_snapshot_respects_declared_and_derived_fields( "total_open_high", "total_open_critical", } - assert rollups["security"]["cohort_repository_count"] == 1 - assert rollups["security"]["cohort_unknown_count"] == 1 + assert rollups["security"]["cohort_repository_count"] == 0 + assert rollups["security"]["cohort_unknown_count"] == 0 assert rollups["security"]["cohort_complete_count"] == 0 assert set(rollups["decision"]) == { "decision_needed_count", @@ -560,6 +560,7 @@ def test_live_catalog_produces_exact_tier_zero_attention_semantics( assert personal_ops.identity.project_key == "supp:personal-ops" assert personal_ops.derived.activity_status == "stale" assert personal_ops.derived.attention_state == "active-infra" + assert personal_ops.security.cohort_member is False assert ( personal_ops.provenance["derived.context_quality"]["source"] == "supplementary-registry" From 06f2531ddcbdbfaea78abd57c10aaac1c5fc3c21 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 02:35:18 -0700 Subject: [PATCH 06/15] fix(portfolio): preserve groups and security decisions --- src/portfolio_truth_reconcile.py | 13 ++++++++++--- tests/test_portfolio_truth.py | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index fe7efba..67016c0 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -382,7 +382,11 @@ def _cataloged_supplementary_projects( "project_path": None, "path": canonical_key, "top_level_dir": "supplementary", - "group_entry": {}, + "group_entry": { + "group_key": str( + supplementary.get("group_key") or "operator_infra" + ) + }, "has_git": False, "repo_full_name": "", "default_branch": "", @@ -726,6 +730,9 @@ def _build_truth_project( ) -> PortfolioTruthProject: relative_path = raw_project["path"] group_entry = group_entry_for_path(relative_path, catalog_data) + supplementary_group = raw_project.get("group_entry") + if isinstance(supplementary_group, dict): + group_entry = {**group_entry, **supplementary_group} repo_entry = catalog_entry_for_repo( { "name": raw_project["name"], @@ -1305,6 +1312,8 @@ def _attention_state_for( return "experiment" if lifecycle_state == "manual-only": return "manual-only" + if risk_entry.get("security_risk"): + return "decision-needed" if lifecycle_state == "active" and operating_path == "maintain": if category == "infrastructure": return "active-infra" @@ -1316,8 +1325,6 @@ def _attention_state_for( # on a release branch or waiting at a human/publication gate), so do not # silently collapse it back into the parked pool. return "decision-needed" if operating_path == "finish" else "parked" - if risk_entry.get("security_risk"): - return "decision-needed" if activity_status in {"active", "recent"} and operating_path in { "maintain", "finish", diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 6e716ce..b47f81e 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -553,6 +553,7 @@ def test_live_catalog_produces_exact_tier_zero_attention_semantics( entry["canonical_key"]: entry for entry in registry["entries"] } assert registry_by_key["supp:personal-ops"]["lifecycle_state"] == "active" + assert registry_by_key["supp:personal-ops"]["group_key"] == "operator_infra" assert ( result.catalog_data["repos"]["personal-ops"]["lifecycle_state"] == "active" ) @@ -662,6 +663,30 @@ def test_attention_state_classifier_separates_activity_from_operator_attention() ) == "active-infra" ) + assert ( + _attention_state_for( + activity_status="active", + archived=False, + lifecycle_state="active", + operating_path="maintain", + category="commercial", + path_override="", + risk_entry={"security_risk": True}, + ) + == "decision-needed" + ) + assert ( + _attention_state_for( + activity_status="recent", + archived=False, + lifecycle_state="active", + operating_path="maintain", + category="infrastructure", + path_override="", + risk_entry={"security_risk": True}, + ) + == "decision-needed" + ) assert ( _attention_state_for( activity_status="active", From 0f7fc8cc4bff3437e22056688a7e2a4eb349ce66 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 02:44:17 -0700 Subject: [PATCH 07/15] fix(portfolio): separate supplementary evidence and dormant state --- config/portfolio-catalog.yaml | 2 +- src/portfolio_truth_reconcile.py | 24 ++++++++++++++++++++++-- tests/test_portfolio_catalog.py | 2 +- tests/test_portfolio_truth.py | 17 +++++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/config/portfolio-catalog.yaml b/config/portfolio-catalog.yaml index acb2734..0bbc714 100644 --- a/config/portfolio-catalog.yaml +++ b/config/portfolio-catalog.yaml @@ -710,7 +710,7 @@ repos: category: vanity GPT_RAG: owner: d - lifecycle_state: manual-only + lifecycle_state: dormant review_cadence: monthly operating_path: maintain category: infrastructure diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index 67016c0..5a17b32 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -417,6 +417,11 @@ def _build_coverage_envelope( notion_context_carried_forward: bool, notion_context_rows: int, ) -> list[dict[str, Any]]: + workspace_project_count = sum( + project.identity.top_level_dir != "supplementary" + for project in projects + ) + supplementary_project_count = len(projects) - workspace_project_count complete = sum( project.security.coverage_state == "complete" for project in projects ) @@ -447,8 +452,12 @@ def _build_coverage_envelope( git_observed = sum( project.repository_state.get("state") == "observed" for project in projects ) - return [ - {"source": "workspace", "state": "observed", "project_count": len(projects)}, + coverage = [ + { + "source": "workspace", + "state": "observed", + "project_count": workspace_project_count, + }, { "source": "git", "state": "observed" if git_observed else "unknown", @@ -487,6 +496,15 @@ def _build_coverage_envelope( "observed_count": notion_context_rows, }, ] + if supplementary_project_count: + coverage.append( + { + "source": "supplementary_registry", + "state": "observed", + "project_count": supplementary_project_count, + } + ) + return coverage def _build_input_envelope( @@ -1312,6 +1330,8 @@ def _attention_state_for( return "experiment" if lifecycle_state == "manual-only": return "manual-only" + if lifecycle_state == "dormant": + return "parked" if risk_entry.get("security_risk"): return "decision-needed" if lifecycle_state == "active" and operating_path == "maintain": diff --git a/tests/test_portfolio_catalog.py b/tests/test_portfolio_catalog.py index 2bf92e8..fda2550 100644 --- a/tests/test_portfolio_catalog.py +++ b/tests/test_portfolio_catalog.py @@ -241,7 +241,6 @@ def test_live_catalog_matches_operator_attention_reconciliation() -> None: "SignalDecay", "Afterimage", "Liminal", - "GPT_RAG", "DeepTank", "BattleGrid", "OddworksCabinet", @@ -253,6 +252,7 @@ def test_live_catalog_matches_operator_attention_reconciliation() -> None: } for repo_name in manual_only: assert catalog["repos"][repo_name.lower()]["lifecycle_state"] == "manual-only" + assert catalog["repos"]["gpt_rag"]["lifecycle_state"] == "dormant" def test_catalog_entry_matches_full_name_then_bare_name(): diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index b47f81e..6ca0436 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -554,6 +554,11 @@ def test_live_catalog_produces_exact_tier_zero_attention_semantics( } assert registry_by_key["supp:personal-ops"]["lifecycle_state"] == "active" assert registry_by_key["supp:personal-ops"]["group_key"] == "operator_infra" + coverage_by_source = { + row["source"]: row for row in result.snapshot.coverage + } + assert coverage_by_source["workspace"]["project_count"] == 15 + assert coverage_by_source["supplementary_registry"]["project_count"] == 1 assert ( result.catalog_data["repos"]["personal-ops"]["lifecycle_state"] == "active" ) @@ -699,6 +704,18 @@ def test_attention_state_classifier_separates_activity_from_operator_attention() ) == "manual-only" ) + assert ( + _attention_state_for( + activity_status="active", + archived=False, + lifecycle_state="dormant", + operating_path="maintain", + category="infrastructure", + path_override="", + risk_entry={"security_risk": False}, + ) + == "parked" + ) assert ( _attention_state_for( activity_status="active", From 6ebeeef3f49725571ae8a24362d2feff91f8935c Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 02:54:43 -0700 Subject: [PATCH 08/15] fix(portfolio): keep stale security alerts actionable --- src/portfolio_risk.py | 15 +++++++++++---- tests/test_portfolio_risk.py | 12 ++++++------ tests/test_portfolio_truth.py | 12 ++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/portfolio_risk.py b/src/portfolio_risk.py index 5651bf8..4feb6d4 100644 --- a/src/portfolio_risk.py +++ b/src/portfolio_risk.py @@ -95,16 +95,23 @@ def build_risk_entry( if criticality in {"high", "critical"} and not known_risks_present: factors.append("undocumented-risks") - # Active repo carrying open high- or critical-severity Dependabot alerts. + # A currently active repo, or a stale repo intentionally kept on the maintain + # path, carrying open high- or critical-severity Dependabot alerts. # High alerts contribute one normal factor toward the 3+ elevation threshold; # an open critical alert force-elevates on its own (see is_elevated below) — a # lone unpatched critical CVE cannot hide in an otherwise-clean repo. - active = activity_status in ACTIVE_STATUSES - if active and (security_high_alerts > 0 or security_critical_alerts > 0): + security_relevant = ( + activity_status in ACTIVE_STATUSES or operating_path == "maintain" + ) + if security_relevant and ( + security_high_alerts > 0 or security_critical_alerts > 0 + ): factors.append("active-high-severity-alerts") # Derive tier - security_forces_elevated = active and security_critical_alerts > 0 + security_forces_elevated = ( + security_relevant and security_critical_alerts > 0 + ) is_elevated = ( len(factors) >= 3 or ("weak-context-active" in factors and "investigate-override" in factors) diff --git a/tests/test_portfolio_risk.py b/tests/test_portfolio_risk.py index cdec18c..fa74567 100644 --- a/tests/test_portfolio_risk.py +++ b/tests/test_portfolio_risk.py @@ -148,9 +148,9 @@ def test_security_no_alerts_leaves_security_risk_false(): assert "active-high-severity-alerts" not in result["risk_factors"] -def test_security_alerts_ignored_when_not_active(): - # Alerts on a stale (non-active) repo on the maintain path do not fire the - # factor or force elevation — the factor is gated on active status, like the others. +def test_security_alerts_on_stale_maintain_path_remain_actionable(): + # A stale repo deliberately kept on the maintain path still needs a security + # decision when receipt-backed high/critical alerts remain open. result = build_risk_entry( **_baseline_kwargs( activity_status="stale", @@ -159,9 +159,9 @@ def test_security_alerts_ignored_when_not_active(): security_high_alerts=5, ) ) - assert result["risk_tier"] != "elevated" - assert result["security_risk"] is False - assert "active-high-severity-alerts" not in result["risk_factors"] + assert result["risk_tier"] == "elevated" + assert result["security_risk"] is True + assert "active-high-severity-alerts" in result["risk_factors"] def test_security_alerts_do_not_override_deferred_short_circuit(): diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 6ca0436..50f6bda 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -680,6 +680,18 @@ def test_attention_state_classifier_separates_activity_from_operator_attention() ) == "decision-needed" ) + assert ( + _attention_state_for( + activity_status="stale", + archived=False, + lifecycle_state="active", + operating_path="maintain", + category="commercial", + path_override="", + risk_entry={"security_risk": True}, + ) + == "decision-needed" + ) assert ( _attention_state_for( activity_status="recent", From 5548b8b753e0abb6580d38c998fd91f438b39dcd Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 03:05:14 -0700 Subject: [PATCH 09/15] Exclude supplementary identities from security totals --- src/portfolio_truth_reconcile.py | 34 +++++++++------ src/portfolio_truth_types.py | 73 ++++++++++++++++---------------- tests/test_portfolio_truth.py | 5 +++ 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index 5a17b32..e3a6101 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -417,35 +417,41 @@ def _build_coverage_envelope( notion_context_carried_forward: bool, notion_context_rows: int, ) -> list[dict[str, Any]]: - workspace_project_count = sum( - project.identity.top_level_dir != "supplementary" + workspace_projects = [ + project for project in projects - ) + if project.identity.top_level_dir != "supplementary" + ] + workspace_project_count = len(workspace_projects) supplementary_project_count = len(projects) - workspace_project_count complete = sum( - project.security.coverage_state == "complete" for project in projects + project.security.coverage_state == "complete" for project in workspace_projects + ) + partial = sum( + project.security.coverage_state == "partial" for project in workspace_projects ) - partial = sum(project.security.coverage_state == "partial" for project in projects) - stale = sum(project.security.coverage_state == "stale" for project in projects) - unknown = len(projects) - complete - partial - stale - cohort_count = sum(project.security.cohort_member for project in projects) + stale = sum( + project.security.coverage_state == "stale" for project in workspace_projects + ) + unknown = workspace_project_count - complete - partial - stale + cohort_count = sum(project.security.cohort_member for project in workspace_projects) cohort_complete = sum( project.security.cohort_member and project.security.coverage_state == "complete" - for project in projects + for project in workspace_projects ) cohort_partial = sum( project.security.cohort_member and project.security.coverage_state == "partial" - for project in projects + for project in workspace_projects ) cohort_stale = sum( project.security.cohort_member and project.security.coverage_state == "stale" - for project in projects + for project in workspace_projects ) cohort_unknown = cohort_count - cohort_complete - cohort_partial - cohort_stale provider_counts = { provider: sum( project.security.provider_state(provider) == "observed" - for project in projects + for project in workspace_projects ) for provider in ("dependabot", "code_scanning", "secret_scanning") } @@ -468,7 +474,7 @@ def _build_coverage_envelope( "source": "github_security", "state": ( "known" - if complete == len(projects) + if complete == workspace_project_count else "partial" if complete or partial else "unknown" @@ -484,7 +490,7 @@ def _build_coverage_envelope( "cohort_stale_count": cohort_stale, "cohort_unknown_count": cohort_unknown, "provider_observed_counts": provider_counts, - "project_count": len(projects), + "project_count": workspace_project_count, }, { "source": "notion", diff --git a/src/portfolio_truth_types.py b/src/portfolio_truth_types.py index c769a7e..0191545 100644 --- a/src/portfolio_truth_types.py +++ b/src/portfolio_truth_types.py @@ -308,43 +308,44 @@ def from_projects( tier = project.risk.risk_tier if tier in risk_tier_counts: risk_tier_counts[tier] += 1 - security = project.security - if security.cohort_member: - cohort_repository_count += 1 - provider_states = { - provider: security.provider_state(provider) - for provider in ("dependabot", "code_scanning", "secret_scanning") - } - dependabot_observed = provider_states["dependabot"] == "observed" - if dependabot_observed: - dependabot_observed_count += 1 - if security.open_high_critical > 0: - repos_with_open_high_critical += 1 - total_open_high += security.dependabot_high or 0 - total_open_critical += security.dependabot_critical or 0 - if provider_states["code_scanning"] == "observed": - code_scanning_observed_count += 1 - if provider_states["secret_scanning"] == "observed": - secret_scanning_observed_count += 1 - if security.coverage_state == "complete": - complete_repo_count += 1 - scanned_count += 1 + if project.identity.top_level_dir != "supplementary": + security = project.security if security.cohort_member: - cohort_complete_count += 1 - elif security.coverage_state == "partial": - partial_repo_count += 1 - if security.cohort_member: - cohort_partial_count += 1 - elif security.coverage_state == "stale": - stale_count += 1 - if security.cohort_member: - cohort_stale_count += 1 - else: - unknown_count += 1 - if security.cohort_member: - cohort_unknown_count += 1 - if not security.alerts_available: - unavailable_count += 1 + cohort_repository_count += 1 + provider_states = { + provider: security.provider_state(provider) + for provider in ("dependabot", "code_scanning", "secret_scanning") + } + dependabot_observed = provider_states["dependabot"] == "observed" + if dependabot_observed: + dependabot_observed_count += 1 + if security.open_high_critical > 0: + repos_with_open_high_critical += 1 + total_open_high += security.dependabot_high or 0 + total_open_critical += security.dependabot_critical or 0 + if provider_states["code_scanning"] == "observed": + code_scanning_observed_count += 1 + if provider_states["secret_scanning"] == "observed": + secret_scanning_observed_count += 1 + if security.coverage_state == "complete": + complete_repo_count += 1 + scanned_count += 1 + if security.cohort_member: + cohort_complete_count += 1 + elif security.coverage_state == "partial": + partial_repo_count += 1 + if security.cohort_member: + cohort_partial_count += 1 + elif security.coverage_state == "stale": + stale_count += 1 + if security.cohort_member: + cohort_stale_count += 1 + else: + unknown_count += 1 + if security.cohort_member: + cohort_unknown_count += 1 + if not security.alerts_available: + unavailable_count += 1 attention = project.derived.attention_state if attention == "decision-needed": decision_needed_count += 1 diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 50f6bda..2eccca4 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -559,6 +559,11 @@ def test_live_catalog_produces_exact_tier_zero_attention_semantics( } assert coverage_by_source["workspace"]["project_count"] == 15 assert coverage_by_source["supplementary_registry"]["project_count"] == 1 + assert coverage_by_source["github_security"]["project_count"] == 15 + assert coverage_by_source["github_security"]["unknown_count"] == 15 + security_rollup = result.snapshot.to_dict()["rollups"]["security"] + assert security_rollup["unknown_count"] == 15 + assert security_rollup["unavailable_count"] == 15 assert ( result.catalog_data["repos"]["personal-ops"]["lifecycle_state"] == "active" ) From 006a17d3bc57cef78eae0e3b824960dd60666aca Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 03:14:04 -0700 Subject: [PATCH 10/15] Prioritize verified security risk over parked lifecycle --- src/portfolio_truth_reconcile.py | 4 ++-- tests/test_portfolio_truth.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index e3a6101..b64fa6b 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -1334,12 +1334,12 @@ def _attention_state_for( return "archived" if operating_path == "experiment" or lifecycle_state == "experimental": return "experiment" + if risk_entry.get("security_risk"): + return "decision-needed" if lifecycle_state == "manual-only": return "manual-only" if lifecycle_state == "dormant": return "parked" - if risk_entry.get("security_risk"): - return "decision-needed" if lifecycle_state == "active" and operating_path == "maintain": if category == "infrastructure": return "active-infra" diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 2eccca4..2aec329 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -733,6 +733,18 @@ def test_attention_state_classifier_separates_activity_from_operator_attention() ) == "parked" ) + assert ( + _attention_state_for( + activity_status="stale", + archived=False, + lifecycle_state="dormant", + operating_path="maintain", + category="infrastructure", + path_override="", + risk_entry={"security_risk": True}, + ) + == "decision-needed" + ) assert ( _attention_state_for( activity_status="active", From 2d98d47137d208a646684e52f5aeb9ebf7606273 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 03:27:28 -0700 Subject: [PATCH 11/15] Classify supplementary Operator OS outputs explicitly --- src/portfolio_truth_reconcile.py | 16 +++++++++++++--- src/project_registry.py | 6 ++++++ tests/test_portfolio_truth.py | 4 ++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index b64fa6b..f9b118c 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -383,9 +383,17 @@ def _cataloged_supplementary_projects( "path": canonical_key, "top_level_dir": "supplementary", "group_entry": { - "group_key": str( - supplementary.get("group_key") or "operator_infra" - ) + "group_key": str(supplementary.get("group_key") or "operator_infra"), + "group_label": str( + supplementary.get("group_label") or "Operator Infrastructure" + ), + "section_marker": str( + supplementary.get("section_marker") + or "Supplementary Projects" + ), + "section_label": str( + supplementary.get("section_label") or "Operator OS" + ), }, "has_git": False, "repo_full_name": "", @@ -1279,6 +1287,8 @@ def _resolve_group_key( def _resolve_group_label( group_entry: dict[str, Any], raw_project: dict[str, Any] ) -> str: + if group_entry.get("group_label"): + return str(group_entry["group_label"]) if group_entry.get("section_label"): return str(group_entry["section_label"]) if "Swift" in raw_project.get("stack", []): diff --git a/src/project_registry.py b/src/project_registry.py index d81fe54..c669698 100644 --- a/src/project_registry.py +++ b/src/project_registry.py @@ -98,6 +98,9 @@ "display_name": "personal-ops", "repo_full_name": None, "group_key": "operator_infra", + "group_label": "Operator Infrastructure", + "section_marker": "Supplementary Projects", + "section_label": "Operator OS", "lifecycle_state": "active", "note": ( "Local operator control plane (127.0.0.1:46210). Most active " @@ -109,6 +112,9 @@ "display_name": "SecondBrain", "repo_full_name": None, "group_key": "operator_infra", + "group_label": "Operator Infrastructure", + "section_marker": "Supplementary Projects", + "section_label": "Operator OS", "lifecycle_state": "active", "note": ( "4-layer knowledge vault at /Users/d/Documents/SecondBrain " diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 2aec329..44a2ba8 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -569,6 +569,10 @@ def test_live_catalog_produces_exact_tier_zero_attention_semantics( ) personal_ops = by_display_name["personal-ops"] assert personal_ops.identity.project_key == "supp:personal-ops" + assert personal_ops.identity.group_key == "operator_infra" + assert personal_ops.identity.group_label == "Operator Infrastructure" + assert personal_ops.identity.section_marker == "Supplementary Projects" + assert personal_ops.identity.section_label == "Operator OS" assert personal_ops.derived.activity_status == "stale" assert personal_ops.derived.attention_state == "active-infra" assert personal_ops.security.cohort_member is False From 3984cd31ea6e33104be9bbb4310e7e7018d0c8d7 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 03:38:12 -0700 Subject: [PATCH 12/15] Version attention semantics and correct git coverage --- src/portfolio_truth_reconcile.py | 5 +++-- src/portfolio_truth_types.py | 2 +- tests/test_portfolio_truth.py | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index f9b118c..b75a574 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -464,7 +464,8 @@ def _build_coverage_envelope( for provider in ("dependabot", "code_scanning", "secret_scanning") } git_observed = sum( - project.repository_state.get("state") == "observed" for project in projects + project.repository_state.get("state") == "observed" + for project in workspace_projects ) coverage = [ { @@ -476,7 +477,7 @@ def _build_coverage_envelope( "source": "git", "state": "observed" if git_observed else "unknown", "observed_count": git_observed, - "project_count": len(projects), + "project_count": workspace_project_count, }, { "source": "github_security", diff --git a/src/portfolio_truth_types.py b/src/portfolio_truth_types.py index 0191545..c7efa43 100644 --- a/src/portfolio_truth_types.py +++ b/src/portfolio_truth_types.py @@ -15,7 +15,7 @@ # activity_status); derived.archived added as a first-class lifecycle boolean; # source_summary.registry_status_counts replaced by activity_status_counts + archived_count. LEGACY_SCHEMA_VERSIONS = {"0.7.0", "0.8.0", "0.9.0", "0.10.0"} -DERIVATION_POLICY_VERSION = "portfolio_attention.v2" +DERIVATION_POLICY_VERSION = "portfolio_attention.v3" # The published "latest" portfolio-truth artifact. The producer # (portfolio_truth_publish) writes it; every reader resolves it through diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 44a2ba8..0e382e2 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -393,7 +393,7 @@ def test_truth_snapshot_respects_declared_and_derived_fields( assert gamma.derived.stack == ["Swift"] assert result.snapshot.schema_version == "0.11.0" - assert result.snapshot.derivation_policy_version == "portfolio_attention.v2" + assert result.snapshot.derivation_policy_version == "portfolio_attention.v3" assert result.snapshot.inputs["catalog"]["sha256"] assert result.snapshot.inputs["notion"]["mode"] == "unavailable" assert result.snapshot.exclusions == { @@ -558,6 +558,7 @@ def test_live_catalog_produces_exact_tier_zero_attention_semantics( row["source"]: row for row in result.snapshot.coverage } assert coverage_by_source["workspace"]["project_count"] == 15 + assert coverage_by_source["git"]["project_count"] == 15 assert coverage_by_source["supplementary_registry"]["project_count"] == 1 assert coverage_by_source["github_security"]["project_count"] == 15 assert coverage_by_source["github_security"]["unknown_count"] == 15 From 3b0db3783f94b2a5ae0f19c212321f1bfcbaa038 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 03:53:43 -0700 Subject: [PATCH 13/15] fix: identify supplementary projects by canonical key --- src/portfolio_truth_reconcile.py | 13 ++++++-- src/portfolio_truth_types.py | 2 +- tests/test_portfolio_truth.py | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index b75a574..d66c17a 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -263,8 +263,17 @@ def build_portfolio_truth_snapshot( now=now, exclusion_counts=exclusion_counts, ) + discovered_names = { + _normalize(str(project.get("name") or "")) + for project in workspace_projects + } workspace_projects.extend( - _cataloged_supplementary_projects(catalog_data=catalog_data, now=now) + project + for project in _cataloged_supplementary_projects( + catalog_data=catalog_data, + now=now, + ) + if _normalize(str(project.get("name") or "")) not in discovered_names ) projects = [ _build_truth_project( @@ -428,7 +437,7 @@ def _build_coverage_envelope( workspace_projects = [ project for project in projects - if project.identity.top_level_dir != "supplementary" + if not project.identity.project_key.startswith("supp:") ] workspace_project_count = len(workspace_projects) supplementary_project_count = len(projects) - workspace_project_count diff --git a/src/portfolio_truth_types.py b/src/portfolio_truth_types.py index c7efa43..9a33c19 100644 --- a/src/portfolio_truth_types.py +++ b/src/portfolio_truth_types.py @@ -308,7 +308,7 @@ def from_projects( tier = project.risk.risk_tier if tier in risk_tier_counts: risk_tier_counts[tier] += 1 - if project.identity.top_level_dir != "supplementary": + if not project.identity.project_key.startswith("supp:"): security = project.security if security.cohort_member: cohort_repository_count += 1 diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 0e382e2..a08a871 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -583,6 +583,63 @@ def test_live_catalog_produces_exact_tier_zero_attention_semantics( ) +def test_discovered_personal_ops_replaces_supplementary_registry_identity( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + project = workspace / "personal-ops" + project.mkdir(parents=True) + _write(project / "README.md", "# personal-ops\n\nWorkspace checkout.\n") + + result = build_portfolio_truth_snapshot( + workspace_root=workspace, + catalog_path=Path(__file__).parents[1] / "config" / "portfolio-catalog.yaml", + include_notion=False, + now=datetime(2026, 7, 18, 12, tzinfo=timezone.utc), + ) + matches = [ + item + for item in result.snapshot.projects + if item.identity.display_name == "personal-ops" + ] + + assert len(matches) == 1 + assert matches[0].identity.project_key == "personal-ops" + assert not any( + row["source"] == "supplementary_registry" + for row in result.snapshot.coverage + ) + + +def test_workspace_supplementary_directory_stays_in_coverage_denominators( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + project = workspace / "supplementary" / "real-repo" + project.mkdir(parents=True) + _write(project / "README.md", "# real-repo\n\nWorkspace checkout.\n") + + result = build_portfolio_truth_snapshot( + workspace_root=workspace, + catalog_path=Path(__file__).parents[1] / "config" / "portfolio-catalog.yaml", + include_notion=False, + now=datetime(2026, 7, 18, 12, tzinfo=timezone.utc), + ) + by_source = {row["source"]: row for row in result.snapshot.coverage} + real_repo = next( + item + for item in result.snapshot.projects + if item.identity.display_name == "real-repo" + ) + + assert real_repo.identity.top_level_dir == "supplementary" + assert real_repo.identity.project_key == "supplementary/real-repo" + assert by_source["workspace"]["project_count"] == 1 + assert by_source["git"]["project_count"] == 1 + assert by_source["github_security"]["project_count"] == 1 + assert result.snapshot.rollups.security["unknown_count"] == 1 + + def test_truth_snapshot_operating_path_catalog_field_matches_legacy_disposition( portfolio_workspace: Path, legacy_registry: Path, From 5803da88840b3f157a31f47e3926c2d400108192 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 04:06:18 -0700 Subject: [PATCH 14/15] fix: retain missing origins in security cohort --- src/portfolio_truth_reconcile.py | 2 +- tests/test_portfolio_truth.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index d66c17a..6f3c607 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -973,7 +973,7 @@ def _build_truth_project( "active-product", "active-infra", "decision-needed", - } and str(raw_project.get("repo_full_name") or "").strip(): + } and not identity.project_key.startswith("supp:"): security = replace( security, cohort_member=True, diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index a08a871..12161f8 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -436,8 +436,8 @@ def test_truth_snapshot_respects_declared_and_derived_fields( "total_open_high", "total_open_critical", } - assert rollups["security"]["cohort_repository_count"] == 0 - assert rollups["security"]["cohort_unknown_count"] == 0 + assert rollups["security"]["cohort_repository_count"] == 1 + assert rollups["security"]["cohort_unknown_count"] == 1 assert rollups["security"]["cohort_complete_count"] == 0 assert set(rollups["decision"]) == { "decision_needed_count", @@ -1432,6 +1432,18 @@ def test_catalog_backed_high_criticality_infra_readme_support_promotes_to_standa infra.provenance["derived.context_quality"]["detail"] == "minimum-viable->standard" ) + assert infra.identity.has_git is True + assert infra.identity.repo_full_name == "" + assert infra.derived.attention_state == "active-infra" + assert infra.security.cohort_member is True + assert infra.security.coverage_state == "unknown" + security_coverage = next( + row + for row in result.snapshot.coverage + if row["source"] == "github_security" + ) + assert security_coverage["cohort_repository_count"] == 1 + assert security_coverage["cohort_unknown_count"] == 1 def test_substantive_readme_support_does_not_promote_non_infra_repo( From 53f4dc463fcadb95ccee5cd3c9bf2e1e369e963c Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sat, 18 Jul 2026 04:17:49 -0700 Subject: [PATCH 15/15] fix: merge repo-less supplementary discoveries --- src/portfolio_truth_reconcile.py | 52 ++++++++++++++++++++++++++------ tests/test_portfolio_truth.py | 11 ++++--- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index 6f3c607..e7ca1f4 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -263,17 +263,12 @@ def build_portfolio_truth_snapshot( now=now, exclusion_counts=exclusion_counts, ) - discovered_names = { - _normalize(str(project.get("name") or "")) - for project in workspace_projects - } - workspace_projects.extend( - project - for project in _cataloged_supplementary_projects( + workspace_projects = _merge_supplementary_discoveries( + discovered=workspace_projects, + supplementary=_cataloged_supplementary_projects( catalog_data=catalog_data, now=now, - ) - if _normalize(str(project.get("name") or "")) not in discovered_names + ), ) projects = [ _build_truth_project( @@ -428,6 +423,45 @@ def _cataloged_supplementary_projects( return projects +def _merge_supplementary_discoveries( + *, + discovered: list[dict[str, Any]], + supplementary: list[dict[str, Any]], +) -> list[dict[str, Any]]: + """Keep one canonical identity while retaining local checkout observations.""" + supplementary_by_name = { + _normalize(str(project.get("name") or "")): project + for project in supplementary + } + used: set[str] = set() + merged: list[dict[str, Any]] = [] + for project in discovered: + normalized_name = _normalize(str(project.get("name") or "")) + supplement = supplementary_by_name.get(normalized_name) + if supplement is None: + merged.append(project) + continue + used.add(normalized_name) + if str(project.get("repo_full_name") or "").strip(): + merged.append(project) + continue + merged.append( + { + **project, + "path": supplement["path"], + "top_level_dir": supplement["top_level_dir"], + "group_entry": supplement["group_entry"], + "source": "workspace+supplementary-registry", + } + ) + merged.extend( + project + for normalized_name, project in supplementary_by_name.items() + if normalized_name not in used + ) + return merged + + def _build_coverage_envelope( *, projects: list[PortfolioTruthProject], diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 12161f8..ce0d567 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -604,11 +604,14 @@ def test_discovered_personal_ops_replaces_supplementary_registry_identity( ] assert len(matches) == 1 - assert matches[0].identity.project_key == "personal-ops" - assert not any( - row["source"] == "supplementary_registry" - for row in result.snapshot.coverage + assert matches[0].identity.project_key == "supp:personal-ops" + assert matches[0].security.cohort_member is False + assert matches[0].provenance["derived.context_quality"]["source"].startswith( + "workspace+supplementary-registry" ) + by_source = {row["source"]: row for row in result.snapshot.coverage} + assert by_source["workspace"]["project_count"] == 0 + assert by_source["supplementary_registry"]["project_count"] == 1 def test_workspace_supplementary_directory_stays_in_coverage_denominators(