ModerRAS commited on
Commit
f2c565a
·
1 Parent(s): fb3fd2d

Parallelize Pi RPC title-boundary reviews

Browse files
AGENTS.md CHANGED
@@ -116,6 +116,40 @@ cargo run --release --manifest-path tools\schema_v2_synthetic_augment\Cargo.toml
116
  --manifest data\schema_v2_synthetic_aug.manifest.json
117
  ```
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  Preferred synthetic follow-up training is a second stage from the best repaired
120
  hard-focus checkpoint, not a replacement for hard-focus. Keep this path
121
  Rust-cache-first: build one combined encoded cache from hard-focus JSONL plus
 
116
  --manifest data\schema_v2_synthetic_aug.manifest.json
117
  ```
118
 
119
+ When auditing Rust DMHY title-boundary recipe decisions with an LLM, prefer the
120
+ Rust reviewer cache flow. The LM reviewer is optional evidence only; it must not
121
+ mutate recipes or training JSONL directly. Use Pi RPC for stronger model-backed
122
+ reviews, and keep the default deterministic Rust heuristic as the source of
123
+ truth until reviewed decisions are explicitly consumed by a separate Rust merge
124
+ step:
125
+
126
+ ```powershell
127
+ cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
128
+ --review-title-boundaries-pi-rpc `
129
+ --recipes reports\dmhy_template_recipes.full_top5000.title_boundary_trial.seed.jsonl `
130
+ --title-boundary-decisions-output reports\dmhy_title_boundary_pi_rpc_decisions.title_boundary_trial.jsonl `
131
+ --title-boundary-min-similarity 0.25 `
132
+ --title-boundary-max-similarity 0.85 `
133
+ --threads 4
134
+ ```
135
+
136
+ For reviewer calibration, run stratified samples before full review. Use
137
+ `--title-boundary-heuristic-contains` to inspect high-similar duplicate/alias
138
+ keeps, structural keeps, and path-boundary keeps that are normally skipped by
139
+ the ambiguous merge review:
140
+
141
+ ```powershell
142
+ cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
143
+ --review-title-boundaries-pi-rpc `
144
+ --recipes reports\dmhy_template_recipes.full_top5000.title_boundary_trial.seed.jsonl `
145
+ --title-boundary-decisions-output reports\dmhy_title_boundary_pi_rpc_highsim_keep_sample.jsonl `
146
+ --title-boundary-heuristic-contains keep_similar_duplicate_or_alias `
147
+ --title-boundary-min-similarity 0.85 `
148
+ --title-boundary-max-similarity 1.0 `
149
+ --threads 4 `
150
+ --limit 20
151
+ ```
152
+
153
  Preferred synthetic follow-up training is a second stage from the best repaired
154
  hard-focus checkpoint, not a replacement for hard-focus. Keep this path
155
  Rust-cache-first: build one combined encoded cache from hard-focus JSONL plus
tools/rust_dmhy_template_apply/README.md CHANGED
@@ -77,9 +77,36 @@ cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml --
77
  --pi-rpc-model openai/gpt-5.5 `
78
  --title-boundary-min-similarity 0.25 `
79
  --title-boundary-max-similarity 0.85 `
 
80
  --limit 50
81
  ```
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  Optional controls:
84
 
85
  ```powershell
 
77
  --pi-rpc-model openai/gpt-5.5 `
78
  --title-boundary-min-similarity 0.25 `
79
  --title-boundary-max-similarity 0.85 `
80
+ --threads 4 `
81
  --limit 50
82
  ```
83
 
