Skip to content

feat: composite model governance#5

Merged
vigneshnarayanaswamy merged 16 commits intomainfrom
feat/composite-models
Apr 16, 2026
Merged

feat: composite model governance#5
vigneshnarayanaswamy merged 16 commits intomainfrom
feat/composite-models

Conversation

@vigneshnarayanaswamy
Copy link
Copy Markdown
Collaborator

Summary

  • Add composite model governance to the Ledger SDK: add_member, remove_member, members (event-replay), membership_at (point-in-time), groups (current membership aware)
  • Automatic member_changed propagation: domain events on members surface to parent composites (one level, no recursion, internal events filtered)
  • Governance lifecycle: record_observation, resolve_observation, record_validation, composite_summary
  • investigate tool surfaces last_validated and open_observation_count for composites
  • Zero ruff lint errors, zero mypy type errors (was 93+93), pre-commit hooks (ruff, mypy, gitleaks, standard checks)
  • rp1 knowledge base, project charter, and main PRD

Test plan

  • 611 tests pass, 4 skipped
  • ruff check — 0 errors
  • mypy src/ — 0 errors
  • Pre-commit hooks pass (13 checks)
  • Composite membership: add, remove, re-add after remove, event replay
  • Change propagation: one-level only, no recursion, stops after removal, internal events filtered
  • Point-in-time: membership_at with naive datetime coercion
  • Governance: observations, validations, composite_summary
  • Investigate tool: governance fields for composites

🤖 Generated with Claude Code

Vignesh Narayanaswamy and others added 16 commits April 15, 2026 14:01
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>
@vigneshnarayanaswamy vigneshnarayanaswamy merged commit 950c4df into main Apr 16, 2026
3 checks passed
@vigneshnarayanaswamy vigneshnarayanaswamy deleted the feat/composite-models branch April 16, 2026 21:41
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +623 to +629
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +825 to +835
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)
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 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant