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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,serve,semantic]"
pip install -e ".[dev,serve,semantic,build]"
- name: Verify portable PortfolioTruth consumer contract
run: python scripts/generate_portfolio_truth_contract_fixture.py --check
- name: Run tests
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

## [Unreleased]

- Reject reckoning path collisions under the saved truth workspace root and
validate canonical truth before exporting private overlay workbooks.

- Prefer live checkouts over archive copies, validate private overlay inputs in
CLI preflight, preserve overlays on shared workbook refresh, and isolate
synthetic gates and exports from private runtime settings.

- Show every reckoning family once, aggregate matched truth activity and attention,
and expose unmatched members and unreckoned truth projects in the workbook.

- Read private runtime reckoning verdicts into an optional visible workbook sheet
without changing canonical truth; discover `_archive` children with archived defaults.

- Refuse new portfolio generation publication when staged truth lacks valid
GitHub security receipt and content identities or disagrees with the admitted
manifest; preserve historical generation readback compatibility.
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,34 @@ audit triage <github-username> --approval-center

Treat campaign/writeback, GitHub Projects, Notion sync, catalog overrides, scorecards overrides, and `--excel-mode template` as advanced paths.

Private family verdicts can be displayed in a visible `Reckoning` workbook sheet
with `--reckoning-overlay PATH` or `GHRA_RECKONING_OVERLAY` (the flag takes
precedence). The YAML's `schema` value is `reckoning-verdicts`, then a slash, then
`1`; invalid verdicts stop CLI preflight before the GitHub audit. The overlay is
read once at startup, so a long `--watch` run picks up edits after a restart. Export also requires the saved
`portfolio-truth-latest.json` beside the audit report, or in the exporter API's
`truth_dir`. Each overlay family gets one row in overlay order, including
families with no saved truth match. `Members` lists the family's member keys;
`Matched projects` counts matched truth projects and `Unmatched members` lists
member keys with no match. Matching retains a GitHub repository ID tier, but
canonical truth carries no repository ID today, so current truth matches by
`owner/repo`, then workspace-relative path. `Truth attention state` is a single
value using the existing urgency order (`decision-needed`, `active-product`,
`active-infra`), with unranked ties resolved in saved-truth order. `Attention states`
lists all distinct matched states. Last activity is the
latest matched timestamp. An `Unreckoned projects` block below the family table
reports the count and first 50 keys of truth projects with no family.

Keep the overlay outside this repository and published artifacts. It is read
only, is never copied into output, and adds no fields to canonical truth. The
resulting workbook contains private verdicts and should be treated as private.
Without an overlay, no `Reckoning` sheet is added. Both workbook modes can be
checked without private inputs using
`make workbook-gate ARGS="--synthetic-reckoning-overlay"`. This gate writes to
`output/workbook-gate/synthetic-reckoning/`, keeping the normal desktop signoff
artifacts separate. The environment variable is read only by the audit CLI;
demo, benchmark, and direct exporter calls require an explicit overlay argument.

## Demo and Guides

- Safe demo path: run `make demo` after a local clone to generate sample artifacts from the committed fixture without a GitHub token.
Expand Down
6 changes: 6 additions & 0 deletions config/portfolio-catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ defaults:
maturity_program: maintain
target_maturity: operating
groups:
archive:
label: Archive
path_prefixes:
- _archive
lifecycle_state: archived
operating_path: archive
itprjsviaclaude:
label: IT Tools
section_marker: ITPRJsViaClaude/
Expand Down
1 change: 1 addition & 0 deletions src/github_repo_auditor/app/run_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,7 @@ def _write_report_outputs(
collection=args.collection,
excel_mode=args.excel_mode,
truth_dir=output_dir,
reckoning_overlay=getattr(args, "reckoning_overlay", None),
)
report.runtime_breakdown["workbook_build_seconds"] = round(perf_counter() - workbook_start, 3)
md_path = write_markdown_report(report, output_dir, diff_data=diff_dict)
Expand Down
50 changes: 50 additions & 0 deletions src/github_repo_auditor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,42 @@
DEFAULT_EXPECTED_GITHUB_COHORT_COUNT,
DEFAULT_MAX_COHORT_SIZE,
)
from github_repo_auditor.reckoning_overlay import (
ReckoningOverlayError,
load_reckoning_overlay,
)


# Emitted at most once per process when legacy flat invocation is used.
_LEGACY_WARNING_EVENTS: set[str] = set()