84
+ For stratified audits, select heuristic strata explicitly:
85
+
86
+ ```powershell
87
+ # High-similarity duplicate/alias keep candidates.
88
+ cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
89
+ --review-title-boundaries-pi-rpc `
90
+ --recipes reports\dmhy_template_recipes.full_top5000.seed.jsonl `
91
+ --title-boundary-decisions-output reports\dmhy_title_boundary_pi_rpc_highsim_keep.jsonl `
92
+ --title-boundary-heuristic-contains keep_similar_duplicate_or_alias `
93
+ --title-boundary-min-similarity 0.85 `
94
+ --title-boundary-max-similarity 1.0 `
95
+ --threads 4 `
96
+ --limit 20
97
+
98
+ # Structural keep candidates normally excluded from ambiguous merge review.
99
+ cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
100
+ --review-title-boundaries-pi-rpc `
101
+ --recipes reports\dmhy_template_recipes.full_top5000.seed.jsonl `
102
+ --title-boundary-decisions-output reports\dmhy_title_boundary_pi_rpc_structural_keep.jsonl `
103
+ --title-boundary-heuristic-contains keep_structural_boundary `
104
+ --title-boundary-min-similarity 0.0 `
105
+ --title-boundary-max-similarity 1.0 `
106
+ --threads 4 `
107
+ --limit 20
108
+ ```
109
+
110
  Optional controls:
111
 
