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
22 changes: 22 additions & 0 deletions config/portfolio-catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,17 @@ repos:
lifecycle_state: active
review_cadence: weekly
intended_disposition: experiment
engraph:
owner: d
lifecycle_state: archived
review_cadence: quarterly
intended_disposition: archive
category: vanity
tool_provenance: claude-code
maturity_program: archive
target_maturity: operating
automation_eligible: false
notes: Settled portfolio-recovery exclusion; keep manual-only unless the operator explicitly reactivates it.
EvolutionSandbox:
owner: d
lifecycle_state: active
Expand All @@ -183,6 +194,17 @@ repos:
intended_disposition: maintain
category: vanity
tool_provenance: claude-code
reliability-vault:
owner: d
lifecycle_state: archived
review_cadence: quarterly
intended_disposition: archive
category: vanity
tool_provenance: unknown
maturity_program: archive
target_maturity: operating
automation_eligible: false
notes: Settled portfolio-recovery exclusion; keep manual-only unless the operator explicitly reactivates it.
JSMTicketAnalyticsExport:
owner: d
lifecycle_state: active
Expand Down
95 changes: 95 additions & 0 deletions src/portfolio_decision_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

from __future__ import annotations

import argparse
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any

CONTRACT_VERSION = "decision_queue_v1"
DIGEST_CONTRACT_VERSION = "portfolio_decision_digest_v1"
MAX_DECISION_QUEUE_ITEMS = 5

NON_DEFAULT_STATES = frozenset(
Expand Down Expand Up @@ -145,3 +149,94 @@ def summarize_decision_queue(items: list[dict[str, Any]]) -> dict[str, Any]:
"decision_queue_count": len(items),
"decision_queue_type_counts": type_counts,
}


def build_decision_digest(portfolio_truth: dict[str, Any]) -> dict[str, Any]:
"""Build a deterministic, truth-native operator digest.

The digest deliberately contains only the decision queue. It does not
reintroduce stale-repo, weak-context, or prose-based "unshipped" watch
lists that the portfolio attention contract excludes from decisions.
"""
decision_queue = build_decision_queue(portfolio_truth)
summary = summarize_decision_queue(decision_queue)
return {
"contract_version": DIGEST_CONTRACT_VERSION,
"source": {
"schema_version": _text(portfolio_truth.get("schema_version")) or "unknown",
"generated_at": _text(portfolio_truth.get("generated_at")) or "unknown",
},
"decision_queue": decision_queue,
"summary": summary,
}


def render_decision_digest_markdown(digest: dict[str, Any]) -> str:
"""Render the compact nightly decision digest as deterministic Markdown."""
source = _mapping(digest.get("source"))
generated_at = _text(source.get("generated_at")) or "unknown"
schema_version = _text(source.get("schema_version")) or "unknown"
date_label = generated_at[:10] if generated_at != "unknown" else "unknown"
decision_queue = [
item for item in digest.get("decision_queue") or [] if isinstance(item, dict)
]
summary = _mapping(digest.get("summary"))
count = int(summary.get("decision_queue_count") or len(decision_queue))

lines = [
f"## Portfolio Decision Digest — {date_label}",
"",
"### Decision Queue",
]
if not decision_queue:
lines.append("- No portfolio decisions clear the current evidence bar.")
else:
for item in decision_queue:
project = _text(item.get("project")) or "Unknown project"
decision_type = _text(item.get("decision_type")) or "unknown"
why_now = _text(item.get("why_now")) or "No current rationale recorded."
next_action = (
_text(item.get("recommended_action")) or "Resolve the current decision."
)
lines.append(
f"- **{project}** [{decision_type}]: {why_now} Next: {next_action}"
)

lines.extend(
[
"",
"### Source Freshness",
f"- PortfolioTruthV1 schema `{schema_version}`, generated `{generated_at}`.",
"",
"### Summary",
f"{count} decision{'s' if count != 1 else ''} | contract `{CONTRACT_VERSION}`",

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 Use the digest contract in Markdown

When the nightly scheduler consumes the Markdown digest instead of JSON, this line labels the rendered artifact with the embedded queue contract (decision_queue_v1) even though build_decision_digest() emits the artifact as portfolio_decision_digest_v1. That makes the provenance marker unable to distinguish digest-format changes from raw DecisionQueueV1 output; render DIGEST_CONTRACT_VERSION here, or include both contracts explicitly.

Useful? React with 👍 / 👎.

]
)
return "\n".join(lines) + "\n"


