Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 0 additions & 53 deletions .github/workflows/issue-triage.yml

This file was deleted.

1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
39 changes: 21 additions & 18 deletions apps/elf-api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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))
Expand Down
108 changes: 94 additions & 14 deletions apps/elf-api/tests/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
}
Expand Down Expand Up @@ -562,18 +563,77 @@ 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"]
.as_array()
.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) \
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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": {
Expand All @@ -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()
Expand Down Expand Up @@ -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.");
}
Expand All @@ -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": {
Expand All @@ -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;
Expand Down
19 changes: 12 additions & 7 deletions docs/guide/development/issue_labeling.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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`
2 changes: 1 addition & 1 deletion docs/guide/evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading
Loading