| 563 | } |
| 564 | |
| 565 | static void poll_project(const char *key, void *val, void *ud) { |
| 566 | (void)key; |
| 567 | poll_ctx_t *ctx = ud; |
| 568 | project_state_t *s = val; |
| 569 | if (!s) { |
| 570 | return; |
| 571 | } |
| 572 | |
| 573 | /* Stale-root pruning (#286): classify the root BEFORE the baseline / |
| 574 | * is_git / interval gates so vanished roots are noticed even for |
| 575 | * non-git projects and regardless of adaptive backoff. */ |
| 576 | int stat_errno = 0; |
| 577 | root_status_t rs = root_status(s->root_path, &stat_errno); |
| 578 | if (rs == ROOT_UNCERTAIN) { |
| 579 | /* EACCES / EIO / network blip / TCC revocation — the root may still |
| 580 | * exist. Never count toward pruning; restart the streak so only an |
| 581 | * uninterrupted run of genuine ENOENT/ENOTDIR observations can |
| 582 | * delete user data. */ |
| 583 | if (s->missing_root_count > 0) { |
| 584 | s->missing_root_count = 0; |
| 585 | s->first_missing_ms = 0; |
| 586 | } |
| 587 | cbm_log_warn("watcher.root_stat_error", "project", s->project_name, "path", s->root_path, |
| 588 | "errno", itoa_buf(stat_errno)); |
| 589 | return; |
| 590 | } |
| 591 | if (rs == ROOT_MISSING) { |
| 592 | uint64_t now_ms = cbm_now_ms(); |
| 593 | if (s->missing_root_count == 0) { |
| 594 | s->first_missing_ms = now_ms; |
| 595 | } |
| 596 | s->missing_root_count++; |
| 597 | cbm_log_warn("watcher.root_missing", "project", s->project_name, "path", s->root_path, |
| 598 | "polls", itoa_buf(s->missing_root_count)); |
| 599 | if (s->missing_root_count >= MISSING_ROOT_DELETE_AFTER && |
| 600 | now_ms - s->first_missing_ms >= (uint64_t)prune_grace_s() * CBM_MSEC_PER_SEC) { |
| 601 | prune_missing_project(ctx->w, s); |
| 602 | } |
| 603 | return; |
| 604 | } |
| 605 | if (s->missing_root_count > 0) { |
| 606 | cbm_log_info("watcher.root_restored", "project", s->project_name, "path", s->root_path); |
| 607 | s->missing_root_count = 0; |
| 608 | s->first_missing_ms = 0; |
| 609 | } |
| 610 | |
| 611 | /* Initialize baseline on first poll */ |
| 612 | if (!s->baseline_done) { |
| 613 | init_baseline(s); |
| 614 | return; |
| 615 | } |
| 616 | |
| 617 | /* Skip non-git projects */ |
| 618 | if (!s->is_git) { |
| 619 | return; |
| 620 | } |
| 621 | |
| 622 | /* Respect adaptive interval */ |
no test coverage detected