@lamentis/naome 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +117 -47
  3. package/bin/naome.js +65 -12
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/context_commands.rs +47 -0
  6. package/crates/naome-cli/src/dispatcher.rs +12 -2
  7. package/crates/naome-cli/src/main.rs +78 -29
  8. package/crates/naome-cli/src/quality_commands.rs +238 -34
  9. package/crates/naome-cli/src/quality_output.rs +34 -0
  10. package/crates/naome-cli/src/quality_reconcile_command.rs +45 -0
  11. package/crates/naome-cli/src/repository_model_commands.rs +84 -0
  12. package/crates/naome-cli/src/task_commands.rs +62 -0
  13. package/crates/naome-cli/src/workflow_commands.rs +120 -3
  14. package/crates/naome-core/Cargo.toml +1 -1
  15. package/crates/naome-core/src/context/helpers.rs +75 -0
  16. package/crates/naome-core/src/context/select.rs +134 -0
  17. package/crates/naome-core/src/context/types.rs +43 -0
  18. package/crates/naome-core/src/context.rs +6 -0
  19. package/crates/naome-core/src/decision/states.rs +1 -1
  20. package/crates/naome-core/src/decision.rs +4 -1
  21. package/crates/naome-core/src/git.rs +4 -2
  22. package/crates/naome-core/src/install_plan.rs +20 -0
  23. package/crates/naome-core/src/journal.rs +2 -7
  24. package/crates/naome-core/src/lib.rs +35 -8
  25. package/crates/naome-core/src/quality/adapter_ios.rs +131 -0
  26. package/crates/naome-core/src/quality/adapter_support.rs +67 -0
  27. package/crates/naome-core/src/quality/adapters.rs +81 -18
  28. package/crates/naome-core/src/quality/baseline.rs +8 -0
  29. package/crates/naome-core/src/quality/cache.rs +151 -0
  30. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +19 -8
  31. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  32. package/crates/naome-core/src/quality/checks.rs +7 -8
  33. package/crates/naome-core/src/quality/cleanup.rs +36 -3
  34. package/crates/naome-core/src/quality/config.rs +21 -3
  35. package/crates/naome-core/src/quality/mod.rs +189 -10
  36. package/crates/naome-core/src/quality/reconcile.rs +138 -0
  37. package/crates/naome-core/src/quality/reconcile_anchors.rs +64 -0
  38. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  39. package/crates/naome-core/src/quality/scanner/analysis.rs +175 -0
  40. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  41. package/crates/naome-core/src/quality/scanner.rs +235 -217
  42. package/crates/naome-core/src/quality/semantic/checks.rs +151 -0
  43. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  44. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  45. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  46. package/crates/naome-core/src/quality/semantic.rs +68 -0
  47. package/crates/naome-core/src/quality/structure/adapter_ios.rs +149 -0
  48. package/crates/naome-core/src/quality/structure/adapters.rs +60 -42
  49. package/crates/naome-core/src/quality/structure/checks/directory.rs +13 -21
  50. package/crates/naome-core/src/quality/structure/checks.rs +1 -1
  51. package/crates/naome-core/src/quality/structure/classify/roles.rs +51 -5
  52. package/crates/naome-core/src/quality/structure/classify.rs +52 -0
  53. package/crates/naome-core/src/quality/structure/config.rs +24 -3
  54. package/crates/naome-core/src/quality/structure/mod.rs +5 -2
  55. package/crates/naome-core/src/quality/structure/model.rs +8 -1
  56. package/crates/naome-core/src/quality/types.rs +59 -2
  57. package/crates/naome-core/src/repository_model/detect.rs +188 -0
  58. package/crates/naome-core/src/repository_model/explain.rs +121 -0
  59. package/crates/naome-core/src/repository_model/path_scan.rs +67 -0
  60. package/crates/naome-core/src/repository_model/path_support.rs +59 -0
  61. package/crates/naome-core/src/repository_model/types.rs +152 -0
  62. package/crates/naome-core/src/repository_model/world.rs +48 -0
  63. package/crates/naome-core/src/repository_model/world_adapters.rs +145 -0
  64. package/crates/naome-core/src/repository_model/world_path_facts.rs +55 -0
  65. package/crates/naome-core/src/repository_model/world_paths.rs +168 -0
  66. package/crates/naome-core/src/repository_model.rs +164 -0
  67. package/crates/naome-core/src/route/builtin_checks.rs +41 -16
  68. package/crates/naome-core/src/task_ledger/import.rs +142 -0
  69. package/crates/naome-core/src/task_ledger/model.rs +13 -0
  70. package/crates/naome-core/src/task_ledger/proof_record.rs +52 -0
  71. package/crates/naome-core/src/task_ledger/read.rs +118 -0
  72. package/crates/naome-core/src/task_ledger/render.rs +55 -0
  73. package/crates/naome-core/src/task_ledger/write.rs +38 -0
  74. package/crates/naome-core/src/task_ledger.rs +48 -0
  75. package/crates/naome-core/src/task_state/api.rs +4 -2
  76. package/crates/naome-core/src/task_state/completed_refresh.rs +5 -16
  77. package/crates/naome-core/src/task_state/diff.rs +2 -2
  78. package/crates/naome-core/src/task_state/evidence.rs +8 -3
  79. package/crates/naome-core/src/task_state/mod.rs +1 -1
  80. package/crates/naome-core/src/task_state/progress.rs +13 -0
  81. package/crates/naome-core/src/task_state/proof_model.rs +8 -8
  82. package/crates/naome-core/src/task_state/repair.rs +2 -2
  83. package/crates/naome-core/src/task_state/task_diff_api.rs +9 -18
  84. package/crates/naome-core/src/task_state/types.rs +24 -0
  85. package/crates/naome-core/src/verification.rs +29 -18
  86. package/crates/naome-core/src/workflow/agent/capability.rs +194 -0
  87. package/crates/naome-core/src/workflow/agent/context_delta.rs +42 -0
  88. package/crates/naome-core/src/workflow/agent/decision.rs +32 -0
  89. package/crates/naome-core/src/workflow/agent/execution.rs +80 -0
  90. package/crates/naome-core/src/workflow/agent/proof.rs +24 -0
  91. package/crates/naome-core/src/workflow/agent/support.rs +58 -0
  92. package/crates/naome-core/src/workflow/agent/watchdog.rs +47 -0
  93. package/crates/naome-core/src/workflow/agent.rs +34 -0
  94. package/crates/naome-core/src/workflow/agent_types.rs +105 -0
  95. package/crates/naome-core/src/workflow/doctor.rs +183 -0
  96. package/crates/naome-core/src/workflow/mod.rs +13 -0
  97. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  98. package/crates/naome-core/src/workflow/output.rs +8 -2
  99. package/crates/naome-core/src/workflow/phase_inference.rs +1 -1
  100. package/crates/naome-core/tests/context.rs +99 -0
  101. package/crates/naome-core/tests/harness_health.rs +4 -0
  102. package/crates/naome-core/tests/install_plan.rs +14 -0
  103. package/crates/naome-core/tests/quality.rs +190 -5
  104. package/crates/naome-core/tests/quality_performance.rs +268 -0
  105. package/crates/naome-core/tests/quality_structure_adapters.rs +39 -0
  106. package/crates/naome-core/tests/quality_structure_policy.rs +19 -0
  107. package/crates/naome-core/tests/repo_support/mod.rs +5 -1
  108. package/crates/naome-core/tests/repo_support/verification_values.rs +55 -0
  109. package/crates/naome-core/tests/repository_model.rs +281 -0
  110. package/crates/naome-core/tests/route_user_diff.rs +59 -7
  111. package/crates/naome-core/tests/semantic_legacy.rs +174 -0
  112. package/crates/naome-core/tests/task_ledger.rs +328 -0
  113. package/crates/naome-core/tests/task_state.rs +28 -0
  114. package/crates/naome-core/tests/verification.rs +29 -36
  115. package/crates/naome-core/tests/workflow_agent.rs +233 -0
  116. package/crates/naome-core/tests/workflow_agent_support/mod.rs +159 -0
  117. package/crates/naome-core/tests/workflow_doctor.rs +45 -0
  118. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  119. package/installer/codex-hooks.js +121 -0
  120. package/installer/context.js +10 -0
  121. package/installer/filesystem.js +4 -0
  122. package/installer/flows.js +8 -4
  123. package/installer/git-boundary.js +1 -0
  124. package/installer/harness-files.js +6 -0
  125. package/installer/install-plan.js +4 -0
  126. package/installer/main.js +1 -1
  127. package/installer/native.js +1 -1
  128. package/native/darwin-arm64/naome +0 -0
  129. package/native/linux-x64/naome +0 -0
  130. package/package.json +1 -1
  131. package/templates/naome-root/.codex/config.toml +2 -0
  132. package/templates/naome-root/.codex/hooks.json +70 -0
  133. package/templates/naome-root/.naome/bin/check-harness-health.js +8 -6
  134. package/templates/naome-root/.naome/bin/check-task-state.js +12 -7
  135. package/templates/naome-root/.naome/bin/codex-hook-io.js +122 -0
  136. package/templates/naome-root/.naome/bin/codex-hook-policy.js +180 -0
  137. package/templates/naome-root/.naome/bin/codex-hook-runtime.js +174 -0
  138. package/templates/naome-root/.naome/bin/codex-hook.js +6 -0
  139. package/templates/naome-root/.naome/bin/naome.js +45 -7
  140. package/templates/naome-root/.naome/manifest.json +12 -6
  141. package/templates/naome-root/.naome/repository-model.json +6 -0
  142. package/templates/naome-root/.naome/repository-quality.json +3 -1
  143. package/templates/naome-root/.naome/verification.json +15 -1
  144. package/templates/naome-root/.naomeignore +1 -0
  145. package/templates/naome-root/AGENTS.md +38 -83
  146. package/templates/naome-root/docs/naome/agent-workflow.md +66 -28
  147. package/templates/naome-root/docs/naome/codex-hooks.md +82 -0
  148. package/templates/naome-root/docs/naome/context-economy.md +73 -0
  149. package/templates/naome-root/docs/naome/first-run.md +25 -14
  150. package/templates/naome-root/docs/naome/index.md +18 -10
  151. package/templates/naome-root/docs/naome/repository-model.md +92 -0
  152. package/templates/naome-root/docs/naome/repository-quality.md +104 -5
  153. package/templates/naome-root/docs/naome/repository-structure.md +10 -3
  154. package/templates/naome-root/docs/naome/task-ledger.md +71 -0
  155. package/templates/naome-root/docs/naome/testing.md +16 -3