def _load_portfolio_truth(path: Path) -> dict[str, Any]:
raw = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raise ValueError("portfolio truth root must be an object")
return raw


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Render the DecisionQueueV1 digest from PortfolioTruthV1."
)
parser.add_argument("--truth", type=Path, required=True)
parser.add_argument("--format", choices=("json", "markdown"), default="markdown")
args = parser.parse_args(argv)

digest = build_decision_digest(_load_portfolio_truth(args.truth))
if args.format == "json":
print(json.dumps(digest, indent=2, sort_keys=True))
else:
print(render_decision_digest_markdown(digest), end="")
return 0


if __name__ == "__main__":
raise SystemExit(main())
14 changes: 13 additions & 1 deletion tests/test_portfolio_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,18 @@ def test_catalog_entry_for_repo_defaults_automation_eligible_false():
assert entry["automation_eligible"] is False


def test_live_catalog_keeps_settled_recovery_exclusions_archived() -> None:
catalog_path = Path(__file__).parents[1] / "config" / "portfolio-catalog.yaml"
catalog = load_portfolio_catalog(catalog_path)

for repo_name in ("engraph", "reliability-vault"):
entry = catalog["repos"][repo_name]
assert entry["lifecycle_state"] == "archived"
assert entry["intended_disposition"] == "archive"
assert entry["maturity_program"] == "archive"
assert entry["automation_eligible"] is False


def test_catalog_entry_matches_full_name_then_bare_name():
catalog = {
"repos": {
Expand Down Expand Up @@ -388,4 +400,4 @@ def test_catalog_line_and_summaries_cover_missing_contracts():
assert catalog_summary["cataloged_repo_count"] == 1
assert catalog_summary["missing_contract_count"] == 1
assert alignment_summary["counts"]["aligned"] == 1
assert alignment_summary["counts"]["missing-contract"] == 1
assert alignment_summary["counts"]["missing-contract"] == 1
80 changes: 79 additions & 1 deletion tests/test_portfolio_decision_queue.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
from __future__ import annotations

from src.portfolio_decision_queue import build_decision_queue, summarize_decision_queue
import json
from pathlib import Path

import pytest

from src.portfolio_decision_queue import (
build_decision_digest,
build_decision_queue,
main,
render_decision_digest_markdown,
summarize_decision_queue,
)


def _project(
Expand Down Expand Up @@ -94,3 +105,70 @@ def test_archived_security_risk_stays_out_of_queue() -> None:
}

assert build_decision_queue(truth) == []


def test_decision_digest_zero_state_is_truth_native() -> None:
truth = {
"schema_version": "0.8.0",
"generated_at": "2026-07-11T09:00:10+00:00",
"projects": [_project("Infra", attention_state="active-infra")],
}

digest = build_decision_digest(truth)
rendered = render_decision_digest_markdown(digest)

assert digest["contract_version"] == "portfolio_decision_digest_v1"
assert digest["decision_queue"] == []
assert digest["summary"]["decision_queue_count"] == 0
assert "No portfolio decisions clear the current evidence bar." in rendered
assert "PortfolioTruthV1 schema `0.8.0`" in rendered
assert "stale" not in rendered.lower()
assert "weak context" not in rendered.lower()
assert "unshipped" not in rendered.lower()


def test_decision_digest_renders_ranked_decisions_and_caps_at_five() -> None:
truth = {
"schema_version": "0.8.0",
"generated_at": "2026-07-11T09:00:10+00:00",
"projects": [
_project(f"Decision-{index}", attention_state="decision-needed")
for index in range(7)
],
}

digest = build_decision_digest(truth)
rendered = render_decision_digest_markdown(digest)

assert len(digest["decision_queue"]) == 5
assert digest["summary"]["decision_queue_count"] == 5
assert rendered.count("[owner or human decision]") == 5
assert "Decision-0" in rendered
assert "Decision-5" not in rendered


def test_cli_json_and_markdown_are_deterministic(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
truth_path = tmp_path / "portfolio-truth.json"
truth_path.write_text(
json.dumps(
{
"schema_version": "0.8.0",
"generated_at": "2026-07-11T09:00:10+00:00",
"projects": [_project("NeedsDecision", attention_state="decision-needed")],
}
),
encoding="utf-8",
)

assert main(["--truth", str(truth_path), "--format", "json"]) == 0
json_first = capsys.readouterr().out
assert main(["--truth", str(truth_path), "--format", "json"]) == 0
json_second = capsys.readouterr().out
assert json_first == json_second

assert main(["--truth", str(truth_path), "--format", "markdown"]) == 0
markdown = capsys.readouterr().out
assert "## Portfolio Decision Digest — 2026-07-11" in markdown
assert "**NeedsDecision** [owner or human decision]" in markdown