Skip to content

DL-5: Implement ProposedChange CRUD operations for MongoDB#67

Merged
spuentesp merged 13 commits into
masterfrom
copilot/manage-proposed-changes
Dec 30, 2025
Merged

DL-5: Implement ProposedChange CRUD operations for MongoDB#67
spuentesp merged 13 commits into
masterfrom
copilot/manage-proposed-changes

Conversation

Copy link
Copy Markdown

Copilot AI commented Dec 29, 2025

Adds MongoDB operations for staging canonical changes that CanonKeeper evaluates at scene end. Supports fact, entity, relationship, state_change, and event proposals with status transitions (pending → accepted/rejected).

Changes

Schemas (proposed_changes.py)

  • ProposedChangeCreate - Validates scene_id XOR story_id, stores flexible JSON content
  • ProposedChangeUpdate - Enforces status transitions, requires DecisionMetadata
  • DecisionMetadata - Tracks decided_by, decided_at, reason, canonical_ref (for accepted)
  • Evidence - Links supporting evidence (turns, snippets, sources, rules)

Operations (mongodb_tools.py)

  • mongodb_create_proposed_change - Validates scene/story exists, links to scene's proposed_changes array
  • mongodb_get_proposed_change - Returns full document with evidence and decision metadata
  • mongodb_list_proposed_changes - Filters by scene_id, story_id, status, change_type with pagination
  • mongodb_update_proposed_change - Validates status transitions, rejects updates to accepted/rejected proposals

Authority (auth.py)

  • Create/read: ["*"] (any agent)
  • Update: ["CanonKeeper"] (only CanonKeeper can accept/reject)

Example

# Agent proposes entity creation
proposal = mongodb_create_proposed_change(ProposedChangeCreate(
    scene_id=scene_id,
    change_type=ProposalType.ENTITY,
    content={"name": "Gandalf", "entity_type": "character"},
    evidence=[Evidence(type="turn", ref_id=turn_id)],
    confidence=0.95,
    proposer="Narrator"
))

# CanonKeeper accepts and links to Neo4j node
mongodb_update_proposed_change(proposal.proposal_id, ProposedChangeUpdate(
    status=ProposalStatus.ACCEPTED,
    decision_metadata=DecisionMetadata(
        decided_by="CanonKeeper",
        decided_at=datetime.now(timezone.utc),
        reason="Valid character introduction",
        canonical_ref=neo4j_entity_id
    )
))

Test Coverage

19 tests covering all change types, filters, status transitions, and validation (98% coverage)

Original prompt

This section details on the original issue you should resolve

<issue_title>DL-5: Manage Proposed Changes</issue_title>
<issue_description>Category: data-layer | Epic: 0 | Priority: high

Summary

Implement CRUD operations for ProposedChange documents in MongoDB. These are
staging documents for canonical changes that CanonKeeper evaluates at scene end.
Supports different change types (fact, entity, relationship, state_change, event)
and status transitions (pending → accepted/rejected).

Acceptance Criteria

  • mongodb_create_proposed_change creates ProposedChange document
  • mongodb_create_proposed_change validates change_type enum
  • mongodb_create_proposed_change stores content payload as flexible JSON
  • mongodb_create_proposed_change links to scene_id or story_id
  • mongodb_get_proposed_change returns full document with evidence
  • mongodb_list_proposed_changes supports filtering by status, change_type, scene_id
  • mongodb_update_proposed_change allows status transitions
  • mongodb_update_proposed_change validates status transitions (pending only → accepted/rejected)
  • mongodb_update_proposed_change records decision metadata (decided_by, decided_at, reason)
  • Accepted changes include reference to created canonical entity
  • All operations track timestamps and proposer identity
  • Unit tests achieve >= 80% coverage

Dependencies

This use case depends on:

  • DL-4

Blocks

This use case blocks:

  • P-3
  • CF-3

Implementation

Layer: 1

Files to create:

  • packages/data-layer/src/monitor_data/schemas/proposed_changes.py
  • packages/data-layer/tests/test_tools/test_proposed_change_tools.py
    Files to modify:
  • packages/data-layer/src/monitor_data/tools/mongodb_tools.py
  • packages/data-layer/src/monitor_data/middleware/auth.py

MONGODB Operations:

  • mongodb_create_proposed_change (authority: *)
  • mongodb_get_proposed_change (authority: *)
  • mongodb_list_proposed_changes (authority: *)
  • mongodb_update_proposed_change (authority: CanonKeeper)

Notes:

  • Any agent can propose changes (authority: *)
  • Only CanonKeeper can accept/reject (update status)
  • Accepted changes should include canonical_ref pointing to created node
  • Consider indexing by scene_id for efficient batch retrieval