@@ -1,8 +1,9 @@
1
1
  use std::fs;
2
2
 
3
3
  use naome_core::{
4
- check_repository_quality, init_repository_quality, plan_quality_cleanup, route_quality_cleanup,
5
- seed_builtin_verification_checks, QualityMode,
4
+ check_repository_quality, init_repository_quality, init_repository_quality_with_mode,
5
+ plan_quality_cleanup, reconcile_repository_quality, route_quality_cleanup,
6
+ seed_builtin_verification_checks, QualityInitMode, QualityMode,
6
7
  };
7
8
  use serde_json::Value;
8
9
 
@@ -65,6 +66,7 @@ fn changed_check_blocks_new_duplicate_blocks_against_existing_code() {
65
66
  "export function existing() {\n const alpha = input.alpha;\n const beta = input.beta;\n const total = alpha + beta;\n return total;\n}\n",
66
67
  );
67
68
  repo.commit_all("existing helper");
69
+ let _ = check_repository_quality(repo.path(), QualityMode::DeepReport).unwrap();
68
70
 
69
71
  repo.write(
70
72
  "src/copied.js",
@@ -92,6 +94,7 @@ fn duplicate_blocks_report_one_finding_per_overlapping_region() {
92
94
  "export function existing() {\n const alpha = input.alpha;\n const beta = input.beta;\n const gamma = input.gamma;\n const total = alpha + beta + gamma;\n return total;\n}\n",
93
95
  );
94
96
  repo.commit_all("existing helper");
97
+ let _ = check_repository_quality(repo.path(), QualityMode::DeepReport).unwrap();
95
98
 
96
99
  repo.write(
97
100
  "src/copied.js",
@@ -186,7 +189,7 @@ fn near_duplicate_functions_do_not_compare_containers_with_child_symbols() {
186
189
  }
187
190
 
188
191
  #[test]
189
- fn init_writes_default_config_and_baselines_existing_debt() {
192
+ fn init_writes_default_config_and_marks_baseline_pending() {
190
193
  let repo = QualityFixture::new("quality-init-baseline");
191
194
  let large_file = (0..520)
192
195
  .map(|index| format!("export const value{index} = {index};\n"))
@@ -197,13 +200,19 @@ fn init_writes_default_config_and_baselines_existing_debt() {
197
200
  let init = init_repository_quality(repo.path()).unwrap();
198
201
 
199
202
  assert!(init.config_written);
200
- assert!(init.baseline_written);
203
+ assert!(!init.baseline_written);
204
+ assert!(init.baseline_pending);
201
205
  assert!(repo.path().join(".naome/repository-quality.json").is_file());
202
206
  assert!(repo
203
207
  .path()
204
208
  .join(".naome/repository-quality-baseline.json")
205
209
  .is_file());
206
- assert!(init.baseline_violations > 0);
210
+ assert_eq!(init.baseline_violations, 0);
211
+
212
+ let baseline =
213
+ init_repository_quality_with_mode(repo.path(), QualityInitMode::Baseline).unwrap();
214
+ assert!(baseline.baseline_written);
215
+ assert!(baseline.baseline_violations > 0);
207
216
  }
208
217
 
209
218
  #[test]
@@ -239,6 +248,174 @@ fn init_generates_adapter_selection_without_product_specific_path_rules() {
239
248
  assert!(!serialized_config.contains("naome-template-harness"));
240
249
  }
241
250
 
251
+ #[test]
252
+ fn init_auto_detects_ios_swift_adapter_set() {
253
+ let repo = QualityFixture::new("quality-ios-swift-adapters");
254
+ repo.write(
255
+ "Package.swift",
256
+ "// swift-tools-version: 5.9\nimport PackageDescription\n",
257
+ );
258
+ repo.write(
259
+ "Demo.xcodeproj/project.pbxproj",
260
+ "// !$*UTF8*$!\n{ archiveVersion = 1; }\n",
261
+ );
262
+ repo.write(
263
+ "Sources/App/App.swift",
264
+ "import SwiftUI\n@main struct DemoApp: App { var body: some Scene { WindowGroup { ContentView() } } }\n",
265
+ );
266
+ repo.write(
267
+ "Tests/AppTests/AppTests.swift",
268
+ "import XCTest\nfinal class AppTests: XCTestCase {}\n",
269
+ );
270
+ repo.write(
271
+ "Resources/Assets.xcassets/AppIcon.appiconset/Contents.json",
272
+ "{}\n",
273
+ );
274
+ repo.write("Generated/R.generated.swift", "enum R {}\n");
275
+ repo.commit_all("ios swift project");
276
+
277
+ init_repository_quality(repo.path()).unwrap();
278
+ let quality: Value = serde_json::from_str(
279
+ &fs::read_to_string(repo.path().join(".naome/repository-quality.json")).unwrap(),
280
+ )
281
+ .unwrap();
282
+ let structure: Value = serde_json::from_str(
283
+ &fs::read_to_string(repo.path().join(".naome/repository-structure.json")).unwrap(),
284
+ )
285
+ .unwrap();
286
+
287
+ for adapter in [
288
+ "swift",
289
+ "xcode",
290
+ "xctest",
291
+ "swiftui",
292
+ "ios-app-structure",
293
+ "swift-package",
294
+ "ios-resources",
295
+ "generated-ios",
296
+ ] {
297
+ assert!(
298
+ has_adapter(&quality, adapter),
299
+ "missing quality adapter {adapter}: {quality:#?}"
300
+ );
301
+ assert!(
302
+ has_adapter(&structure, adapter),
303
+ "missing structure adapter {adapter}: {structure:#?}"
304
+ );
305
+ }
306
+ }
307
+
308
+ #[test]
309
+ fn changed_check_reports_stale_adapter_policy_after_stack_changes() {
310
+ let repo = QualityFixture::new("quality-stale-adapter-policy");
311
+ repo.write_quality_config_with_adapters(450, 100, 10, &[]);
312
+ repo.write_structure_config(serde_json::json!({
313
+ "enabledAdapters": []
314
+ }));
315
+ repo.commit_all("initial quality policy");
316
+ repo.write(
317
+ "Package.swift",
318
+ "// swift-tools-version: 5.9\nimport PackageDescription\n",
319
+ );
320
+ repo.write(
321
+ "Sources/App/App.swift",
322
+ "public struct DemoApp { public init() {} }\n",
323
+ );
324
+
325
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
326
+
327
+ assert!(!report.ok);
328
+ assert!(report.violations.iter().any(|violation| {
329
+ violation.check_id == "adapter-policy-stale"
330
+ && violation.path == ".naome/repository-quality.json"
331
+ && violation.message.contains("swift")
332
+ && violation
333
+ .related_paths
334
+ .contains(&".naome/repository-structure.json".to_string())
335
+ }));
336
+ }
337
+
338
+ #[test]
339
+ fn changed_check_reports_stale_repository_model_after_stack_changes() {
340
+ let repo = QualityFixture::new("quality-repository-model-stale");
341
+ repo.write_quality_config();
342
+ repo.write_structure_config(serde_json::json!({}));
343
+ repo.write(
344
+ ".naome/repository-model.json",
345
+ "{\"schema\":\"naome.repository-model.v1\",\"version\":1,\"status\":\"ready\",\"facts\":[]}\n",
346
+ );
347
+ repo.write("README.md", "# Baseline\n");
348
+ repo.commit_all("baseline");
349
+
350
+ repo.write("Package.swift", "// swift-tools-version: 5.9\n");
351
+
352
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
353
+
354
+ assert!(!report.ok);
355
+ assert!(report
356
+ .summary
357
+ .reason_codes
358
+ .contains(&"repository_model_stale".to_string()));
359
+ assert!(report.violations.iter().any(|violation| {
360
+ violation.check_id == "repository-model-stale"
361
+ && violation.path == ".naome/repository-model.json"
362
+ && violation
363
+ .related_paths
364
+ .contains(&"Package.swift".to_string())
365
+ }));
366
+ }
367
+
368
+ #[test]
369
+ fn reconcile_write_updates_stale_quality_and_structure_adapter_policy() {
370
+ let repo = QualityFixture::new("quality-reconcile-adapters");
371
+ repo.write_quality_config_with_adapters(450, 100, 10, &[]);
372
+ repo.write_structure_config(serde_json::json!({
373
+ "enabledAdapters": []
374
+ }));
375
+ repo.write(
376
+ "Package.swift",
377
+ "// swift-tools-version: 5.9\nimport PackageDescription\n",
378
+ );
379
+ repo.write(
380
+ "Sources/App/App.swift",
381
+ "public struct DemoApp { public init() {} }\n",
382
+ );
383
+
384
+ let planned = reconcile_repository_quality(repo.path(), false).unwrap();
385
+ assert!(!planned.ok);
386
+ assert!(planned.stale);
387
+ assert!(planned
388
+ .missing_quality_adapters
389
+ .contains(&"swift".to_string()));
390
+ assert!(planned.updated_paths.is_empty());
391
+
392
+ let written = reconcile_repository_quality(repo.path(), true).unwrap();
393
+ assert!(written.ok);
394
+ assert!(!written.stale);
395
+ assert_eq!(
396
+ written.updated_paths,
397
+ vec![
398
+ ".naome/repository-quality.json".to_string(),
399
+ ".naome/repository-structure.json".to_string()
400
+ ]
401
+ );
402
+
403
+ let quality: Value = serde_json::from_str(
404
+ &fs::read_to_string(repo.path().join(".naome/repository-quality.json")).unwrap(),
405
+ )
406
+ .unwrap();
407
+ let structure: Value = serde_json::from_str(
408
+ &fs::read_to_string(repo.path().join(".naome/repository-structure.json")).unwrap(),
409
+ )
410
+ .unwrap();
411
+ assert!(has_adapter(&quality, "swift"));
412
+ assert!(has_adapter(&structure, "swift"));
413
+
414
+ let repeated = reconcile_repository_quality(repo.path(), true).unwrap();
415
+ assert!(repeated.ok);
416
+ assert!(repeated.updated_paths.is_empty());
417
+ }
418
+
242
419
  #[test]
243
420
  fn enabled_adapters_apply_their_rules_without_materialized_path_rules() {
244
421
  let repo = QualityFixture::new("quality-adapter-runtime-config");
@@ -257,6 +434,14 @@ fn enabled_adapters_apply_their_rules_without_materialized_path_rules() {
257
434
  }));
258
435
  }
259
436
 
437
+ fn has_adapter(config: &Value, adapter: &str) -> bool {
438
+ config["enabledAdapters"]
439
+ .as_array()
440
+ .unwrap()
441
+ .iter()
442
+ .any(|value| value.as_str() == Some(adapter))
443
+ }
444
+
260
445
  #[test]
261
446
  fn seeding_builtin_verification_wires_repository_quality_into_change_types() {
262
447
  let repo = QualityFixture::new("quality-verification-wiring");
@@ -0,0 +1,268 @@
1
+ mod repo_support;
2
+
3
+ use std::fs;
4
+
5
+ use naome_core::{
6
+ check_repository_quality, check_repository_quality_paths, check_semantic_legacy,
7
+ init_repository_quality, init_repository_quality_with_mode, quality_cache_status,
8
+ QualityInitMode, QualityMode,
9
+ };
10
+
11
+ use repo_support::TestRepo;
12
+
13
+ #[test]
14
+ fn quality_init_seeds_policy_without_baseline_scan() {
15
+ let repo = quality_repo("quality-init-seed-only");
16
+ repo.write_file("src/large.js", &large_file());
17
+ repo.commit_all("legacy debt");
18
+
19
+ let init = init_repository_quality(repo.path()).unwrap();
20
+
21
+ assert!(init.config_written);
22
+ assert!(init.structure_config_written);
23
+ assert!(!init.baseline_written);
24
+ assert!(init.baseline_pending);
25
+ assert_eq!(init.baseline_violations, 0);
26
+ assert!(repo
27
+ .path()
28
+ .join(".naome/repository-quality-baseline.json")
29
+ .is_file());
30
+ }
31
+
32
+ #[test]
33
+ fn quality_baseline_modes_are_explicit() {
34
+ let repo = quality_repo("quality-init-explicit-baseline");
35
+ repo.write_file("src/large.js", &large_file());
36
+ repo.commit_all("legacy debt");
37
+
38
+ let baseline =
39
+ init_repository_quality_with_mode(repo.path(), QualityInitMode::Baseline).unwrap();
40
+ assert!(baseline.baseline_written);
41
+ assert!(!baseline.baseline_pending);
42
+ assert!(baseline.baseline_violations > 0);
43
+
44
+ let deep =
45
+ init_repository_quality_with_mode(repo.path(), QualityInitMode::DeepBaseline).unwrap();
46
+ assert_eq!(deep.mode, "deep-baseline");
47
+ assert!(deep.baseline_violations >= baseline.baseline_violations);
48
+ }
49
+
50
+ #[test]
51
+ fn changed_fast_scans_only_changed_file_contents() {
52
+ let repo = quality_repo("quality-changed-fast-focused");
53
+ for index in 0..1000 {
54
+ repo.write_file(
55
+ &format!("src/unchanged_{index}.js"),
56
+ "export const value = 1;\n",
57
+ );
58
+ }
59
+ repo.write_file("src/changed.js", "export const before = 1;\n");
60
+ repo.commit_all("large baseline");
61
+ repo.write_file("src/changed.js", "export const after = 2;\n");
62
+
63
+ let report = check_repository_quality(repo.path(), QualityMode::ChangedFast).unwrap();
64
+
65
+ assert_eq!(report.mode, "changed");
66
+ assert_eq!(report.summary.scanned_files, 1);
67
+ assert_eq!(report.summary.scanned_path_count, 1);
68
+ assert!(report.changed_paths.contains(&"src/changed.js".to_string()));
69
+ }
70
+
71
+ #[test]
72
+ fn path_scoped_quality_scans_only_requested_file_after_write() {
73
+ let repo = quality_repo("quality-path-scoped-focused");
74
+ repo.write_file("src/target.js", "export const before = 1;\n");
75
+ repo.write_file("src/unrelated.js", "export const before = 1;\n");
76
+ repo.commit_all("baseline");
77
+ repo.write_file("src/target.js", &large_file());
78
+ repo.write_file("src/unrelated.js", &large_file());
79
+
80
+ let report = check_repository_quality_paths(repo.path(), &["src/target.js"]).unwrap();
81
+
82
+ assert_eq!(report.mode, "path");
83
+ assert_eq!(report.changed_paths, vec!["src/target.js"]);
84
+ assert_eq!(report.summary.scanned_files, 1);
85
+ assert_eq!(report.summary.scanned_path_count, 1);
86
+ assert!(report
87
+ .violations
88
+ .iter()
89
+ .any(|violation| violation.check_id == "file-length" && violation.path == "src/target.js"));
90
+ assert!(!report
91
+ .violations
92
+ .iter()
93
+ .any(|violation| violation.path == "src/unrelated.js"));
94
+ }
95
+
96
+ #[test]
97
+ fn path_scoped_quality_rejects_paths_outside_repository() {
98
+ let repo = quality_repo("quality-path-scoped-boundary");
99
+ repo.write_file("src/target.js", "export const before = 1;\n");
100
+ repo.commit_all("baseline");
101
+
102
+ let error = check_repository_quality_paths(repo.path(), &["../outside.js"]).unwrap_err();
103
+
104
+ assert!(error.to_string().contains("repository-relative path"));
105
+ }
106
+
107
+ #[test]
108
+ fn semantic_changed_check_scans_only_changed_file_contents() {
109
+ let repo = quality_repo("semantic-changed-fast-focused");
110
+ for index in 0..1000 {
111
+ repo.write_file(
112
+ &format!("src/unchanged_{index}.js"),
113
+ &legacy_fixture("oldConfig"),
114
+ );
115
+ }
116
+ repo.write_file("scripts/changed.test.js", "const value = 1;\n");
117
+ repo.commit_all("large baseline");
118
+ repo.write_file("scripts/changed.test.js", &legacy_fixture("newConfig"));
119
+
120
+ let report = check_semantic_legacy(repo.path(), QualityMode::ChangedFast).unwrap();
121
+
122
+ assert_eq!(report.mode, "changed");
123
+ assert_eq!(report.summary.scanned_files, 1);
124
+ }
125
+
126
+ #[test]
127
+ fn second_report_uses_file_analysis_cache() {
128
+ let repo = quality_repo("quality-cache-second-report");
129
+ repo.write_file("src/a.js", "export const value = 1;\n");
130
+ repo.write_file("src/b.js", "export const other = 2;\n");
131
+ repo.commit_all("baseline");
132
+
133
+ let first = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
134
+ let cache = quality_cache_status(repo.path()).unwrap();
135
+ let second = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
136
+
137
+ assert_eq!(first.summary.scanned_files, 2);
138
+ assert!(cache.entry_count >= 2);
139
+ assert_eq!(second.summary.cache_hits, 2);
140
+ assert_eq!(second.summary.cache_misses, 0);
141
+ }
142
+
143
+ #[test]
144
+ fn report_budget_marks_truncated_reports() {
145
+ let repo = quality_repo("quality-report-budget");
146
+ for index in 0..5005 {
147
+ repo.write_file(&format!("src/file_{index}.js"), "export const value = 1;\n");
148
+ }
149
+ repo.commit_all("large baseline");
150
+
151
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
152
+
153
+ assert!(report.summary.truncated);
154
+ assert!(report
155
+ .summary
156
+ .reason_codes
157
+ .contains(&"max_scanned_files".to_string()));
158
+ assert_eq!(report.summary.scanned_files, 5000);
159
+ }
160
+
161
+ #[test]
162
+ fn report_skips_deep_only_duplicate_checks() {
163
+ let repo = quality_repo("quality-deep-only-duplicates");
164
+ repo.write_file("src/a.js", &duplicate_block("one"));
165
+ repo.write_file("src/b.js", &duplicate_block("two"));
166
+ repo.commit_all("duplicate baseline");
167
+
168
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
169
+ let deep = check_repository_quality(repo.path(), QualityMode::DeepReport).unwrap();
170
+
171
+ assert!(report
172
+ .summary
173
+ .reason_codes
174
+ .contains(&"deep_checks_skipped".to_string()));
175
+ assert!(!report
176
+ .violations
177
+ .iter()
178
+ .any(|violation| violation.check_id == "duplicate-blocks"));
179
+ assert!(deep
180
+ .violations
181
+ .iter()
182
+ .any(|violation| violation.check_id == "duplicate-blocks"));
183
+ }
184
+
185
+ #[test]
186
+ fn changed_fast_compares_changed_files_to_cached_candidates() {
187
+ let repo = quality_repo("quality-changed-cache-duplicates");
188
+ repo.write_file("src/existing.js", &duplicate_block("existing"));
189
+ repo.write_file("src/new.js", "export const value = 1;\n");
190
+ repo.commit_all("baseline");
191
+ let _ = check_repository_quality(repo.path(), QualityMode::DeepReport).unwrap();
192
+
193
+ repo.write_file("src/new.js", &duplicate_block("newCopy"));
194
+ let report = check_repository_quality(repo.path(), QualityMode::ChangedFast).unwrap();
195
+
196
+ assert!(report.summary.cache_hits >= 1);
197
+ assert!(report.violations.iter().any(|violation| {
198
+ violation.check_id == "duplicate-blocks" && violation.path == "src/new.js"
199
+ }));
200
+ }
201
+
202
+ #[test]
203
+ fn changed_fast_compares_changed_files_to_cold_cache_candidates() {
204
+ let repo = quality_repo("quality-changed-cold-cache-duplicates");
205
+ repo.write_file("src/existing.js", &duplicate_block("existing"));
206
+ repo.write_file("src/new.js", "export const value = 1;\n");
207
+ repo.commit_all("baseline");
208
+
209
+ repo.write_file("src/new.js", &duplicate_block("newCopy"));
210
+ let report = check_repository_quality(repo.path(), QualityMode::ChangedFast).unwrap();
211
+
212
+ assert_eq!(report.summary.cache_hits, 0);
213
+ assert!(report.summary.cache_misses >= 2);
214
+ assert_eq!(report.summary.scanned_files, 1);
215
+ assert!(report.violations.iter().any(|violation| {
216
+ violation.check_id == "duplicate-blocks"
217
+ && violation.path == "src/new.js"
218
+ && violation
219
+ .related_paths
220
+ .contains(&"src/existing.js".to_string())
221
+ }));
222
+ }
223
+
224
+ fn quality_repo(name: &str) -> TestRepo {
225
+ let repo = TestRepo::new(name);
226
+ repo.init_git();
227
+ fs::create_dir_all(repo.path().join(".naome")).unwrap();
228
+ repo
229
+ }
230
+
231
+ fn large_file() -> String {
232
+ (0..520)
233
+ .map(|index| format!("export const value{index} = {index};\n"))
234
+ .collect()
235
+ }
236
+
237
+ fn legacy_fixture(name: &str) -> String {
238
+ format!(
239
+ r#"const {name} = {{
240
+ schema: "naome.performance-fixture.v1",
241
+ version: 1,
242
+ status: "ready",
243
+ checks: [],
244
+ changeTypes: [],
245
+ releaseGates: []
246
+ }};
247
+ "#
248
+ )
249
+ }
250
+
251
+ fn duplicate_block(name: &str) -> String {
252
+ format!(
253
+ r#"export function {name}() {{
254
+ const one = 1;
255
+ const two = 2;
256
+ const three = 3;
257
+ const four = 4;
258
+ const five = 5;
259
+ const six = 6;
260
+ const seven = 7;
261
+ const eight = 8;
262
+ const nine = 9;
263
+ const ten = 10;
264
+ return one + two + three + four + five + six + seven + eight + nine + ten;
265
+ }}
266
+ "#
267
+ )
268
+ }
@@ -40,6 +40,45 @@ fn javascript_typescript_adapter_recognizes_package_source_and_tests() {
40
40
  assert_eq!(test.role, "test");
41
41
  }
