-
Notifications
You must be signed in to change notification settings - Fork 0
Fail closed on truth authority and tighten portfolio attention #184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
379ae6b
e07af4d
e3dd471
b825440
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import subprocess | ||
| from datetime import UTC, datetime | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
|
|
||
| def observe_repository_state(path: Path, *, observed_at: datetime) -> dict[str, Any]: | ||
| """Read local Git/worktree state without changing refs or exposing file names.""" | ||
| if not (path / ".git").exists(): | ||
| return { | ||
| "state": "not_a_repository", | ||
| "observed_at": observed_at.astimezone(UTC).isoformat(), | ||
| } | ||
| try: | ||
| head = _git(path, "rev-parse", "HEAD") | ||
| branch = _git(path, "branch", "--show-current") or None | ||
| dirty = _git(path, "status", "--porcelain", "--untracked-files=all") | ||
| upstream = _git_optional(path, "rev-parse", "--abbrev-ref", "@{upstream}") | ||
| ahead = behind = None | ||
| if upstream: | ||
| counts = _git(path, "rev-list", "--left-right", "--count", f"{upstream}...HEAD") | ||
| behind_text, ahead_text = counts.split() | ||
| behind, ahead = int(behind_text), int(ahead_text) | ||
| worktrees = [] | ||
| for item in _worktrees(path): | ||
| worktree_path = Path(item["path"]) | ||
| worktree_dirty = _git( | ||
| worktree_path, "status", "--porcelain", "--untracked-files=all" | ||
| ) | ||
| worktrees.append( | ||
| { | ||
| "path": str(worktree_path), | ||
| "head": item.get("head"), | ||
| "branch": item.get("branch"), | ||
| "detached": item.get("detached", False), | ||
| "dirty": bool(worktree_dirty), | ||
| "dirty_path_count": len(worktree_dirty.splitlines()) if worktree_dirty else 0, | ||
| } | ||
| ) | ||
| return { | ||
| "state": "observed", | ||
| "observed_at": observed_at.astimezone(UTC).isoformat(), | ||
| "local": { | ||
| "path": str(path), | ||
| "head": head, | ||
| "branch": branch, | ||
| "dirty": bool(dirty), | ||
| "dirty_path_count": len(dirty.splitlines()) if dirty else 0, | ||
| "upstream": upstream, | ||
| "upstream_observation_source": "local_tracking_ref" if upstream else "unavailable", | ||
| "ahead": ahead, | ||
| "behind": behind, | ||
| }, | ||
| "remote_default_branch": { | ||
| "state": "unknown", | ||
| "reason": "no independent live remote read was performed by portfolio generation", | ||
| }, | ||
| "worktrees": worktrees, | ||
| } | ||
| except (OSError, subprocess.CalledProcessError, ValueError) as exc: | ||
| return { | ||
| "state": "unknown", | ||
| "observed_at": observed_at.astimezone(UTC).isoformat(), | ||
| "reason": str(exc), | ||
| } | ||
|
|
||
|
|
||
| def _git(path: Path, *args: str) -> str: | ||
| return subprocess.run( | ||
| ["git", "-C", str(path), *args], check=True, capture_output=True, text=True | ||
| ).stdout.strip() | ||
|
|
||
|
|
||
| def _git_optional(path: Path, *args: str) -> str | None: | ||
| try: | ||
| return _git(path, *args) or None | ||
| except subprocess.CalledProcessError: | ||
| return None | ||
|
|
||
|
|
||
| def _worktrees(path: Path) -> list[dict[str, Any]]: | ||
| output = _git(path, "worktree", "list", "--porcelain") | ||
| items: list[dict[str, Any]] = [] | ||
| current: dict[str, Any] = {} | ||
| for line in output.splitlines() + [""]: | ||
| if not line: | ||
| if current: | ||
| items.append(current) | ||
| current = {} | ||
| continue | ||
| key, _, value = line.partition(" ") | ||
| if key == "worktree": | ||
| current["path"] = value | ||
| elif key == "HEAD": | ||
| current["head"] = value | ||
| elif key == "branch": | ||
| current["branch"] = value.removeprefix("refs/heads/") | ||
| elif key == "detached": | ||
| current["detached"] = True | ||
| return items | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| from src.portfolio_context_contract import has_substantive_readme_support | ||
| from src.portfolio_pathing import build_operating_path_entry | ||
| from src.portfolio_risk import build_risk_entry | ||
| from src.portfolio_repository_state import observe_repository_state | ||
| from src.portfolio_truth_sources import ( | ||
| WORKSPACE_DISCOVERY_POLICY_VERSION, | ||
| discover_workspace_projects, | ||
|
|
@@ -337,6 +338,11 @@ def build_portfolio_truth_snapshot( | |
| notion_context_carried_forward=notion_context_carried_forward, | ||
| prior_notion_generated_at=prior_notion_generated_at, | ||
| ), | ||
| coverage=_build_coverage_envelope( | ||
| projects=projects, | ||
| notion_context_carried_forward=notion_context_carried_forward, | ||
| notion_context_rows=len(notion_context), | ||
| ), | ||
| exclusions={ | ||
| "policy_version": WORKSPACE_DISCOVERY_POLICY_VERSION, | ||
| "counts": dict(sorted(exclusion_counts.items())), | ||
|
|
@@ -347,6 +353,24 @@ def build_portfolio_truth_snapshot( | |
| ) | ||
|
|
||
|
|
||
| def _build_coverage_envelope( | ||
| *, | ||
| projects: list[PortfolioTruthProject], | ||
| notion_context_carried_forward: bool, | ||
| notion_context_rows: int, | ||
| ) -> list[dict[str, Any]]: | ||
| scanned = sum(project.security.alerts_available for project in projects) | ||
| git_observed = sum( | ||
| project.repository_state.get("state") == "observed" for project in projects | ||
| ) | ||
| return [ | ||
| {"source": "workspace", "state": "observed", "project_count": len(projects)}, | ||
| {"source": "git", "state": "observed" if git_observed else "unknown", "observed_count": git_observed, "project_count": len(projects)}, | ||
| {"source": "github_security", "state": "known" if scanned == len(projects) else "partial" if scanned else "unknown", "scanned_count": scanned, "project_count": len(projects)}, | ||
| {"source": "notion", "state": "carried_forward" if notion_context_carried_forward else "observed" if notion_context_rows else "unknown", "observed_count": notion_context_rows}, | ||
| ] | ||
|
|
||
|
|
||
| def _build_input_envelope( | ||
| *, | ||
| workspace_root: Path, | ||
|
|
@@ -483,6 +507,7 @@ def _count(source: dict[str, Any], key: str) -> int: | |
|
|
||
| return SecurityFields( | ||
| alerts_available=bool(dependabot.get("available", False)), | ||
| coverage_state="known" if dependabot.get("available", False) else "unknown", | ||
| dependabot_critical=_count(dependabot, "critical"), | ||
| dependabot_high=_count(dependabot, "high"), | ||
| dependabot_medium=_count(dependabot, "medium"), | ||
|
|
@@ -701,6 +726,14 @@ def _build_truth_project( | |
| security_high_alerts=security.dependabot_high, | ||
| security_critical_alerts=security.dependabot_critical, | ||
| ) | ||
| if ( | ||
| not security.alerts_available | ||
| and risk_entry.get("risk_summary") == "No elevated risk factors." | ||
| ): | ||
| risk_entry["risk_summary"] = ( | ||
| "No non-security risk factors detected; security posture is unknown " | ||
| "because alert coverage is unavailable." | ||
| ) | ||
| attention_state = _attention_state_for( | ||
| activity_status=activity_status, | ||
| archived=archived, | ||
|
|
@@ -860,6 +893,11 @@ def _build_truth_project( | |
| risk=risk, | ||
| security=security, | ||
| advisory=advisory, | ||
| repository_state=( | ||
| observe_repository_state(project_path, observed_at=now) | ||
| if project_path is not None and has_git | ||
| else {"state": "not_a_repository", "observed_at": now.isoformat()} | ||
| ), | ||
| provenance=provenance, | ||
| warnings=warnings, | ||
| ) | ||
|
|
@@ -1062,17 +1100,15 @@ def _attention_state_for( | |
| return "archived" | ||
| if operating_path == "experiment" or lifecycle_state == "experimental": | ||
| return "experiment" | ||
| if lifecycle_state == "manual-only": | ||
| return "manual-only" | ||
| 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 | ||
| # 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 ( | ||
| path_override == "investigate" | ||
| or not operating_path | ||
| or risk_entry.get("security_risk") | ||
| ): | ||
| if risk_entry.get("security_risk"): | ||
| return "decision-needed" | ||
|
Comment on lines
+1111
to
1112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For active/recent commercial or infrastructure repos whose operating-path evidence sets Useful? React with 👍 / 👎. |
||
| if activity_status in {"active", "recent"} and operating_path in { | ||
| "maintain", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a repository has a stale linked-worktree record,
git worktree list --porcelainincludes that worktree andgit -C <missing> statusexits 128; this call then trips the outer exception handler and returnsstate: "unknown"for the entire primary repo. In that scenario the canonical truth snapshot loses otherwise valid local head/dirty/ahead evidence just because one linked worktree is prunable, so record that worktree as unavailable or skip it instead of failing the whole observation.Useful? React with 👍 / 👎.