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
1 change: 1 addition & 0 deletions config/portfolio-catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@ repos:
lifecycle_state: archived
review_cadence: quarterly
intended_disposition: archive
maturity_program: archive
category: commercial
tool_provenance: gpt
Nexus:
Expand Down
17 changes: 17 additions & 0 deletions src/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,7 @@ def _write_control_center_artifacts(
write_weekly_command_center_artifacts,
)

_filter_control_center_snapshot_for_default_view(snapshot)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild summary when filtering the control-center queue

When the top queue item is archived or experimental, this call removes it only from snapshot["operator_queue"]; snapshot["operator_summary"] was already built from the unfiltered queue and can still contain primary_target, total_items/counts, and what_to_do_next for the hidden repo. The following weekly digest and Markdown rendering consume that same summary, so the generated default artifacts can still recommend acting on an archive item while omitting it from the queue. Filter before summary construction or recompute/scrub the summary alongside the queue.

Useful? React with 👍 / 👎.

json_path, md_path = _latest_control_center_paths(output_dir, username, generated_at)
snapshot.setdefault("operator_summary", {})["control_center_reference"] = str(json_path)
portfolio_truth_path, portfolio_truth = load_latest_portfolio_truth(output_dir)
Expand Down Expand Up @@ -4758,13 +4759,29 @@ def _should_print_control_center_item(item: dict) -> bool:
intended = str(catalog.get("intended_disposition") or "").strip().lower()
program = str(catalog.get("maturity_program") or "").strip().lower()
operating_path = str(item.get("operating_path") or catalog.get("operating_path") or "").strip().lower()
if lifecycle in {"archived", "archive"}:
return False
if intended == "archive" or program == "archive" or operating_path == "archive":
return False
if lifecycle in {"experiment", "experimental"}:
return False
if intended == "experiment" or program == "experiment" or operating_path == "experiment":
return False
return True


def _filter_control_center_snapshot_for_default_view(snapshot: dict) -> dict:
queue = snapshot.get("operator_queue")
if not isinstance(queue, list):
return snapshot
snapshot["operator_queue"] = [
item
for item in queue
if isinstance(item, dict) and _should_print_control_center_item(item)
]
return snapshot