42
42
 
43
+ #[test]
44
+ fn ios_swift_adapters_recognize_xcode_swiftui_tests_resources_and_generated_code() {
45
+ let repo = StructureFixture::new("structure-ios-swift-adapters");
46
+ repo.write(
47
+ "Package.swift",
48
+ "// swift-tools-version: 5.9\nimport PackageDescription\n",
49
+ );
50
+ repo.write(
51
+ "Demo.xcodeproj/project.pbxproj",
52
+ "// !$*UTF8*$!\n{ archiveVersion = 1; }\n",
53
+ );
54
+ repo.write(
55
+ "Sources/App/ContentView.swift",
56
+ "import SwiftUI\nstruct ContentView: View {}\n",
57
+ );
58
+ repo.write(
59
+ "Tests/AppTests/ContentViewTests.swift",
60
+ "import XCTest\nfinal class ContentViewTests: XCTestCase {}\n",
61
+ );
62
+ repo.write("Resources/Info.plist", "<?xml version=\"1.0\"?>\n");
63
+ repo.write("Resources/Assets.xcassets/Contents.json", "{}\n");
64
+ repo.write("Generated/R.generated.swift", "enum R {}\n");
65
+
66
+ let source =
67
+ explain_repository_structure(repo.path(), "Sources/App/ContentView.swift").unwrap();
68
+ let test =
69
+ explain_repository_structure(repo.path(), "Tests/AppTests/ContentViewTests.swift").unwrap();
70
+ let plist = explain_repository_structure(repo.path(), "Resources/Info.plist").unwrap();
71
+ let generated =
72
+ explain_repository_structure(repo.path(), "Generated/R.generated.swift").unwrap();
73
+
74
+ assert_eq!(source.role, "source");
75
+ assert_eq!(source.module.as_deref(), Some("App"));
76
+ assert_eq!(source.language.as_deref(), Some("swift"));
77
+ assert_eq!(test.role, "test");
78
+ assert_eq!(plist.role, "config");
79
+ assert_eq!(generated.role, "generated");
80
+ }
81
+
43
82
  #[test]
