@@ -35,6 +35,7 @@ enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6, P
3535#include "foundation/compat_thread.h"
3636#include "foundation/profile.h"
3737#include "foundation/mem.h"
38+ #include "yyjson/yyjson.h"
3839
3940#include <stdint.h>
4041#include <stdio.h>
@@ -153,6 +154,83 @@ static const char *itoa_buf(int val) {
153154 return bufs [i ];
154155}
155156
157+ const char * cbm_pipeline_mode_name (cbm_index_mode_t mode ) {
158+ switch (mode ) {
159+ case CBM_MODE_FULL :
160+ return "full" ;
161+ case CBM_MODE_MODERATE :
162+ return "moderate" ;
163+ case CBM_MODE_FAST :
164+ return "fast" ;
165+ default :
166+ return "unknown" ;
167+ }
168+ }
169+
170+ typedef enum {
171+ INDEX_MODE_METADATA_OK = 0 ,
172+ INDEX_MODE_METADATA_MISSING_OR_INVALID ,
173+ INDEX_MODE_METADATA_ERROR ,
174+ } index_mode_metadata_status_t ;
175+
176+ static index_mode_metadata_status_t parse_index_mode (const char * properties_json ,
177+ size_t properties_len ,
178+ cbm_index_mode_t * out_mode ) {
179+ if (!properties_json || !out_mode ) {
180+ return INDEX_MODE_METADATA_ERROR ;
181+ }
182+
183+ yyjson_read_err read_error = {0 };
184+ yyjson_doc * doc =
185+ yyjson_read_opts ((char * )properties_json , properties_len , 0 , NULL , & read_error );
186+ if (!doc ) {
187+ return read_error .code == YYJSON_READ_ERROR_MEMORY_ALLOCATION
188+ ? INDEX_MODE_METADATA_ERROR
189+ : INDEX_MODE_METADATA_MISSING_OR_INVALID ;
190+ }
191+
192+ yyjson_val * root = yyjson_doc_get_root (doc );
193+ yyjson_val * mode_value = yyjson_is_obj (root ) ? yyjson_obj_get (root , "index_mode" ) : NULL ;
194+ index_mode_metadata_status_t status = INDEX_MODE_METADATA_OK ;
195+ if (yyjson_equals_str (mode_value , "full" )) {
196+ * out_mode = CBM_MODE_FULL ;
197+ } else if (yyjson_equals_str (mode_value , "moderate" )) {
198+ * out_mode = CBM_MODE_MODERATE ;
199+ } else if (yyjson_equals_str (mode_value , "fast" )) {
200+ * out_mode = CBM_MODE_FAST ;
201+ } else {
202+ status = INDEX_MODE_METADATA_MISSING_OR_INVALID ;
203+ }
204+
205+ yyjson_doc_free (doc );
206+ return status ;
207+ }
208+
209+ static bool index_mode_covers (cbm_index_mode_t stored_mode , cbm_index_mode_t requested_mode ) {
210+ return stored_mode >= CBM_MODE_FULL && stored_mode <= CBM_MODE_FAST &&
211+ requested_mode >= CBM_MODE_FULL && requested_mode <= CBM_MODE_FAST &&
212+ stored_mode <= requested_mode ;
213+ }
214+
215+ static index_mode_metadata_status_t read_stored_index_mode (cbm_store_t * store , const char * project ,
216+ cbm_index_mode_t * out_mode ) {
217+ char * properties_json = NULL ;
218+ size_t properties_len = 0 ;
219+ int rc = cbm_store_find_node_properties_by_qn (store , project , project , & properties_json ,
220+ & properties_len );
221+ if (rc == CBM_STORE_NOT_FOUND ) {
222+ return INDEX_MODE_METADATA_MISSING_OR_INVALID ;
223+ }
224+ if (rc != CBM_STORE_OK ) {
225+ return INDEX_MODE_METADATA_ERROR ;
226+ }
227+
228+ index_mode_metadata_status_t status =
229+ parse_index_mode (properties_json , properties_len , out_mode );
230+ free (properties_json );
231+ return status ;
232+ }
233+
156234/* Log current + peak RSS at a pipeline phase boundary (memory profiling). */
157235static void log_phase_mem (const char * phase ) {
158236 enum { PL_BYTES_PER_MB = 1024 * 1024 };
@@ -448,7 +526,11 @@ static int pass_structure(cbm_pipeline_t *p, const cbm_file_info_t *files, int f
448526 cbm_log_info ("pass.start" , "pass" , "structure" , "files" , itoa_buf (file_count ));
449527
450528 /* Project node */
451- cbm_gbuf_upsert_node (p -> gbuf , "Project" , p -> project_name , p -> project_name , NULL , 0 , 0 , "{}" );
529+ char project_props [CBM_SZ_64 ];
530+ snprintf (project_props , sizeof (project_props ), "{\"index_mode\":\"%s\"}" ,
531+ cbm_pipeline_mode_name (p -> mode ));
532+ cbm_gbuf_upsert_node (p -> gbuf , "Project" , p -> project_name , p -> project_name , NULL , 0 , 0 ,
533+ project_props );
452534 const char * branch_qn = p -> branch_qn ? p -> branch_qn : p -> project_name ;
453535 const char * branch_name = p -> git_ctx .branch ? p -> git_ctx .branch : "working-tree" ;
454536 char branch_props [CBM_SZ_2K ];
@@ -1116,17 +1198,37 @@ static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *file
11161198 if (check_store && cbm_store_check_integrity (check_store )) {
11171199 cbm_file_hash_t * hashes = NULL ;
11181200 int hash_count = 0 ;
1201+ cbm_index_mode_t stored_mode = CBM_MODE_FAST ;
1202+ index_mode_metadata_status_t stored_mode_status =
1203+ read_stored_index_mode (check_store , p -> project_name , & stored_mode );
1204+ if (stored_mode_status == INDEX_MODE_METADATA_ERROR ) {
1205+ cbm_log_error ("pipeline.route" , "path" , "metadata_read_error" , "stored_mode" , "error" ,
1206+ "requested_mode" , cbm_pipeline_mode_name (p -> mode ), "action" ,
1207+ "preserve_db" );
1208+ cbm_store_close (check_store );
1209+ free (db_path );
1210+ return CBM_PIPELINE_ABORT_PRESERVE_DB ;
1211+ }
1212+ bool stored_mode_known = stored_mode_status == INDEX_MODE_METADATA_OK ;
11191213 cbm_store_get_file_hashes (check_store , p -> project_name , & hashes , & hash_count );
11201214 cbm_store_free_file_hashes (hashes , hash_count );
11211215 cbm_store_close (check_store );
1122- if (hash_count > 0 && file_count <= hash_count + (hash_count / PAIR_LEN )) {
1216+ bool mode_covered = stored_mode_known && index_mode_covers (stored_mode , p -> mode );
1217+ if (hash_count > 0 && mode_covered && file_count <= hash_count + (hash_count / PAIR_LEN )) {
11231218 cbm_log_info ("pipeline.route" , "path" , "incremental" , "stored_hashes" ,
1124- itoa_buf (hash_count ));
1125- int rc = cbm_pipeline_run_incremental (p , db_path , files , file_count );
1219+ itoa_buf (hash_count ), "stored_mode" , cbm_pipeline_mode_name (stored_mode ),
1220+ "requested_mode" , cbm_pipeline_mode_name (p -> mode ), "effective_mode" ,
1221+ cbm_pipeline_mode_name (stored_mode ));
1222+ int rc = cbm_pipeline_run_incremental (p , db_path , files , file_count , stored_mode ,
1223+ p -> persistence );
11261224 free (db_path );
11271225 return rc ;
11281226 }
1129- if (hash_count > 0 ) {
1227+ if (hash_count > 0 && !mode_covered ) {
1228+ cbm_log_info ("pipeline.route" , "path" , "mode_upgrade_reindex" , "stored_mode" ,
1229+ stored_mode_known ? cbm_pipeline_mode_name (stored_mode ) : "unknown" ,
1230+ "requested_mode" , cbm_pipeline_mode_name (p -> mode ));
1231+ } else if (hash_count > 0 ) {
11301232 cbm_log_info ("pipeline.route" , "path" , "mode_change_reindex" , "stored_hashes" ,
11311233 itoa_buf (hash_count ), "discovered" , itoa_buf (file_count ));
11321234 }
@@ -1173,19 +1275,6 @@ static int64_t stat_mtime_ns(const struct stat *fst) {
11731275#endif
11741276}
11751277
1176- static const char * pipeline_mode_name (cbm_index_mode_t mode ) {
1177- switch (mode ) {
1178- case CBM_MODE_FULL :
1179- return "full" ;
1180- case CBM_MODE_MODERATE :
1181- return "moderate" ;
1182- case CBM_MODE_FAST :
1183- return "fast" ;
1184- default :
1185- return "unknown" ;
1186- }
1187- }
1188-
11891278/* Dump graph to SQLite and persist file hashes for incremental indexing. */
11901279static int dump_and_persist_hashes (cbm_pipeline_t * p , const cbm_file_info_t * files , int file_count ,
11911280 struct timespec * t ) {
@@ -1334,7 +1423,7 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil
13341423 : (p -> ignored_total > p -> ignored_count ? "truncated" : "complete" );
13351424 cbm_coverage_meta_t coverage_meta = {
13361425 .generation = have_project_info ? project_info .indexed_at : NULL ,
1337- .index_mode = pipeline_mode_name (p -> mode ),
1426+ .index_mode = cbm_pipeline_mode_name (p -> mode ),
13381427 .recording_status = recording_status ,
13391428 .ignored_files_stored = p -> ignored_count ,
13401429 .ignored_files_total = p -> ignored_total ,
@@ -1377,16 +1466,22 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil
13771466 free (p -> saved_adr );
13781467 p -> saved_adr = NULL ;
13791468
1380- /* Export persistent artifact if enabled */
1381- if (p -> persistence ) {
1469+ /* Export when persistence is explicitly enabled, or refresh an artifact
1470+ * created by an earlier run so a full rebuild cannot leave it stale. */
1471+ bool persistence_required = p -> persistence ;
1472+ if (persistence_required || cbm_artifact_exists (p -> repo_path )) {
13821473 CBM_PROF_START (t_art );
13831474 int arc = cbm_artifact_export (db_path , p -> repo_path , p -> project_name , CBM_ARTIFACT_BEST );
13841475 CBM_PROF_END ("persist" , "6_artifact_export" , t_art );
13851476 if (arc != 0 ) {
13861477 const char * err = cbm_artifact_export_last_error ();
13871478 cbm_log_error ("pipeline.err" , "phase" , "artifact_export" , "err" , err ? err : "unknown" );
1388- /* A failed persistence export intentionally fails the run; this used to be ignored. */
1389- return arc ;
1479+ /* An explicitly requested persistence export is part of the run's
1480+ * contract. Refreshing a pre-existing artifact remains best-effort,
1481+ * matching the incremental pipeline. */
1482+ if (persistence_required ) {
1483+ return arc ;
1484+ }
13901485 }
13911486 }
13921487
0 commit comments