def _prepare_reckoning_export(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
"""Load requested private workbook inputs before any audit or shared refresh."""
overlay_path = getattr(args, "reckoning_overlay", None)
if overlay_path is None:
configured_path = os.environ.get("GHRA_RECKONING_OVERLAY")
overlay_path = Path(configured_path).expanduser() if configured_path else None

try:
overlay = load_reckoning_overlay(overlay_path)
if overlay is None:
args.reckoning_overlay = None
return

# A dry run never exports a workbook, so only the overlay itself is checked.
if not getattr(args, "dry_run", False):
from github_repo_auditor.excel_export_truth_helpers import load_reckoning_truth

load_reckoning_truth(Path(args.output_dir))
except (ReckoningOverlayError, OSError, ValueError) as exc:
parser.error(str(exc))

# export_excel accepts an already validated object, avoiding environment
# lookup or overlay parsing anywhere below the CLI boundary.
args.reckoning_overlay = overlay


DEFAULT_ANALYSIS_WORKERS = 1
MAX_ANALYSIS_WORKERS = 8
DEFAULT_PORTFOLIO_WORKSPACE = Path.home() / "Projects"
Expand Down Expand Up @@ -170,6 +201,10 @@ def _build_run_subparser(subparsers: argparse._SubParsersAction) -> None: # typ
formatter_class=argparse.RawDescriptionHelpFormatter,
)
_add_global_flags(p)
p.add_argument(
"--reckoning-overlay", type=Path, metavar="PATH",
help=argparse.SUPPRESS, # Advanced option; keep primary subcommand help bounded.
)
p.add_argument(
"--repos",
nargs="+",
Expand Down Expand Up @@ -574,6 +609,12 @@ def _build_report_subparser(subparsers: argparse._SubParsersAction) -> None: #
help="Run context quality triage across the portfolio",
)
_add_automation_proposal_flags(p)
p.add_argument(
"--reckoning-overlay",
type=Path,
metavar="PATH",
help=argparse.SUPPRESS, # Documented in flat help and the workbook guide.
)
p.add_argument(
"--excel-mode",
choices=["template", "standard"],
Expand Down Expand Up @@ -1053,6 +1094,12 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="Generate PDF audit report",
)
parser.add_argument(
"--reckoning-overlay",
type=Path,
metavar="PATH",
help="Private verdict overlay for the workbook (or GHRA_RECKONING_OVERLAY)",
)
parser.add_argument(
"--excel-mode",
choices=["template", "standard"],
Expand Down Expand Up @@ -2188,6 +2235,7 @@ def main() -> None:
or args.review_governance
or args.review_packet
):
_prepare_reckoning_export(args, parser)
_run_approval_capture_mode(args, parser)
return

Expand Down Expand Up @@ -2305,6 +2353,7 @@ def main() -> None:
return

if args.watch:
_prepare_reckoning_export(args, parser)
_run_watch_mode(args, config_inspection)
return

Expand All @@ -2314,6 +2363,7 @@ def main() -> None:
run_semantic_search_mode(args, query)
return

_prepare_reckoning_export(args, parser)
_run_main_audit_cycle(args, config_inspection)


Expand Down
38 changes: 38 additions & 0 deletions src/github_repo_auditor/excel_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
import json
from pathlib import Path

from github_repo_auditor.reckoning_overlay import (
ReckoningOverlay,
load_reckoning_overlay,
)
from github_repo_auditor.excel_portfolio_sheet_helpers import build_reckoning_sheet