44
83
  fn unknown_repository_gets_generic_roles() {
45
84
  let repo = StructureFixture::new("structure-generic-roles");
@@ -62,6 +62,25 @@ fn quality_report_and_cleanup_route_include_structure_findings() {
62
62
  .any(|violation| violation.check_id == "dumping-ground-directory"));
63
63
  }
64
64
 
65
+ #[test]
66
+ fn cleanup_route_gives_specific_test_pairing_instructions() {
67
+ let repo = StructureFixture::new("structure-cleanup-test-pairing");
68
+ repo.write_js_package();
69
+ repo.write(
70
+ "src/billing/invoice.js",
71
+ "export function invoiceTotal() {\n return 1;\n}\n",
72
+ );
73
+
74
+ let route = route_quality_cleanup(repo.path(), "src/billing/invoice.js").unwrap();
75
+
76
+ assert!(route
77
+ .agent_instructions
78
+ .contains("Create or update a nearby or module-matched test"));
79
+ assert!(route
80
+ .required_checks
81
+ .contains(&"naome quality check --changed".to_string()));
82
+ }
83
+
65
84
  #[test]
66
85
  fn wildcard_module_roots_explain_monorepo_module_names() {
67
86
  let repo = StructureFixture::new("structure-wildcard-module-root");
@@ -13,4 +13,8 @@ pub use routes::{
13
13
  route_commit_request, route_new_task, route_readme_task, try_route_new_task,
14
14
  try_route_readme_task,
15
15
  };
16
- pub use verification_values::{change_type, diff_check, verification_value};
16
+ pub use verification_values::{
17
+ change_type, diff_check, quality_check, repository_quality_config_source,
18
+ repository_quality_config_value, semantic_repository_quality_fixture_source,
19
+ verification_value,
20
+ };
@@ -73,6 +73,35 @@ pub fn verification_value(
73
73
  })