112
  ```powershell
tools/rust_dmhy_template_apply/src/main.rs CHANGED
@@ -81,6 +81,8 @@ struct Args {
81
  lmstudio_timeout_seconds: u64,
82
  #[arg(long)]
83
  pi_rpc_model: Option<String>,
 
 
84
  #[arg(long, default_value_t = 50)]
85
  audit_max_count: u64,
86
  #[arg(long)]
@@ -1176,6 +1178,23 @@ fn run_rich_annotations(args: &Args) -> Result<()> {
1176
  Ok(())
1177
  }
1178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1179
  fn run_review_title_boundaries(args: &Args) -> Result<()> {
1180
  if !(0.0..=1.0).contains(&args.title_boundary_min_similarity)
1181
  || !(0.0..=1.0).contains(&args.title_boundary_max_similarity)
@@ -1183,34 +1202,38 @@ fn run_review_title_boundaries(args: &Args) -> Result<()> {
1183
  {
1184
  bail!("title boundary similarity thresholds must satisfy 0 <= min <= max <= 1");
1185
  }
1186
- let recipes = load_recipe_values(&args.recipes)?;
1187
- if recipes.is_empty() {
1188
- bail!("no recipe rows found in {}", args.recipes.display());
1189
- }
1190
- if let Some(parent) = args.title_boundary_decisions_output.parent() {
1191
- fs::create_dir_all(parent)?;
1192
- }
1193
- let mut writer = BufWriter::new(File::create(&args.title_boundary_decisions_output)?);
1194
- let mut reviewed = 0usize;
1195
- let mut skipped = 0usize;
1196
  if args.review_title_boundaries_lmstudio && args.review_title_boundaries_pi_rpc {
1197
  bail!("choose only one title-boundary review backend: LM Studio or Pi RPC");
1198
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1199
  let endpoint = format!(
1200
  "{}/chat/completions",
1201
  args.lmstudio_base_url.trim_end_matches('/')
1202
  );
1203
- let mut pi_rpc = if args.review_title_boundaries_pi_rpc {
1204
- Some(PiRpcClient::start(args.pi_rpc_model.as_deref())?)
1205
- } else {
1206
- None
1207
- };
 
 
 
1208
 
1209
- for recipe in recipes {
1210
- let template_id = recipe
1211
- .get("template_id")
1212
- .and_then(Value::as_str)
1213
- .unwrap_or("unknown");
1214
  let decisions = recipe
1215
  .get("title_boundary_decisions")
1216
  .and_then(Value::as_array)
@@ -1224,92 +1247,188 @@ fn run_review_title_boundaries(args: &Args) -> Result<()> {
1224
  let heuristic_decision = decision
1225
  .get("decision")
1226
  .and_then(Value::as_str)
1227
- .unwrap_or("unknown");
1228
- if heuristic_decision.contains("path_boundary")
 
 
 
 
 
 
1229
  || heuristic_decision.contains("structural_boundary")
1230
- || similarity < args.title_boundary_min_similarity
 
 
 
 
1231
  || similarity > args.title_boundary_max_similarity
1232
  {
1233
  skipped += 1;
1234
  continue;
1235
  }
1236
  if let Some(limit) = args.limit {
1237
- if reviewed >= limit {
1238
- break;
1239
  }
1240
  }
1241
- let prompt = title_boundary_review_prompt(&recipe, &decision);
1242
- let content = if let Some(client) = pi_rpc.as_mut() {
1243
- client.review(&prompt).with_context(|| {
1244
- format!("Pi RPC title-boundary review failed for template {template_id}")
1245
- })?
1246
- } else {
1247
- let response = call_lmstudio_title_boundary(
1248
- &endpoint,
1249
- &args.lmstudio_model,
1250
- args.lmstudio_temperature,
1251
- args.lmstudio_timeout_seconds,
1252
- &prompt,
1253
- )
1254
- .with_context(|| {
1255
- format!(
1256
- "LM Studio title-boundary review failed for template {template_id} via {endpoint}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1257
  )
1258
- })?;
1259
- response
1260
- .get("choices")
1261
- .and_then(Value::as_array)
1262
- .and_then(|choices| choices.first())
1263
- .and_then(|choice| choice.get("message"))
1264
- .and_then(|message| message.get("content"))
1265
- .and_then(Value::as_str)
1266
- .unwrap_or("")
1267
- .to_string()
1268
- };
1269
- let parsed = parse_lmstudio_json_object(&content).unwrap_or_else(|| {
1270
- json!({
1271
- "decision": "skip",
1272
- "kind": "parse_error",
1273
- "confidence": 0.0,
1274
- "reason": content,
1275
  })
1276
- });
1277
- let output = json!({
1278
- "template_id": recipe.get("template_id").cloned().unwrap_or(Value::Null),
1279
- "template": recipe.get("template").cloned().unwrap_or(Value::Null),
1280
- "left_role_index": decision.get("left_role_index").cloned().unwrap_or(Value::Null),
1281
- "right_role_index": decision.get("right_role_index").cloned().unwrap_or(Value::Null),
1282
- "similarity": similarity,
1283
- "heuristic_decision": heuristic_decision,
1284
- "model_decision": parsed.get("decision").cloned().unwrap_or(Value::Null),
1285
- "model_kind": parsed.get("kind").cloned().unwrap_or(Value::Null),
1286
- "confidence": parsed.get("confidence").cloned().unwrap_or(Value::Null),
1287
- "reason": parsed.get("reason").cloned().unwrap_or(Value::Null),
1288
- "source": if args.review_title_boundaries_pi_rpc { "pi-rpc" } else { "lmstudio" },
1289
- "model": if args.review_title_boundaries_pi_rpc { args.pi_rpc_model.as_deref().unwrap_or("pi-default") } else { args.lmstudio_model.as_str() },
1290
- "reviewed_at": Utc::now().to_rfc3339(),
1291
- });
1292
- serde_json::to_writer(&mut writer, &output)?;
1293
- writer.write_all(b"\n")?;
1294
- reviewed += 1;
1295
  }
 
 
 
 
 
 
 
 
1296
  }
1297
  writer.flush()?;
1298
  let summary = json!({
1299
  "recipes": args.recipes.to_string_lossy(),
1300
  "output": args.title_boundary_decisions_output.to_string_lossy(),
1301
- "reviewed": reviewed,
1302
  "skipped": skipped,
1303
- "model": if args.review_title_boundaries_pi_rpc { args.pi_rpc_model.as_deref().unwrap_or("pi-default") } else { args.lmstudio_model.as_str() },
1304
- "backend": if args.review_title_boundaries_pi_rpc { "pi-rpc" } else { "lmstudio" },
1305
  "base_url": if args.review_title_boundaries_pi_rpc { "pi --mode rpc" } else { args.lmstudio_base_url.as_str() },
1306
  "similarity_range": [args.title_boundary_min_similarity, args.title_boundary_max_similarity],
 
 
1307
  "generated_at": Utc::now().to_rfc3339(),
1308
  });
1309
  println!("{}", serde_json::to_string_pretty(&summary)?);
1310
  Ok(())
1311
  }
1312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1313
  fn load_recipe_values(path: &PathBuf) -> Result<Vec<Value>> {
1314
  let file = File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
1315
  let reader = BufReader::new(file);
 
81
  lmstudio_timeout_seconds: u64,
82
  #[arg(long)]
83
  pi_rpc_model: Option<String>,
84
+ #[arg(long)]
85
+ title_boundary_heuristic_contains: Option<String>,
86
  #[arg(long, default_value_t = 50)]
87
  audit_max_count: u64,
88
  #[arg(long)]
 
1178
  Ok(())
1179
  }
1180
 
1181
+ #[derive(Clone, Copy)]
1182
+ enum TitleBoundaryReviewBackend {
1183
+ LmStudio,
1184
+ PiRpc,
1185
+ }
1186
+
1187
+ #[derive(Clone)]
1188
+ struct TitleBoundaryReviewTask {
1189
+ sequence: usize,
1190
+ recipe: Value,
1191
+ decision: Value,
1192
+ similarity: f64,
1193
+ heuristic_decision: String,
1194
+ template_id: String,
1195
+ prompt: String,
1196
+ }
1197
+
1198
  fn run_review_title_boundaries(args: &Args) -> Result<()> {
1199
  if !(0.0..=1.0).contains(&args.title_boundary_min_similarity)
1200
  || !(0.0..=1.0).contains(&args.title_boundary_max_similarity)
 
1202
  {
1203
  bail!("title boundary similarity thresholds must satisfy 0 <= min <= max <= 1");
1204
  }
 
 
 
 
 
 
 
 
 
 
1205
  if args.review_title_boundaries_lmstudio && args.review_title_boundaries_pi_rpc {
1206
  bail!("choose only one title-boundary review backend: LM Studio or Pi RPC");
1207
  }
1208
+ let backend = if args.review_title_boundaries_pi_rpc {
1209
+ TitleBoundaryReviewBackend::PiRpc
1210
+ } else {
1211
+ TitleBoundaryReviewBackend::LmStudio
1212
+ };
1213
+ let source = match backend {
1214
+ TitleBoundaryReviewBackend::LmStudio => "lmstudio",
1215
+ TitleBoundaryReviewBackend::PiRpc => "pi-rpc",
1216
+ };
1217
+ let model_name = match backend {
1218
+ TitleBoundaryReviewBackend::LmStudio => args.lmstudio_model.as_str(),
1219
+ TitleBoundaryReviewBackend::PiRpc => args.pi_rpc_model.as_deref().unwrap_or("pi-default"),
1220
+ };
1221
  let endpoint = format!(
1222
  "{}/chat/completions",
1223
  args.lmstudio_base_url.trim_end_matches('/')
1224
  );
1225
+ let worker_count = args.threads.unwrap_or(1).max(1);
1226
+ let recipes = load_recipe_values(&args.recipes)?;
1227
+ if recipes.is_empty() {
1228
+ bail!("no recipe rows found in {}", args.recipes.display());
1229
+ }
1230
+ if let Some(parent) = args.title_boundary_decisions_output.parent() {
1231
+ fs::create_dir_all(parent)?;
1232
+ }
1233
 
1234
+ let mut skipped = 0usize;
1235
+ let mut tasks = Vec::new();
1236
+ 'recipes: for recipe in recipes {
 
 
1237
  let decisions = recipe
1238
  .get("title_boundary_decisions")
1239
  .and_then(Value::as_array)
 
1247
  let heuristic_decision = decision
1248
  .get("decision")
1249
  .and_then(Value::as_str)
1250
+ .unwrap_or("unknown")
1251
+ .to_string();
1252
+ if let Some(needle) = args.title_boundary_heuristic_contains.as_deref() {
1253
+ if !heuristic_decision.contains(needle) {
1254
+ skipped += 1;
1255
+ continue;
1256
+ }
1257
+ } else if heuristic_decision.contains("path_boundary")
1258
  || heuristic_decision.contains("structural_boundary")
1259
+ {
1260
+ skipped += 1;
1261
+ continue;
1262
+ }
1263
+ if similarity < args.title_boundary_min_similarity
1264
  || similarity > args.title_boundary_max_similarity
1265
  {
1266
  skipped += 1;
1267
  continue;
1268
  }
1269
  if let Some(limit) = args.limit {
1270
+ if tasks.len() >= limit {
1271
+ break 'recipes;
1272
  }
1273
  }
1274
+ let template_id = recipe
1275
+ .get("template_id")
1276
+ .and_then(Value::as_str)
1277
+ .unwrap_or("unknown")
1278
+ .to_string();
1279
+ tasks.push(TitleBoundaryReviewTask {
1280
+ sequence: tasks.len(),
1281
+ prompt: title_boundary_review_prompt(&recipe, &decision),
1282
+ recipe: recipe.clone(),
1283
+ decision,
1284
+ similarity,
1285
+ heuristic_decision,
1286
+ template_id,
1287
+ });
1288
+ }
1289
+ }
1290
+
1291
+ let mut outputs = if worker_count == 1 || tasks.len() <= 1 {
1292
+ review_title_boundary_chunk(
1293
+ &tasks,
1294
+ backend,
1295
+ &endpoint,
1296
+ model_name,
1297
+ args.pi_rpc_model.as_deref(),
1298
+ args.lmstudio_temperature,
1299
+ args.lmstudio_timeout_seconds,
1300
+ source,
1301
+ )?
1302
+ } else {
1303
+ let chunk_size = ((tasks.len() + worker_count - 1) / worker_count).max(1);
1304
+ let pool = rayon::ThreadPoolBuilder::new()
1305
+ .num_threads(worker_count)
1306
+ .build()
1307
+ .context("failed to configure title-boundary review thread pool")?;
1308
+ let chunk_results: Vec<Result<Vec<(usize, Value)>>> = pool.install(|| {
1309
+ tasks
1310
+ .par_chunks(chunk_size)
1311
+ .map(|chunk| {
1312
+ review_title_boundary_chunk(
1313
+ chunk,
1314
+ backend,
1315
+ &endpoint,
1316
+ model_name,
1317
+ args.pi_rpc_model.as_deref(),
1318
+ args.lmstudio_temperature,
1319
+ args.lmstudio_timeout_seconds,
1320
+ source,
1321
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1322
  })
1323
+ .collect()
1324
+ });
1325
+ let mut combined = Vec::new();
1326
+ for result in chunk_results {
1327
+ combined.extend(result?);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1328
  }
1329
+ combined
1330
+ };
1331
+ outputs.sort_by_key(|(sequence, _)| *sequence);
1332
+
1333
+ let mut writer = BufWriter::new(File::create(&args.title_boundary_decisions_output)?);
1334
+ for (_, output) in &outputs {
1335
+ serde_json::to_writer(&mut writer, output)?;
1336
+ writer.write_all(b"\n")?;
1337
  }
1338
  writer.flush()?;
1339
  let summary = json!({
1340
  "recipes": args.recipes.to_string_lossy(),
1341
  "output": args.title_boundary_decisions_output.to_string_lossy(),
1342
+ "reviewed": outputs.len(),
1343
  "skipped": skipped,
1344
+ "model": model_name,
1345
+ "backend": source,
1346
  "base_url": if args.review_title_boundaries_pi_rpc { "pi --mode rpc" } else { args.lmstudio_base_url.as_str() },
1347
  "similarity_range": [args.title_boundary_min_similarity, args.title_boundary_max_similarity],
1348
+ "heuristic_contains": args.title_boundary_heuristic_contains.as_deref(),
1349
+ "threads": worker_count,
1350
  "generated_at": Utc::now().to_rfc3339(),
1351
  });
1352
  println!("{}", serde_json::to_string_pretty(&summary)?);
1353
  Ok(())
1354
  }
1355
 
1356
+ fn review_title_boundary_chunk(
1357
+ tasks: &[TitleBoundaryReviewTask],
1358
+ backend: TitleBoundaryReviewBackend,
1359
+ endpoint: &str,
1360
+ model_name: &str,
1361
+ pi_rpc_model: Option<&str>,
1362
+ lmstudio_temperature: f64,
1363
+ lmstudio_timeout_seconds: u64,
1364
+ source: &str,
1365
+ ) -> Result<Vec<(usize, Value)>> {
1366
+ let mut pi_rpc = if matches!(backend, TitleBoundaryReviewBackend::PiRpc) {
1367
+ Some(PiRpcClient::start(pi_rpc_model)?)
1368
+ } else {
1369
+ None
1370
+ };
1371
+ let mut outputs = Vec::with_capacity(tasks.len());
1372
+ for task in tasks {
1373
+ let content = if let Some(client) = pi_rpc.as_mut() {
1374
+ client.review(&task.prompt).with_context(|| {
1375
+ format!(
1376
+ "Pi RPC title-boundary review failed for template {}",
1377
+ task.template_id
1378
+ )
1379
+ })?
1380
+ } else {
1381
+ let response = call_lmstudio_title_boundary(
1382
+ endpoint,
1383
+ model_name,
1384
+ lmstudio_temperature,
1385
+ lmstudio_timeout_seconds,
1386
+ &task.prompt,
1387
+ )
1388
+ .with_context(|| {
1389
+ format!(
1390
+ "LM Studio title-boundary review failed for template {} via {endpoint}",
1391
+ task.template_id
1392
+ )
1393
+ })?;
1394
+ response
1395
+ .get("choices")
1396
+ .and_then(Value::as_array)
1397
+ .and_then(|choices| choices.first())
1398
+ .and_then(|choice| choice.get("message"))
1399
+ .and_then(|message| message.get("content"))
1400
+ .and_then(Value::as_str)
1401
+ .unwrap_or("")
1402
+ .to_string()
1403
+ };
1404
+ let parsed = parse_lmstudio_json_object(&content).unwrap_or_else(|| {
1405
+ json!({
1406
+ "decision": "skip",
1407
+ "kind": "parse_error",
1408
+ "confidence": 0.0,
1409
+ "reason": content,
1410
+ })
1411
+ });
1412
+ let output = json!({
1413
+ "template_id": task.recipe.get("template_id").cloned().unwrap_or(Value::Null),
1414
+ "template": task.recipe.get("template").cloned().unwrap_or(Value::Null),
1415
+ "left_role_index": task.decision.get("left_role_index").cloned().unwrap_or(Value::Null),
1416
+ "right_role_index": task.decision.get("right_role_index").cloned().unwrap_or(Value::Null),
1417
+ "similarity": task.similarity,
1418
+ "heuristic_decision": task.heuristic_decision,
1419
+ "model_decision": parsed.get("decision").cloned().unwrap_or(Value::Null),
1420
+ "model_kind": parsed.get("kind").cloned().unwrap_or(Value::Null),
1421
+ "confidence": parsed.get("confidence").cloned().unwrap_or(Value::Null),
1422
+ "reason": parsed.get("reason").cloned().unwrap_or(Value::Null),
1423
+ "source": source,
1424
+ "model": model_name,
1425
+ "reviewed_at": Utc::now().to_rfc3339(),
1426
+ });
1427
+ outputs.push((task.sequence, output));
1428
+ }
1429
+ Ok(outputs)
1430
+ }
1431
+
1432
  fn load_recipe_values(path: &PathBuf) -> Result<Vec<Value>> {
1433
  let file = File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
1434
  let reader = BufReader::new(file);