from openpyxl import Workbook
from openpyxl.chart import BubbleChart, Reference
from openpyxl.formatting.rule import ColorScaleRule, DataBarRule, IconSetRule
Expand Down Expand Up @@ -1760,6 +1766,8 @@ def _build_excel_workbook(
collection: str | None = None,
excel_mode: str = "standard",
risk_lookup: dict[str, str] | None = None,
reckoning_overlay: ReckoningOverlay | None = None,
portfolio_truth: dict | None = None,
) -> None:
_build_excel_workbook_runner(
wb,
Expand All @@ -1771,7 +1779,11 @@ def _build_excel_workbook(
collection=collection,
excel_mode=excel_mode,
risk_lookup=risk_lookup,
reckoning_overlay=reckoning_overlay,
portfolio_truth=portfolio_truth,
**_build_excel_workbook_runtime(
build_reckoning_sheet=_build_reckoning_sheet,
reckoning_present=reckoning_overlay is not None,
build_dashboard=_build_dashboard,
build_all_repos=_build_all_repos,
build_portfolio_explorer=_build_portfolio_explorer,
Expand Down Expand Up @@ -1824,6 +1836,25 @@ def _build_excel_workbook(
)


def _build_reckoning_sheet(
wb: Workbook,
truth: dict | None,
overlay: ReckoningOverlay | None,
) -> None:
build_reckoning_sheet(
wb,
truth,
overlay,
get_or_create_sheet=_get_or_create_sheet,
set_sheet_header=_set_sheet_header,
style_header_row=style_header_row,
style_data_cell=style_data_cell,
apply_zebra_stripes=apply_zebra_stripes,
add_table=_add_table,
auto_width=auto_width,
)


def _build_risk_summary_sheet(wb: Workbook, risk_posture: dict) -> None:
ws = _get_or_create_sheet(wb, "Risk Summary")
ws.sheet_properties.tabColor = "DC2626"
Expand Down Expand Up @@ -1856,8 +1887,14 @@ def export_excel(
excel_mode: str = "standard",
template_path: Path | None = None,
truth_dir: Path | None = None,
reckoning_overlay: Path | ReckoningOverlay | None = None,
) -> Path:
"""Generate the flagship Excel dashboard."""
overlay = (
reckoning_overlay
if isinstance(reckoning_overlay, ReckoningOverlay)
else load_reckoning_overlay(reckoning_overlay)
)
return _export_excel_workbook(
report_path,
output_path,
Expand All @@ -1869,6 +1906,7 @@ def export_excel(
excel_mode=excel_mode,
template_path=template_path,
truth_dir=truth_dir,
reckoning_overlay=overlay,
load_risk_truth=_load_risk_truth,
default_template_path=DEFAULT_TEMPLATE_PATH,
resolve_template_path=resolve_template_path,
Expand Down
13 changes: 10 additions & 3 deletions src/github_repo_auditor/excel_export_registry_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Any, Callable

from github_repo_auditor.excel_workbook_helpers import (
CORE_VISIBLE_SHEETS,
core_visible_sheets_for_overlay,
DEFAULT_PREFERRED_SHEET_ORDER,
)
from github_repo_auditor.excel_workbook_helpers import (
Expand Down Expand Up @@ -70,18 +70,25 @@ def build_excel_workbook_runtime(
finalize_workbook_structure: Callable[..., Any] | None = None,
core_visible_sheets: set[str] | None = None,
preferred_order: list[str] | None = None,
build_reckoning_sheet: Callable[..., Any] | None = None,
reckoning_present: bool = False,
) -> dict[str, Any]:
build_steps_runner = run_workbook_build_steps or default_run_workbook_build_steps
workbook_finalizer = finalize_workbook_structure or default_finalize_workbook_structure
workbook_finalizer = (
finalize_workbook_structure or default_finalize_workbook_structure
)
visible_sheets = set(
CORE_VISIBLE_SHEETS if core_visible_sheets is None else core_visible_sheets
core_visible_sheets_for_overlay(reckoning_present)
if core_visible_sheets is None
else core_visible_sheets
)
sheet_order = list(
DEFAULT_PREFERRED_SHEET_ORDER if preferred_order is None else preferred_order
)

return {
"run_workbook_build_steps": build_steps_runner,
"build_reckoning_sheet": build_reckoning_sheet,
"build_dashboard": build_dashboard,
"build_all_repos": build_all_repos,
"build_portfolio_explorer": build_portfolio_explorer,
Expand Down
24 changes: 23 additions & 1 deletion src/github_repo_auditor/excel_export_runner_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import json
from pathlib import Path
from typing import Any
from typing import Any, Callable

from openpyxl import Workbook
from openpyxl.utils import get_column_letter

from github_repo_auditor.excel_export_truth_helpers import load_reckoning_truth
from github_repo_auditor.reckoning_overlay import ReckoningOverlay


def build_excel_workbook(
wb: Workbook,
Expand Down Expand Up @@ -71,6 +74,9 @@ def build_excel_workbook(
core_visible_sheets: set[str],
template_info_sheet: str,
preferred_order: list[str],
build_reckoning_sheet: Callable[..., Any] | None = None,
reckoning_overlay: ReckoningOverlay | None = None,
portfolio_truth: dict[str, Any] | None = None,
) -> None:
run_workbook_build_steps(
[
Expand Down Expand Up @@ -165,6 +171,10 @@ def build_excel_workbook(
),
]
)
if reckoning_overlay is not None:
if build_reckoning_sheet is None:
raise ValueError("Reckoning requires a sheet builder")
build_reckoning_sheet(wb, portfolio_truth, reckoning_overlay)
inject_sheet_navigation(wb)
apply_workbook_named_ranges(
wb,
Expand Down Expand Up @@ -202,9 +212,14 @@ def export_excel_workbook(
build_risk_summary_sheet,
inject_native_sparklines,
build_template_sparkline_specs,
reckoning_overlay: ReckoningOverlay | None = None,
) -> Path:
data = json.loads(report_path.read_text())

portfolio_truth = None
if reckoning_overlay is not None:
portfolio_truth = load_reckoning_truth(truth_dir or report_path.parent)

risk_lookup, risk_posture = load_risk_truth(truth_dir)

if excel_mode not in {"template", "standard"}:
Expand All @@ -217,6 +232,12 @@ def export_excel_workbook(
else:
wb = Workbook()

reckoning_kwargs = {}
if reckoning_overlay is not None:
reckoning_kwargs = {
"reckoning_overlay": reckoning_overlay,
"portfolio_truth": portfolio_truth,
}
build_excel_workbook(
wb,
data,
Expand All @@ -227,6 +248,7 @@ def export_excel_workbook(
collection=collection,
excel_mode=excel_mode,
risk_lookup=risk_lookup,
**reckoning_kwargs,
)

if risk_posture:
Expand Down
Loading
Loading