feat: composite model governance#5
Conversation
Introduces two new Ledger methods for composite model membership management: add_member() records a member_added snapshot and creates a member_of dependency link; remove_member() records a member_removed snapshot (append-only, no link deletion). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replays member_added/member_removed snapshots up to a given date to reconstruct which models were members of a composite at that moment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When record() is called on any model that belongs to a composite, the composite automatically receives a member_changed snapshot carrying the member name, member hash, original event type, and original snapshot hash. A _propagating flag and an _INTERNAL_EVENTS blocklist prevent infinite recursion and suppress noise from ledger bookkeeping events. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thin governance convenience wrappers around record() that emit observation_issued, observation_resolved, and validated event types with structured payloads for MRM observation lifecycle tracking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns a list of dicts for all composite models with derived fields: member_count, last_validated timestamp, and open_observation_count computed by replaying observation_issued/observation_resolved events. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds last_validated and open_observation_count fields to InvestigateOutput, populated for composite model types by scanning snapshot history for validated and observation_issued/resolved events. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RY helpers - membership_at() now seeds from depends_on/member_of links (like members() does) before overlaying events, so groups seeded via register_group() show their members at any queried point in time - Added observation_issued, observation_resolved, and validated to _INTERNAL_EVENTS so governance events recorded on a composite do not propagate as member_changed to grandparent composites - Moved _INTERNAL_EVENTS frozenset to module level (recreated on every record() call previously) - Extracted observation-counting logic into Ledger._open_observation_count() and replaced duplicated code in composite_summary() and investigate.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generated KB with parallel map-reduce analysis (139 files): - architecture.md: layer diagram, data flows, deployment - modules.md: 14 modules, 37 components, dependency graph - patterns.md: coding conventions, extension mechanisms - concept_map.md: MRM domain concepts and terminology - charter.md: project vision, users, scope, success criteria - main PRD: requirements, milestones, assumptions Also adds rp1 gitignore rules and CLAUDE.md KB loading guidance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitHub's Mermaid renderer doesn't support the `&` operator for multi-target edges. Expand to individual edge declarations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
'Link' is a reserved keyword in Mermaid's parser. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
'graph' and 'sdk' collide with Mermaid keywords. Use mod_ prefix with explicit labels. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Auto-fix 33 ruff lint issues (import sorting, unused imports) - Auto-format 42 files with ruff format - Fix Ledger.list shadowing builtin: use builtins.list in annotations - Add or-[] guards for nullable list iterations across tools/sdk - Fix cli/app.py: typer.Exit types, deleted variable access, export args - Wrap demo.py strings in DataPort() constructors - Add type: ignore for snowflake write_pandas dict unpacking - Fix draft_version __exit__ return type - Add mypy overrides for optional deps (xgboost, sklearn, etc.) - Add per-file E501 ignores for SQL/HTML template files - Add ruff auto-format to pre-commit hook ruff: 0 errors, mypy: 0 errors, pytest: 610 passed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Based on block/mcp-jupyter patterns. Hooks: - pre-commit-hooks: trailing-whitespace, end-of-file-fixer, check-yaml, check-json, check-toml, check-added-large-files, check-merge-conflict, debug-statements, mixed-line-ending - ruff: auto-fix lint + format on commit - gitleaks: OSS boundary secret scanning Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Catches type errors before they hit remote. Runs on the full src/ directory with pydantic and httpx stubs installed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- groups() now replays member_added/member_removed events so removed members no longer trigger change propagation to former composites - membership_at() coerces naive datetimes to UTC to prevent TypeError - Rename _open_observation_count to public (tools layer was calling it) - Use already-resolved ref in propagation instead of re-resolving - Remove redundant or-[] guard in investigate.py - Fix duplicate member in TestInvestigateComposite setup - Add test_no_propagation_after_member_removed - Fix mypy pre-commit config and optional dep overrides 611 passed, 4 skipped Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: db22f23ea0
ℹ️ 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".
| candidates = [d["model"] for d in deps if d.get("relationship") == "member_of"] | ||
| ref = self._resolve_model(model) | ||
| result: builtins.list[ModelRef] = [] | ||
| for comp in candidates: | ||
| current_members = self.members(comp) | ||
| if any(m.model_hash == ref.model_hash for m in current_members): | ||
| result.append(comp) |
There was a problem hiding this comment.
Deduplicate parent groups before member_changed propagation
groups() builds candidates from every historical member_of edge and appends each matching composite without deduplication. Because add_member() creates a new dependency link and remove_member() is append-only, a remove/re-add cycle leaves multiple historical edges for the same composite, so groups() can return duplicates. record() then emits one member_changed snapshot per returned group, producing duplicated propagated events and inflated composite histories for a single member change.
Useful? React with 👍 / 👎.
| issued_ids: set[str] = set() | ||
| resolved_ids: set[str] = set() | ||
| for s in sorted(snapshots, key=lambda snap: snap.timestamp): | ||
| obs_id = s.payload.get("observation_id") | ||
| if not obs_id: | ||
| continue | ||
| if s.event_type == "observation_issued": | ||
| issued_ids.add(obs_id) | ||
| elif s.event_type == "observation_resolved": | ||
| resolved_ids.add(obs_id) | ||
| return len(issued_ids - resolved_ids) |
There was a problem hiding this comment.
Preserve observation event order when counting open items
open_observation_count() uses set subtraction (issued_ids - resolved_ids), which ignores event order and collapses repeated IDs. If an observation is resolved and later re-opened with the same observation_id, the ID stays in both sets and is counted as closed, so summaries and investigate output under-report open observations in that workflow.
Useful? React with 👍 / 👎.
Summary
add_member,remove_member,members(event-replay),membership_at(point-in-time),groups(current membership aware)member_changedpropagation: domain events on members surface to parent composites (one level, no recursion, internal events filtered)record_observation,resolve_observation,record_validation,composite_summaryinvestigatetool surfaceslast_validatedandopen_observation_countfor compositesTest plan
ruff check— 0 errorsmypy src/— 0 errorsmembership_atwith naive datetime coercion🤖 Generated with Claude Code