74
74
  }
75
75
 
76
+ pub fn repository_quality_config_value() -> serde_json::Value {
77
+ json!({
78
+ "schema": "naome.repository-quality.v1",
79
+ "version": 1,
80
+ "status": "ready",
81
+ "limits": {
82
+ "maxFileLines": 450,
83
+ "maxNewFileLines": 300,
84
+ "maxDiffAddedLines": 180,
85
+ "maxFunctionLines": 100,
86
+ "maxTopLevelSymbols": 30,
87
+ "duplicateBlockLines": 10,
88
+ "nearDuplicateSimilarity": 0.9
89
+ },
90
+ "enabledAdapters": [],
91
+ "disabledChecks": [],
92
+ "ignoredPaths": [],
93
+ "generatedPaths": [],
94
+ "pathRules": []
95
+ })
96
+ }
97
+
98
+ pub fn repository_quality_config_source() -> String {
99
+ format!(
100
+ "{}\n",
101
+ serde_json::to_string_pretty(&repository_quality_config_value()).unwrap()
102
+ )
103
+ }
104
+
76
105
  pub fn change_type(
77
106
  id: &str,
78
107
  description: &str,
@@ -121,6 +150,32 @@ pub fn repository_quality_check() -> serde_json::Value {
121
150
  })
122
151
  }
