diff --git a/README.md b/README.md index 85eeefbb..8e6f65f5 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,13 @@ provider-backed ELF evidence was required. This improves local Dreaming runtime authority and auditability, but it does not prove Pulse, ChatGPT Tasks, Claude Dreams, hosted managed-memory, or private-corpus parity. +- Dreaming review queue after XY-1021: the June 20 follow-up adds + `elf.dreaming_review_queue/v1` through service, HTTP, and MCP readback. The queue + sits over consolidation proposals and exposes source refs, affected refs, + confidence, unsupported-claim lint, diff, policy, and review audit for existing + Dreaming suites plus tag, duplicate-merge, page-rebuild, memory-promotion, + graph-fact, and correction variants. It keeps source mutation disallowed and limits + auto-apply to approved low-risk derived organization candidates. - Live knowledge-page rebuild/lint after XY-935: the June 20 follow-up adds `cargo make real-world-memory-live-knowledge`, a Docker-contained ELF service materialization command for `knowledge_compilation`. The slice runs @@ -353,6 +360,7 @@ Detailed evidence and interpretation: - [Service-Native Dreaming Readback Report - June 19, 2026](docs/evidence/benchmarking/2026-06-19-service-native-dreaming-readback-report.md) - [OpenMemory UI/Export Product Readback Report - June 19, 2026](docs/evidence/benchmarking/2026-06-19-openmemory-ui-export-product-readback-report.md) - [Operator-Approved Public-Proxy Production-Private Addendum - June 19, 2026](docs/evidence/benchmarking/2026-06-19-operator-approved-public-proxy-production-private-addendum.md) +- [Dreaming Review Queue Report - June 20, 2026](docs/evidence/benchmarking/2026-06-20-dreaming-review-queue-report.md) - [Graph Topic-Map Report - June 20, 2026](docs/evidence/benchmarking/2026-06-20-graph-topic-map-report.md) - [Knowledge Workspace Version-Diff Report - June 20, 2026](docs/evidence/benchmarking/2026-06-20-knowledge-workspace-version-diff-report.md) - [Live Knowledge-Page Rebuild/Lint Report - June 20, 2026](docs/evidence/benchmarking/2026-06-20-live-knowledge-page-rebuild-lint-report.md) diff --git a/apps/elf-api/src/routes.rs b/apps/elf-api/src/routes.rs index 55b0c728..7465fcd0 100644 --- a/apps/elf-api/src/routes.rs +++ b/apps/elf-api/src/routes.rs @@ -52,19 +52,20 @@ use elf_service::{ CoreBlockUpsertRequest, CoreBlockUpsertResponse, CoreBlocksGetRequest, CoreBlocksResponse, DeleteRequest, DeleteResponse, DocType, DocsExcerptResponse, DocsExcerptsGetRequest, DocsGetRequest, DocsGetResponse, DocsPutRequest, DocsPutResponse, DocsSearchL0Request, - DocsSearchL0Response, EntityMemoryViewRequest, EntityMemoryViewResponse, Error, EventMessage, - GranteeKind, GraphQueryEntityRef, GraphQueryPredicateRef, GraphQueryRequest, - GraphQueryResponse, GraphReportRequest, GraphReportResponse, IngestionProfileSelector, - KnowledgePageGetRequest, KnowledgePageLintRequest, KnowledgePageLintResponse, - KnowledgePageRebuildRequest, KnowledgePageRebuildResponse, KnowledgePageResponse, - KnowledgePageSearchRequest, KnowledgePageSearchResponse, KnowledgePagesListRequest, - KnowledgePagesListResponse, ListRequest, ListResponse, MemoryHistoryGetRequest, - MemoryHistoryResponse, NoteFetchRequest, NoteFetchResponse, NoteProvenanceBundleResponse, - NoteProvenanceGetRequest, PayloadLevel, PublishNoteRequest, QueryPlan, RankingRequestOverride, - RebuildReport, SearchDetailsRequest, SearchDetailsResult, SearchExplainRequest, - SearchExplainResponse, SearchIndexItem, SearchRequest, SearchResponse, SearchSessionGetRequest, - SearchTimelineGroup, SearchTimelineRequest, SearchTrajectoryResponse, SearchTrajectorySummary, - ShareScope, SpaceGrantRevokeRequest, SpaceGrantRevokeResponse, SpaceGrantUpsertRequest, + DocsSearchL0Response, DreamingReviewQueueRequest, DreamingReviewQueueResponse, + EntityMemoryViewRequest, EntityMemoryViewResponse, Error, EventMessage, GranteeKind, + GraphQueryEntityRef, GraphQueryPredicateRef, GraphQueryRequest, GraphQueryResponse, + GraphReportRequest, GraphReportResponse, IngestionProfileSelector, KnowledgePageGetRequest, + KnowledgePageLintRequest, KnowledgePageLintResponse, KnowledgePageRebuildRequest, + KnowledgePageRebuildResponse, KnowledgePageResponse, KnowledgePageSearchRequest, + KnowledgePageSearchResponse, KnowledgePagesListRequest, KnowledgePagesListResponse, + ListRequest, ListResponse, MemoryHistoryGetRequest, MemoryHistoryResponse, NoteFetchRequest, + NoteFetchResponse, NoteProvenanceBundleResponse, NoteProvenanceGetRequest, PayloadLevel, + PublishNoteRequest, QueryPlan, RankingRequestOverride, RebuildReport, SearchDetailsRequest, + SearchDetailsResult, SearchExplainRequest, SearchExplainResponse, SearchIndexItem, + SearchRequest, SearchResponse, SearchSessionGetRequest, SearchTimelineGroup, + SearchTimelineRequest, SearchTrajectoryResponse, SearchTrajectorySummary, ShareScope, + SpaceGrantRevokeRequest, SpaceGrantRevokeResponse, SpaceGrantUpsertRequest, SpaceGrantsListRequest, TextPositionSelector, TextQuoteSelector, TraceBundleGetRequest, TraceBundleResponse, TraceGetRequest, TraceGetResponse, TraceRecentListRequest, TraceRecentListResponse, TraceTrajectoryGetRequest, UnpublishNoteRequest, UpdateRequest, @@ -146,6 +147,7 @@ const VIEWER_HTML: &str = include_str!("../static/viewer.html"); consolidation_proposals_list, consolidation_proposal_get, consolidation_proposal_review, + dreaming_review_queue, knowledge_page_rebuild, knowledge_pages_list, knowledge_pages_search, @@ -178,6 +180,7 @@ const VIEWER_HTML: &str = include_str!("../static/viewer.html"); (name = "search", description = "Progressive search sessions and raw search diagnostics."), (name = "graph", description = "Graph query and predicate administration."), (name = "consolidation", description = "Reviewable derived consolidation proposals."), + (name = "dreaming", description = "Dreaming review queue and derived memory organization."), (name = "knowledge", description = "Derived knowledge page rebuild and lint readback."), (name = "admin", description = "Local admin and operator inspection routes."), ) @@ -411,6 +414,13 @@ struct ConsolidationProposalReviewBody { review_comment: Option, } +#[derive(Clone, Debug, Deserialize)] +struct DreamingReviewQueueQuery { + run_id: Option, + review_state: Option, + limit: Option, +} + #[derive(Clone, Debug, Deserialize)] struct KnowledgePageRebuildBody { page_kind: KnowledgePageKind, @@ -742,6 +752,7 @@ pub fn admin_router(state: AppState) -> Router { "/v2/admin/consolidation/proposals/{proposal_id}/review", routing::post(consolidation_proposal_review), ) + .route("/v2/admin/dreaming/review-queue", routing::get(dreaming_review_queue)) .route("/v2/admin/knowledge/pages", routing::get(knowledge_pages_list)) .route("/v2/admin/knowledge/pages/rebuild", routing::post(knowledge_page_rebuild)) .route("/v2/admin/knowledge/pages/search", routing::post(knowledge_pages_search)) @@ -3060,6 +3071,53 @@ async fn consolidation_proposal_review( Ok(Json(response)) } +#[utoipa::path( + get, + path = "/v2/admin/dreaming/review-queue", + tag = "dreaming", + params( + ("run_id" = Option, Query, description = "Optional consolidation run filter."), + ("review_state" = Option, Query, description = "Optional review-state filter."), + ("limit" = Option, Query, description = "Maximum queue items to return."), + ), + responses( + (status = 200, description = "Dreaming review queue items.", body = Value), + (status = 400, description = "Invalid request.", body = ErrorBody), + (status = 401, description = "Authentication required.", body = ErrorBody), + (status = 403, description = "Admin access required.", body = ErrorBody), + (status = 500, description = "Internal error.", body = ErrorBody), + ) +)] +async fn dreaming_review_queue( + State(state): State, + headers: HeaderMap, + query: Result, QueryRejection>, +) -> Result, ApiError> { + let ctx = RequestContext::from_headers(&headers)?; + let Query(query) = query.map_err(|err| { + tracing::warn!(error = %err, "Invalid query parameters."); + + json_error( + StatusCode::BAD_REQUEST, + "INVALID_REQUEST", + "Invalid query parameters.".to_string(), + None, + ) + })?; + let response = state + .service + .dreaming_review_queue(DreamingReviewQueueRequest { + tenant_id: ctx.tenant_id, + project_id: ctx.project_id, + run_id: query.run_id, + review_state: query.review_state, + limit: query.limit, + }) + .await?; + + Ok(Json(response)) +} + #[utoipa::path( post, path = "/v2/admin/knowledge/pages/rebuild", diff --git a/apps/elf-eval/fixtures/report_snapshots/2026-06-20-dreaming-review-queue-report.json b/apps/elf-eval/fixtures/report_snapshots/2026-06-20-dreaming-review-queue-report.json new file mode 100644 index 00000000..9e3780aa --- /dev/null +++ b/apps/elf-eval/fixtures/report_snapshots/2026-06-20-dreaming-review-queue-report.json @@ -0,0 +1,109 @@ +{ + "schema": "elf.dreaming_review_queue_report/v1", + "authority": "XY-1021", + "generated_at": "2026-06-20T00:00:00Z", + "summary": { + "queue_schema": "elf.dreaming_review_queue/v1", + "source_mutation_allowed": false, + "high_impact_requires_review": true, + "review_audit_required": true, + "auto_apply_policy": "approved_low_risk_derived_organization_only", + "variant_count": 9, + "covered_existing_suites": [ + "memory_summary", + "proactive_brief", + "scheduled_memory", + "consolidation" + ], + "covered_future_variants": [ + "tag", + "duplicate_merge", + "page_rebuild", + "memory_promotion", + "graph_fact", + "correction" + ] + }, + "service_paths": [ + "ElfService::dreaming_review_queue", + "GET /v2/admin/dreaming/review-queue", + "MCP elf_dreaming_review_queue" + ], + "required_item_fields": [ + "source_refs", + "affected_refs", + "confidence", + "unsupported_claim_flags", + "diff", + "policy", + "review_audit" + ], + "queue_variants": [ + { + "variant": "memory_summary", + "source": "existing_suite", + "auto_apply_allowed": false, + "reason": "reviewable derived summary" + }, + { + "variant": "proactive_brief", + "source": "existing_suite", + "auto_apply_allowed": false, + "reason": "reviewable derived brief" + }, + { + "variant": "scheduled_memory", + "source": "existing_suite", + "auto_apply_allowed": false, + "reason": "reviewable scheduled derived output" + }, + { + "variant": "tag", + "source": "future_queue_variant", + "auto_apply_allowed": "after_approval_when_confidence_gte_0_9_and_no_lint", + "reason": "low-risk derived organization" + }, + { + "variant": "duplicate_merge", + "source": "future_queue_variant", + "auto_apply_allowed": "after_approval_when_confidence_gte_0_9_and_no_lint", + "reason": "low-risk derived organization" + }, + { + "variant": "page_rebuild", + "source": "future_queue_variant", + "auto_apply_allowed": false, + "reason": "derived knowledge page output stays reviewable" + }, + { + "variant": "memory_promotion", + "source": "future_queue_variant", + "auto_apply_allowed": false, + "reason": "high-impact memory note proposals require review" + }, + { + "variant": "graph_fact", + "source": "future_queue_variant", + "auto_apply_allowed": false, + "reason": "high-impact graph fact proposals require review" + }, + { + "variant": "correction", + "source": "future_queue_variant", + "auto_apply_allowed": false, + "reason": "correction proposals are high impact" + } + ], + "claim_boundaries": { + "allowed": [ + "ELF exposes a source-backed Dreaming review queue over consolidation proposals.", + "The queue item contract includes source refs, affected refs, confidence, unsupported-claim lint, diff, policy, and review audit.", + "Auto-apply is limited to approved low-risk derived organization candidates and never permits authoritative source mutation." + ], + "not_allowed": [ + "Do not claim provider-backed private corpus Dreaming readiness from this queue surface.", + "Do not claim source documents, notes, traces, or graph facts can be silently mutated by the queue.", + "Do not claim broad Dreaming product superiority without external competitor queue artifacts." + ] + } +} diff --git a/apps/elf-eval/tests/real_world_job_benchmark.rs b/apps/elf-eval/tests/real_world_job_benchmark.rs index 21faa114..dcad0d75 100644 --- a/apps/elf-eval/tests/real_world_job_benchmark.rs +++ b/apps/elf-eval/tests/real_world_job_benchmark.rs @@ -250,6 +250,10 @@ fn service_native_dreaming_readback_materialization_json_path() -> Result Result { + report_snapshot_path("2026-06-20-dreaming-review-queue-report.json") +} + fn openmemory_ui_export_product_readback_report_json_path() -> Result { report_snapshot_path("2026-06-19-openmemory-ui-export-product-readback-report.json") } @@ -288,6 +292,14 @@ fn service_native_dreaming_readback_report_markdown_path() -> Result { .join("2026-06-19-service-native-dreaming-readback-report.md")) } +fn dreaming_review_queue_report_markdown_path() -> Result { + Ok(workspace_root()? + .join("docs") + .join("evidence") + .join("benchmarking") + .join("2026-06-20-dreaming-review-queue-report.md")) +} + fn openmemory_ui_export_product_readback_report_markdown_path() -> Result { Ok(workspace_root()? .join("docs") @@ -3577,6 +3589,93 @@ fn assert_service_native_dreaming_docs(markdown: &str, benchmarking_index: &str, assert!(readme.contains("real-world-memory-service-native-dreaming")); } +#[test] +fn dreaming_review_queue_report_wires_reviewable_policy_contract() -> Result<()> { + let report = serde_json::from_str::(&fs::read_to_string( + dreaming_review_queue_report_json_path()?, + )?)?; + let markdown = fs::read_to_string(dreaming_review_queue_report_markdown_path()?)?; + let benchmarking_index = fs::read_to_string(benchmarking_index_path()?)?; + let readme = fs::read_to_string(readme_path()?)?; + let workspace = workspace_root()?; + let service = + fs::read_to_string(workspace.join("packages/elf-service/src/dreaming_review_queue.rs"))?; + let service_lib = fs::read_to_string(workspace.join("packages/elf-service/src/lib.rs"))?; + let routes = fs::read_to_string(workspace.join("apps/elf-api/src/routes.rs"))?; + let mcp = fs::read_to_string(workspace.join("apps/elf-mcp/src/server.rs"))?; + let consolidation_spec = + fs::read_to_string(workspace.join("docs/spec/system_consolidation_proposals_v1.md"))?; + let service_spec = + fs::read_to_string(workspace.join("docs/spec/system_elf_memory_service_v2.md"))?; + + assert_eq!( + report.pointer("/schema").and_then(Value::as_str), + Some("elf.dreaming_review_queue_report/v1") + ); + assert_eq!(report.pointer("/authority").and_then(Value::as_str), Some("XY-1021")); + assert_eq!( + report.pointer("/summary/queue_schema").and_then(Value::as_str), + Some("elf.dreaming_review_queue/v1") + ); + assert_eq!( + report.pointer("/summary/source_mutation_allowed").and_then(Value::as_bool), + Some(false) + ); + assert_eq!( + report.pointer("/summary/high_impact_requires_review").and_then(Value::as_bool), + Some(true) + ); + assert_eq!(report.pointer("/summary/variant_count").and_then(Value::as_u64), Some(9)); + + for suite in ["memory_summary", "proactive_brief", "scheduled_memory", "consolidation"] { + assert!(array_contains_str(&report, "/summary/covered_existing_suites", suite)?); + } + for variant in + ["tag", "duplicate_merge", "page_rebuild", "memory_promotion", "graph_fact", "correction"] + { + assert!(array_contains_str(&report, "/summary/covered_future_variants", variant)?); + + find_by_field(array_at(&report, "/queue_variants")?, "/variant", variant)?; + } + for field in [ + "source_refs", + "affected_refs", + "confidence", + "unsupported_claim_flags", + "diff", + "policy", + "review_audit", + ] { + assert!(array_contains_str(&report, "/required_item_fields", field)?); + } + + assert!(service.contains("ELF_DREAMING_REVIEW_QUEUE_SCHEMA_V1")); + assert!(service.contains("pub async fn dreaming_review_queue")); + assert!(service.contains("source_mutation_allowed: false")); + assert!(service.contains("low_risk_derived_organization")); + assert!(service.contains("available_review_actions")); + assert!(service_lib.contains("pub mod dreaming_review_queue")); + assert!(service_lib.contains("DreamingReviewQueueResponse")); + assert!(routes.contains("/v2/admin/dreaming/review-queue")); + assert!(routes.contains("DreamingReviewQueueRequest")); + assert!(routes.contains("async fn dreaming_review_queue")); + assert!(mcp.contains("elf_dreaming_review_queue")); + assert!(mcp.contains("dreaming_review_queue_schema")); + assert!(mcp.contains("/v2/admin/dreaming/review-queue")); + assert!(consolidation_spec.contains("elf.dreaming_review_queue/v1")); + assert!(consolidation_spec.contains("source_mutation_allowed")); + assert!(consolidation_spec.contains("duplicate_merge")); + assert!(service_spec.contains("GET /v2/admin/dreaming/review-queue")); + assert!(service_spec.contains("source refs, affected refs, confidence")); + assert!(markdown.contains("Dreaming Review Queue Report")); + assert!(markdown.contains("Auto-apply is limited to approved low-risk")); + assert!(benchmarking_index.contains("2026-06-20-dreaming-review-queue-report.md")); + assert!(readme.contains("Dreaming review queue after XY-1021")); + assert!(readme.contains("elf.dreaming_review_queue/v1")); + + Ok(()) +} + #[test] fn operator_approved_public_proxy_private_addendum_preserves_boundary() -> Result<()> { let report = serde_json::from_str::(&fs::read_to_string( diff --git a/apps/elf-mcp/src/server.rs b/apps/elf-mcp/src/server.rs index 63440001..16c55f9a 100644 --- a/apps/elf-mcp/src/server.rs +++ b/apps/elf-mcp/src/server.rs @@ -361,6 +361,18 @@ impl ElfMcp { self.forward(HttpMethod::Get, "/v2/entity-memory", params, None).await } + #[rmcp::tool( + name = "elf_dreaming_review_queue", + description = "List source-backed Dreaming review queue proposals with variants, affected refs, lint flags, policy gates, and review audit.", + input_schema = dreaming_review_queue_schema() + )] + async fn elf_dreaming_review_queue( + &self, + params: JsonObject, + ) -> Result { + self.forward(HttpMethod::Get, "/v2/admin/dreaming/review-queue", params, None).await + } + #[rmcp::tool( name = "elf_searches_create", description = "Create a search session using quick-find or planned-search mode. Response includes optional trajectory_summary for staged retrieval progress.", @@ -1237,6 +1249,25 @@ fn entity_memory_get_schema() -> Arc { })) } +fn dreaming_review_queue_schema() -> Arc { + Arc::new(rmcp::object!({ + "type": "object", + "additionalProperties": true, + "properties": { + "run_id": { "type": ["string", "null"], "format": "uuid" }, + "review_state": { + "type": ["string", "null"], + "enum": ["proposed", "approved", "rejected", "applied", "archived", null] + }, + "limit": { + "type": ["integer", "null"], + "minimum": 1, + "maximum": 200 + } + } + })) +} + fn searches_create_schema() -> Arc { let filter_schema = rmcp::object!({ "type": "object", @@ -1616,7 +1647,7 @@ mod tests { type RequestRecorder = Arc>>>; - const ALL_TOOL_DEFINITIONS: [ToolDefinition; 32] = [ + const ALL_TOOL_DEFINITIONS: [ToolDefinition; 33] = [ ToolDefinition::new( "elf_notes_ingest", HttpMethod::Post, @@ -1659,6 +1690,12 @@ mod tests { "/v2/entity-memory", "Fetch an entity-scoped memory view across attached core blocks and graph-linked archival notes.", ), + ToolDefinition::new( + "elf_dreaming_review_queue", + HttpMethod::Get, + "/v2/admin/dreaming/review-queue", + "List source-backed Dreaming review queue proposals with variants, affected refs, lint flags, policy gates, and review audit.", + ), ToolDefinition::new( "elf_searches_get", HttpMethod::Get, @@ -1865,6 +1902,7 @@ mod tests { "elf_space_grant_upsert", "elf_space_grant_revoke", "elf_admin_traces_recent_list", + "elf_dreaming_review_queue", "elf_admin_trace_get", "elf_admin_trajectory_get", "elf_admin_trace_item_get", diff --git a/docs/evidence/benchmarking/2026-06-20-dreaming-review-queue-report.md b/docs/evidence/benchmarking/2026-06-20-dreaming-review-queue-report.md new file mode 100644 index 00000000..1975d05f --- /dev/null +++ b/docs/evidence/benchmarking/2026-06-20-dreaming-review-queue-report.md @@ -0,0 +1,96 @@ +--- +type: Evidence +title: "Dreaming Review Queue Report - June 20, 2026" +description: "Checked-in benchmark evidence record for the source-backed Dreaming review queue." +resource: docs/evidence/benchmarking/2026-06-20-dreaming-review-queue-report.md +status: active +authority: current_state +owner: evidence +last_verified: 2026-06-20 +tags: + - docs + - evidence + - benchmarking +--- +# Dreaming Review Queue Report - June 20, 2026 + +Goal: Close XY-1021 by expanding Dreaming from derived readback and consolidation +proposal storage into a single source-backed review queue for organization and +correction proposals. + +Inputs: +`packages/elf-service/src/dreaming_review_queue.rs`, +`apps/elf-api/src/routes.rs`, `apps/elf-mcp/src/server.rs`, +`docs/spec/system_consolidation_proposals_v1.md`, and +`apps/elf-eval/fixtures/report_snapshots/2026-06-20-dreaming-review-queue-report.json`. + +## Executive Judgment + +ELF now has a service-native Dreaming review queue surface over +`consolidation_proposals`. This is not a new mutating worker and not a live provider +Dreaming loop. It is the readback and policy layer that lets an agent or operator +inspect proposed tags, duplicate merges, page rebuilds, memory promotions, graph +facts, proactive briefs, scheduled outputs, and corrections before any downstream +derived artifact is applied. + +## Contract Coverage + +| Requirement | ELF coverage | +| --- | --- | +| Source refs | Queue items expose proposal `source_refs` and immutable `source_snapshot`. | +| Affected refs | Queue items expose `target_ref` plus payload-level affected pages, memories, facts, and notes. | +| Confidence | Queue items preserve proposal confidence and use it in auto-apply policy. | +| Unsupported-claim lint | Queue items expose `unsupported_claim_flags`, contradiction markers, and staleness markers. | +| Diff | Queue items expose the reviewable proposal `diff`. | +| Review audit | Queue items include current review state, available actions, last reviewer metadata, and append-only review events. | +| Source mutation safety | Queue policy returns `source_mutation_allowed = false` and blocks source-mutation-key payloads from auto-apply. | + +## Variant Coverage + +| Variant | Coverage source | Auto-apply policy | +| --- | --- | --- | +| `memory_summary` | Existing service-native Dreaming suite | Reviewable derived output only. | +| `proactive_brief` | Existing service-native Dreaming suite | Reviewable derived output only. | +| `scheduled_memory` | Existing service-native Dreaming suite | Reviewable derived output only. | +| `tag` | Queue contract and benchmark snapshot | Candidate only after approval, confidence `>= 0.9`, no lint, and no source mutation request. | +| `duplicate_merge` | Queue contract and benchmark snapshot | Candidate only after approval, confidence `>= 0.9`, no lint, and no source mutation request. | +| `page_rebuild` | Queue contract, knowledge-page reports, and benchmark snapshot | Reviewable derived page output only. | +| `memory_promotion` | Queue contract and benchmark snapshot | High-impact review-gated memory proposal. | +| `graph_fact` | Queue contract, graph report surface, and benchmark snapshot | High-impact review-gated proposal. | +| `correction` | Queue contract and benchmark snapshot | High-impact review-gated proposal. | + +## Command Evidence + +| Command | Status | Purpose | +| --- | --- | --- | +| `cargo test -p elf-service dreaming_review_queue -- --nocapture` | pass | Unit-check queue variant normalization, source-mutation detection, review actions, affected-ref extraction, and auto-apply policy decisions. | +| `cargo test -p elf-mcp registers_all_tools -- --nocapture` | pass | Guard MCP tool registration for `elf_dreaming_review_queue`. | +| `cargo test -p elf-eval --test real_world_job_benchmark dreaming_review_queue_report_wires_reviewable_policy_contract -- --nocapture` | pass | Guard service/API/MCP/docs/snapshot coverage for XY-1021. | +| `cargo make check` | pass | Full repo gate: fmt/check-docs/check/clippy/vstyle plus 311 nextest tests, 87 skipped. | + +## Claim Boundaries + +Allowed: + +- ELF exposes `elf.dreaming_review_queue/v1` as a source-backed queue over + consolidation proposals. +- Queue items include source refs, affected refs, confidence, unsupported-claim lint, + diff, policy, and review audit. +- Auto-apply is limited to approved low-risk derived organization candidates and + never permits authoritative source mutation. + +Not allowed: + +- Do not claim provider-backed private corpus Dreaming readiness from this queue + surface. +- Do not claim source documents, notes, traces, or graph facts can be silently mutated + by the queue. +- Do not claim broad Dreaming product superiority without comparable external + competitor queue artifacts. + +## Next Optimization Direction + +The next useful product layer is an operator UI that groups these queue items by +variant, risk, affected target, and review action. Provider-backed Dreaming remains a +separate gate: it should generate proposals into this same queue, not bypass the +review and source-mutation policy. diff --git a/docs/evidence/benchmarking/index.md b/docs/evidence/benchmarking/index.md index 557d75d1..02e22aaf 100644 --- a/docs/evidence/benchmarking/index.md +++ b/docs/evidence/benchmarking/index.md @@ -43,6 +43,7 @@ Routes to: Benchmarking evidence concepts under `docs/evidence/benchmarking/`. - `2026-06-19-operator-approved-public-proxy-production-private-addendum.md`: Operator-Approved Public-Proxy Production-Private Addendum - June 19, 2026; closes the current XY-930 proxy/simulated-corpus stage with 8/8 query pass, 0 wrong_result, and explicit boundaries that this is not real private-corpus or provider-backed proof. - `2026-06-19-qmd-debug-ergonomics-dreaming-retest-report.md`: qmd Debug-Ergonomics Dreaming Retest Report - June 19, 2026; confirms qmd's default top-k/replay edge is unchanged while ELF keeps the narrow operator-debug trace/stage visibility wins. - `2026-06-19-service-native-dreaming-readback-report.md`: Service-Native Dreaming Readback Report - June 19, 2026; materializes memory summary, proactive brief, and scheduled-memory derived outputs through `ElfService` readback with 9 pass, 0 wrong_result, and 2 typed XY-930 blockers. +- `2026-06-20-dreaming-review-queue-report.md`: Dreaming Review Queue Report - June 20, 2026; adds the `elf.dreaming_review_queue/v1` source-backed queue over consolidation proposals with source refs, affected refs, lint, diff, policy, and review audit coverage for existing Dreaming suites plus tag, duplicate, page, memory-promotion, graph, and correction variants. - `2026-06-20-graph-topic-map-report.md`: Graph Topic-Map Report - June 20, 2026; adds the ELF-native `elf.graph_report/v1` readback for Postgres graph-lite facts with sourced, inferred, ambiguous, stale, and superseded topic-map markers. - `2026-06-20-knowledge-workspace-version-diff-report.md`: Knowledge Workspace Version-Diff Report - June 20, 2026; proves ELF knowledge pages now expose previous-version diff metadata without perturbing page content hashes while preserving citation, lint, and source-of-truth boundaries. - `2026-06-20-live-knowledge-page-rebuild-lint-report.md`: Live Knowledge-Page Rebuild/Lint Report - June 20, 2026; adds a Docker-contained ELF service-native knowledge-page materialization command while preserving llm-wiki, gbrain, GraphRAG, RAGFlow, LightRAG, and graphify as separate comparison targets until they emit comparable scored page artifacts. diff --git a/docs/spec/system_consolidation_proposals_v1.md b/docs/spec/system_consolidation_proposals_v1.md index 65c3629b..7fd25875 100644 --- a/docs/spec/system_consolidation_proposals_v1.md +++ b/docs/spec/system_consolidation_proposals_v1.md @@ -23,6 +23,8 @@ Status: normative Read this when: You are implementing, validating, or reviewing dreaming-inspired consolidation storage, jobs, proposals, or review flows. Not this document: Live LLM consolidation generation, viewer UI behavior, retrieval observability panels, or agentmemory import adapters. Defines: `elf.consolidation/v1` runs, proposals, source snapshots, lineage, review lifecycle, and source immutability rules. +Also defines: `elf.dreaming_review_queue/v1` readback as a policy view over +consolidation proposals. Related inputs: @@ -287,6 +289,57 @@ The first implementation exposes fixture-driven service flows: These flows must not call LLM, embedding, rerank, or external provider adapters. +## Dreaming Review Queue + +The Dreaming review queue is a readback and policy surface over +`consolidation_proposals`. It does not create a new source-of-truth table and does not +run provider generation. The canonical response schema is: + +```text +elf.dreaming_review_queue/v1 +``` + +Queue items must expose: + +- proposal id, run id, proposal kind, queue variant, apply intent, and review state +- `source_refs` and `source_snapshot` +- `target_ref` and derived `affected_refs` +- `confidence` +- `unsupported_claim_flags`, contradiction markers, and staleness markers +- reviewable `diff` +- proposed derived payload +- per-item policy readback +- current review state, available review actions, reviewer metadata, and append-only + review events + +The queue variant is inferred from explicit proposal payload metadata first, then from +`proposal_kind`, then from `apply_intent`. The required supported variants are: + +- `memory_summary` +- `proactive_brief` +- `scheduled_memory` +- `tag` +- `duplicate_merge` +- `page_rebuild` +- `memory_promotion` +- `graph_fact` +- `correction` + +Policy rules: + +- `source_mutation_allowed` must be `false`. +- Source mutation keys in `diff`, `target_ref`, or `proposed_payload` must prevent + auto-apply. +- High-impact variants such as `memory_promotion`, `graph_fact`, and `correction` + require explicit review. +- `tag` and `duplicate_merge` are the only low-risk derived organization variants. +- Low-risk derived organization may be marked auto-applyable only after approval, with + confidence at or above the queue threshold, no unsupported-claim flags, no + contradiction or staleness markers, and no source mutation request. +- Applying a queue item still means applying a derived target or marking review state; + it must not update, delete, overwrite, or deprecate authoritative notes, docs, + events, traces, graph facts, or source pointers. + ## Future Connections Future viewer work should render proposals as reviewable records with source refs, diff --git a/docs/spec/system_elf_memory_service_v2.md b/docs/spec/system_elf_memory_service_v2.md index 66e89bdf..f9d8dacb 100644 --- a/docs/spec/system_elf_memory_service_v2.md +++ b/docs/spec/system_elf_memory_service_v2.md @@ -1093,6 +1093,7 @@ Admin consolidation proposal review: - GET /v2/admin/consolidation/proposals - GET /v2/admin/consolidation/proposals/{proposal_id} - POST /v2/admin/consolidation/proposals/{proposal_id}/review +- GET /v2/admin/dreaming/review-queue Behavior: - These endpoints expose fixture-driven or manually supplied consolidation runs and @@ -1108,6 +1109,16 @@ Behavior: starts from `proposed`. - Every review action writes append-only review audit events returned by proposal detail readback. +- `GET /v2/admin/dreaming/review-queue` exposes + `elf.dreaming_review_queue/v1`, a read-only policy view over consolidation + proposals for Dreaming variants such as memory summaries, proactive briefs, + scheduled memories, tags, duplicate merges, page rebuilds, memory promotions, + graph facts, and corrections. +- Dreaming queue items must include source refs, affected refs, confidence, + unsupported-claim lint, diff, policy, and review audit. The queue must report + `source_mutation_allowed = false`; low-risk derived organization auto-apply is + limited to approved tag or duplicate-merge candidates with no lint or source + mutation request. - These endpoints must not call LLM, embedding, rerank, or external provider adapters. - They must not mutate authoritative source notes, docs, events, traces, graph facts, or search traces. diff --git a/packages/elf-service/src/dreaming_review_queue.rs b/packages/elf-service/src/dreaming_review_queue.rs new file mode 100644 index 00000000..39f948c4 --- /dev/null +++ b/packages/elf-service/src/dreaming_review_queue.rs @@ -0,0 +1,732 @@ +//! Dreaming review queue readback over consolidation proposals. + +use std::collections::BTreeSet; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::{ + ConsolidationProposalResponse, ConsolidationProposalReviewEventResponse, ElfService, Result, +}; +use elf_domain::consolidation::ConsolidationReviewState; +use elf_storage::consolidation; + +/// Schema identifier for Dreaming review queue responses. +pub const ELF_DREAMING_REVIEW_QUEUE_SCHEMA_V1: &str = "elf.dreaming_review_queue/v1"; + +const DEFAULT_QUEUE_LIMIT: u32 = 50; +const MAX_QUEUE_LIMIT: u32 = 200; +const HIGH_CONFIDENCE_AUTO_APPLY_FLOOR: f32 = 0.9; +const FORBIDDEN_SOURCE_MUTATION_KEYS: [&str; 8] = [ + "delete_source", + "delete_sources", + "overwrite_source", + "source_delete", + "source_mutation", + "source_mutations", + "source_note_updates", + "update_source", +]; + +/// Request payload for Dreaming review queue readback. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DreamingReviewQueueRequest { + /// Tenant that owns the review queue. + pub tenant_id: String, + /// Project that owns the review queue. + pub project_id: String, + /// Optional run filter. + pub run_id: Option, + /// Optional review-state filter. + pub review_state: Option, + /// Maximum number of queue items to return. + pub limit: Option, +} + +/// Dreaming review queue response. +#[derive(Clone, Debug, Serialize)] +pub struct DreamingReviewQueueResponse { + /// Response schema identifier. + pub schema: String, + /// Queue policy applied to every returned item. + pub policy: DreamingReviewQueuePolicy, + /// Aggregate queue summary. + pub summary: DreamingReviewQueueSummary, + /// Returned queue items. + pub items: Vec, +} + +/// Global review queue policy. +#[derive(Clone, Debug, Serialize)] +pub struct DreamingReviewQueuePolicy { + /// Authoritative source mutation is never allowed by this queue surface. + pub source_mutation_allowed: bool, + /// Whether high-impact proposals require explicit review. + pub high_impact_requires_review: bool, + /// Low-risk derived organization variants that may become auto-apply candidates. + pub low_risk_derived_organization_variants: Vec, + /// Review actions supported by the underlying consolidation proposal lifecycle. + pub review_actions: Vec, + /// Human-readable policy summary. + pub summary: String, +} +impl Default for DreamingReviewQueuePolicy { + fn default() -> Self { + Self { + source_mutation_allowed: false, + high_impact_requires_review: true, + low_risk_derived_organization_variants: vec![ + "tag".to_string(), + "duplicate_merge".to_string(), + ], + review_actions: vec![ + "approve".to_string(), + "apply".to_string(), + "defer".to_string(), + "discard".to_string(), + ], + summary: "Dreaming review queue proposals are source-backed derived outputs; authoritative source mutation is disallowed, and high-impact memory or graph changes remain review-gated.".to_string(), + } + } +} + +/// Aggregate queue summary. +#[derive(Clone, Debug, Default, Serialize)] +pub struct DreamingReviewQueueSummary { + /// Returned item count. + pub item_count: usize, + /// Items still waiting for review. + pub proposed_count: usize, + /// Items approved but not marked applied. + pub approved_count: usize, + /// Items marked applied to derived targets. + pub applied_count: usize, + /// Items discarded by review. + pub discarded_count: usize, + /// Items deferred for later audit. + pub deferred_count: usize, + /// Items classified as high impact. + pub high_impact_count: usize, + /// Items that request source mutation and therefore cannot be auto-applied. + pub source_mutation_requested_count: usize, + /// Items eligible for low-risk derived organization auto-apply after approval. + pub auto_apply_candidate_count: usize, + /// Items that currently satisfy the queue's auto-apply policy. + pub auto_apply_allowed_count: usize, + /// Number of distinct queue variants represented by the response. + pub variant_count: usize, +} + +/// One Dreaming review queue item. +#[derive(Clone, Debug, Serialize)] +pub struct DreamingReviewQueueItem { + /// Consolidation proposal identifier. + pub proposal_id: Uuid, + /// Parent consolidation run identifier. + pub run_id: Uuid, + /// Consolidation proposal kind. + pub proposal_kind: String, + /// Dreaming queue variant inferred from proposal metadata. + pub queue_variant: String, + /// Derived-output apply intent. + pub apply_intent: String, + /// Current review state. + pub review_state: String, + /// Source references supporting the proposal. + pub source_refs: Value, + /// Aggregate immutable source snapshot. + pub source_snapshot: Value, + /// Target affected by the proposal, when supplied. + pub target_ref: Value, + /// Affected pages, memories, facts, or derived artifacts extracted for reviewer scan. + pub affected_refs: Vec, + /// Reviewable diff. + pub diff: Value, + /// Proposal confidence. + pub confidence: f32, + /// Unsupported-claim lint flags. + pub unsupported_claim_flags: Value, + /// Contradiction markers for review. + pub contradiction_markers: Value, + /// Staleness markers for review. + pub staleness_markers: Value, + /// Proposed derived payload. + pub proposed_payload: Value, + /// Per-item policy decision. + pub policy: DreamingReviewQueueItemPolicy, + /// Review audit readback. + pub review_audit: DreamingReviewQueueAudit, + #[serde(with = "crate::time_serde")] + /// Item creation timestamp. + pub created_at: OffsetDateTime, + #[serde(with = "crate::time_serde")] + /// Item update timestamp. + pub updated_at: OffsetDateTime, +} +impl From for DreamingReviewQueueItem { + fn from(proposal: ConsolidationProposalResponse) -> Self { + let queue_variant = queue_variant_for( + proposal.proposal_kind.as_str(), + proposal.apply_intent.as_str(), + &proposal.proposed_payload, + ); + let source_mutation_requested = contains_forbidden_source_mutation_key(&proposal.diff) + || contains_forbidden_source_mutation_key(&proposal.proposed_payload) + || contains_forbidden_source_mutation_key(&proposal.target_ref); + let high_impact = high_impact_variant(queue_variant.as_str()); + let has_unsupported_claims = non_empty_json_array(&proposal.unsupported_claim_flags); + let has_review_markers = non_empty_json_array(&proposal.contradiction_markers) + || non_empty_json_array(&proposal.staleness_markers); + let auto_apply_candidate = low_risk_derived_organization(queue_variant.as_str()) + && proposal.confidence >= HIGH_CONFIDENCE_AUTO_APPLY_FLOOR + && !has_unsupported_claims + && !has_review_markers + && !source_mutation_requested; + let manual_apply_allowed = + proposal.review_state.as_str() == "approved" && !source_mutation_requested; + let auto_apply_allowed = auto_apply_candidate && manual_apply_allowed; + let requires_review = source_mutation_requested + || !matches!(proposal.review_state.as_str(), "approved" | "applied"); + let policy = DreamingReviewQueueItemPolicy { + source_mutation_requested, + high_impact, + requires_review, + auto_apply_candidate, + auto_apply_allowed, + reason: policy_reason( + source_mutation_requested, + high_impact, + has_unsupported_claims, + has_review_markers, + auto_apply_candidate, + auto_apply_allowed, + manual_apply_allowed, + ), + }; + let review_audit = DreamingReviewQueueAudit { + review_state: proposal.review_state.clone(), + available_actions: available_review_actions( + proposal.review_state.as_str(), + manual_apply_allowed, + ), + reviewer_agent_id: proposal.reviewer_agent_id.clone(), + review_comment: proposal.review_comment.clone(), + reviewed_at: proposal.reviewed_at, + review_events: proposal.review_events.clone(), + }; + + Self { + proposal_id: proposal.proposal_id, + run_id: proposal.run_id, + proposal_kind: proposal.proposal_kind, + queue_variant, + apply_intent: proposal.apply_intent, + review_state: proposal.review_state, + source_refs: proposal.source_refs, + source_snapshot: proposal.source_snapshot, + affected_refs: affected_refs(&proposal.target_ref, &proposal.proposed_payload), + target_ref: proposal.target_ref, + diff: proposal.diff, + confidence: proposal.confidence, + unsupported_claim_flags: proposal.unsupported_claim_flags, + contradiction_markers: proposal.contradiction_markers, + staleness_markers: proposal.staleness_markers, + proposed_payload: proposal.proposed_payload, + policy, + review_audit, + created_at: proposal.created_at, + updated_at: proposal.updated_at, + } + } +} + +/// Per-item policy readback. +#[derive(Clone, Debug, Serialize)] +pub struct DreamingReviewQueueItemPolicy { + /// Whether this proposal requests mutation of authoritative sources. + pub source_mutation_requested: bool, + /// Whether this item is considered high impact. + pub high_impact: bool, + /// Whether reviewer approval is required before downstream application. + pub requires_review: bool, + /// Whether this item is a low-risk derived organization auto-apply candidate. + pub auto_apply_candidate: bool, + /// Whether this item currently satisfies auto-apply policy. + pub auto_apply_allowed: bool, + /// Reason for the policy decision. + pub reason: String, +} + +/// Review audit readback for one queue item. +#[derive(Clone, Debug, Serialize)] +pub struct DreamingReviewQueueAudit { + /// Current review state. + pub review_state: String, + /// Actions currently accepted by the consolidation proposal lifecycle. + pub available_actions: Vec, + /// Agent that last reviewed the item. + pub reviewer_agent_id: Option, + /// Last reviewer comment. + pub review_comment: Option, + #[serde(with = "crate::time_serde::option")] + /// Last review timestamp. + pub reviewed_at: Option, + /// Append-only review events. + pub review_events: Vec, +} + +impl ElfService { + /// Lists consolidation proposals as a Dreaming review queue. + pub async fn dreaming_review_queue( + &self, + req: DreamingReviewQueueRequest, + ) -> Result { + let limit = bounded_queue_limit(req.limit); + let review_state = req.review_state.map(ConsolidationReviewState::as_str); + let proposals = consolidation::list_consolidation_proposals( + &self.db.pool, + req.tenant_id.as_str(), + req.project_id.as_str(), + req.run_id, + review_state, + limit, + ) + .await?; + let mut items = Vec::with_capacity(proposals.len()); + + for proposal in proposals { + let review_events = consolidation::list_consolidation_proposal_review_events( + &self.db.pool, + req.tenant_id.as_str(), + req.project_id.as_str(), + proposal.proposal_id, + ) + .await? + .into_iter() + .map(ConsolidationProposalReviewEventResponse::from) + .collect(); + let mut response = ConsolidationProposalResponse::from(proposal); + + response.review_events = review_events; + + items.push(DreamingReviewQueueItem::from(response)); + } + + Ok(DreamingReviewQueueResponse { + schema: ELF_DREAMING_REVIEW_QUEUE_SCHEMA_V1.to_string(), + policy: DreamingReviewQueuePolicy::default(), + summary: summarize_items(&items), + items, + }) + } +} + +fn summarize_items(items: &[DreamingReviewQueueItem]) -> DreamingReviewQueueSummary { + let mut summary = DreamingReviewQueueSummary { + item_count: items.len(), + ..DreamingReviewQueueSummary::default() + }; + let mut variants = BTreeSet::new(); + + for item in items { + match item.review_state.as_str() { + "proposed" => summary.proposed_count += 1, + "approved" => summary.approved_count += 1, + "applied" => summary.applied_count += 1, + "rejected" => summary.discarded_count += 1, + "archived" => summary.deferred_count += 1, + _ => {}, + } + + if item.policy.high_impact { + summary.high_impact_count += 1; + } + if item.policy.source_mutation_requested { + summary.source_mutation_requested_count += 1; + } + if item.policy.auto_apply_candidate { + summary.auto_apply_candidate_count += 1; + } + if item.policy.auto_apply_allowed { + summary.auto_apply_allowed_count += 1; + } + + variants.insert(item.queue_variant.as_str()); + } + + summary.variant_count = variants.len(); + + summary +} + +fn queue_variant_for(proposal_kind: &str, apply_intent: &str, proposed_payload: &Value) -> String { + for pointer in [ + "/queue_variant", + "/dreaming_variant", + "/proposal_variant", + "/variant", + "/artifact_kind", + "/metadata/queue_variant", + "/metadata/dreaming_variant", + "/metadata/artifact_kind", + ] { + if let Some(raw) = proposed_payload.pointer(pointer).and_then(Value::as_str) + && let Some(variant) = normalize_variant(raw) + { + return variant; + } + } + + if let Some(variant) = normalize_variant(proposal_kind) { + return variant; + } + + match apply_intent { + "create_derived_knowledge_page" | "update_derived_knowledge_page" => + "page_rebuild".to_string(), + "create_derived_graph_view" => "graph_fact".to_string(), + "create_derived_note" | "update_derived_note" => "memory_promotion".to_string(), + _ => "other".to_string(), + } +} + +fn normalize_variant(raw: &str) -> Option { + let token = raw.trim().to_ascii_lowercase().replace(['-', ' '], "_"); + + if token.is_empty() { + return None; + } + if token.contains("duplicate") || token.contains("dedupe") { + return Some("duplicate_merge".to_string()); + } + if token.contains("tag") || token.contains("taxonomy") { + return Some("tag".to_string()); + } + if token.contains("knowledge_page") || token.contains("page_rebuild") { + return Some("page_rebuild".to_string()); + } + if token.contains("graph_fact") || token.contains("graph_view") { + return Some("graph_fact".to_string()); + } + if token.contains("proactive_brief") || token.contains("daily_brief") { + return Some("proactive_brief".to_string()); + } + if token.contains("scheduled_memory") || token.contains("weekly_summary") { + return Some("scheduled_memory".to_string()); + } + if token.contains("memory_summary") || token.contains("summary") { + return Some("memory_summary".to_string()); + } + if token.contains("memory_promotion") || token.contains("derived_note") { + return Some("memory_promotion".to_string()); + } + if token.contains("correction") || token.contains("repair") { + return Some("correction".to_string()); + } + + Some(token) +} + +fn affected_refs(target_ref: &Value, proposed_payload: &Value) -> Vec { + let mut refs = Vec::new(); + + push_non_empty_object(&mut refs, target_ref); + + for pointer in [ + "/affected_refs", + "/affected_pages", + "/affected_memories", + "/affected_facts", + "/affected_notes", + ] { + match proposed_payload.pointer(pointer) { + Some(Value::Array(values)) => refs.extend(values.iter().cloned()), + Some(value) if non_empty_json_object(value) => refs.push(value.clone()), + _ => {}, + } + } + + refs +} + +fn push_non_empty_object(refs: &mut Vec, value: &Value) { + if non_empty_json_object(value) { + refs.push(value.clone()); + } +} + +fn non_empty_json_object(value: &Value) -> bool { + value.as_object().is_some_and(|object| !object.is_empty()) +} + +fn non_empty_json_array(value: &Value) -> bool { + value.as_array().is_some_and(|array| !array.is_empty()) +} + +fn contains_forbidden_source_mutation_key(value: &Value) -> bool { + match value { + Value::Object(map) => map.iter().any(|(key, nested)| { + FORBIDDEN_SOURCE_MUTATION_KEYS.contains(&key.as_str()) + || contains_forbidden_source_mutation_key(nested) + }), + Value::Array(items) => items.iter().any(contains_forbidden_source_mutation_key), + _ => false, + } +} + +fn low_risk_derived_organization(queue_variant: &str) -> bool { + matches!(queue_variant, "tag" | "duplicate_merge") +} + +fn high_impact_variant(queue_variant: &str) -> bool { + matches!(queue_variant, "memory_promotion" | "graph_fact" | "correction") +} + +fn available_review_actions(review_state: &str, manual_apply_allowed: bool) -> Vec { + let actions = match review_state { + "proposed" => &["approve", "defer", "discard"][..], + "approved" if manual_apply_allowed => &["apply", "defer", "discard"][..], + "approved" => &["defer", "discard"][..], + _ => &[][..], + }; + + actions.iter().map(|action| (*action).to_string()).collect() +} + +fn policy_reason( + source_mutation_requested: bool, + high_impact: bool, + has_unsupported_claims: bool, + has_review_markers: bool, + auto_apply_candidate: bool, + auto_apply_allowed: bool, + manual_apply_allowed: bool, +) -> String { + if source_mutation_requested { + return "source mutation is requested, so the proposal cannot be applied by the queue" + .to_string(); + } + if has_unsupported_claims || has_review_markers { + return "lint or review markers require explicit reviewer inspection".to_string(); + } + if auto_apply_allowed { + return "approved low-risk derived organization proposal satisfies auto-apply policy" + .to_string(); + } + if manual_apply_allowed { + return "approved review-gated proposal may be manually applied to a derived target" + .to_string(); + } + if high_impact { + return "high-impact memory, graph, or correction proposal requires approval before apply" + .to_string(); + } + + if auto_apply_candidate { + return "low-risk derived organization proposal is a candidate after reviewer approval" + .to_string(); + } + + "proposal remains reviewable derived output".to_string() +} + +fn bounded_queue_limit(limit: Option) -> i64 { + i64::from(limit.unwrap_or(DEFAULT_QUEUE_LIMIT).clamp(1, MAX_QUEUE_LIMIT)) +} + +#[cfg(test)] +mod tests { + use serde_json; + use time::OffsetDateTime; + use uuid::Uuid; + + use crate::{ConsolidationProposalResponse, dreaming_review_queue}; + + #[test] + fn queue_variant_prefers_payload_and_normalizes_future_variants() { + let payload = serde_json::json!({ + "metadata": { "queue_variant": "Duplicate Merge" } + }); + + assert_eq!( + dreaming_review_queue::queue_variant_for( + "derived_note", + "create_derived_note", + &payload + ), + "duplicate_merge" + ); + assert_eq!( + dreaming_review_queue::queue_variant_for( + "knowledge_page", + "update_derived_knowledge_page", + &serde_json::json!({}) + ), + "page_rebuild" + ); + assert_eq!( + dreaming_review_queue::queue_variant_for( + "correction", + "no_op", + &serde_json::json!({ "affected_notes": [] }) + ), + "correction" + ); + } + + #[test] + fn policy_detects_source_mutation_and_review_actions() { + assert!(dreaming_review_queue::contains_forbidden_source_mutation_key( + &serde_json::json!({ + "after": { "source_note_updates": [{ "note_id": "n1" }] } + }) + )); + assert_eq!( + dreaming_review_queue::available_review_actions("proposed", false), + vec!["approve", "defer", "discard"] + ); + assert_eq!( + dreaming_review_queue::available_review_actions("approved", true), + vec!["apply", "defer", "discard"] + ); + assert_eq!( + dreaming_review_queue::available_review_actions("approved", false), + vec!["defer", "discard"] + ); + assert!(dreaming_review_queue::available_review_actions("applied", false).is_empty()); + } + + #[test] + fn affected_refs_include_target_and_payload_refs() { + let refs = dreaming_review_queue::affected_refs( + &serde_json::json!({ "kind": "knowledge_page", "page_key": "architecture" }), + &serde_json::json!({ + "affected_pages": [{ "page_key": "architecture" }], + "affected_facts": [{ "fact_id": "f1" }] + }), + ); + + assert_eq!(refs.len(), 3); + } + + #[test] + fn queue_item_policy_separates_review_apply_and_auto_apply() { + let proposed_tag = dreaming_review_queue::DreamingReviewQueueItem::from(proposal( + "tag", + "no_op", + "proposed", + 0.95, + serde_json::json!({ "queue_variant": "tag" }), + serde_json::json!({ "summary": "tag", "before": {}, "after": {} }), + )); + + assert!(proposed_tag.policy.auto_apply_candidate); + assert!(!proposed_tag.policy.auto_apply_allowed); + assert!(proposed_tag.policy.requires_review); + assert_eq!( + proposed_tag.review_audit.available_actions, + vec!["approve", "defer", "discard"] + ); + + let approved_tag = dreaming_review_queue::DreamingReviewQueueItem::from(proposal( + "tag", + "no_op", + "approved", + 0.95, + serde_json::json!({ "queue_variant": "tag" }), + serde_json::json!({ "summary": "tag", "before": {}, "after": {} }), + )); + + assert!(approved_tag.policy.auto_apply_allowed); + assert!(!approved_tag.policy.requires_review); + assert_eq!(approved_tag.review_audit.available_actions, vec!["apply", "defer", "discard"]); + + let approved_graph = dreaming_review_queue::DreamingReviewQueueItem::from(proposal( + "graph_fact", + "create_derived_graph_view", + "approved", + 0.95, + serde_json::json!({ "queue_variant": "graph_fact" }), + serde_json::json!({ "summary": "graph", "before": {}, "after": {} }), + )); + + assert!(approved_graph.policy.high_impact); + assert!(!approved_graph.policy.auto_apply_allowed); + assert_eq!( + approved_graph.review_audit.available_actions, + vec!["apply", "defer", "discard"] + ); + + let source_mutation = dreaming_review_queue::DreamingReviewQueueItem::from(proposal( + "tag", + "no_op", + "approved", + 0.95, + serde_json::json!({ "queue_variant": "tag" }), + serde_json::json!({ + "summary": "source mutation", + "before": {}, + "after": { "source_mutation": true } + }), + )); + + assert!(source_mutation.policy.source_mutation_requested); + assert!(!source_mutation.policy.auto_apply_allowed); + assert!(source_mutation.policy.requires_review); + assert_eq!(source_mutation.review_audit.available_actions, vec!["defer", "discard"]); + + let memory_promotion = dreaming_review_queue::DreamingReviewQueueItem::from(proposal( + "derived_note", + "create_derived_note", + "proposed", + 0.95, + serde_json::json!({}), + serde_json::json!({ "summary": "promote", "before": {}, "after": {} }), + )); + + assert_eq!(memory_promotion.queue_variant, "memory_promotion"); + assert!(memory_promotion.policy.high_impact); + assert!(!memory_promotion.policy.auto_apply_candidate); + } + + fn proposal( + proposal_kind: &str, + apply_intent: &str, + review_state: &str, + confidence: f32, + proposed_payload: serde_json::Value, + diff: serde_json::Value, + ) -> ConsolidationProposalResponse { + let now = OffsetDateTime::UNIX_EPOCH; + + ConsolidationProposalResponse { + proposal_id: Uuid::nil(), + run_id: Uuid::nil(), + tenant_id: "tenant".to_string(), + project_id: "project".to_string(), + agent_id: "agent".to_string(), + contract_schema: "elf.consolidation/v1".to_string(), + proposal_kind: proposal_kind.to_string(), + apply_intent: apply_intent.to_string(), + review_state: review_state.to_string(), + source_refs: serde_json::json!([]), + source_snapshot: serde_json::json!({}), + lineage: serde_json::json!({}), + diff, + confidence, + unsupported_claim_flags: serde_json::json!([]), + contradiction_markers: serde_json::json!([]), + staleness_markers: serde_json::json!([]), + target_ref: serde_json::json!({}), + proposed_payload, + reviewer_agent_id: None, + review_comment: None, + reviewed_at: None, + created_at: now, + updated_at: now, + review_events: Vec::new(), + } + } +} diff --git a/packages/elf-service/src/lib.rs b/packages/elf-service/src/lib.rs index d95b02c7..a776f838 100644 --- a/packages/elf-service/src/lib.rs +++ b/packages/elf-service/src/lib.rs @@ -10,6 +10,7 @@ pub mod consolidation; pub mod core_blocks; pub mod delete; pub mod docs; +pub mod dreaming_review_queue; pub mod entity_memory; pub mod graph; pub mod graph_query; @@ -61,6 +62,11 @@ pub use self::{ DocsPutRequest, DocsPutResponse, DocsSearchL0Request, DocsSearchL0Response, TextPositionSelector, TextQuoteSelector, }, + dreaming_review_queue::{ + DreamingReviewQueueAudit, DreamingReviewQueueItem, DreamingReviewQueueItemPolicy, + DreamingReviewQueuePolicy, DreamingReviewQueueRequest, DreamingReviewQueueResponse, + DreamingReviewQueueSummary, ELF_DREAMING_REVIEW_QUEUE_SCHEMA_V1, + }, entity_memory::{ ELF_ENTITY_MEMORY_VIEW_SCHEMA_V1, EntityMemoryEntity, EntityMemoryItem, EntityMemoryRelation, EntityMemorySummary, EntityMemoryViewRequest,