Testing Requirements

Minimum coverage: 80%

Unit tests:

  • test_create_proposed_change_fact: change_type=fact → valid doc
  • test_create_proposed_change_entity: change_type=entity → valid doc
  • test_create_proposed_change_relationship: change_type=relationship → valid doc
  • test_list_by_scene: scene_id filter returns relevant proposals
  • test_list_by_status: pending filter works
  • ... and 3 more

Integration tests:

  • test_proposal_lifecycle: create → list → accept → verify canonical created
  • test_batch_canonization: multiple proposals → batch accept/reject

References

Documentation:


Generated from /home/sebas/monitor2/docs/use-cases/data-layer/DL-5.yml</issue_description>

<agent_instructions># Autonomous Agent Prompt Template

Use this template with GPT-4 Cortex (or similar) to implement a single use case in the MONITOR repo.

You are an autonomous coding agent working on the MONITOR repo. Follow these rules strictly:
- Work on ONE use case only: <USE CASE ID AND TITLE>.
- Layer order is mandatory: implement/verify Layer 1 (data-layer) first (schemas, tools, tests), then Layer 2 (agents) with tests, then Layer 3 (CLI) with tests. Do not touch upper layers until lower layers exist and are tested.
- CanonKeeper is the only Neo4j writer; Orchestrator may create Story. CLI never calls data-layer directly.
- Branch must be named feature/<USECASE>-short-desc. Every commit and PR must include the use-case ID.
- You must add/update tests for any code changes. Use shared fakes in tests/conftest.py; mark integration/e2e as needed.
- Valid use-case prefixes: DL-, P-, M-, Q-, I-, SYS-, CF-, ST-, RS-, DOC-.
- Run and satisfy all checks:
  python scripts/check_layer_dependencies.py
  python scripts/require_use_case_reference.py --base <base_sha>
  python scripts/check_commit_use_case.py --base <base_sha>
  python scripts/require_tests_for_code_changes.py --base <base_sha>
  bash scripts/block_todo.sh
  python scripts/check_ontology_use_cases.py
  ruff check .
  black --check .
  mypy packages
  pytest packages/data-layer --cov=packages/data-layer --cov-fail-under=70
  pytest packages/agents --cov=packages/agents --cov-fail-under=70
  pytest packages/cli --cov=packages/cli --cov-fail-under=70
  (optional) markdownlint "**/*.md" "!**/node_modules/**"
- Use the PR template and keep scope single-responsibility.

Context files to read: AGENT_SETUP.md, CLAUDE.md, docs/USE_C...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes spuentesp/monitor_dm_system#40

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI and others added 2 commits December 29, 2025 02:45
Co-authored-by: spuentesp <112034353+spuentesp@users.noreply.github.com>
Co-authored-by: spuentesp <112034353+spuentesp@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement CRUD operations for ProposedChange documents DL-5: Implement ProposedChange CRUD operations for MongoDB Dec 29, 2025
Copilot AI requested a review from spuentesp December 29, 2025 02:55
@github-actions github-actions Bot added area/data-layer Data layer changes type/tests Tests touched labels Dec 29, 2025
@spuentesp spuentesp added the copilot Human GM assistant (CF-*) label Dec 30, 2025
@spuentesp spuentesp marked this pull request as ready for review December 30, 2025 01:40
Copilot AI review requested due to automatic review settings December 30, 2025 01:40
@spuentesp spuentesp merged commit 09a9a48 into master Dec 30, 2025
23 of 25 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements MongoDB CRUD operations for ProposedChange documents, which serve as staging records for canonical changes that CanonKeeper evaluates at scene end. The implementation supports five types of proposals (fact, entity, relationship, state_change, event) with status transitions from pending to accepted/rejected.

Key Changes

  • Introduced comprehensive schemas for ProposedChange operations including validation, evidence tracking, and decision metadata
  • Implemented four MongoDB operations: create, get, list (with filtering/pagination), and update with status transition enforcement
  • Added authorization rules allowing any agent to create/read proposals while restricting status updates to CanonKeeper only

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/data-layer/src/monitor_data/schemas/proposed_changes.py Defines schemas for ProposedChange CRUD operations with validation for scene_id/story_id, Evidence, and DecisionMetadata
packages/data-layer/src/monitor_data/tools/mongodb_tools.py Implements create, get, list, and update operations with scene/story validation, flexible filtering, and status transition enforcement
packages/data-layer/src/monitor_data/middleware/auth.py Adds authority rules for the four new operations (create/read: all agents, update: CanonKeeper only)
packages/data-layer/tests/test_tools/test_proposed_change_tools.py Provides 19 comprehensive tests covering all change types, filters, status transitions, and validation scenarios
packages/data-layer/tests/conftest.py Adds shared story_data fixture for use across test files

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +144 to +186
def test_create_proposed_change_entity(
mock_get_mongo: Mock,
mock_get_neo4j: Mock,
mock_mongodb_client: Mock,
mock_neo4j_client: Mock,
scene_doc_data: Dict[str, Any],
):
"""Test creating a proposed change with type 'entity'."""
mock_get_mongo.return_value = mock_mongodb_client
mock_get_neo4j.return_value = mock_neo4j_client

