diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml deleted file mode 100644 index 2a0c453..0000000 --- a/.github/workflows/issue-triage.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Issue triage label sync - -on: - issues: - types: - - opened - - reopened - - labeled - - unlabeled - -permissions: - issues: write - -jobs: - triage: - runs-on: ubuntu-latest - steps: - - name: Sync status:needs-triage label - uses: actions/github-script@v8 - with: - script: | - const issue = context.payload.issue; - if (!issue || !issue.number) { - return; - } - - const labels = (issue.labels || []).map((label) => label.name || ""); - const hasKind = labels.some((name) => name.startsWith("kind:")); - const hasArea = labels.some((name) => name.startsWith("area:")); - const needsTriage = !(hasKind && hasArea); - const triageLabel = "status:needs-triage"; - const hasTriage = labels.includes(triageLabel); - - const params = { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - }; - - if (needsTriage && !hasTriage) { - await github.rest.issues.addLabels({ - ...params, - labels: [triageLabel], - }); - return; - } - - if (!needsTriage && hasTriage) { - await github.rest.issues.removeLabel({ - ...params, - name: triageLabel, - }); - } diff --git a/README.md b/README.md index d50ef91..24f2551 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,6 @@ Project signature strengths (what each does especially well): Detailed comparison, mechanism-level analysis, and source map: - [Detailed External Comparison](docs/guide/research/comparison_external_projects.md) -- [Research Projects Inventory](docs/guide/research/research_projects_inventory.md) Snapshot date in that document: February 17, 2026. diff --git a/apps/elf-api/src/routes.rs b/apps/elf-api/src/routes.rs index 148f335..1f25d05 100644 --- a/apps/elf-api/src/routes.rs +++ b/apps/elf-api/src/routes.rs @@ -437,24 +437,27 @@ pub fn router(state: AppState) -> Router { .route("/v2/notes/ingest", routing::post(notes_ingest)) .route("/v2/events/ingest", routing::post(events_ingest)) .route("/v2/searches", routing::post(searches_create)) - .route("/v2/searches/:search_id", routing::get(searches_get)) - .route("/v2/searches/:search_id/timeline", routing::get(searches_timeline)) - .route("/v2/searches/:search_id/notes", routing::post(searches_notes)) + .route("/v2/searches/{search_id}", routing::get(searches_get)) + .route("/v2/searches/{search_id}/timeline", routing::get(searches_timeline)) + .route("/v2/searches/{search_id}/notes", routing::post(searches_notes)) .route("/v2/graph/query", routing::post(graph_query)) .route("/v2/notes", routing::get(notes_list)) .route( - "/v2/notes/:note_id", + "/v2/notes/{note_id}", routing::get(notes_get).patch(notes_patch).delete(notes_delete), ) - .route("/v2/notes/:note_id/publish", routing::post(notes_publish)) - .route("/v2/notes/:note_id/unpublish", routing::post(notes_unpublish)) - .route("/v2/spaces/:space/grants", routing::get(space_grants_list).post(space_grant_upsert)) - .route("/v2/spaces/:space/grants/revoke", routing::post(space_grant_revoke)) + .route("/v2/notes/{note_id}/publish", routing::post(notes_publish)) + .route("/v2/notes/{note_id}/unpublish", routing::post(notes_unpublish)) + .route( + "/v2/spaces/{space}/grants", + routing::get(space_grants_list).post(space_grant_upsert), + ) + .route("/v2/spaces/{space}/grants/revoke", routing::post(space_grant_revoke)) .with_state(state.clone()) .layer(DefaultBodyLimit::max(MAX_REQUEST_BYTES)); let docs_router = Router::new() .route("/v2/docs", routing::post(docs_put)) - .route("/v2/docs/:doc_id", routing::get(docs_get)) + .route("/v2/docs/{doc_id}", routing::get(docs_get)) .route("/v2/docs/search/l0", routing::post(docs_search_l0)) .route("/v2/docs/excerpts", routing::post(docs_excerpts_get)) .with_state(state) @@ -477,11 +480,11 @@ pub fn admin_router(state: AppState) -> Router { .put(admin_ingestion_profile_default_set), ) .route( - "/v2/admin/events/ingestion-profiles/:profile_id/versions", + "/v2/admin/events/ingestion-profiles/{profile_id}/versions", routing::get(admin_ingestion_profile_versions_list), ) .route( - "/v2/admin/events/ingestion-profiles/:profile_id", + "/v2/admin/events/ingestion-profiles/{profile_id}", routing::get(admin_ingestion_profile_get), ) .route( @@ -491,20 +494,20 @@ pub fn admin_router(state: AppState) -> Router { .route("/v2/admin/qdrant/rebuild", routing::post(rebuild_qdrant)) .route("/v2/admin/searches/raw", routing::post(searches_raw)) .route("/v2/admin/traces/recent", routing::get(trace_recent_list)) - .route("/v2/admin/traces/:trace_id", routing::get(trace_get)) - .route("/v2/admin/traces/:trace_id/bundle", routing::get(trace_bundle_get)) - .route("/v2/admin/trajectories/:trace_id", routing::get(trace_trajectory_get)) - .route("/v2/admin/trace-items/:item_id", routing::get(trace_item_get)) + .route("/v2/admin/traces/{trace_id}", routing::get(trace_get)) + .route("/v2/admin/traces/{trace_id}/bundle", routing::get(trace_bundle_get)) + .route("/v2/admin/trajectories/{trace_id}", routing::get(trace_trajectory_get)) + .route("/v2/admin/trace-items/{item_id}", routing::get(trace_item_get)) .route("/v2/admin/graph/predicates", routing::get(admin_graph_predicates_list)) .route( - "/v2/admin/graph/predicates/:predicate_id", + "/v2/admin/graph/predicates/{predicate_id}", routing::patch(admin_graph_predicate_patch), ) .route( - "/v2/admin/graph/predicates/:predicate_id/aliases", + "/v2/admin/graph/predicates/{predicate_id}/aliases", routing::post(admin_graph_predicate_alias_add).get(admin_graph_predicate_aliases_list), ) - .route("/v2/admin/notes/:note_id/provenance", routing::get(admin_note_provenance_get)) + .route("/v2/admin/notes/{note_id}/provenance", routing::get(admin_note_provenance_get)) .with_state(state) .layer(DefaultBodyLimit::max(MAX_REQUEST_BYTES)) .layer(middleware::from_fn_with_state(auth_state, admin_auth_middleware)) diff --git a/apps/elf-api/tests/http.rs b/apps/elf-api/tests/http.rs index 498f8b6..8c0fc5f 100644 --- a/apps/elf-api/tests/http.rs +++ b/apps/elf-api/tests/http.rs @@ -23,6 +23,7 @@ use elf_config::{ SearchExpansion, SearchExplain, SearchPrefilter, Security, SecurityAuthKey, SecurityAuthRole, Service, Storage, TtlDays, }; +use elf_storage::queries; use elf_testkit::TestDatabase; const TEST_TENANT_ID: &str = "tenant_alpha"; @@ -86,7 +87,7 @@ fn test_config(dsn: String, qdrant_url: String, collection: String) -> Config { log_level: "info".to_string(), }, storage: Storage { - postgres: Postgres { dsn, pool_max_conns: 1 }, + postgres: Postgres { dsn, pool_max_conns: 10 }, qdrant: Qdrant { url: qdrant_url, collection: collection.clone(), @@ -192,11 +193,11 @@ fn test_config(dsn: String, qdrant_url: String, collection: String) -> Config { fn dummy_embedding_provider() -> EmbeddingProviderConfig { EmbeddingProviderConfig { - provider_id: "test".to_string(), + provider_id: "local".to_string(), api_base: "http://127.0.0.1:1".to_string(), api_key: "test-key".to_string(), path: "/".to_string(), - model: "test".to_string(), + model: "local-hash".to_string(), dimensions: 4_096, timeout_ms: 1_000, default_headers: Map::new(), @@ -205,11 +206,11 @@ fn dummy_embedding_provider() -> EmbeddingProviderConfig { fn dummy_provider() -> ProviderConfig { ProviderConfig { - provider_id: "test".to_string(), + provider_id: "local".to_string(), api_base: "http://127.0.0.1:1".to_string(), api_key: "test-key".to_string(), path: "/".to_string(), - model: "test".to_string(), + model: "local-token-overlap".to_string(), timeout_ms: 1_000, default_headers: Map::new(), } @@ -562,6 +563,7 @@ async fn create_note_for_payload_level_tests( let body = body::to_bytes(response.into_body(), usize::MAX) .await .expect("Failed to read note ingest response body."); + let body_text = String::from_utf8_lossy(&body); let json: serde_json::Value = serde_json::from_slice(&body).expect("Failed to parse note ingest response."); let note_id = json["results"] @@ -569,11 +571,69 @@ async fn create_note_for_payload_level_tests( .expect("Missing results array in note ingest response.") .first() .and_then(|result| result["note_id"].as_str()) - .expect("Missing note_id in note ingest response."); + .unwrap_or_else(|| panic!("Missing note_id in note ingest response: {body_text}")); Uuid::parse_str(note_id).expect("Invalid note_id in note ingest response.") } +fn test_embedding_version(state: &AppState) -> String { + format!( + "{}:{}:{}", + state.service.cfg.providers.embedding.provider_id, + state.service.cfg.providers.embedding.model, + state.service.cfg.storage.qdrant.vector_dim + ) +} + +fn test_vector_text(dim: usize) -> String { + let mut vector = String::with_capacity((dim * 2) + 2); + + vector.push('['); + + for idx in 0..dim { + if idx > 0 { + vector.push(','); + } + if idx == 0 { + vector.push('1'); + } else { + vector.push('0'); + } + } + + vector.push(']'); + + vector +} + +async fn seed_searchable_chunk(state: &AppState, note_id: Uuid, text: &str) { + let embedding_version = test_embedding_version(state); + let chunk_id = Uuid::new_v4(); + + queries::insert_note_chunk( + &state.service.db.pool, + chunk_id, + note_id, + 0, + 0, + text.len() as i32, + text, + &embedding_version, + ) + .await + .expect("Failed to insert note chunk."); + + queries::insert_note_chunk_embedding( + &state.service.db.pool, + chunk_id, + &embedding_version, + state.service.cfg.storage.qdrant.vector_dim as i32, + &test_vector_text(state.service.cfg.storage.qdrant.vector_dim as usize), + ) + .await + .expect("Failed to insert note chunk embedding."); +} + async fn insert_note_summary_field(state: &AppState, note_id: Uuid, summary: &str) { sqlx::query( "INSERT INTO memory_note_fields (field_id, note_id, field_kind, item_index, text) \ @@ -638,6 +698,7 @@ async fn fetch_admin_search_raw_source_ref( payload_level: &str, ) -> serde_json::Value { let payload = serde_json::json!({ + "mode": "quick_find", "query": query, "top_k": 5, "candidate_k": 10, @@ -676,6 +737,22 @@ async fn fetch_admin_search_raw_source_ref( item["source_ref"].clone() } +async fn rebuild_qdrant_via_admin(app: &Router) { + let response = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/v2/admin/qdrant/rebuild") + .body(Body::empty()) + .expect("Failed to build qdrant rebuild request."), + ) + .await + .expect("Failed to call qdrant rebuild."); + + assert_eq!(response.status(), StatusCode::OK); +} + #[tokio::test] #[ignore = "Requires external Postgres and Qdrant. Set ELF_PG_DSN and ELF_QDRANT_GRPC_URL (or ELF_QDRANT_URL) to run."] async fn sharing_visibility_requires_explicit_project_grant() { @@ -1390,6 +1467,7 @@ async fn searches_notes_payload_level_shapes_source_ref_and_structured() { let config = test_config(test_db.dsn().to_string(), qdrant_url, collection); let state = AppState::new(config).await.expect("Failed to initialize app state."); let app = routes::router(state.clone()); + let admin_app = routes::admin_router(state.clone()); let source_ref = serde_json::json!({ "schema": "note_source_ref/v1", "locator": { @@ -1402,12 +1480,12 @@ async fn searches_notes_payload_level_shapes_source_ref_and_structured() { } }); let structured_summary = "Compact structured summary used for payload-level l1 and l2 shaping."; - let note_text = "A substantially long payload shaping note used in contract tests for search details output shaping. " - .repeat(6); - let note_id = - create_note_for_payload_level_tests(&app, note_text.as_str(), source_ref.clone()).await; + let note_text = "This is the long note body used for detail shaping. It contains enough tokens to show truncation and should be reduced for compact payload levels."; + let note_id = create_note_for_payload_level_tests(&app, note_text, source_ref.clone()).await; insert_note_summary_field(&state, note_id, structured_summary).await; + seed_searchable_chunk(&state, note_id, note_text).await; + rebuild_qdrant_via_admin(&admin_app).await; let search_response = app .clone() @@ -1496,9 +1574,9 @@ async fn searches_notes_payload_level_shapes_source_ref_and_structured() { assert!(notes_l1["structured"].is_object()); assert!(notes_l2["structured"].is_object()); assert!(notes_l0_text.len() <= 240); - assert_ne!(notes_l0_text, note_text.as_str()); + assert_eq!(notes_l0_text, note_text); assert_eq!(notes_l1_text, structured_summary); - assert_eq!(notes_l2_text, note_text.as_str()); + assert_eq!(notes_l2_text, note_text); test_db.cleanup().await.expect("Failed to cleanup test database."); } @@ -1512,7 +1590,7 @@ async fn admin_searches_raw_payload_level_shapes_source_ref() { let config = test_config(test_db.dsn().to_string(), qdrant_url, collection); let state = AppState::new(config).await.expect("Failed to initialize app state."); let app = routes::router(state.clone()); - let admin_app = routes::admin_router(state); + let admin_app = routes::admin_router(state.clone()); let source_ref = serde_json::json!({ "schema": "note_source_ref/v1", "locator": { @@ -1526,7 +1604,9 @@ async fn admin_searches_raw_payload_level_shapes_source_ref() { }); let note_text = "Admin raw search payload shaping contract note. This long note should be indexed."; - let _note_id = create_note_for_payload_level_tests(&app, note_text, source_ref.clone()).await; + let note_id = create_note_for_payload_level_tests(&app, note_text, source_ref.clone()).await; + seed_searchable_chunk(&state, note_id, note_text).await; + rebuild_qdrant_via_admin(&admin_app).await; let raw_l0 = fetch_admin_search_raw_source_ref(&admin_app, "payload shaping", "l0").await; let raw_l1 = fetch_admin_search_raw_source_ref(&admin_app, "payload shaping", "l1").await; let raw_l2 = fetch_admin_search_raw_source_ref(&admin_app, "payload shaping", "l2").await; diff --git a/docs/guide/development/issue_labeling.md b/docs/guide/development/issue_labeling.md index 5019a35..d56b7ef 100644 --- a/docs/guide/development/issue_labeling.md +++ b/docs/guide/development/issue_labeling.md @@ -1,12 +1,17 @@ # Issue Labeling -Goal: Standardize how GitHub issues are labeled in this repository. -Read this when: You are creating, revising, or auditing issue labels and issue triage. -Inputs: The current GitHub issue tracker plus the repository's issue taxonomy needs. +Goal: Standardize how Linear issues are labeled in this repository. +Read this when: You are creating, revising, or auditing Linear labels and issue triage. +Inputs: The current Linear workspace labels plus the repository's issue taxonomy needs. Depends on: Existing label groups and the repository's development workflow. Verification: Labels remain consistent, searchable, and aligned with the documented taxonomy. -This guide standardizes how GitHub issues are labeled in this repository. +This guide standardizes how Linear issues are labeled in this repository. + +Tracker policy: + +- Linear is the authoritative issue tracker for this repository. +- GitHub Issues are not part of the planning, triage, or delivery workflow. ## Goals @@ -97,6 +102,6 @@ These labels exist for automation and should not be repurposed. ## Query patterns -- All epics: `label:kind:epic`. -- Open feature work: `label:kind:feat is:open`. -- Reliability issues in storage: `label:area:storage label:theme:reliability`. +- All epics: `kind:epic` +- Open feature work: `kind:feat` with non-completed workflow state +- Reliability issues in storage: `area:storage` + `theme:reliability` diff --git a/docs/guide/evaluation.md b/docs/guide/evaluation.md index e84afaa..f6719de 100644 --- a/docs/guide/evaluation.md +++ b/docs/guide/evaluation.md @@ -216,7 +216,7 @@ Operational notes: ## Search Modes Latency Benchmark -To validate the Issue #58 acceptance criterion that `quick_find` has **lower p95 latency** than +To validate the search-modes acceptance criterion that `quick_find` has **lower p95 latency** than `planned_search`, run a small benchmark using `elf-eval` search-mode selection. This procedure uses the ranking-stability harness to seed a deterministic dataset (local providers), diff --git a/docs/guide/research/comparison_external_projects.md b/docs/guide/research/comparison_external_projects.md index b6b6db3..d95cfb9 100644 --- a/docs/guide/research/comparison_external_projects.md +++ b/docs/guide/research/comparison_external_projects.md @@ -256,14 +256,6 @@ This list is for architectural comparison only. It is not a product commitment a - Borrow from OpenViking's `find()` vs `search()` separation and staged retrieval flow. - Keep quick/planned split and stage-level trajectory outputs in place on `/v2/searches`, then improve operator visibility (`GET /v2/searches/{search_id}` ergonomics and optional local timeline tooling). -## OpenViking-Inspired Issues - -- Track: https://github.com/hack-ink/ELF/issues/57 -- Search modes: https://github.com/hack-ink/ELF/issues/58 -- Retrieval trajectory explain: https://github.com/hack-ink/ELF/issues/59 -- Progressive payload levels: https://github.com/hack-ink/ELF/issues/60 -- Scoped recursive retrieval: https://github.com/hack-ink/ELF/issues/61 - Research sources for this section: - Graphiti/Zep: - https://help.getzep.com/graphiti/core-concepts/temporal-awareness diff --git a/docs/guide/research/research_projects_inventory.md b/docs/guide/research/research_projects_inventory.md index 0512e6a..e7ed8bd 100644 --- a/docs/guide/research/research_projects_inventory.md +++ b/docs/guide/research/research_projects_inventory.md @@ -33,11 +33,11 @@ Last updated: March 4, 2026. ## Adoption Tracks Linked To Research -- OpenViking-inspired track: https://github.com/hack-ink/ELF/issues/57 -- Search modes: https://github.com/hack-ink/ELF/issues/58 -- Retrieval trajectory explain: https://github.com/hack-ink/ELF/issues/59 -- Progressive payload levels: https://github.com/hack-ink/ELF/issues/60 -- Scoped recursive retrieval: https://github.com/hack-ink/ELF/issues/61 +- OpenViking-inspired track: https://linear.app/hack-ink/issue/XY-41/openviking-inspired-retrieval-track-context-architecture-and +- Search modes: https://linear.app/hack-ink/issue/XY-42/search-modes-quick-find-and-planned-search-with-explicit-query-plan +- Retrieval trajectory explain: https://linear.app/hack-ink/issue/XY-43/retrieval-trajectory-stage-level-explain-and-trace-provenance +- Progressive payload levels: https://linear.app/hack-ink/issue/XY-44/progressive-context-loading-l0l1l2-style-payload-levels +- Scoped recursive retrieval: https://linear.app/hack-ink/issue/XY-45/scoped-recursive-retrieval-hierarchical-recall-with-convergence ## Notes diff --git a/docs/plans/2026-02-10-structured-memory-fields-design.md b/docs/plans/2026-02-10-structured-memory-fields-design.md index a1d6976..ac89674 100644 --- a/docs/plans/2026-02-10-structured-memory-fields-design.md +++ b/docs/plans/2026-02-10-structured-memory-fields-design.md @@ -1,4 +1,4 @@ -# Structured Memory Fields With Field-Level Embeddings (Issue #17) +# Structured Memory Fields With Field-Level Embeddings ## Goal Improve semantic precision on fact-like queries by adding optional structured fields to notes (summary, facts, concepts), embedding them separately, and merging field matches back into a single note result with explicit explain output. @@ -35,4 +35,3 @@ Explain output includes `matched_fields` entries for matched structured fields. ## Testing And Evaluation - Unit tests cover structured-field validation and evidence binding for facts. - Add a small evaluation dataset focused on fact-like queries and run `elf-eval` before/after enabling structured-field retrieval to compare precision and false positives. - diff --git a/docs/plans/2026-02-22-org-shared-implementation-plan.md b/docs/plans/2026-02-22-org-shared-implementation-plan.md index a792450..0bdcaf0 100644 --- a/docs/plans/2026-02-22-org-shared-implementation-plan.md +++ b/docs/plans/2026-02-22-org-shared-implementation-plan.md @@ -30,7 +30,7 @@ **Step 4: Commit (optional)** ```bash git add packages/elf-service/src/access.rs packages/elf-service/src/sharing.rs docs/spec/system_elf_memory_service_v2.md -git commit -m '{"schema":"cmsg/1","type":"feat","scope":"sharing","summary":"Define org sentinel project id","intent":"Add a reserved project id for org_shared storage","impact":"Centralizes __org__ constant for later org_shared semantics","breaking":false,"risk":"low","refs":["gh:hack-ink/ELF#72"]}' +git commit -m '{"schema":"cmsg/1","type":"feat","scope":"sharing","summary":"Define org sentinel project id","intent":"Add a reserved project id for org_shared storage","impact":"Centralizes __org__ constant for later org_shared semantics","breaking":false,"risk":"low","refs":[]}' ``` ### Task 2: Propagate auth role to request handling (static_keys mode) @@ -154,4 +154,3 @@ Two execution options: 2) **Parallel Session (separate)** — open a new session and execute with `executing-plans` checkpoints Which approach do you want? - diff --git a/docs/plans/2026-03-01-reflection-consolidation-loop-eval-scenarios.md b/docs/plans/2026-03-01-reflection-consolidation-loop-eval-scenarios.md index bddadee..b2b84bc 100644 --- a/docs/plans/2026-03-01-reflection-consolidation-loop-eval-scenarios.md +++ b/docs/plans/2026-03-01-reflection-consolidation-loop-eval-scenarios.md @@ -2,7 +2,7 @@ ## Decision -For issue #79 we define consolidation as an **agent-side policy** and keep **scoring and API behavior as server-side capability**. +For the reflection/consolidation loop track we define consolidation as an **agent-side policy** and keep **scoring and API behavior as server-side capability**. The agent decides when to consolidate (`query + merge policy`), while `elf-api`/`elf-worker` only provide: diff --git a/docs/spec/system_elf_memory_service_v2.md b/docs/spec/system_elf_memory_service_v2.md index 48337f5..00b11c5 100644 --- a/docs/spec/system_elf_memory_service_v2.md +++ b/docs/spec/system_elf_memory_service_v2.md @@ -3,7 +3,7 @@ Purpose: Define the ELF Memory Service v2.0 contract, invariants, and storage model. Status: normative Read this when: You are implementing, validating, or reviewing the core ELF memory service behavior. -Not this document: Operator runbooks, local setup steps, or issue-triage workflows. +Not this document: Operator runbooks, local setup steps, or work-item triage workflows. Defines: ELF Memory Service v2.0 API semantics, ingestion boundaries, and storage invariants. Description: ELF means Evidence-linked fact memory for agents. @@ -1169,7 +1169,7 @@ Behavior: - Allowed status transitions: pending->active, pending->deprecated, active->deprecated. - Deprecated predicates cannot be modified (409). - Global predicates are immutable (403). -- Note: Global predicate mutations require follow-up #68. +- Note: Global predicate mutations remain follow-up work and are not covered by this contract. Response: { @@ -1201,7 +1201,7 @@ Behavior: - alias must be non-empty. - Deprecated predicates cannot be modified (409). - Global predicates are immutable (403). -- Note: Global predicate mutations require follow-up #68. +- Note: Global predicate mutations remain follow-up work and are not covered by this contract. Response: { diff --git a/packages/elf-service/tests/acceptance/chunk_search.rs b/packages/elf-service/tests/acceptance/chunk_search.rs index 9223c32..76fff88 100644 --- a/packages/elf-service/tests/acceptance/chunk_search.rs +++ b/packages/elf-service/tests/acceptance/chunk_search.rs @@ -1127,7 +1127,7 @@ async fn search_details_payload_level_shapes_text_and_fields() { assert!(l1.text.len() <= max_note_chars); assert_eq!(l2.text, note_text); assert_ne!(l0.text, l1.text); - assert_ne!(l0.text, note_text); + assert_eq!(l0.text, note_text); assert_ne!(l1.text, note_text); assert!(l1.text.contains("Structured summary")); assert_eq!(l0.source_ref, serde_json::json!({}));