def _fetch_repo_metadata(args, client: GitHubClient) -> tuple[list[RepoMetadata], list[dict]]:
if args.graphql and args.token:
from src.graphql_client import bulk_fetch_repos
Expand Down
31 changes: 25 additions & 6 deletions src/portfolio_truth_reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import logging
import re
from collections import Counter
from dataclasses import dataclass
from datetime import datetime, timezone
Expand Down Expand Up @@ -787,7 +788,7 @@ def _select_with_legacy(
return value
legacy_value = str(legacy.get(field, "") or "").strip()
if field == "notes":
legacy_value = _strip_generated_note_purpose_prefix(
legacy_value = _strip_generated_registry_note_decorations(
legacy_value, repo_entry=repo_entry, group_entry=group_entry
)
if legacy_value:
Expand All @@ -799,19 +800,37 @@ def _select_with_legacy(
return ""


def _strip_generated_note_purpose_prefix(
_GENERATED_SECURITY_NOTE_RE = re.compile(r"^(?:\[security: [^\]]+\]\s*)+", re.IGNORECASE)
_GENERATED_PATH_NOTE_PREFIXES = (
"Stable path is ",
"No stable operating path is declared yet.",
)
_GENERATED_PATH_NOTE_MARKERS = (
"Declared maturity program and intended disposition point at different paths.",
"Context quality is still too weak for path guidance to stand on its own.",
"Treat this repo as investigate until path confidence improves.",
)


def _strip_generated_registry_note_decorations(
notes: str,
*,
repo_entry: dict[str, Any],
group_entry: dict[str, Any],
) -> str:
"""Keep generated registry markdown idempotent when used as legacy input."""
value = notes.strip()
value = _GENERATED_SECURITY_NOTE_RE.sub("", value).strip()

purpose = str(repo_entry.get("purpose") or group_entry.get("purpose") or "").strip()
if not purpose:
return value
while value == purpose or value.startswith(f"{purpose} "):
value = value[len(purpose) :].strip()
if purpose:
while value == purpose or value.startswith(f"{purpose} "):
value = value[len(purpose) :].strip()

if value.startswith(_GENERATED_PATH_NOTE_PREFIXES) or any(
marker in value for marker in _GENERATED_PATH_NOTE_MARKERS
):
return ""
return value


Expand Down
40 changes: 40 additions & 0 deletions tests/test_cli_hardening.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,46 @@ def test_control_center_default_print_hides_experiment_items() -> None:
)


def test_control_center_default_view_hides_archive_items() -> None:
assert not cli._should_print_control_center_item(
{
"repo": "archive-repo",
"operating_path": "archive",
"portfolio_catalog": {
"lifecycle_state": "archived",
"intended_disposition": "archive",
"maturity_program": "archive",
},
}
)


def test_control_center_artifact_filter_drops_archive_queue_items() -> None:
snapshot = {
"operator_summary": {},
"operator_queue": [
{
"repo": "active-repo",
"operating_path": "maintain",
"portfolio_catalog": {"lifecycle_state": "active"},
},
{
"repo": "archive-repo",
"operating_path": "archive",
"portfolio_catalog": {
"lifecycle_state": "archived",
"intended_disposition": "archive",
"maturity_program": "archive",
},
},
],
}

cli._filter_control_center_snapshot_for_default_view(snapshot)

assert [item["repo"] for item in snapshot["operator_queue"]] == ["active-repo"]


def test_main_control_center_requires_latest_report(monkeypatch):
args = _make_args(control_center=True)

Expand Down
53 changes: 53 additions & 0 deletions tests/test_portfolio_truth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,59 @@ def test_generated_registry_notes_do_not_accumulate_purpose_prefix(
assert alpha.declared.notes == "handoff note"


def test_generated_registry_notes_drop_security_and_path_boilerplate(
portfolio_workspace: Path,
tmp_path: Path,
) -> None:
archived = portfolio_workspace / "FreeLanceInvoice"
archived.mkdir()
_write(archived / "README.md", "# FreeLanceInvoice\n\nArchived invoice project.\n")

catalog_path = tmp_path / "portfolio-catalog.yaml"
catalog_path.write_text(
"""
repos:
FreeLanceInvoice:
owner: d
lifecycle_state: archived
review_cadence: quarterly
intended_disposition: archive
maturity_program: archive
category: commercial
tool_provenance: gpt
"""
)
legacy_registry_path = tmp_path / "project-registry.md"
legacy_registry_path.write_text(
"""
# Project Registry

## Standalone Projects (Root Level)

| Project | Status | Tool | Context Quality | Stack | Context Files | Category | Notes |
|---------|--------|------|-----------------|-------|---------------|----------|-------|
| FreeLanceInvoice | archived | gpt | weak | Python | README.md | commercial | [security: 1 critical / 2 high open Dependabot alerts] [security: 1 critical / 2 high open Dependabot alerts] Stable path is Archive from intended disposition. Declared maturity program and intended disposition point at different paths. Context quality is still too weak for path guidance to stand on its own. Treat this repo as investigate until path confidence improves. |
"""
)

result = build_portfolio_truth_snapshot(
workspace_root=portfolio_workspace,
catalog_path=catalog_path,
legacy_registry_path=legacy_registry_path,
include_notion=False,
)

invoice = next(
project
for project in result.snapshot.projects
if project.identity.display_name == "FreeLanceInvoice"
)
assert invoice.declared.maturity_program == "archive"
assert invoice.declared.notes == ""
assert invoice.provenance["declared.maturity_program"]["source"] == "catalog_repo"
assert invoice.provenance["declared.notes"]["source"] != "legacy_registry"


def test_publish_failure_leaves_live_files_untouched(
portfolio_workspace: Path,
portfolio_catalog: Path,
Expand Down