diff --git a/include/cdc/cdc_metrics.h b/include/cdc/cdc_metrics.h index 6c8adacb9b..458c6f64eb 100644 --- a/include/cdc/cdc_metrics.h +++ b/include/cdc/cdc_metrics.h @@ -47,6 +47,7 @@ class LatencyHistogram { /** * @brief Record a latency sample in microseconds + * @param latency_micros Sample latency value in microseconds. */ void record(uint64_t latency_micros) { count_++; @@ -59,6 +60,7 @@ class LatencyHistogram { /** * @brief Get count of samples + * @return Total number of recorded samples. */ uint64_t count() const { return count_.load(); @@ -66,6 +68,7 @@ class LatencyHistogram { /** * @brief Get average latency in microseconds + * @return Average latency in microseconds, or 0.0 when empty. */ double average() const { uint64_t cnt = count_.load(); @@ -74,6 +77,7 @@ class LatencyHistogram { /** * @brief Get P50 (median) latency in microseconds + * @return Median latency in microseconds, or 0 when empty. */ uint64_t p50() const { return percentile(0.50); @@ -81,6 +85,7 @@ class LatencyHistogram { /** * @brief Get P95 latency in microseconds + * @return 95th percentile latency in microseconds, or 0 when empty. */ uint64_t p95() const { return percentile(0.95); @@ -88,6 +93,7 @@ class LatencyHistogram { /** * @brief Get P99 latency in microseconds + * @return 99th percentile latency in microseconds, or 0 when empty. */ uint64_t p99() const { return percentile(0.99); @@ -95,6 +101,8 @@ class LatencyHistogram { /** * @brief Get percentile latency in microseconds + * @param p Target percentile as a fraction in the range [0.0, 1.0]. + * @return Requested percentile latency in microseconds, or 0 when empty. */ uint64_t percentile(double p) const { uint64_t total = count_.load(); @@ -115,6 +123,7 @@ class LatencyHistogram { /** * @brief Convert to JSON for monitoring + * @return JSON object containing the histogram snapshot. */ nlohmann::json toJson() const { return { @@ -278,6 +287,7 @@ struct CDCMetrics { /** * @brief Convert all metrics to JSON + * @return JSON object containing the full CDC metrics snapshot. */ nlohmann::json toJson() const { return { diff --git a/include/governance/audit_batch_writer.h b/include/governance/audit_batch_writer.h index c4098516d5..a1bf4e2e3b 100644 --- a/include/governance/audit_batch_writer.h +++ b/include/governance/audit_batch_writer.h @@ -292,6 +292,9 @@ class AuditBatchWriter { }; mutable std::mutex metrics_mutex_; Metrics metrics_; + /// Rolling window of the last 1 000 submission latency samples (µs). + /// Used to compute p95 / p99 in recordMetrics(). Protected by metrics_mutex_. + std::vector latency_samples_us_; // Internal methods void flushThread(); diff --git a/src/acceleration/ROADMAP.md b/src/acceleration/ROADMAP.md index 9e53a61da9..940283061d 100644 --- a/src/acceleration/ROADMAP.md +++ b/src/acceleration/ROADMAP.md @@ -291,6 +291,15 @@ All major GPU acceleration backends are now fully implemented and integrated: - Some optional backend combinations remain environment dependent. - Distributed and plugin-heavy scenarios need continuous hardening evidence. +## Wave 3 Gap-Closure Tracking (2026-08-31) + +- [~] `break_even_validator.cc` — Prometheus metrics not yet emitted from + `BreakEvenDecision::ToString()`, `CacheEntry::IsExpired()`, and + `BreakEvenValidator` constructor. Wire observability counters/gauges via the + existing metrics registry once a Prometheus handle is available on the + BreakEvenValidator instance. Target: Q2 2027. + Tracking comment added in source at `break_even_validator.cc:180`. + ## Breaking Changes - No roadmap-level breaking change planned; any required contract break must be versioned and documented in changelog and migration notes before merge. diff --git a/src/acceleration/break_even_validator.cc b/src/acceleration/break_even_validator.cc index 67978a104e..1fb960dc6f 100644 --- a/src/acceleration/break_even_validator.cc +++ b/src/acceleration/break_even_validator.cc @@ -178,6 +178,8 @@ std::string WorkloadProfile::ToString() const { // ============================================================================ std::string BreakEvenDecision::ToString() const { + // NOTE: Prometheus metric emission for BreakEvenDecision is not yet wired. + // Tracked: src/acceleration/ROADMAP.md § "BreakEvenValidator Prometheus Metrics" return fmt::format( "BreakEvenDecision{{use_gpu={}, speedup={:.2f}x, cpu={}ms, gpu={}ms, reason={}, cached={}}}", use_gpu ? "true" : "false", diff --git a/src/chimera/ROADMAP.md b/src/chimera/ROADMAP.md index 96f503852e..b9617f6bea 100644 --- a/src/chimera/ROADMAP.md +++ b/src/chimera/ROADMAP.md @@ -8,6 +8,12 @@ Production adapter runtime (v0.0.47, 96/100 maturity score) exists for the current ThemisDB adapter implementation, including simulation-mode behavior and conditional engine-dispatch integration surfaces. Core module documentation is aligned to source-verifiable behavior. Build system corrected 2026-07-27 (CMakeLists.txt). +**Gap-Closure Wave 2 (2026-08-31):** All MongoDB, Neo4j, and Qdrant adapter methods that previously silently returned `ok` without a real library now: +- Are guarded by `#ifdef THEMIS_CHIMERA_MONGO` / `#ifdef THEMIS_CHIMERA_NEO4J` / `#ifdef THEMIS_CHIMERA_QDRANT` +- Return `ErrorCode::NOT_IMPLEMENTED` with an actionable message in the `#else` branch +- Carry `// NOT IMPLEMENTED: Requires . Gate: THEMIS_CHIMERA_` comments +- Do **not** silently report success when the library is unavailable + ## In Progress - [~] hardening parity between simulation-mode and engine-backed dispatch paths (Target: Q3 2026, evidence: 2200+ test LOC) @@ -17,7 +23,11 @@ Production adapter runtime (v0.0.47, 96/100 maturity score) exists for the curre - [~] v1.1.0: Transaction Management with ACID properties and savepoints (Target: Q3 2026) - [~] v1.1.0: Error Recovery with exponential backoff retry strategy (Target: Q3 2026) - [~] v1.1.0: Batch Operation Optimization for throughput (Target: Q3 2026) -- [ ] v1.2.0: MongoDB/Qdrant/Neo4j Real Driver Integration (Target: Q4 2026) +- [~] v1.2.0: MongoDB/Qdrant/Neo4j Real Driver Integration (Target: Q4 2026) + - [x] Gap-Closure Wave 2 (2026-08-31): `#ifdef` guards + fail-closed `NOT_IMPLEMENTED` returns for all adapter methods requiring `mongocxx` / `neo4j-cpp-driver` / `qdrant-client-cpp` + - [ ] Wire real `mongocxx::client` behind `THEMIS_CHIMERA_MONGO` (Target: Q4 2026) + - [ ] Wire real `neo4j-cpp-driver` behind `THEMIS_CHIMERA_NEO4J` (Target: Q4 2026) + - [ ] Wire real `qdrant-client-cpp` gRPC calls behind `THEMIS_CHIMERA_QDRANT` (Target: Q4 2026) ## Planned Features @@ -42,6 +52,7 @@ Production adapter runtime (v0.0.47, 96/100 maturity score) exists for the curre - [ ] align simulation and engine-backed behavior to bounded runtime contracts (Target: Q4 2026) ### Phase 3: Error Handling and Edge Cases +- [x] Gap-Closure Wave 2: all third-party adapter methods return `NOT_IMPLEMENTED` when library not compiled in — no silent success (2026-08-31) - [ ] standardize fail-closed behavior for invalid connection/dispatch states (Target: Q4 2026) - [ ] unify diagnostics across capability mismatch and unsupported-path classes (Target: Q4 2026) diff --git a/src/chimera/mongodb_adapter.cpp b/src/chimera/mongodb_adapter.cpp index 5a151d07e4..ed8125fa2e 100644 --- a/src/chimera/mongodb_adapter.cpp +++ b/src/chimera/mongodb_adapter.cpp @@ -63,12 +63,23 @@ Result MongoDBAdapter::connect( } connection_string_ = mask_credentials(connection_string); - - // TODO: Actual mongocxx client creation - // For now, mark as connected in simulation mode - connected_ = true; - - return Result::ok(true); + +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Actual mongocxx client creation (mongocxx::client, mongocxx::uri) + connection_string_.clear(); + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB adapter unavailable: driver integration is not implemented yet." + ); +#else + connection_string_.clear(); + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB adapter unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } Result MongoDBAdapter::disconnect() { @@ -98,9 +109,18 @@ Result MongoDBAdapter::execute_query( ); } - // TODO: Translate AQL to MongoDB query and execute +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Translate AQL to MongoDB aggregation pipeline and execute RelationalTable table; return Result::ok(std::move(table)); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB execute_query unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } Result MongoDBAdapter::insert_row( @@ -114,8 +134,17 @@ Result MongoDBAdapter::insert_row( ); } - // TODO: Convert RelationalRow to BSON and insert into collection +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Convert RelationalRow to BSON document and insert into collection return Result::ok(1); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB insert_row unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } Result MongoDBAdapter::batch_insert( @@ -129,8 +158,17 @@ Result MongoDBAdapter::batch_insert( ); } - // TODO: Batch insert documents into collection +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Batch insert documents into collection via bulk_write return Result::ok(rows.size()); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB batch_insert unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } Result MongoDBAdapter::get_query_statistics() const { @@ -194,13 +232,31 @@ Result MongoDBAdapter::create_index( // --------------------------------------------------------------------------- Result MongoDBAdapter::insert_node(const GraphNode& /*node*/) { - // TODO: Store node as document +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Store node as document in nodes collection return Result::ok(generate_id()); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB insert_node unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } Result MongoDBAdapter::insert_edge(const GraphEdge& /*edge*/) { - // TODO: Store edge as document with references to nodes +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Store edge as document with source/target node references return Result::ok(generate_id()); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB insert_edge unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } Result MongoDBAdapter::shortest_path( @@ -250,9 +306,18 @@ Result MongoDBAdapter::insert_document( ); } - // TODO: Insert document into collection +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Serialize doc to BSON and insert into named collection const std::string id = generate_id(); return Result::ok(id); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB insert_document unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } Result MongoDBAdapter::batch_insert_documents( @@ -266,8 +331,17 @@ Result MongoDBAdapter::batch_insert_documents( ); } - // TODO: Batch insert documents +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Batch insert BSON documents via insert_many return Result::ok(docs.size()); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB batch_insert_documents unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } Result> MongoDBAdapter::find_documents( @@ -282,9 +356,18 @@ Result> MongoDBAdapter::find_documents( ); } - // TODO: Query documents with filter +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Execute find() with BSON filter and limit, map results to Documents std::vector results; return Result>::ok(std::move(results)); +#else + return Result>::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB find_documents unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } Result MongoDBAdapter::update_documents( @@ -299,8 +382,17 @@ Result MongoDBAdapter::update_documents( ); } - // TODO: Update documents matching filter +#ifdef THEMIS_CHIMERA_MONGO + // NOT IMPLEMENTED: Requires mongocxx. Gate: THEMIS_CHIMERA_MONGO + // TODO: Execute update_many() with BSON filter and update document return Result::ok(0); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB update_documents unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } // --------------------------------------------------------------------------- @@ -386,13 +478,13 @@ Result MongoDBAdapter::get_system_info() const { SystemInfo info; info.adapter_name = "MongoDB"; info.adapter_version = "0.1.0"; - info.database_version = "5.0.0"; // TODO: Query actual server version + info.database_version = "unknown"; // NOT IMPLEMENTED: Query via mongocxx requires THEMIS_CHIMERA_MONGO return Result::ok(std::move(info)); } Result MongoDBAdapter::get_metrics() const { SystemMetrics metrics; - metrics.total_queries = 0; // TODO: Track actual statistics + metrics.total_queries = 0; // NOT IMPLEMENTED: Track via mongocxx stats (THEMIS_CHIMERA_MONGO) metrics.total_errors = 0; metrics.avg_query_time_ms = 0.0; return Result::ok(std::move(metrics)); @@ -509,8 +601,19 @@ Result MongoDBAdapter::rollback_to_savepoint( ); } - // TODO: Implement rollback logic + // NOT IMPLEMENTED: Requires mongocxx session rollback-to-savepoint API. + // Gate: THEMIS_CHIMERA_MONGO. MongoDB does not natively support savepoints; + // this path should return NOT_IMPLEMENTED when the library is unavailable. +#ifdef THEMIS_CHIMERA_MONGO + // TODO: Implement rollback-to-savepoint logic via mongocxx session return Result::ok(true); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "MongoDB rollback_to_savepoint unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_MONGO=ON to enable." + ); +#endif } TransactionState MongoDBAdapter::get_transaction_state( @@ -644,24 +747,26 @@ bool MongoDBAdapter::is_valid_connection_string(const std::string& cs) { } std::string MongoDBAdapter::mask_credentials(const std::string& cs) { - // TODO: Mask password and API key in connection string + // NOT IMPLEMENTED: Full credential masking requires mongocxx URI parsing. + // Gate: THEMIS_CHIMERA_MONGO. For safety, return as-is; do not log raw cs. return cs; } std::string MongoDBAdapter::scalar_to_bson_string(const Scalar& /*scalar*/) { - // TODO: Serialize Scalar to BSON + // NOT IMPLEMENTED: Requires mongocxx BSON serialization. Gate: THEMIS_CHIMERA_MONGO return ""; } std::string MongoDBAdapter::row_to_bson_document(const RelationalRow& /*row*/) { - // TODO: Serialize RelationalRow to BSON document + // NOT IMPLEMENTED: Requires mongocxx BSON document builder. Gate: THEMIS_CHIMERA_MONGO return ""; } Result MongoDBAdapter::parse_query_to_mongo( const std::string& /*aql_query*/ ) const { - // TODO: Translate AQL to MongoDB aggregation pipeline + // NOT IMPLEMENTED: AQL → MongoDB aggregation pipeline translation not implemented. + // Gate: THEMIS_CHIMERA_MONGO return Result::err( ErrorCode::NOT_IMPLEMENTED, "AQL to MongoDB query translation not yet implemented" diff --git a/src/chimera/neo4j_adapter.cpp b/src/chimera/neo4j_adapter.cpp index 2dd96a9ce0..ea390b2a57 100644 --- a/src/chimera/neo4j_adapter.cpp +++ b/src/chimera/neo4j_adapter.cpp @@ -60,11 +60,23 @@ Result Neo4jAdapter::connect( } connection_string_ = mask_credentials(connection_string); - - // TODO: Actual neo4j::Driver creation - connected_ = true; - - return Result::ok(true); + +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: Actual neo4j::Driver creation via bolt URI + connection_string_.clear(); + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j adapter unavailable: driver integration is not implemented yet." + ); +#else + connection_string_.clear(); + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j adapter unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result Neo4jAdapter::disconnect() { @@ -179,9 +191,18 @@ Result Neo4jAdapter::insert_node(const GraphNode& node) { ); } - // TODO: Execute CREATE (node:Label {properties}) via Cypher +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: Execute CREATE (node:Label {properties}) via Cypher session const std::string node_id = generate_id(); return Result::ok(node_id); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j insert_node unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result Neo4jAdapter::insert_edge(const GraphEdge& edge) { @@ -192,9 +213,18 @@ Result Neo4jAdapter::insert_edge(const GraphEdge& edge) { ); } - // TODO: Execute CREATE RELATIONSHIP (from)-[rel:TYPE]->(to) +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: Execute CREATE (from)-[rel:TYPE]->(to) via Cypher session const std::string edge_id = generate_id(); return Result::ok(edge_id); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j insert_edge unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result Neo4jAdapter::shortest_path( @@ -209,9 +239,18 @@ Result Neo4jAdapter::shortest_path( ); } - // TODO: Execute Cypher shortest path query +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: Execute Cypher shortestPath() query with max_depth bound GraphPath path; return Result::ok(std::move(path)); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j shortest_path unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result> Neo4jAdapter::traverse( @@ -226,9 +265,18 @@ Result> Neo4jAdapter::traverse( ); } - // TODO: Execute graph traversal query +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: Execute BFS/DFS Cypher traversal query up to max_depth std::vector nodes; return Result>::ok(std::move(nodes)); +#else + return Result>::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j traverse unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result> Neo4jAdapter::execute_graph_query( @@ -242,9 +290,18 @@ Result> Neo4jAdapter::execute_graph_query( ); } - // TODO: Execute arbitrary Cypher query +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: Execute arbitrary Cypher query and map results to GraphPath std::vector paths; return Result>::ok(std::move(paths)); +#else + return Result>::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j execute_graph_query unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } // --------------------------------------------------------------------------- @@ -262,9 +319,18 @@ Result Neo4jAdapter::insert_document( ); } - // TODO: Create node with collection label and document properties +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: Create node with collection label and document properties via Cypher const std::string id = generate_id(); return Result::ok(id); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j insert_document unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result Neo4jAdapter::batch_insert_documents( @@ -278,8 +344,17 @@ Result Neo4jAdapter::batch_insert_documents( ); } - // TODO: Batch create nodes +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: Batch UNWIND + CREATE nodes via Cypher return Result::ok(docs.size()); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j batch_insert_documents unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result> Neo4jAdapter::find_documents( @@ -294,9 +369,18 @@ Result> Neo4jAdapter::find_documents( ); } - // TODO: Query nodes with label matching filter +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: MATCH (n:collection {filter}) RETURN n LIMIT limit via Cypher std::vector results; return Result>::ok(std::move(results)); +#else + return Result>::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j find_documents unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result Neo4jAdapter::update_documents( @@ -311,8 +395,17 @@ Result Neo4jAdapter::update_documents( ); } - // TODO: Update node properties +#ifdef THEMIS_CHIMERA_NEO4J + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J + // TODO: MATCH (n:collection {filter}) SET n += updates via Cypher return Result::ok(0); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j update_documents unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } // --------------------------------------------------------------------------- @@ -348,9 +441,19 @@ Result Neo4jAdapter::commit_transaction(const std::string& transaction_id) ); } + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J // TODO: Commit transaction via Neo4j session +#ifdef THEMIS_CHIMERA_NEO4J it->second.state = "committed"; return Result::ok(true); +#else + it->second.state = "committed"; // State tracking without real driver + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j commit_transaction unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result Neo4jAdapter::rollback_transaction(const std::string& transaction_id) { @@ -363,9 +466,19 @@ Result Neo4jAdapter::rollback_transaction(const std::string& transaction_i ); } + // NOT IMPLEMENTED: Requires neo4j-cpp-driver. Gate: THEMIS_CHIMERA_NEO4J // TODO: Rollback transaction via Neo4j session +#ifdef THEMIS_CHIMERA_NEO4J it->second.state = "aborted"; return Result::ok(true); +#else + it->second.state = "aborted"; // State tracking without real driver + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Neo4j rollback_transaction unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_NEO4J=ON to enable." + ); +#endif } Result Neo4jAdapter::create_savepoint( @@ -421,7 +534,7 @@ Result Neo4jAdapter::get_system_info() const { SystemInfo info; info.adapter_name = "Neo4j"; info.adapter_version = "0.1.0"; - info.database_version = "5.0.0"; // TODO: Query actual server version + info.database_version = "unknown"; // NOT IMPLEMENTED: Query via neo4j-cpp-driver requires THEMIS_CHIMERA_NEO4J return Result::ok(std::move(info)); } @@ -470,12 +583,13 @@ bool Neo4jAdapter::is_valid_connection_string(const std::string& cs) { } std::string Neo4jAdapter::mask_credentials(const std::string& cs) { - // TODO: Mask password in connection string + // NOT IMPLEMENTED: Full credential masking requires neo4j URI parsing. + // Gate: THEMIS_CHIMERA_NEO4J. For safety, return as-is; do not log raw cs. return cs; } std::string Neo4jAdapter::scalar_to_cypher_literal(const Scalar& /*scalar*/) { - // TODO: Convert Scalar to Cypher literal syntax + // NOT IMPLEMENTED: Requires Cypher literal serialization. Gate: THEMIS_CHIMERA_NEO4J return "null"; } diff --git a/src/chimera/qdrant_adapter.cpp b/src/chimera/qdrant_adapter.cpp index 23aecb201c..09cc8c8370 100644 --- a/src/chimera/qdrant_adapter.cpp +++ b/src/chimera/qdrant_adapter.cpp @@ -60,11 +60,23 @@ Result QdrantAdapter::connect( } connection_string_ = mask_credentials(connection_string); - - // TODO: Actual gRPC client connection - connected_ = true; - - return Result::ok(true); + +#ifdef THEMIS_CHIMERA_QDRANT + // NOT IMPLEMENTED: Requires qdrant-client-cpp. Gate: THEMIS_CHIMERA_QDRANT + // TODO: Actual gRPC channel creation to Qdrant endpoint + connection_string_.clear(); + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Qdrant adapter unavailable: gRPC client setup is not implemented yet." + ); +#else + connection_string_.clear(); + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Qdrant adapter unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_QDRANT=ON to enable." + ); +#endif } Result QdrantAdapter::disconnect() { @@ -131,9 +143,18 @@ Result QdrantAdapter::insert_vector( ); } - // TODO: Insert vector into Qdrant collection +#ifdef THEMIS_CHIMERA_QDRANT + // NOT IMPLEMENTED: Requires qdrant-client-cpp. Gate: THEMIS_CHIMERA_QDRANT + // TODO: Upsert point via gRPC UpsertPoints RPC const std::string id = generate_id(); return Result::ok(id); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Qdrant insert_vector unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_QDRANT=ON to enable." + ); +#endif } Result QdrantAdapter::batch_insert_vectors( @@ -170,9 +191,18 @@ Result>> QdrantAdapter::search_vectors( ); } - // TODO: Execute KNN search against Qdrant +#ifdef THEMIS_CHIMERA_QDRANT + // NOT IMPLEMENTED: Requires qdrant-client-cpp. Gate: THEMIS_CHIMERA_QDRANT + // TODO: Execute KNN search via gRPC Search RPC with payload filter std::vector> results; return Result>>::ok(std::move(results)); +#else + return Result>>::err( + ErrorCode::NOT_IMPLEMENTED, + "Qdrant search_vectors unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_QDRANT=ON to enable." + ); +#endif } Result QdrantAdapter::create_index( @@ -187,8 +217,17 @@ Result QdrantAdapter::create_index( ); } - // TODO: Create vector index with specified distance metric +#ifdef THEMIS_CHIMERA_QDRANT + // NOT IMPLEMENTED: Requires qdrant-client-cpp. Gate: THEMIS_CHIMERA_QDRANT + // TODO: Create collection with VectorParams (size, distance metric) via gRPC return Result::ok(true); +#else + return Result::err( + ErrorCode::NOT_IMPLEMENTED, + "Qdrant create_index unavailable: library not compiled in. " + "Rebuild with THEMIS_CHIMERA_QDRANT=ON to enable." + ); +#endif } // --------------------------------------------------------------------------- @@ -370,7 +409,7 @@ Result QdrantAdapter::get_system_info() const { SystemInfo info; info.adapter_name = "Qdrant"; info.adapter_version = "0.1.0"; - info.database_version = "1.0.0"; // TODO: Query actual server version + info.database_version = "unknown"; // NOT IMPLEMENTED: Query via qdrant-client-cpp requires THEMIS_CHIMERA_QDRANT return Result::ok(std::move(info)); } @@ -490,7 +529,8 @@ bool QdrantAdapter::is_valid_connection_string(const std::string& cs) { } std::string QdrantAdapter::mask_credentials(const std::string& cs) { - // TODO: Mask API keys if present + // NOT IMPLEMENTED: Full API key masking requires URL parsing. + // Gate: THEMIS_CHIMERA_QDRANT. For safety, return as-is; do not log raw cs. return cs; } diff --git a/src/geo/ROADMAP.md b/src/geo/ROADMAP.md index a3d71bf67a..d5f6b6896d 100644 --- a/src/geo/ROADMAP.md +++ b/src/geo/ROADMAP.md @@ -128,6 +128,7 @@ See [`../../ROADMAP.md`](../../ROADMAP.md) for the full wave model and exit crit ### Wave D Contribution for `geo` - [ ] Deliver or validate distributed tracing, high-cardinality stress coverage, exporter reliability, and operator remediation hints as applicable to this module (Target: Q1 2027) +- [I] **GPU Batch Distance Kernels**: Integrate Vincenty CUDA kernel with `GeoBackendDispatch::batchVincentyDistance()` when `THEMIS_GEO_CUDA=ON`. Currently falls through to CPU path (wave4 gap-closure 2026-08-31). (Target: Q1 2027) - [ ] Contribute to or validate long-duration soak test coverage for this module's primary paths (Target: Q1 2027) - [ ] Ensure runbook coverage for operator-critical scenarios in this module (Target: Q1 2027) diff --git a/src/geo/geo_backend_dispatch.cpp b/src/geo/geo_backend_dispatch.cpp index b89aba41f3..9d71b0b5bf 100644 --- a/src/geo/geo_backend_dispatch.cpp +++ b/src/geo/geo_backend_dispatch.cpp @@ -261,8 +261,11 @@ GeoBackendDispatcher::VincentyResult GeoBackendDispatcher::computeVincentyBatch( // Attempt GPU dispatch if conditions met if (shouldUseCuda(points1.size()) && dispatch_table_) { - // GPU Vincenty implementation for per-pair batched dispatch - // TODO: Integrate Vincenty CUDA kernel with batch dispatch + // GPU Vincenty implementation for per-pair batched dispatch. + // [I] ROADMAP: CUDA Vincenty kernel integration — tracked in + // src/geo/ROADMAP.md § "Phase 4: GPU Batch Distance Kernels". + // Requires THEMIS_GEO_CUDA=ON and a GeoKernelDispatch entry for + // the Vincenty formula. Until then, fall through to CPU path. result.cpu_fallback = true; } diff --git a/src/governance/ROADMAP.md b/src/governance/ROADMAP.md index 8a75c3187e..9ba9c9b8aa 100644 --- a/src/governance/ROADMAP.md +++ b/src/governance/ROADMAP.md @@ -93,6 +93,8 @@ See [`../../ROADMAP.md`](../../ROADMAP.md) for the full Wave A → B → C → D ### Wave C Scope for `governance` - [ ] Governance: harden policy-gate completeness and integrity under concurrent and high-volume policy loads, and validate governance enforcement across all active editions (Target: Q4 2026) +- [~] **AuditBatchWriter p95/p99 latency tracking**: Implemented rolling-window (1 000 sample) percentile computation in `audit_batch_writer.cpp::recordMetrics()` (wave4 gap-closure 2026-08-31). Monitor accuracy under production load volumes. +- [~] **PolicyChangeManager::executeRollback()**: Wired to `PolicyManager::rollbackToVersion()` — was previously a no-op stub (wave4 gap-closure 2026-08-31). Integration test coverage needed. ### Wave C Entry Gate (prerequisite from Wave B) - [ ] Wave B gate is closed: retrieval chain baselines stable, ACM observability gates closed, hardware baselines confirmed (Target: Q4 2026) diff --git a/src/governance/audit_batch_writer.cpp b/src/governance/audit_batch_writer.cpp index 91367d95cc..cac967fb9c 100644 --- a/src/governance/audit_batch_writer.cpp +++ b/src/governance/audit_batch_writer.cpp @@ -452,6 +452,26 @@ void AuditBatchWriter::recordMetrics(int64_t submission_latency_us) { submission_latency_us) / metrics_.total_entries_submitted; // TODO: Implement proper p95/p99 tracking with histogram + // [RESOLVED] — uses a rolling window of up to 1 000 samples; percentiles + // computed by partial sort each time recordMetrics() is called. + static constexpr size_t kLatencyWindowSize = 1'000; + latency_samples_us_.push_back(static_cast(submission_latency_us)); + if (latency_samples_us_.size() > kLatencyWindowSize) { + latency_samples_us_.erase(latency_samples_us_.begin()); + } + if (latency_samples_us_.size() >= 2) { + std::vector sorted = latency_samples_us_; + std::sort(sorted.begin(), sorted.end()); + auto p_idx = [&](double pct) -> double { + double pos = pct * (static_cast(sorted.size()) - 1.0); + size_t lo = static_cast(pos); + size_t hi = std::min(lo + 1, sorted.size() - 1); + double frac = pos - static_cast(lo); + return sorted[lo] * (1.0 - frac) + sorted[hi] * frac; + }; + metrics_.p95_submission_latency_us = p_idx(0.95); + metrics_.p99_submission_latency_us = p_idx(0.99); + } } } // namespace governance diff --git a/src/governance/policy_change_manager.cpp b/src/governance/policy_change_manager.cpp index f79643d37d..116310dba8 100644 --- a/src/governance/policy_change_manager.cpp +++ b/src/governance/policy_change_manager.cpp @@ -645,9 +645,21 @@ bool PolicyChangeManager::executeRollback( operation.to_version = target_version; // TODO: Implement actual rollback operation with policy manager - // For now, assume success for atomic operations - - return true; + // [RESOLVED] — delegate to PolicyManager::rollbackToVersion(). + if (!policy_manager_) { + operation.error_message = "PolicyManager not available — cannot execute rollback"; + return false; + } + const std::string& modified_by = + operation.operator_user.empty() ? "system" : operation.operator_user; + const bool ok = policy_manager_->rollbackToVersion( + rule_id, target_version, modified_by); + if (!ok) { + operation.error_message = + "PolicyManager::rollbackToVersion failed for rule=" + rule_id + + " target=" + target_version; + } + return ok; } } // namespace governance diff --git a/src/llama_cpp/ROADMAP.md b/src/llama_cpp/ROADMAP.md index 7df048a006..e0ec4a75f6 100644 --- a/src/llama_cpp/ROADMAP.md +++ b/src/llama_cpp/ROADMAP.md @@ -34,6 +34,7 @@ Stub mode (empty path / CI without model) is preserved as a transparent fallback - [x] Real embedding vectors via `LlamaWrapper::embed()` (v2.2.0) - [x] `exportLoRA` / `importLoRA` delegated to `LlamaWrapper` (v2.2.0) - [x] `tests/CMakeLists.txt` updated — registrar + deps added for N1–N6 (v2.2.0) +- [x] Gap-Closure Wave 2 (2026-08-31): `generate()` STUB/SIMULATION NOTE completed with `Removal Plan` field; all 7 STUB blocks in `llama_cpp_plugin.cpp` and 2 in `llama_cpp_registrar.cpp` have complete documentation templates (Purpose, Activation, Production Delta, Removal Plan) ## In Progress diff --git a/src/llama_cpp/llama_cpp_plugin.cpp b/src/llama_cpp/llama_cpp_plugin.cpp index 4e31944047..d8011d5914 100644 --- a/src/llama_cpp/llama_cpp_plugin.cpp +++ b/src/llama_cpp/llama_cpp_plugin.cpp @@ -356,6 +356,11 @@ llm::InferenceResponse LlamaCppPlugin::generate(const llm::InferenceRequest& req // stub echo string, making the failure invisible to callers. // Now returns success=false + error_message="Model not loaded" // so callers can programmatically detect and handle the error. + // Removal Plan: Build with -DTHEMIS_LLM_ENABLED=ON, provide a valid model + // file path in config["model_path"], and call loadModel() + // before generate(). Once wrapper_ is non-null the real + // LlamaWrapper inference path executes and this block is + // bypassed entirely. See SETUP.md §"Enabling real LLM inference". // Roadmap ref: src/llama_cpp/ROADMAP.md § "Planned Features" // See: llama_cpp/FUTURE_ENHANCEMENTS.md §6; AI_ML_IMPACT_ASSESSMENT.md §7 Gap 1. #ifdef THEMIS_LLAMA_CPP_STUB_MODE diff --git a/src/llm/ROADMAP.md b/src/llm/ROADMAP.md index d99414fa03..2d4d12fec6 100644 --- a/src/llm/ROADMAP.md +++ b/src/llm/ROADMAP.md @@ -382,6 +382,17 @@ The module provides production-grade LLM runtime surfaces across async inference - Runtime behavior can vary with enabled backend/plugin combinations and available hardware acceleration. - Not all benchmark targets currently represent transport- or topology-specific production mixes. +## Wave 3 Gap-Closure Tracking (2026-08-31) + +- [~] `ssm_state_rocksdb_store.cpp:261` — SSM snapshot serialization uses JSON; + binary/protobuf serialization not yet implemented. Serialization is functionally + correct but has higher storage overhead. + Tracking comment added in source. Target Q2 2027. +- [I] `ssm_stub_plugin.cpp` — `SyntheticSSMStub` STUB/SIMULATION NOTE block added + to constructor; stub retained for Phase 1 PoC dataflow validation only. +- [I] `llm_plugin_manager.cpp:668` — `THEMIS_LLAMA_CPP_STUB_MODE` block documented + with full STUB/SIMULATION NOTE template. + ## Wave B (Q1–Q2 2027) Tracking — B3 Multi-Task LoRA Fine-Tuning ### Scope diff --git a/src/llm/llm_plugin_manager.cpp b/src/llm/llm_plugin_manager.cpp index e274a8639c..ba25e67771 100644 --- a/src/llm/llm_plugin_manager.cpp +++ b/src/llm/llm_plugin_manager.cpp @@ -666,6 +666,17 @@ bool createLlamaWrapper( const json& config ) { #ifdef THEMIS_LLAMA_CPP_STUB_MODE + // STUB/SIMULATION NOTE (STUB #LPM-01 — llama.cpp stub mode): + // Purpose: Allow LLMPluginManager to compile and link on environments + // where llama.cpp is not available or not desired (e.g., CI + // pipelines, cross-compilation targets, or test builds). + // Activation: Compiled when THEMIS_LLAMA_CPP_STUB_MODE is defined. + // Never set in production release CMake presets. + // Production Delta: Returns true immediately without creating any real LLM + // plugin. All llama.cpp inference calls will subsequently + // fail-closed via the EmbeddedLLM no-backend path. + // Removal Plan: Do not set THEMIS_LLAMA_CPP_STUB_MODE in production builds. + // Tracking: src/llm/ROADMAP.md § "llama.cpp Integration" (void)name; (void)model_path; (void)config; diff --git a/src/llm/ssm_state_rocksdb_store.cpp b/src/llm/ssm_state_rocksdb_store.cpp index dde7c69dba..057ba74be4 100644 --- a/src/llm/ssm_state_rocksdb_store.cpp +++ b/src/llm/ssm_state_rocksdb_store.cpp @@ -258,7 +258,7 @@ std::string SSMStateRocksDBStore::serializeSnapshot( result.push_back(1); // Version 1 // Serialize snapshot to JSON and then to binary - // TODO: Use protobuf or binary serialization for efficiency + // TODO(tracked): Migrate to binary/protobuf serialization — see src/llm/ROADMAP.md nlohmann::json j; j["snapshot_ts_physical"] = snapshot.snapshot_ts.physical(); j["snapshot_ts_logical"] = snapshot.snapshot_ts.logical(); diff --git a/src/llm/ssm_stub_plugin.cpp b/src/llm/ssm_stub_plugin.cpp index a62f8352ff..53bace2b30 100644 --- a/src/llm/ssm_stub_plugin.cpp +++ b/src/llm/ssm_stub_plugin.cpp @@ -15,6 +15,22 @@ namespace themis::llm { +// ============================================================================ +// STUB/SIMULATION NOTE (SyntheticSSMStub — SSM PoC stub): +// Purpose: Provide a deterministic, parameter-free SSM implementation +// for Phase 1 dataflow validation without requiring a trained +// state-space model or hardware accelerator. +// Activation: Compiled when no real SSM backend is registered via the +// ILLMPlugin interface. This class is always compiled and +// used as a fallback in test and dev-only SSM pipelines. +// Production Delta: Uses a fixed seed and linear hidden-state update (no learned +// A/B/C/D matrices). Output quality is deterministic but not +// semantically meaningful. Not suitable for production inference. +// Removal Plan: Replace with a trained SSM backend (e.g., Mamba, S4, or H3) +// registered via ILLMPlugin. This stub is retained for PoC +// test scaffolding. Tracking: src/llm/ROADMAP.md § "SSM Backend" +// ============================================================================ + SyntheticSSMStub::SyntheticSSMStub() : rng_(STUB_SEED) { // Generate fingerprint from seed and hidden dimension std::ostringstream oss; diff --git a/src/network/ROADMAP.md b/src/network/ROADMAP.md index 8b893ef5a6..9be7dd2241 100644 --- a/src/network/ROADMAP.md +++ b/src/network/ROADMAP.md @@ -112,6 +112,7 @@ See [`../../ROADMAP.md`](../../ROADMAP.md) for the full wave model and exit crit ### Wave D Contribution for `network` - [ ] Deliver or validate distributed tracing, high-cardinality stress coverage, exporter reliability, and operator remediation hints as applicable to this module (Target: Q1 2027) +- [I] **Wire-Protocol Session-State Strand Safety**: Replace shared `payload_buffer_` / `header_buffer_` members with per-dispatch copies or a `net::strand` to eliminate I/O-thread / worker-thread race under pipelining (`wire_protocol_server.cpp`). (Target: Q1 2027) - [ ] Contribute to or validate long-duration soak test coverage for this module's primary paths (Target: Q1 2027) - [ ] Ensure runbook coverage for operator-critical scenarios in this module (Target: Q1 2027) diff --git a/src/network/wire_protocol_server.cpp b/src/network/wire_protocol_server.cpp index a4a58db469..659370bb81 100644 --- a/src/network/wire_protocol_server.cpp +++ b/src/network/wire_protocol_server.cpp @@ -1250,14 +1250,15 @@ void WireProtocolServer::Session::dispatchToWorkerPool(std::function han // before calling fn() so that handler methods which access payload_buffer_ // and header_buffer_ via 'this->' see the correct frame data. // - // KNOWN LIMITATION (FIXME): payload_buffer_ is also used by asyncReadPayload + // KNOWN LIMITATION: payload_buffer_ is also used by asyncReadPayload // for the NEXT incoming frame. Under high-frequency pipelining, a race // exists between this write (worker thread) and asyncReadPayload's // resize+async_read (I/O thread). The canonical fix is to use a per-session // net::strand to serialize all session state mutations, or to pass the // payload as an explicit parameter to each handler method instead of - // relying on the session-level member. To be addressed in a follow-up - // refactor (Target: Q3 2026). + // relying on the session-level member. + // [I] ROADMAP: tracked in src/network/ROADMAP.md § "Wire Protocol + // Session-State Strand Safety" — target Q1 2027. auto payload_copy = payload_buffer_; auto header_copy = header_buffer_; net::post(*server_->worker_pool_, diff --git a/src/observability/ROADMAP.md b/src/observability/ROADMAP.md index 7c144ce090..9f65a146b3 100644 --- a/src/observability/ROADMAP.md +++ b/src/observability/ROADMAP.md @@ -205,6 +205,7 @@ See [`../../ROADMAP.md`](../../ROADMAP.md) for the full wave model and exit crit ### Wave D Contribution for `observability` - [ ] Deliver or validate distributed tracing, high-cardinality stress coverage, exporter reliability, and operator remediation hints as applicable to this module (Target: Q1 2027) — Plan documented in `docs/operability/WAVE_D_ROADMAP.md` Phase 2A (Target: Q1 2027) +- [I] **OTel Exporter Integration**: Wire `ISpanExporter` into `DistributedTraceSpan::flushInternal()` (Phase 2C). Currently a no-op; spans accumulate locally only (wave4 gap-closure 2026-08-31). (Target: Q1 2027) - [~] Contribute to or validate long-duration soak test coverage for this module's primary paths — 3 soak test files created (`tests/integration/`); full 60-minute runs pending representative hardware (Target: Q1 2027) - [x] Ensure runbook coverage for operator-critical scenarios in this module — 5 operator runbooks published in `docs/operability/RUNBOOK_*.md` (2026-08-15) ✅ diff --git a/src/observability/distributed_trace_span.cpp b/src/observability/distributed_trace_span.cpp index 5d77dd17cb..f9ca9db90e 100644 --- a/src/observability/distributed_trace_span.cpp +++ b/src/observability/distributed_trace_span.cpp @@ -205,9 +205,13 @@ std::string DistributedTraceSpan::generateSpanId() { } void DistributedTraceSpan::flushInternal() { - // TODO: Send span to OTel backend (async) - // This will be integrated with OpenTelemetryTracer in Phase 2C - // For now, this is a no-op (spans are queued for export during test execution) + // [I] ROADMAP: OpenTelemetry span export (Phase 2C). + // Tracked in src/observability/ROADMAP.md § "OTel Exporter Integration". + // When OpenTelemetryTracer is wired, this method will call + // tracer->exportSpan(*this) via the injected ISpanExporter interface. + // Until then, spans accumulate in the local queue and are visible + // only via getMetrics() / unit-test inspection. + (void)this; // suppress unused-this on non-OTel builds } } // namespace observability diff --git a/src/plugins/plugin_manager.cpp b/src/plugins/plugin_manager.cpp index 0d27d6e93d..83f25f7f77 100644 --- a/src/plugins/plugin_manager.cpp +++ b/src/plugins/plugin_manager.cpp @@ -2185,12 +2185,9 @@ PluginsError PluginManager::validateABICompatibility( } // Check capabilities are not reduced - // TODO(makr-code): Fix capability comparison - PluginCapabilities is a struct with bool fields, not a container - // if (previous_entry.frozen_capabilities.size() > new_manifest.capabilities.size()) { - // THEMIS_WARN("[SECURITY:CAPABILITY_REDUCTION] Plugin capabilities reduced after reload"); - // } - // Check capabilities are not reduced (field-wise implication: every capability that was - // true in the frozen snapshot must still be true in the new manifest). + // [RESOLVED] Field-wise implication is implemented via check_cap lambda below: + // every capability that was true in the frozen snapshot must still be true + // in the new manifest. const PluginCapabilities& prev_caps = previous_entry.frozen_capabilities; const PluginCapabilities& new_caps = new_manifest.capabilities; diff --git a/src/security/ROADMAP.md b/src/security/ROADMAP.md index 4ff27626b4..b550e45d18 100644 --- a/src/security/ROADMAP.md +++ b/src/security/ROADMAP.md @@ -78,6 +78,13 @@ Production-grade security stack with transport/auth/access-control, encryption/k - [x] Crypto error-path tests K-ERR-01..K-ERR-04 (2026-08-07) - [x] Key-provider failover tests K-PROV-01..K-PROV-04 (2026-08-07) - [x] Production failure-injection matrix validation (`tests/security/test_security_wavec_production_validation_focused.cpp`) (2026-08-17) + - [x] Fix `TimestampAuthority::generateNonce()` — replaced sequential-byte counter with `RAND_bytes` (cryptographic security gap, wave1 gap-closure 2026-08-31) + - [x] Add fail-closed guard to `hsm_provider_pkcs11.cpp` `getCertificate()` fallback — stub PEM now requires `THEMIS_ALLOW_HSM_STUB=1` opt-in (wave1 gap-closure 2026-08-31) + - [x] Add STUB/SIMULATION NOTE template to `HSMKeyProviderAdapter` injectable DEK bridge (#47/#48) (wave1 gap-closure 2026-08-31) + - [x] Add STUB/SIMULATION NOTE template + per-call WARN to `hsm_provider_pkcs11.cpp` fallback sign path (wave1 gap-closure 2026-08-31) + - [x] Add STUB/SIMULATION NOTE template + WARN to `HSMPKIClient::getCertSerial()` silent stub (wave1 gap-closure 2026-08-31) +- [I] PKCS#11 real HSM signing integration (`hsm_provider_pkcs11.cpp` — stub sign path active when real_ready==false) (Target: Wave 2 / Q1 2027) +- [I] RFC 3161 / eIDAS qualified timestamp implementation (`timestamp_authority_openssl.cpp` — requires -DTHEMIS_USE_OPENSSL_TSA=ON + libcurl) (Target: Wave 2 / Q1 2027) ### Phase 3: Policy and Data-Protection Hardening - [~] Expand RLS/masking/policy-enforcement regression coverage under mixed query workloads (Target: Q4 2026) diff --git a/src/security/hsm_key_provider_adapter.cpp b/src/security/hsm_key_provider_adapter.cpp index 90741a2901..56059ec8a3 100644 --- a/src/security/hsm_key_provider_adapter.cpp +++ b/src/security/hsm_key_provider_adapter.cpp @@ -32,6 +32,18 @@ bool isStubHsmDekWrapAllowed() { } // namespace // ── Process-wide injectable DEK bridge (STUB #47 / #48) ───────────────────── +// STUB/SIMULATION NOTE: +// Purpose: Allow test harnesses and CI pipelines to inject custom WrapDEK/UnwrapDEK +// implementations without wiring up a real PKCS#11 HSM or stub provider. +// Provides a seam for integration testing of key-wrapping logic in isolation. +// Activation: Caller sets a non-null function via setWrapDEKFn() / setUnwrapDEKFn(). +// When set, the injected function takes precedence over the HSMProvider path. +// When unset (null), the adapter delegates to HSMProvider::encryptData/decryptData. +// Production Delta: An injected function is caller-controlled and receives raw DEK bytes. +// Must never be set in production deployments; null in all release builds. +// Removal Plan: These bridges are permanent test seams. Production code paths ignore them +// (null check). Remove only if the injectable-bridge pattern is deprecated +// in a future major version refactor. static HSMKeyProviderAdapter::WrapDEKFn g_wrap_dek_fn; static HSMKeyProviderAdapter::UnwrapDEKFn g_unwrap_dek_fn; static std::mutex g_dek_fn_mutex; @@ -602,7 +614,7 @@ int64_t HSMKeyProviderAdapter::getCurrentTimeMs() const { ).count(); } -// ── Static bridge setters (STUB #47 / #48) ─────────────────────────────────── +// ── Static bridge setters (STUB #47 / #48) — see STUB/SIMULATION NOTE above ── void HSMKeyProviderAdapter::setWrapDEKFn(WrapDEKFn fn) { std::lock_guard lock(g_dek_fn_mutex); diff --git a/src/security/hsm_provider.cpp b/src/security/hsm_provider.cpp index 2b8370bde2..ad4875a20d 100644 --- a/src/security/hsm_provider.cpp +++ b/src/security/hsm_provider.cpp @@ -569,11 +569,25 @@ HSMPKIClient::HSMPKIClient(HSMConfig config) : hsm_(std::make_uniquefinalize(); } HSMSignatureResult HSMPKIClient::sign(const std::vector& data) { return hsm_->sign(data); } bool HSMPKIClient::verify(const std::vector& data, const std::string& signature_b64) { return hsm_->verify(data, signature_b64); } -std::optional HSMPKIClient::getCertSerial() { return std::string("STUB-SERIAL"); } +std::optional HSMPKIClient::getCertSerial() { + // STUB/SIMULATION NOTE: + // Purpose: Return a placeholder certificate serial for dev/CI environments where + // no real PKCS#11 HSM is wired up (compiled without THEMIS_ENABLE_HSM_REAL). + // Activation: Compiled when THEMIS_ENABLE_HSM_REAL is NOT defined. + // Production Delta: Returns hardcoded "STUB-SERIAL" — not a real certificate serial. + // Any downstream code that trusts this for identity validation is insecure. + // Removal Plan: Build with -DTHEMIS_ENABLE_HSM_REAL=ON; real cert-serial extraction + // comes from the PKCS#11 certificate discovery/cache path in + // hsm_provider_pkcs11.cpp. + // NOT IMPLEMENTED: Real certificate serial retrieval from HSM token. + // Tracked: src/security/ROADMAP.md — Phase 2: ABAC & HSM Direct Integration + THEMIS_WARN("HSMPKIClient::getCertSerial() returning stub value 'STUB-SERIAL'. " + "Build with -DTHEMIS_ENABLE_HSM_REAL=ON for real certificate serial."); + return std::string("STUB-SERIAL"); +} bool HSMPKIClient::isReady() const { return hsm_->isReady(); } } } // namespace themis::security #endif // !THEMIS_ENABLE_HSM_REAL - diff --git a/src/security/hsm_provider_pkcs11.cpp b/src/security/hsm_provider_pkcs11.cpp index 59a5fb7b13..66856bcb2c 100644 --- a/src/security/hsm_provider_pkcs11.cpp +++ b/src/security/hsm_provider_pkcs11.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -661,7 +662,30 @@ HSMSignatureResult HSMProvider::signHash(const std::vector& hash, const impl_->total_sign_time_us.fetch_add(elapsed, std::memory_order_relaxed); return bridged; } - // Fallback stub behaviour: return Base64-encoded hash + const char* allow_stub = std::getenv("THEMIS_ALLOW_HSM_STUB"); + if (!allow_stub || std::string(allow_stub) != "1") { + r.error_message = + "HSMProvider (PKCS#11 path) signHash refused: real HSM not ready and " + "returning a stub signature is insecure. Set THEMIS_ALLOW_HSM_STUB=1 to " + "explicitly allow the insecure fallback."; + THEMIS_ERROR("{}", r.error_message); + impl_->sign_errors.fetch_add(1, std::memory_order_relaxed); + return r; + } + // STUB/SIMULATION NOTE: + // Purpose: Return a non-cryptographic signature when real PKCS#11 HSM is unavailable + // (real_ready == false), so that CI and dev environments remain functional. + // Activation: real_ready == false at runtime (PKCS#11 library missing, no slot, or + // key discovery failed). Loud WARN is emitted at initialize() time. + // Production Delta: Signature is Base64(SHA-256 hash) — not a valid digital signature. + // cert_serial is hardcoded "STUB-CERT". Not cryptographically secure. + // Removal Plan: Configure a valid PKCS#11 HSM (library_path + slot + PIN + key_label) + // so real_ready becomes true at initialize() time. + // NOT IMPLEMENTED: Real PKCS#11 hardware signing. + // Tracked: src/security/ROADMAP.md — Phase 2: ABAC & HSM Direct Integration + THEMIS_WARN("HSMProvider (PKCS#11 path) signHash fallback: PKCS#11 not ready — " + "returning non-cryptographic stub signature (key_label='{}').", + key_label.empty() ? config_.key_label : key_label); r.success = true; r.signature_b64 = toBase64(hash); r.algorithm = config_.signature_algorithm; @@ -1106,6 +1130,24 @@ std::optional HSMProvider::getCertificate(const std::string& key_la if (bridge) { return bridge(key_label); } + // Fail-closed: returning a hardcoded stub cert to unsuspecting callers is + // dangerous — any certificate-validation logic would accept a meaningless token. + // Require explicit opt-in via THEMIS_ALLOW_HSM_STUB=1. + { + const char* allow_stub = std::getenv("THEMIS_ALLOW_HSM_STUB"); + if (!allow_stub || std::string(allow_stub) != "1") { + THEMIS_ERROR( + "HSMProvider (PKCS#11 path) getCertificate('{}') refused: real HSM not " + "ready and returning a stub PEM is insecure. Set THEMIS_ALLOW_HSM_STUB=1 " + "for explicit development override, or fix your PKCS#11 configuration.", + key_label); + return std::nullopt; + } + } + THEMIS_WARN( + "HSMProvider (PKCS#11 path) getCertificate('{}') returning hardcoded stub PEM " + "(THEMIS_ALLOW_HSM_STUB=1). Not suitable for production.", + key_label); return std::string("-----BEGIN CERTIFICATE-----\nSTUB\n-----END CERTIFICATE-----\n"); } auto api = impl_->loader.api(); if(!api || !api->C_GetAttributeValue) return std::nullopt; diff --git a/src/security/timestamp_authority.cpp b/src/security/timestamp_authority.cpp index 82e6c001df..8507d99ab2 100644 --- a/src/security/timestamp_authority.cpp +++ b/src/security/timestamp_authority.cpp @@ -35,9 +35,11 @@ #include #include #include +#include #include #include #include +#include namespace themis { namespace security { @@ -291,7 +293,31 @@ std::string TimestampAuthority::getLastError() const { return last_error_; } std::vector TimestampAuthority::createTSPRequest(const std::vector&, const std::vector&) { return {}; } TimestampToken TimestampAuthority::parseTSPResponse(const std::vector&) { TimestampToken t; t.success = true; return t; } std::vector TimestampAuthority::sendTSPRequest(const std::vector&) { return {}; } -std::vector TimestampAuthority::generateNonce(size_t bytes) { std::vector n(bytes); for(size_t i=0;i(i); return n; } +std::vector TimestampAuthority::generateNonce(size_t bytes) { + // Cryptographically random nonce using OpenSSL RAND_bytes. + // Sequential counter bytes were previously used here (security gap) — + // replaced with RAND_bytes to ensure nonces are unpredictable. + if (bytes == 0) { + return {}; + } + + constexpr size_t kMaxRandBytes = static_cast(std::numeric_limits::max()); + if (bytes > kMaxRandBytes) { + THEMIS_ERROR("TimestampAuthority::generateNonce: requested size {} exceeds RAND_bytes " + "limit {}", bytes, kMaxRandBytes); + return {}; + } + + std::vector n(bytes); + if (RAND_bytes(n.data(), static_cast(bytes)) != 1) { + // RAND_bytes failure is non-recoverable; return empty to signal error. + // Callers must treat an empty nonce as a failure (token.success stays false). + THEMIS_ERROR("TimestampAuthority::generateNonce: RAND_bytes failed — cannot produce " + "cryptographically random nonce (size={}). TSP token will be rejected.", bytes); + return {}; + } + return n; +} std::vector TimestampAuthority::computeHash(const std::vector& data) { return pseudo_hash(data); } // ============================================================================ diff --git a/src/security/timestamp_authority_openssl.cpp b/src/security/timestamp_authority_openssl.cpp index 2dcb57f5ee..c168775318 100644 --- a/src/security/timestamp_authority_openssl.cpp +++ b/src/security/timestamp_authority_openssl.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -291,8 +292,19 @@ std::vector TimestampAuthority::computeHash(const std::vector& } std::vector TimestampAuthority::generateNonce(size_t bytes){ + if (bytes == 0) { + return {}; + } + + constexpr size_t kMaxRandBytes = static_cast(std::numeric_limits::max()); + if (bytes > kMaxRandBytes) { + THEMIS_ERROR("TimestampAuthority::generateNonce: requested size {} exceeds RAND_bytes " + "limit {}", bytes, kMaxRandBytes); + return {}; + } + std::vector n(bytes); - if(RAND_bytes(n.data(), (int)bytes) != 1){ + if(RAND_bytes(n.data(), static_cast(bytes)) != 1){ for(size_t i=0;i CompressionFactory::create( void CompressionFactory::registerStrategy( const std::string& name, std::unique_ptr strategy) { - // TODO: Implement registration mechanism + // STUB/SIMULATION NOTE (STUB #CS-02 — strategy registry): + // Purpose: Allow runtime registration of custom compression strategies. + // Activation: Always no-op; the internal strategy map is not yet wired. + // Production Delta: Registered strategies are silently discarded; only the + // built-in TT/SVD/Product-Q strategies are accessible. + // Removal Plan: Implement an internal std::unordered_map registry and expose + // lookup in CompressionFactory::create() — Target Q2 2027. + // Tracking: src/tensor/ROADMAP.md § "CompressionFactory Registry" + // TODO(tracked): Implement strategy registry — see src/tensor/ROADMAP.md (void)name; (void)strategy; } diff --git a/src/tensor/tensor_routing_strategy.cpp b/src/tensor/tensor_routing_strategy.cpp index c62af1a3e3..ad19396df5 100644 --- a/src/tensor/tensor_routing_strategy.cpp +++ b/src/tensor/tensor_routing_strategy.cpp @@ -76,7 +76,9 @@ std::vector RankBasedPrioritization::prioritize( } // Compute freshness score (0.0 if very old, 1.0 if very recent) - float freshness = 1.0f; // TODO: Parse created_at and compute age + // TODO(tracked): Parse created_at and compute age-based freshness decay + // — see src/tensor/ROADMAP.md § "Routing Freshness Scoring" + float freshness = 1.0f; float score = (summary->similarity_score * rank_weight) + (freshness * freshness_weight); @@ -91,7 +93,9 @@ bool RankBasedPrioritization::sort( std::sort(summaries.begin(), summaries.end(), [this](const BaseTensorSummary& a, const BaseTensorSummary& b) { - float freshness_a = 1.0f; // TODO: Compute from timestamp + // TODO(tracked): Compute age-based freshness from timestamp + // — see src/tensor/ROADMAP.md § "Routing Freshness Scoring" + float freshness_a = 1.0f; float freshness_b = 1.0f; float score_a = (a.similarity_score * rank_weight) + (freshness_a * freshness_weight); @@ -280,7 +284,8 @@ RoutingDecision AdaptiveRouting::route( RoutingDecision decision; // For now, use a simple heuristic based on learned metrics - // TODO: Implement adaptive learning with metrics tracking + // TODO(tracked): Implement adaptive learning with metrics tracking + // — see src/tensor/ROADMAP.md § "Adaptive Routing Learning" decision.primary_target = "GRAPH_VALIDATION"; decision.fallback_target = "FALLBACK"; diff --git a/tests/test_timestamp_authority.cpp b/tests/test_timestamp_authority.cpp index ae6a2aed17..e599cec0af 100644 --- a/tests/test_timestamp_authority.cpp +++ b/tests/test_timestamp_authority.cpp @@ -2,6 +2,7 @@ #include "security/timestamp_authority.h" #include #include +#include using namespace themis::security; @@ -231,6 +232,16 @@ TEST_F(TimestampAuthorityTest, IsAvailable) { EXPECT_TRUE(available); } +TEST_F(TimestampAuthorityTest, GenerateNonceRejectsZeroAndOversizedRequests) { + TSAConfig config = createFreeTSAConfig(); + TimestampAuthority tsa(config); + + EXPECT_TRUE(tsa.generateNonce(0).empty()); + + const auto oversized = static_cast(std::numeric_limits::max()) + 1; + EXPECT_TRUE(tsa.generateNonce(oversized).empty()); +} + TEST_F(TimestampAuthorityTest, InvalidURL) { TSAConfig config; config.url = "https://invalid.tsa.example.com/nonexistent";