123
152
 
153
+ pub fn semantic_repository_quality_fixture_source(name: &str) -> String {
154
+ [
155
+ format!("const {name} = {{"),
156
+ " schema: \"naome.repository-quality.v1\",".to_string(),
157
+ " version: 1,".to_string(),
158
+ " status: \"ready\",".to_string(),
159
+ " limits: {".to_string(),
160
+ " maxFileLines: 450,".to_string(),
161
+ " maxNewFileLines: 300,".to_string(),
162
+ " maxDiffAddedLines: 180,".to_string(),
163
+ " maxFunctionLines: 100,".to_string(),
164
+ " maxTopLevelSymbols: 30,".to_string(),
165
+ " duplicateBlockLines: 10,".to_string(),
166
+ " nearDuplicateSimilarity: 0.9".to_string(),
167
+ " },".to_string(),
168
+ " enabledAdapters: [],".to_string(),
169
+ " disabledChecks: [],".to_string(),
170
+ " ignoredPaths: [],".to_string(),
171
+ " generatedPaths: [],".to_string(),
172
+ " pathRules: []".to_string(),
173
+ "};".to_string(),
174
+ String::new(),
175
+ ]
176
+ .join("\n")
177
+ }
178
+
124
179
  pub fn mutating_diff_check() -> serde_json::Value {
125
180
  json!({
126
181
  "id": "diff-check",