Migrate portfolio truth lifecycle and operating-path contracts#182
Conversation
…tale->parked moves to render derived.registry_status was a pure synonym table over activity_status (_registry_status_for: "parked" if stale else activity_status), forcing every downstream function (build_risk_entry, _attention_state_for, validators, renderers) to carry two parameters for one fact. Traced the "archived" side door precisely: activity_status already produced "archived" directly inside _activity_status_for (github_archived OR declared lifecycle_state == "archived"), and registry_status only ever passed that value through unchanged. The legacy registry file never fed the computation — it only drove an advisory diff warning. So "archived" was a lifecycle fact smuggled inside a recency observation, not a separate input. - activity_status is now pure recency (active/recent/stale), computed without the archived shortcut. - derived.archived: bool is the new first-class lifecycle field (github_archived or declared lifecycle_state == "archived"), passed independently to build_risk_entry, build_operating_path_entry, and _attention_state_for. - The stale->parked relabel moved to one documented function, portfolio_truth_types.display_activity_status, used by every renderer/report surface that used to read registry_status for display (portfolio_truth_render, weekly_command_center, portfolio_context_recovery, the legacy-diff warning). - Snapshot source_summary.registry_status_counts is replaced by activity_status_counts + archived_count. Schema bumped to 0.9.0; 0.8.0 added to LEGACY_SCHEMA_VERSIONS. - portfolio_automation's ELIGIBLE_REGISTRY_STATUS/AutomationCandidate field renamed to activity_status; run_instructions_audit's archived filter now reads the boolean directly. Behavior parity verified: risk tier, attention state, and displayed status are unchanged for every repo. Full suite: 2881 passed, 2 skipped (matches main baseline exactly). ruff check: clean.
…ting_path (read-compat kept) Wires operating_path as a first-class normalized catalog field end-to-end (loader, reconciler declared_values, scorecard program resolution, intent alignment, catalog summaries) -- it was previously accepted by the resolver but never actually populated from the catalog, so every entry resolved through the intended_disposition fallback. Rewrites config/portfolio-catalog.yaml and the example catalog (179 + 3 entries, identity value mapping) to declare operating_path directly, repoints the scorecards.yaml archive-disposition rule at the same field, and keeps intended_disposition as a deprecated read-compat fallback (resolver, loader, scorecards, catalog validator) with a new loader warning when an entry still declares it.
e32d70f to
ac175fd
Compare
4d48188 to
595339f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0e6d3d6c09
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if _text(derived.get("activity_status")) not in ELIGIBLE_ACTIVITY_STATUS: | ||
| blockers.append("activity-status-not-eligible") |
There was a problem hiding this comment.
Exclude archived repos from automation candidates
After this schema split, a project can now be both recently active and archived (derived.activity_status == "active" plus derived.archived == true, e.g. a GitHub-archived repo with recent local commits). This gate only checks the recency field, so an archived/read-only repo with high path confidence and sufficient context will be emitted as an automation candidate whenever decision quality is trusted, despite the module contract saying archived repos are not eligible. Add an explicit derived.archived blocker before accepting the activity status.
Useful? React with 👍 / 👎.
| "operating_path": _select_declared( | ||
| "operating_path", repo_entry, group_entry, provenance | ||
| ), |
There was a problem hiding this comment.
Preserve repo legacy path precedence over group paths
For partially migrated catalogs, this selects operating_path from the group before a repo-specific legacy intended_disposition is considered. Because resolve_declared_operating_path then prefers any operating_path, a repo still declaring intended_disposition: archive under a group with operating_path: maintain is reconciled as maintain, losing the normal repo-over-group precedence during the promised read-compat window. Resolve the alias per source order (repo operating path, repo legacy disposition, group operating path, group legacy disposition) before passing it to pathing.
Useful? React with 👍 / 👎.
| if _safe_text(derived.get("activity_status")) == "active": | ||
| active_project_count += 1 |
There was a problem hiding this comment.
Do not count archived projects as active registry entries
With activity_status now defined as pure recency, archived projects with recent activity keep activity_status: active while carrying archived: true. This summary still feeds the rendered phrase "active registry entries", so those archived/read-only projects are counted as active and the weekly command center reports a healthier/current portfolio than the truth snapshot says. Exclude derived.archived here or count via display_activity_status.
Useful? React with 👍 / 👎.
| label: Archive Disposition | ||
| check: catalog_field_in | ||
| catalog_field: intended_disposition | ||
| catalog_field: operating_path |
There was a problem hiding this comment.
Keep the archive scorecard compatible with legacy disposition
During the one-release compatibility window, catalog entries that still use intended_disposition: archive are accepted and can still select the archive program via the scorecard resolver, but this migrated rule reads only operating_path. In that supported legacy scenario the Archive Disposition rule becomes not_applicable instead of passing, so archive entries are scored without the disposition gate. Normalize the alias before scoring or let this rule accept the legacy field too.
Useful? React with 👍 / 👎.
Stack 5/7. Separates recency from lifecycle, makes archived first-class, and migrates catalog declarations to operating_path with read compatibility. Depends on stack 4.