diff --git a/cli/tests/test_gitvs.py b/cli/tests/test_gitvs.py index 363a08992..748e1efb5 100644 --- a/cli/tests/test_gitvs.py +++ b/cli/tests/test_gitvs.py @@ -537,6 +537,42 @@ def test_custody_drift_flags_live_partnered_product_org_side() -> None: assert "jt" in drifts[0] +def test_owners_enumerates_declared_shelf_orgs() -> None: + module = _load() + estate = { + "classes": {"g": {"match": ["organvm/**"]}}, + "shelf_assignments": {"shelves": {"organvm-iii-ergon": ["mesh"]}}, + } + + assert module.owners(estate) == ["organvm", "organvm-iii-ergon"] + + +def test_shelf_drift_reports_both_directions_and_absences() -> None: + module = _load() + shelves = {"organvm-iii-ergon": ["mesh", "prima", "ghost-repo"]} + rows = [ + {"full_name": "organvm-iii-ergon/mesh"}, # declared + placed — clean + {"full_name": "organvm/prima"}, # declared but still org-side — transfer owed + {"full_name": "organvm-iii-ergon/squatter"}, # undeclared in the shelf org + {"full_name": "organvm/limen"}, # unrelated engine-room repo — silent + ] + + drifts = module.shelf_drift(shelves, rows) + + assert any("ghost-repo" in d and "absent" in d for d in drifts) + assert any(d.startswith("prima: declared shelf organvm-iii-ergon") for d in drifts) + assert any("squatter" in d and "undeclared" in d for d in drifts) + assert len(drifts) == 3 + + +def test_shelf_drift_clean_when_declared_matches_census() -> None: + module = _load() + shelves = {"organvm-iii-ergon": ["mesh"]} + rows = [{"full_name": "organvm-iii-ergon/mesh"}, {"full_name": "organvm/limen"}] + + assert module.shelf_drift(shelves, rows) == [] + + def test_custody_drift_ignores_personal_estate_undeclared_and_unreadable() -> None: module = _load() ledger = ["victoroff-os", "mesh", "prima"] diff --git a/institutio/github/estate.yaml b/institutio/github/estate.yaml index b681994ca..75271235d 100644 --- a/institutio/github/estate.yaml +++ b/institutio/github/estate.yaml @@ -285,8 +285,15 @@ orgs: actions_spending_limit_usd: 25 owner: gitvs note: "The one repo-holding org (307 repos) — sole owner of the SYSTEM estate. Dual-estate custody (2026-07-29): collaboration-born products are personally-owned assets whose custody home is the personal account (see the repo_custody resource type; membership derives from the constellation register) — sole_owner reads 'all SYSTEM code lives here', not 'no repo may live on the personal estate'. Spending limit + plan are account-level his-hand acts (GitHub billing UI, cited via L-CARD-FRAUD-HOLD / the runbook) — the doctor observes, never mutates billing." + shelves: + match: ["organvm-*", "meta-organvm"] + plan_ok: ["free"] + repos: shelf_derived + enterprise_subscription: none + owner: gitvs + note: "The backbone SHELF orgs (custody v4.0.0 Phase 2): populated from shelf_assignments per tranche; membership is class P's shelf-parity (declared == census owner, BOTH directions) — repos is derived, never a hand count. T1 ERGON populated 2026-07-30; remaining shelves fill on their tranches." reserved: - match: ["organvm-*", "meta-organvm", "a-organvm"] + match: ["a-organvm"] plan_ok: ["free"] repos: 0 enterprise_subscription: none @@ -368,6 +375,64 @@ product_ledger: - writelens - your-fit-tailored +# ── SHELF ASSIGNMENTS (custody v4.0.0 Phase 2 — the library's shelves) ────────────────────── +# Declared placement for the backbone shelf orgs. The class P rung (shelf-parity) asserts +# census owner == declared shelf BOTH directions: a declared repo living elsewhere is drift, +# an undeclared repo squatting in a shelf org is drift. Bare names — GitHub redirects keep +# every old organvm/* link alive. Tranches fill this block; adding a shelf repo = one row here. +shelf_assignments: + provenance: "Phase 2 T1 2026-07-30 — ERGON populated from the product_ledger (org-side ∧ live ∧ not held/twin)" + shelves: + organvm-iii-ergon: + - a-i-chat--exporter + - a-i-council--coliseum + - advocata + - agent--claude-smith + - agent-runtime + - agentic-titan + - anon-hookup-now + - atomic-substrata + - bountyscope + - card-trade-social + - carrier-wave--zeitgeist-thesis + - classroom-rpg-aetheria + - cve-watch + - edgarflash + - enterprise-plugin + - fetch-familiar-friends + - gens + - glyph-cascade + - growth-auditor + - life-my--midst--in + - materia-collider + - mesh + - multi-camera--livestream--framework + - my-block-warfare + - object-lessons + - palimpsest + - parlor-games--ephemera-engine + - prima + - promptscope + - public-record-data-scrapper + - search-local--happy-hour + - select-or-left-or-right-or + - shared-remembrance-gateway + - sign-signal--voice-synth + - sovereign-ecosystem--real-estate-luxury + - sovereign-systems--layer-above-hokage + - specvla-ergon--avditor-mvndi + - speech-score-engine + - surface-engine + - tab-bookmark-manager + - the-actual-news + - trade-perpetual-future + - trendpulse + - universal-waveform-explorer + - vox + - vox--publica + - vulnpulse + - writelens + # ── per-repo-CLASS governance posture ──────────────────────────────────────────────────────── # The desired-state of the `repo` / `branch_protection` types, bucketed by owner/repo glob. `required_checks` # names real .github/workflows job ids (the parity predicate asserts each names a real job — a dead @@ -489,6 +554,35 @@ classes: # (history-asset review owed). Sensitive rationale → estate.private.yaml (gitignored overlay). # Rows land by PR from `gitvs.py classify --emit-overrides` output — never auto-written. repo_overrides: + # ── ERGON shelf rows (Phase 2 T1, 2026-07-30) — movers without prior judgment rows; class + # carried verbatim from the census at move time, so the tranche changed placement, never posture. + organvm-iii-ergon/a-i-council--coliseum: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/agent--claude-smith: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/bountyscope: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/card-trade-social: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/classroom-rpg-aetheria: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/cve-watch: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/edgarflash: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/fetch-familiar-friends: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/life-my--midst--in: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/multi-camera--livestream--framework: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/my-block-warfare: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/parlor-games--ephemera-engine: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/prima: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/promptscope: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/search-local--happy-hour: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/shared-remembrance-gateway: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/sovereign-ecosystem--real-estate-luxury: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/sovereign-systems--layer-above-hokage: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/specvla-ergon--avditor-mvndi: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/surface-engine: {class: operation_private, why: "private product operation — ERGON shelf T1 2026-07-30; owned exposure decision: private core (was private_unreviewed fallthrough; Phase-4 sauce sweep owns any split)"} + organvm-iii-ergon/tab-bookmark-manager: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/the-actual-news: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/trade-perpetual-future: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/trendpulse: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/universal-waveform-explorer: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/vulnpulse: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} + organvm-iii-ergon/writelens: {class: governed_public, why: "product-ledger row — ERGON shelf T1 2026-07-30 (class carried from census at move time)"} 4444J99/victoroff-os: {class: operation_private, why: "collaboration product on the personal estate (transferred from organvm 2026-07, redirect live; dual-estate custody 2026-07-29) — partner build lane, private operation; any public face is a split, never a flip (CONST-VICTOROFF-FACE). Without this row the 4444J99/** portal glob classed it desired-PUBLIC — a latent flip hazard."} organvm/4444J99.github.io: {class: portal_public, why: "traction/value lure — portal tier"} organvm/_agent: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} @@ -498,19 +592,19 @@ repo_overrides: organvm/_dot-config: {class: vault_private, why: "raw config/state corpus — private"} organvm/_limen: {class: vault_private, why: "raw state mirror — private"} organvm/a-i--skills: {class: portal_public, why: "star leader — the estate's traffic head"} - organvm/a-i-chat--exporter: {class: portal_public, why: "traction/value lure — portal tier"} + organvm-iii-ergon/a-i-chat--exporter: {class: portal_public, why: "traction/value lure — portal tier"} organvm/a-mavs-olevm: {class: governed_public, why: "history-asset strip review owed (~1 GB public tree)", oversize: true} - organvm/advocata: {class: operation_private, why: "law-firm OS — private operation"} - organvm/agent-runtime: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} - organvm/agentic-titan: {class: portal_public, why: "traction/value lure — portal tier"} - organvm/anon-hookup-now: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/advocata: {class: operation_private, why: "law-firm OS — private operation"} + organvm-iii-ergon/agent-runtime: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/agentic-titan: {class: portal_public, why: "traction/value lure — portal tier"} + organvm-iii-ergon/anon-hookup-now: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/arca: {class: vault_private, why: "the sealed vault — ciphertext home"} - organvm/atomic-substrata: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/atomic-substrata: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/bound: {class: vault_private, why: "raw corpus w/ secret-shaped paths — private"} organvm/brainstorm-20260423: {class: vault_private, why: "planning/prompt corpus — internal strategy"} organvm/browser-state: {class: vault_private, why: "personal browser corpus — private data"} organvm/call-function--ontological: {class: portal_public, why: "traction/value lure — portal tier"} - organvm/carrier-wave--zeitgeist-thesis: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/carrier-wave--zeitgeist-thesis: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/cind-and-sol-foundation: {class: operation_private, why: "foundation operation — private"} organvm/claude-runtime-state: {class: vault_private, why: "runtime state mirror — private"} organvm/collaboratory: {class: operation_private, why: "collaboratory constellation — private operation"} @@ -541,28 +635,28 @@ repo_overrides: organvm/digital-income-organism-inquiry: {class: vault_private, why: "inquiry corpus — internal strategy"} organvm/domus-genoma: {class: operation_private, why: "the config cartridge — host/factory operation; strategy+secret-shaped paths never publish raw"} organvm/edu-organism: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} - organvm/enterprise-plugin: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/enterprise-plugin: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/euro-trip-2026: {class: vault_private, why: "personal travel — private data"} organvm/generative-abstract-environments-studies: {class: vault_private, why: "studies corpus — internal strategy"} - organvm/gens: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} - organvm/glyph-cascade: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/gens: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/glyph-cascade: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/glyph-cascade-tapes: {class: vault_private, why: "session recordings corpus — private data"} - organvm/growth-auditor: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/growth-auditor: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/hokage-chess--4444j99: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/hospes: {class: operation_private, why: "guest-ops engine; transcripts are vault-class", split: {into: ["organvm/arca"], why: "extract transcripts to the vault; engine stays private"}} organvm/in-my-head: {class: vault_private, why: "personal notes — private data"} organvm/knowledge-corpus: {class: vault_private, why: "distilled corpus — internal strategy"} organvm/manumissio: {class: operation_private, why: "sovereign inference operation — private by design"} - organvm/materia-collider: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/materia-collider: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/md-summoning: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} - organvm/mesh: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/mesh: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/meta-organvm--superproject: {class: vault_private, why: "superproject state mirror — private state"} 4444J99/micro-tato: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated); personal estate 2026-07-30 (custody v4.0.0 transfer)", publish_candidate: true} 4444J99/mirror-mirror: {class: operation_private, why: "seeded value product — wave candidate (sweep decides); personal estate 2026-07-30 (custody v4.0.0 transfer)", publish_candidate: true} organvm/my-knowledge-base: {class: operation_private, why: "value-tier knowledge operation; secret-shaped paths — sweep before any form twin"} organvm/narratological-algorithmic-lenses: {class: portal_public, why: "traction/value lure — portal tier"} organvm/netmode: {class: operation_private, why: "machine infra, not a product (sweep 2026-07-30): network-health receipts, session-blockers, conductor-tranche dependencies ride it — engine room, no publish wave"} - organvm/object-lessons: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/object-lessons: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/opencode-plans: {class: vault_private, why: "planning/prompt corpus — internal strategy"} organvm/organvm-corpvs-testamentvm: {class: vault_private, why: "testament corpus — internal strategy"} organvm/organvm-i-theoria--superproject: {class: vault_private, why: "superproject state mirror — private state"} @@ -573,7 +667,7 @@ repo_overrides: organvm/organvm-v-logos--superproject: {class: vault_private, why: "superproject state mirror — private state"} organvm/organvm-vi-koinonia--superproject: {class: vault_private, why: "superproject state mirror — private state"} organvm/organvm-vii-kerygma--superproject: {class: vault_private, why: "superproject state mirror — private state"} - organvm/palimpsest: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/palimpsest: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/payrail: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} 4444J99/peer-audited--behavioral-blockchain: {class: portal_public, why: "traction/value lure — portal tier; personal estate 2026-07-30 (custody v4.0.0 transfer, jtenen lane intact)"} organvm/persona-fleet: {class: vault_private, why: "persona corpus — private data"} @@ -582,28 +676,28 @@ repo_overrides: organvm/portvs: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/process-environment-enactment-20260609173021: {class: vault_private, why: "planning/prompt corpus — internal strategy"} organvm/prompt-registry-archive: {class: vault_private, why: "planning/prompt corpus — internal strategy"} - organvm/public-record-data-scrapper: {class: portal_public, why: "traction/value lure — portal tier"} + organvm-iii-ergon/public-record-data-scrapper: {class: portal_public, why: "traction/value lure — portal tier"} organvm/quaestor: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/quick-fire--all-command: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/relationship-pipeline: {class: vault_private, why: "personal-relations data — private"} organvm/render-second-amendment: {class: vault_private, why: "held for content-policy review (rationale in private overlay)", oversize: true} organvm/scale-threshold-emergence: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} - organvm/select-or-left-or-right-or: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/select-or-left-or-right-or: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/session-meta: {class: vault_private, why: "4.4 GB raw session corpus; value is mined, never published", oversize: true, split: {into: ["organvm/arca"], why: "evict sealed corpus to the vault; slim/archive the residue"}} organvm/session-stone-sanitized-019ea761-bff5-7841-b772-f2d85475092e-20260609175059: {class: vault_private, why: "session archive — private data"} - organvm/sign-signal--voice-synth: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/sign-signal--voice-synth: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/sovereign--ground: {class: vault_private, why: "ground corpus — internal strategy"} organvm/sovereign--ground--4444j99: {class: vault_private, why: "ground corpus — internal strategy"} 4444J99/sovereign-systems--elevate-align: {class: operation_private, why: "partner-shared build lane — protected, review-gated (repo_collaborators grant); personal estate 2026-07-30 (custody v4.0.0 transfer; flipped private personal-side — the org-side 422 seat-metering block dissolved exactly as doctrine predicted)"} organvm/speculum-opencode: {class: vault_private, why: "planning/prompt corpus — internal strategy"} - organvm/speech-score-engine: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/speech-score-engine: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/studium-generale: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/system-system--system: {class: vault_private, why: "system corpus — internal strategy"} organvm/the-invisible-ledger: {class: operation_private, why: "financial ledger operation — private"} organvm/universal-mail--automation: {class: portal_public, why: "traction/value lure — portal tier"} - organvm/vox: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/vox: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/vox--architectura-gubernatio: {class: vault_private, why: "design-provenance corpus — internal strategy"} - organvm/vox--publica: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} + organvm-iii-ergon/vox--publica: {class: operation_private, why: "product operation; path-sample clean — publish-wave candidate (sweep + lever gated)", publish_candidate: true} organvm/workspace--superproject: {class: vault_private, why: "superproject state mirror — private state"} # ── expected orgs — reserved namespaces are declared, an unexpected org is drift ──────────────── diff --git a/scripts/gitvs.py b/scripts/gitvs.py index 0ae4f8c74..4ca56db9b 100644 --- a/scripts/gitvs.py +++ b/scripts/gitvs.py @@ -218,6 +218,12 @@ def owners(estate: dict) -> list[str]: owner = str(m).split("/", 1)[0] if owner and owner not in ("*", "**") and owner not in derived: derived.append(owner) + # Shelf orgs are declared registry data (shelf_assignments) — enumerate them too, or the + # census never sees shelf repos and class P reads every declared shelf row as absent. + for org in (estate.get("shelf_assignments") or {}).get("shelves") or {}: + o = str(org) + if o and o not in derived: + derived.append(o) return derived or ["organvm"] @@ -905,21 +911,25 @@ def _org_posture(estate: dict, online: bool) -> dict: return out -def _owner_repos(owner: str, token: str | None) -> list[dict] | None: +def _owner_repos( + owner: str, + token: str | None, # allow-secret (type annotation, no value) + user_scoped: bool = False, +) -> list[dict] | None: """Enumerate ALL repos of an owner with per-repo census facts. Tries the org route first — /orgs/{owner}/repos?type=all surfaces the private repos the cascade token can see (the /users route is structurally public-only, the census blindness this Lens fix removes) — then falls back - to /users/{owner}/repos for personal accounts. None ⟺ both routes failed (fail-open).""" + to /users/{owner}/repos for personal accounts. `user_scoped` runs the read on the owner's + NATIVE gh identity: non-canonical owners (personal estate, shelf orgs) sit outside the App + installation, and the App token silently hides their PRIVATE repos. None ⟺ both routes + failed (fail-open).""" jq = ( ".[] | {full_name, private, fork, archived, size, description, homepage, " "stars: .stargazers_count, topics_count: ((.topics // []) | length), pushed_at}" ) for route in (f"/orgs/{owner}/repos?type=all", f"/users/{owner}/repos"): - r = _gh( - ["api", route, "--paginate", "-X", "GET", "-F", "per_page=100", "--jq", jq], - token, - timeout=180, - ) + args = ["api", route, "--paginate", "-X", "GET", "-F", "per_page=100", "--jq", jq] + r = _gh_user(args, timeout=180) if user_scoped else _gh(args, token, timeout=180) if r.returncode != 0: continue if not (r.stdout or "").strip(): @@ -962,18 +972,23 @@ def _collaborator_census(estate: dict, access: dict | None, token: str | None, o policy = access.get("policy") or {} probe = sorted({str(r) for r in grants} | {str(r) for r in (policy.get("never_grant_repos") or [])}) ok = True - org_set = {str(o) for o in owners(estate)} + + def _owner_org_class(repo: str) -> str | None: + # The orgs-registry class is the routing truth: 'canonical' rides the App token; + # everything else (shelf orgs, and the personal account which matches NO orgs row) + # sits outside the App installation and must read user-scoped. NOTE: owners(estate) + # is NOT usable here — it includes the personal account via its class glob. + cls_name, _row = _org_class(repo.split("/", 1)[0], estate) + return cls_name def _roll(repo: str, path: str, jq: str) -> subprocess.CompletedProcess: - # Personal-estate rolls are invisible to the org-installed App identity (dual-estate - # custody: partner lanes live on the personal account) — read them user-scoped. - if repo.split("/", 1)[0] not in org_set: - return _gh_user(["api", path, "--jq", jq], timeout=30) - return _gh(["api", path, "--jq", jq], token, timeout=30) + if _owner_org_class(repo) == "canonical": + return _gh(["api", path, "--jq", jq], token, timeout=30) + return _gh_user(["api", path, "--jq", jq], timeout=30) def _outside_path_jq(repo: str) -> tuple[str, str]: owner, _, _name = repo.partition("/") - if owner not in org_set: + if _owner_org_class(repo) is None: # 'outside' is an ORG affiliation — on a personal repo it misses direct invitees # (victoroff-os: david reads write in the full roll, absent from the outside roll). # The personal-estate lens is every collaborator except the owner. @@ -1085,7 +1100,10 @@ def observe(estate: dict) -> dict: facts_all: list[dict] = [] complete = True for owner in owner_list: - rows = _owner_repos(owner, token) + # Non-canonical owners (personal estate, shelf orgs) sit outside the App + # installation — enumerate them user-scoped or their private repos vanish silently. + cls_name, _row = _org_class(owner, estate) + rows = _owner_repos(owner, token, user_scoped=(cls_name != "canonical")) if rows is None: complete = False continue @@ -1623,6 +1641,33 @@ def custody_drift(ledger: list, grants: dict, by_repo: dict, org_set: set) -> li return out +def shelf_drift(shelves: dict, rows: list) -> list[str]: + """Class P's pure join (shelf parity, custody v4.0.0 Phase 2): declared shelf membership + (estate shelf_assignments, bare names) vs census owner, BOTH directions — a declared repo + living elsewhere is drift, and an undeclared repo squatting in a shelf org is drift. + Deterministic (sorted); reports remediation, never fires a transfer.""" + declared: dict[str, set[str]] = {str(o): {str(n) for n in (ns or [])} for o, ns in (shelves or {}).items()} + owners_by_name: dict[str, set[str]] = {} + for r in rows or []: + full = str((r or {}).get("full_name") or "") + o, _, n = full.partition("/") + if o and n: + owners_by_name.setdefault(n, set()).add(o) + out: list[str] = [] + for org, names in sorted(declared.items()): + for n in sorted(names): + owners_ = owners_by_name.get(n) or set() + if not owners_: + out.append(f"{org}/{n}: declared on the shelf but absent from the census") + elif org not in owners_: + out.append(f"{n}: declared shelf {org}, census owner {'/'.join(sorted(owners_))} — transfer owed") + for n, owners_ in sorted(owners_by_name.items()): + for o in sorted(owners_): + if o in declared and n not in declared[o]: + out.append(f"{o}/{n}: undeclared repo in a shelf org — declare the row or move it out") + return out + + def doctor(estate: dict, *, parity_only: bool, offline: bool, strict: bool = False) -> int: """The Diff operator. Exit 0 ⟺ drift == ∅ (over the rungs that could run). SKIP is never a faked PASS.""" fails: list[str] = [] @@ -1877,6 +1922,17 @@ def doctor(estate: dict, *, parity_only: bool, offline: bool, strict: bool = Fal f"[O custody-drift] {d} → " + (f"{o_atom} (owned, open)" if o_atom in homed else f"{o_atom} (UNHOMED)") ) + # P — shelf parity (custody v4.0.0 Phase 2): declared shelf membership vs census owner, + # both directions. Real drift is RED with the exact remediation named; the rung never + # fires a transfer itself. + shelves_reg = ((estate.get("shelf_assignments") or {}).get("shelves")) or {} + if shelves_reg: + if not rows: + skips.append("[P shelf-parity] no census facts (run census online first)") + else: + for d in shelf_drift(shelves_reg, rows): + fails.append(f"[P shelf-parity] {d}") + # A/D are per-repo posture rungs — the census surfaces the inputs; the full per-repo # assertion arms with the reconcile layer (bounded rotating window). Reported SKIP, never faked. for tag in ("A protection-missing", "D permission-over-grant"):