feat(surgical-sync): add per-IID surgical sync pipeline

Implement lore sync --issue <IID> --mr <IID> -p <project> for on-demand
sync of specific entities without running the full project-wide pipeline.
Completes in seconds by fetching only targeted entities, their discussions,
resource events, and dependent data, then scoping doc regeneration and
embedding to only affected documents.

Pipeline stages: PREFLIGHT -> TOCTOU -> INGEST -> DEPENDENTS -> DOCS -> EMBED

New files:
- src/ingestion/surgical.rs: TOCTOU guard, preflight fetch, per-entity ingest
- src/ingestion/surgical_tests.rs: 17 unit/wiremock tests
- src/cli/commands/sync_surgical.rs: 719-line orchestrator
- src/embedding/pipeline_tests.rs: scoped embedding tests
- src/gitlab/client_tests.rs: get_by_iid wiremock tests
- migrations/027_surgical_sync_runs.sql: 12 surgical columns + indexes

Key changes:
- SyncOptions: issue_iids, mr_iids, project, preflight_only fields
- SyncResult: surgical_mode, surgical_iids, entity_results fields
- SyncRunRecorder: surgical lifecycle methods (set_surgical_metadata, etc)
- GitLabClient: get_issue_by_iid, get_mr_by_iid
- Scoped docs: regenerate_dirty_documents_for_sources
- Scoped embed: embed_documents_by_ids
- run_sync dispatches to run_sync_surgical when is_surgical()
- robot-docs updated with surgical sync schema + workflows
- All 1019 tests pass, clippy clean

Closes: bd-1sc6, bd-tiux, bd-159p, bd-1lja, bd-hs6j, bd-1elx, bd-arka,
        bd-3sez, bd-wcja, bd-kanh, bd-1i4i, bd-3bec
This commit is contained in:
teernisse
2026-02-18 13:29:20 -05:00
parent 53ce20595b
commit a0519a4d0d
34 changed files with 4090 additions and 43 deletions

View File

@@ -143,6 +143,8 @@ pub fn run_who(config: &Config, args: &WhoArgs) -> Result<WhoRun> {
"none"
};
let limit = args.limit.map_or(usize::MAX, usize::from);
match mode {
WhoMode::Expert { path } => {
// Compute as_of first so --since durations are relative to it.
@@ -159,7 +161,6 @@ pub fn run_who(config: &Config, args: &WhoArgs) -> Result<WhoRun> {
} else {
resolve_since_from(args.since.as_deref(), "24m", as_of_ms)?
};
let limit = usize::from(args.limit);
let result = expert::query_expert(
&conn,
&path,
@@ -191,7 +192,7 @@ pub fn run_who(config: &Config, args: &WhoArgs) -> Result<WhoRun> {
.as_deref()
.map(resolve_since_required)
.transpose()?;
let limit = usize::from(args.limit);
let result = workload::query_workload(
&conn,
username,
@@ -231,7 +232,7 @@ pub fn run_who(config: &Config, args: &WhoArgs) -> Result<WhoRun> {
}
WhoMode::Active => {
let since_ms = resolve_since(args.since.as_deref(), "7d")?;
let limit = usize::from(args.limit);
let result =
active::query_active(&conn, project_id, since_ms, limit, args.include_closed)?;
Ok(WhoRun {
@@ -249,7 +250,7 @@ pub fn run_who(config: &Config, args: &WhoArgs) -> Result<WhoRun> {
}
WhoMode::Overlap { path } => {
let since_ms = resolve_since(args.since.as_deref(), "30d")?;
let limit = usize::from(args.limit);
let result = overlap::query_overlap(&conn, &path, project_id, since_ms, limit)?;
Ok(WhoRun {
resolved_input: WhoResolvedInput {