# Mock MongoDB collections
scenes_collection = Mock()
proposed_changes_collection = Mock()
mock_mongodb_client.get_collection.side_effect = lambda name: {
"scenes": scenes_collection,
"proposed_changes": proposed_changes_collection,
}[name]

scenes_collection.find_one.return_value = scene_doc_data
scenes_collection.update_one.return_value = Mock()
proposed_changes_collection.insert_one.return_value = Mock()

params = ProposedChangeCreate(
scene_id=UUID(scene_doc_data["scene_id"]),
change_type=ProposalType.ENTITY,
content={
"name": "Gandalf",
"entity_type": "character",
"properties": {"race": "Maia", "class": "Wizard"},
},
confidence=1.0,
authority=Authority.GM,
proposer="CanonKeeper",
)

result = mongodb_create_proposed_change(params)

assert result.change_type == ProposalType.ENTITY
assert result.content["name"] == "Gandalf"
assert result.authority == Authority.GM
proposed_changes_collection.insert_one.assert_called_once()

Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

Missing test coverage for ProposalType.STATE_CHANGE. While tests cover FACT, ENTITY, RELATIONSHIP, and EVENT types, there's no test creating a proposed change with change_type=ProposalType.STATE_CHANGE. Add a test similar to test_create_proposed_change_entity but for state changes.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +107
@field_validator("scene_id", "story_id")
@classmethod
def validate_scene_or_story(cls, v: Optional[UUID], info) -> Optional[UUID]:
"""Validate that at least one of scene_id or story_id is provided."""
# If this is scene_id being validated and it's None, check if story_id exists
if info.field_name == "scene_id" and v is None:
# We can't check story_id here as it might not be set yet
pass
return v

Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The field_validator for scene_id and story_id doesn't perform any actual validation. The validator body is empty (just a pass statement), making it ineffective. The actual validation is performed in model_post_init, so this validator can be removed entirely as it serves no purpose.

Suggested change
@field_validator("scene_id", "story_id")
@classmethod
def validate_scene_or_story(cls, v: Optional[UUID], info) -> Optional[UUID]:
"""Validate that at least one of scene_id or story_id is provided."""
# If this is scene_id being validated and it's None, check if story_id exists
if info.field_name == "scene_id" and v is None:
# We can't check story_id here as it might not be set yet
pass
return v

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +111
"""Post-initialization validation to ensure scene_id or story_id is provided."""
if self.scene_id is None and self.story_id is None:
raise ValueError("Either scene_id or story_id must be provided")
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The PR description states "Validates scene_id XOR story_id" (exclusive OR), but the validation only checks that at least one is provided, not that exactly one is provided. The model_post_init should also reject cases where both scene_id and story_id are provided simultaneously. Add: if self.scene_id is not None and self.story_id is not None: raise ValueError("Only one of scene_id or story_id can be provided, not both")

Suggested change
"""Post-initialization validation to ensure scene_id or story_id is provided."""
if self.scene_id is None and self.story_id is None:
raise ValueError("Either scene_id or story_id must be provided")
"""Post-initialization validation to ensure exactly one of scene_id or story_id is provided."""
if self.scene_id is None and self.story_id is None:
raise ValueError("Either scene_id or story_id must be provided")
if self.scene_id is not None and self.story_id is not None:
raise ValueError("Only one of scene_id or story_id can be provided, not both")

Copilot uses AI. Check for mistakes.
Comment on lines +331 to +341
def test_create_proposed_change_no_scene_or_story():
"""Test that creating a proposal without scene_id or story_id fails validation."""
with pytest.raises(
ValueError, match="Either scene_id or story_id must be provided"
):
ProposedChangeCreate(
change_type=ProposalType.FACT,
content={"statement": "Test"},
proposer="TestAgent",
)

Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

Missing test coverage for the XOR validation of scene_id and story_id. A test should verify that providing both scene_id and story_id raises a validation error, ensuring the exclusive OR constraint described in the PR description is enforced.

Copilot uses AI. Check for mistakes.
@spuentesp spuentesp deleted the copilot/manage-proposed-changes branch April 25, 2026 00:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/data-layer Data layer changes copilot Human GM assistant (CF-*) type/tests Tests touched

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants