@lamentis/naome 1.1.1 → 1.2.0

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 (126) hide show
  1. package/Cargo.lock +2 -2
  2. package/Cargo.toml +1 -1
  3. package/LICENSE +180 -21
  4. package/README.md +49 -6
  5. package/bin/naome-node.js +44 -4
  6. package/bin/naome.js +54 -16
  7. package/crates/naome-cli/Cargo.toml +1 -1
  8. package/crates/naome-cli/src/check_commands.rs +135 -0
  9. package/crates/naome-cli/src/cli_args.rs +5 -0
  10. package/crates/naome-cli/src/dispatcher.rs +36 -0
  11. package/crates/naome-cli/src/install_bridge.rs +83 -0
  12. package/crates/naome-cli/src/main.rs +57 -341
  13. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  14. package/crates/naome-cli/src/quality_commands.rs +141 -0
  15. package/crates/naome-cli/src/simple_commands.rs +53 -0
  16. package/crates/naome-cli/src/workflow_commands.rs +153 -0
  17. package/crates/naome-core/Cargo.toml +1 -1
  18. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  19. package/crates/naome-core/src/harness_health.rs +14 -126
  20. package/crates/naome-core/src/install_plan.rs +3 -0
  21. package/crates/naome-core/src/intent/classifier.rs +171 -0
  22. package/crates/naome-core/src/intent/envelope.rs +108 -0
  23. package/crates/naome-core/src/intent/legacy.rs +138 -0
  24. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  25. package/crates/naome-core/src/intent/model.rs +71 -0
  26. package/crates/naome-core/src/intent/patterns.rs +170 -0
  27. package/crates/naome-core/src/intent/resolver.rs +162 -0
  28. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  29. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  30. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  31. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  32. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  33. package/crates/naome-core/src/intent/risk.rs +40 -0
  34. package/crates/naome-core/src/intent/segment.rs +170 -0
  35. package/crates/naome-core/src/intent.rs +64 -879
  36. package/crates/naome-core/src/journal.rs +9 -20
  37. package/crates/naome-core/src/lib.rs +13 -0
  38. package/crates/naome-core/src/quality/adapters.rs +178 -0
  39. package/crates/naome-core/src/quality/baseline.rs +75 -0
  40. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  41. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  42. package/crates/naome-core/src/quality/checks.rs +228 -0
  43. package/crates/naome-core/src/quality/cleanup.rs +72 -0
  44. package/crates/naome-core/src/quality/config.rs +109 -0
  45. package/crates/naome-core/src/quality/mod.rs +90 -0
  46. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  47. package/crates/naome-core/src/quality/scanner.rs +367 -0
  48. package/crates/naome-core/src/quality/types.rs +289 -0
  49. package/crates/naome-core/src/route.rs +292 -17
  50. package/crates/naome-core/src/task_state/admission.rs +63 -0
  51. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  52. package/crates/naome-core/src/task_state/api.rs +130 -0
  53. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  54. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  55. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  56. package/crates/naome-core/src/task_state/completion.rs +72 -0
  57. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  58. package/crates/naome-core/src/task_state/diff.rs +95 -0
  59. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  60. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  61. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  62. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  63. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  64. package/crates/naome-core/src/task_state/mod.rs +38 -0
  65. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  66. package/crates/naome-core/src/task_state/progress.rs +123 -0
  67. package/crates/naome-core/src/task_state/proof.rs +139 -0
  68. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  69. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  70. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  71. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  72. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  73. package/crates/naome-core/src/task_state/repair.rs +168 -0
  74. package/crates/naome-core/src/task_state/shape.rs +117 -0
  75. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  76. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  77. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  78. package/crates/naome-core/src/task_state/types.rs +87 -0
  79. package/crates/naome-core/src/task_state/util.rs +137 -0
  80. package/crates/naome-core/src/verification/render.rs +122 -0
  81. package/crates/naome-core/src/verification.rs +176 -58
  82. package/crates/naome-core/src/verification_contract.rs +49 -21
  83. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  84. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  85. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  86. package/crates/naome-core/src/workflow/mod.rs +18 -0
  87. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  88. package/crates/naome-core/src/workflow/output.rs +111 -0
  89. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  90. package/crates/naome-core/src/workflow/phases.rs +169 -0
  91. package/crates/naome-core/src/workflow/policy.rs +156 -0
  92. package/crates/naome-core/src/workflow/processes.rs +91 -0
  93. package/crates/naome-core/src/workflow/types.rs +42 -0
  94. package/crates/naome-core/tests/harness_health.rs +3 -0
  95. package/crates/naome-core/tests/intent.rs +97 -792
  96. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  97. package/crates/naome-core/tests/intent_v2.rs +90 -0
  98. package/crates/naome-core/tests/quality.rs +425 -0
  99. package/crates/naome-core/tests/route.rs +221 -4
  100. package/crates/naome-core/tests/task_state.rs +3 -0
  101. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  102. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  103. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  104. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  105. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  106. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  107. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  108. package/native/darwin-arm64/naome +0 -0
  109. package/native/linux-x64/naome +0 -0
  110. package/package.json +2 -2
  111. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  112. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  113. package/templates/naome-root/.naome/bin/naome.js +34 -63
  114. package/templates/naome-root/.naome/manifest.json +20 -18
  115. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  116. package/templates/naome-root/.naome/repository-quality.json +24 -0
  117. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  118. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  119. package/templates/naome-root/.naome/verification.json +37 -0
  120. package/templates/naome-root/AGENTS.md +3 -0
  121. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  122. package/templates/naome-root/docs/naome/execution.md +25 -21
  123. package/templates/naome-root/docs/naome/index.md +4 -3
  124. package/templates/naome-root/docs/naome/repository-quality.md +43 -0
  125. package/templates/naome-root/docs/naome/testing.md +12 -0
  126. package/crates/naome-core/src/task_state.rs +0 -2210
@@ -0,0 +1,133 @@
1
+ #![allow(dead_code)]
2
+
3
+ use std::fs;
4
+ use std::path::PathBuf;
5
+ use std::process::{Command, Output};
6
+ use std::sync::atomic::{AtomicU64, Ordering};
7
+ use std::time::{SystemTime, UNIX_EPOCH};
8
+
9
+ use naome_core::{evaluate_intent, EvaluationOptions, IntentDecision};
10
+ use serde_json::{json, Value};
11
+
12
+ static REPO_COUNTER: AtomicU64 = AtomicU64::new(0);
13
+
14
+ pub fn env(prompt: &str, workflow: &str, task: &str, risk: &str) -> String {
15
+ format!(
16
+ "```naome-intent-v2\n{{\"schema\":\"naome.intent.v2\",\"workflowAction\":\"{workflow}\",\"taskIntent\":\"{task}\",\"risk\":\"{risk}\"}}\n```\n\n{prompt}"
17
+ )
18
+ }
19
+
20
+ pub fn idle() -> Value {
21
+ json!({ "status": "idle", "activeTask": null })
22
+ }
23
+
24
+ pub fn completed_state(head: &str, valid: bool) -> Value {
25
+ let proof = if valid {
26
+ r#"[{"checkId":"diff-check","command":"git diff --check","cwd":".","exitCode":0,"checkedAt":"2026-05-06T00:00:00.000Z","evidence":["README.md"]}]"#
27
+ } else {
28
+ "[]"
29
+ };
30
+ serde_json::from_str(&format!(r#"{{"schema":"naome.task-state.v1","version":1,"status":"complete","activeTask":{{"id":"readme-task","request":"Change README.","userPrompt":{{"receivedAt":"2026-05-06T00:00:00.000Z","text":"Change README."}},"admission":{{"command":"node .naome/bin/check-task-state.js --admission","cwd":".","exitCode":0,"checkedAt":"2026-05-06T00:00:00.000Z","gitHead":"{head}","changedPaths":[]}},"allowedPaths":["README.md"],"declaredChangeTypes":["product-docs"],"requiredCheckIds":["diff-check"],"proofResults":{proof},"revisions":[],"humanReview":{{"required":false,"approved":false,"reason":null}}}},"blocker":null,"updatedAt":"2026-05-06T00:00:00.000Z"}}"#)).unwrap()
31
+ }
32
+
33
+ pub struct Repo {
34
+ root: PathBuf,
35
+ }
36
+
37
+ impl Repo {
38
+ pub fn clean(name: &str, state: Value) -> Self {
39
+ let repo = Self::new(name);
40
+ repo.git(["init"]);
41
+ repo.git(["config", "user.email", "naome@example.com"]);
42
+ repo.git(["config", "user.name", "NAOME Test"]);
43
+ repo.write("README.md", "# Baseline\n");
44
+ repo.write_base(state);
45
+ repo.git(["add", "."]);
46
+ repo.git(["commit", "-m", "baseline"]);
47
+ repo
48
+ }
49
+
50
+ pub fn completed(name: &str, valid: bool) -> Self {
51
+ let repo = Self::clean(name, idle());
52
+ let head = repo.git_stdout(["rev-parse", "HEAD"]);
53
+ repo.write_base(completed_state(&head, valid));
54
+ repo.git(["add", ".naome/task-state.json"]);
55
+ repo.git(["commit", "-m", "task state"]);
56
+ repo.write("README.md", "# Changed\n");
57
+ repo
58
+ }
59
+
60
+ pub fn intent(&self, prompt: &str) -> IntentDecision {
61
+ evaluate_intent(&self.root, prompt, EvaluationOptions::offline()).unwrap()
62
+ }
63
+
64
+ pub fn write(&self, path: &str, content: &str) {
65
+ let path = self.root.join(path);
66
+ if let Some(parent) = path.parent() {
67
+ fs::create_dir_all(parent).unwrap();
68
+ }
69
+ fs::write(path, content).unwrap();
70
+ }
71
+
72
+ fn new(name: &str) -> Self {
73
+ let nonce = SystemTime::now()
74
+ .duration_since(UNIX_EPOCH)
75
+ .unwrap()
76
+ .as_nanos();
77
+ let root = std::env::temp_dir().join(format!(
78
+ "naome-intent-{name}-{}-{}-{nonce}",
79
+ std::process::id(),
80
+ REPO_COUNTER.fetch_add(1, Ordering::SeqCst)
81
+ ));
82
+ fs::create_dir_all(root.join(".naome")).unwrap();
83
+ Self { root }
84
+ }
85
+
86
+ fn write_base(&self, state: Value) {
87
+ self.write_json(
88
+ "init-state.json",
89
+ json!({ "initialized": true, "intakeStatus": "complete" }),
90
+ );
91
+ self.write_json("upgrade-state.json", json!({ "status": "complete" }));
92
+ self.write_json("verification.json", verification());
93
+ self.write_json("task-state.json", state);
94
+ }
95
+
96
+ fn write_json(&self, name: &str, value: Value) {
97
+ self.write(
98
+ &format!(".naome/{name}"),
99
+ &format!("{}\n", serde_json::to_string_pretty(&value).unwrap()),
100
+ );
101
+ }
102
+
103
+ fn git<const N: usize>(&self, args: [&str; N]) {
104
+ let output = self.git_output(args);
105
+ assert!(
106
+ output.status.success(),
107
+ "git failed: {}",
108
+ String::from_utf8_lossy(&output.stderr)
109
+ );
110
+ }
111
+
112
+ fn git_stdout<const N: usize>(&self, args: [&str; N]) -> String {
113
+ let output = self.git_output(args);
114
+ assert!(output.status.success());
115
+ String::from_utf8(output.stdout).unwrap().trim().to_owned()
116
+ }
117
+
118
+ fn git_output<const N: usize>(&self, args: [&str; N]) -> Output {
119
+ let mut command = Command::new("git");
120
+ command.current_dir(&self.root).args(args);
121
+ command.output().unwrap()
122
+ }
123
+ }
124
+
125
+ impl Drop for Repo {
126
+ fn drop(&mut self) {
127
+ let _ = fs::remove_dir_all(&self.root);
128
+ }
129
+ }
130
+
131
+ fn verification() -> Value {
132
+ serde_json::from_str(r#"{"schema":"naome.verification.v1","version":1,"status":"ready","checks":[{"id":"diff-check","command":"git diff --check","cwd":".","purpose":"Reject whitespace errors.","cost":"fast","source":"test","evidence":["README.md"],"lastVerified":null}],"changeTypes":[],"releaseGates":[]}"#).unwrap()
133
+ }
@@ -0,0 +1,90 @@
1
+ mod intent_support;
2
+
3
+ use intent_support::{env, idle, Repo};
4
+
5
+ #[test]
6
+ fn feature_descriptions_with_workflow_terms_route_as_new_tasks() {
7
+ let repo = Repo::clean("intent-v2-feature-terms", idle());
8
+ for prompt in [
9
+ "Add a context budget dashboard to the product.",
10
+ "Build a commit gate simulator feature for docs.",
11
+ "Implement a review findings feature for pull request analysis.",
12
+ "Create a baseline flow visualization in the UI.",
13
+ ] {
14
+ let intent = repo.intent(prompt);
15
+ assert_eq!(intent.prompt_intent, "new_task", "{prompt}");
16
+ assert_eq!(intent.policy_action, "create_new_task", "{prompt}");
17
+ assert!(!intent.evidence.requests_commit, "{prompt}");
18
+ assert!(!intent.evidence.requests_review, "{prompt}");
19
+ assert!(!intent.evidence.has_risky_terms, "{prompt}");
20
+ }
21
+ }
22
+
23
+ #[test]
24
+ fn enveloped_workflow_requests_keep_their_intent() {
25
+ let repo = Repo::clean("intent-v2-direct-workflow", idle());
26
+ let commit = repo.intent(&env(
27
+ "please commit the current task",
28
+ "commit_request",
29
+ "none",
30
+ "none",
31
+ ));
32
+ assert_eq!(commit.prompt_intent, "commit_request");
33
+ assert!(commit.evidence.requests_commit);
34
+
35
+ let review = repo.intent(&env(
36
+ "review the current diff",
37
+ "review_request",
38
+ "none",
39
+ "none",
40
+ ));
41
+ assert_eq!(review.prompt_intent, "review_request");
42
+ assert!(review.evidence.requests_review);
43
+
44
+ let status = repo.intent(&env("show status", "status_question", "none", "none"));
45
+ assert_eq!(status.prompt_intent, "status_question");
46
+ assert_eq!(status.policy_action, "answer_status_only");
47
+ }
48
+
49
+ #[test]
50
+ fn envelope_is_canonical_over_legacy_prompt_words() {
51
+ let repo = Repo::clean("intent-v2-envelope-canonical", idle());
52
+ let intent = repo.intent(&env(
53
+ "please commit the current task",
54
+ "none",
55
+ "new_task",
56
+ "none",
57
+ ));
58
+ assert_eq!(intent.prompt_intent, "new_task");
59
+ assert!(!intent.evidence.requests_commit);
60
+ }
61
+
62
+ #[test]
63
+ fn token_boundaries_and_code_segments_avoid_false_workflow_actions() {
64
+ let repo = Repo::clean("intent-v2-segments", idle());
65
+ let async_intent = repo.intent("Add async sync-state naming examples to the documentation.");
66
+ assert_eq!(async_intent.prompt_intent, "new_task");
67
+ assert!(!async_intent.evidence.requests_repair);
68
+
69
+ let code_intent =
70
+ repo.intent("Add docs with examples like `naome commit` and ```sh\ngit push\n```.");
71
+ assert_eq!(code_intent.prompt_intent, "new_task");
72
+ assert!(!code_intent.evidence.requests_commit);
73
+ assert!(!code_intent.evidence.has_risky_terms);
74
+ }
75
+
76
+ #[test]
77
+ fn explicit_credential_context_still_routes_as_unsafe() {
78
+ let repo = Repo::clean("intent-v2-risk", idle());
79
+ let intent = repo.intent(&env(
80
+ "Commit the current token and API key so the deployment can use the secret.",
81
+ "commit_request",
82
+ "none",
83
+ "credential_context",
84
+ ));
85
+ assert_eq!(intent.prompt_intent, "unsafe");
86
+ assert!(intent.evidence.has_risky_terms);
87
+ assert!(intent
88
+ .risk_codes
89
+ .contains(&"prompt_contains_risky_terms".to_string()));
90
+ }
@@ -0,0 +1,425 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+ use std::sync::atomic::{AtomicU64, Ordering};
5
+
6
+ use naome_core::{
7
+ check_repository_quality, init_repository_quality, plan_quality_cleanup, route_quality_cleanup,
8
+ seed_builtin_verification_checks, QualityMode,
9
+ };
10
+ use serde_json::{json, Value};
11
+
12
+ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
13
+
14
+ #[test]
15
+ fn changed_check_blocks_touched_legacy_file_until_it_conforms() {
16
+ let repo = QualityFixture::new("quality-touched-legacy");
17
+ repo.write_quality_config(5, 40, 4);
18
+ repo.write(
19
+ "src/large.js",
20
+ "export function legacy() {\n one();\n two();\n three();\n four();\n}\n",
21
+ );
22
+ repo.commit_all("legacy baseline");
23
+
24
+ repo.write(
25
+ "src/large.js",
26
+ "export function legacy() {\n one();\n two();\n three();\n four();\n five();\n}\n",
27
+ );
28
+
29
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
30
+
31
+ assert!(!report.ok);
32
+ assert!(report
33
+ .violations
34
+ .iter()
35
+ .any(|violation| violation.path == "src/large.js" && violation.check_id == "file-length"));
36
+ }
37
+
38
+ #[test]
39
+ fn whole_repo_debt_is_visible_without_blocking_clean_changed_check() {
40
+ let repo = QualityFixture::new("quality-clean-legacy-report");
41
+ repo.write_quality_config(5, 40, 4);
42
+ repo.write(
43
+ "src/large.js",
44
+ "export function legacy() {\n one();\n two();\n three();\n four();\n}\n",
45
+ );
46
+ repo.commit_all("legacy baseline");
47
+
48
+ let changed = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
49
+ let full = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
50
+
51
+ assert!(changed.ok);
52
+ assert!(changed.violations.is_empty());
53
+ assert!(!full.ok);
54
+ assert!(full
55
+ .violations
56
+ .iter()
57
+ .any(|violation| violation.path == "src/large.js" && violation.check_id == "file-length"));
58
+ }
59
+
60
+ #[test]
61
+ fn changed_check_blocks_new_duplicate_blocks_against_existing_code() {
62
+ let repo = QualityFixture::new("quality-duplicate-block");
63
+ repo.write_quality_config(200, 40, 4);
64
+ repo.write(
65
+ "src/existing.js",
66
+ "export function existing() {\n const alpha = input.alpha;\n const beta = input.beta;\n const total = alpha + beta;\n return total;\n}\n",
67
+ );
68
+ repo.commit_all("existing helper");
69
+
70
+ repo.write(
71
+ "src/copied.js",
72
+ "export function copied() {\n const alpha = input.alpha;\n const beta = input.beta;\n const total = alpha + beta;\n return total;\n}\n",
73
+ );
74
+
75
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
76
+
77
+ assert!(!report.ok);
78
+ assert!(report.violations.iter().any(|violation| {
79
+ violation.path == "src/copied.js"
80
+ && violation.check_id == "duplicate-blocks"
81
+ && violation
82
+ .related_paths
83
+ .contains(&"src/existing.js".to_string())
84
+ }));
85
+ }
86
+
87
+ #[test]
88
+ fn duplicate_blocks_report_one_finding_per_overlapping_region() {
89
+ let repo = QualityFixture::new("quality-duplicate-region");
90
+ repo.write_quality_config(200, 80, 4);
91
+ repo.write(
92
+ "src/existing.js",
93
+ "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",
94
+ );
95
+ repo.commit_all("existing helper");
96
+
97
+ repo.write(
98
+ "src/copied.js",
99
+ "export function copied() {\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",
100
+ );
101
+
102
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
103
+ let duplicate_regions = report
104
+ .violations
105
+ .iter()
106
+ .filter(|violation| {
107
+ violation.path == "src/copied.js" && violation.check_id == "duplicate-blocks"
108
+ })
109
+ .count();
110
+
111
+ assert_eq!(duplicate_regions, 1);
112
+ }
113
+
114
+ #[test]
115
+ fn duplicate_blocks_detect_repeated_regions_in_the_same_file() {
116
+ let repo = QualityFixture::new("quality-same-file-duplicate-region");
117
+ repo.write_quality_config(200, 80, 4);
118
+ repo.commit_all("empty baseline");
119
+
120
+ repo.write(
121
+ "src/repeated.js",
122
+ concat!(
123
+ "export function repeated() {\n",
124
+ " const alpha = input.alpha;\n",
125
+ " const beta = input.beta;\n",
126
+ " const total = alpha + beta;\n",
127
+ " save(total);\n",
128
+ " audit(total);\n",
129
+ " const alpha = input.alpha;\n",
130
+ " const beta = input.beta;\n",
131
+ " const total = alpha + beta;\n",
132
+ " save(total);\n",
133
+ " return total;\n",
134
+ "}\n"
135
+ ),
136
+ );
137
+
138
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
139
+
140
+ assert!(!report.ok);
141
+ assert!(report.violations.iter().any(|violation| {
142
+ violation.path == "src/repeated.js"
143
+ && violation.check_id == "duplicate-blocks"
144
+ && violation
145
+ .related_paths
146
+ .contains(&"src/repeated.js".to_string())
147
+ }));
148
+ }
149
+
150
+ #[test]
151
+ fn duplicate_blocks_ignore_generated_sha256_maps() {
152
+ let repo = QualityFixture::new("quality-generated-hash-map");
153
+ repo.write_quality_config(200, 80, 4);
154
+ let generated_map = concat!(
155
+ "const expected = Object.freeze({\n",
156
+ " \"one.js\": \"sha256:1111111111111111111111111111111111111111111111111111111111111111\",\n",
157
+ " \"two.js\": \"sha256:2222222222222222222222222222222222222222222222222222222222222222\",\n",
158
+ " \"three.js\": \"sha256:3333333333333333333333333333333333333333333333333333333333333333\",\n",
159
+ " \"four.js\": \"sha256:4444444444444444444444444444444444444444444444444444444444444444\",\n",
160
+ " \"five.js\": \"sha256:5555555555555555555555555555555555555555555555555555555555555555\"\n",
161
+ "});\n"
162
+ );
163
+ repo.write("src/existing.js", generated_map);
164
+ repo.commit_all("generated hash baseline");
165
+
166
+ repo.write("src/copied.js", generated_map);
167
+
168
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
169
+
170
+ assert!(report.ok);
171
+ }
172
+
173
+ #[test]
174
+ fn near_duplicate_functions_do_not_compare_containers_with_child_symbols() {
175
+ let repo = QualityFixture::new("quality-container-child-similarity");
176
+ repo.write_quality_config(200, 120, 10);
177
+ repo.write(
178
+ "src/lib.rs",
179
+ "pub struct Runner;\n\nimpl Runner {\n pub fn execute(&self) {\n alpha();\n beta();\n gamma();\n delta();\n epsilon();\n zeta();\n eta();\n theta();\n iota();\n kappa();\n lambda();\n mu();\n nu();\n xi();\n omicron();\n pi();\n }\n}\n",
180
+ );
181
+
182
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
183
+
184
+ assert!(!report.violations.iter().any(|violation| {
185
+ violation.path == "src/lib.rs" && violation.check_id == "near-duplicate-functions"
186
+ }));
187
+ }
188
+
189
+ #[test]
190
+ fn init_writes_default_config_and_baselines_existing_debt() {
191
+ let repo = QualityFixture::new("quality-init-baseline");
192
+ let large_file = (0..520)
193
+ .map(|index| format!("export const value{index} = {index};\n"))
194
+ .collect::<String>();
195
+ repo.write("src/large.js", &large_file);
196
+ repo.commit_all("legacy baseline");
197
+
198
+ let init = init_repository_quality(repo.path()).unwrap();
199
+
200
+ assert!(init.config_written);
201
+ assert!(init.baseline_written);
202
+ assert!(repo.path().join(".naome/repository-quality.json").is_file());
203
+ assert!(repo
204
+ .path()
205
+ .join(".naome/repository-quality-baseline.json")
206
+ .is_file());
207
+ assert!(init.baseline_violations > 0);
208
+ }
209
+
210
+ #[test]
211
+ fn init_generates_adapter_selection_without_product_specific_path_rules() {
212
+ let repo = QualityFixture::new("quality-adapter-selected-config");
213
+ repo.write("Cargo.toml", "[workspace]\nmembers = []\n");
214
+ repo.write("package.json", "{\"scripts\":{\"test\":\"node --test\"}}\n");
215
+ repo.write(
216
+ "packages/naome/templates/naome-root/.naome/bin/naome.js",
217
+ "#!/usr/bin/env node\nconsole.log('template');\n",
218
+ );
219
+ repo.commit_all("repo profile");
220
+
221
+ let init = init_repository_quality(repo.path()).unwrap();
222
+ let config: Value = serde_json::from_str(
223
+ &fs::read_to_string(repo.path().join(".naome/repository-quality.json")).unwrap(),
224
+ )
225
+ .unwrap();
226
+ let enabled_adapters = config
227
+ .get("enabledAdapters")
228
+ .and_then(Value::as_array)
229
+ .expect("enabledAdapters should be generated");
230
+ let adapter_ids = enabled_adapters
231
+ .iter()
232
+ .filter_map(Value::as_str)
233
+ .collect::<Vec<_>>();
234
+ let serialized_config = fs::read_to_string(repo.path().join(".naome/repository-quality.json"))
235
+ .expect("config should be readable");
236
+
237
+ assert!(init.config_written);
238
+ assert_eq!(adapter_ids, vec!["rust", "javascript-typescript"]);
239
+ assert!(!serialized_config.contains("packages/naome/"));
240
+ assert!(!serialized_config.contains("naome-template-harness"));
241
+ }
242
+
243
+ #[test]
244
+ fn enabled_adapters_apply_their_rules_without_materialized_path_rules() {
245
+ let repo = QualityFixture::new("quality-adapter-runtime-config");
246
+ repo.write_quality_config_with_adapters(8, 80, 4, &["javascript-typescript"]);
247
+ repo.write(
248
+ "scripts/example.test.js",
249
+ &(0..20)
250
+ .map(|index| format!("export const value{index} = {index};\n"))
251
+ .collect::<String>(),
252
+ );
253
+
254
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
255
+
256
+ assert!(!report.violations.iter().any(|violation| {
257
+ violation.path == "scripts/example.test.js" && violation.check_id == "file-length"
258
+ }));
259
+ }
260
+
261
+ #[test]
262
+ fn seeding_builtin_verification_wires_repository_quality_into_change_types() {
263
+ let repo = QualityFixture::new("quality-verification-wiring");
264
+ repo.write(
265
+ ".naome/verification.json",
266
+ concat!(
267
+ "{\"schema\":\"naome.verification.v1\",\"version\":1,\"status\":\"ready\",",
268
+ "\"checks\":[{\"id\":\"diff-check\",\"command\":\"git diff --check\",",
269
+ "\"cwd\":\".\",\"purpose\":\"Reject whitespace errors.\",\"cost\":\"fast\",",
270
+ "\"source\":\"repository\",\"evidence\":[],\"lastVerified\":null}],",
271
+ "\"changeTypes\":[{\"id\":\"source\",\"description\":\"Source changes.\",",
272
+ "\"paths\":[\"src/**\"],\"requiredChecks\":[\"diff-check\"],",
273
+ "\"recommendedChecks\":[],\"humanReview\":false}],\"releaseGates\":[]}\n"
274
+ ),
275
+ );
276
+
277
+ let changed = seed_builtin_verification_checks(repo.path()).unwrap();
278
+ let verification: Value = serde_json::from_str(
279
+ &fs::read_to_string(repo.path().join(".naome/verification.json")).unwrap(),
280
+ )
281
+ .unwrap();
282
+ let source_change_type = verification
283
+ .get("changeTypes")
284
+ .and_then(Value::as_array)
285
+ .unwrap()
286
+ .iter()
287
+ .find(|change_type| change_type.get("id").and_then(Value::as_str) == Some("source"))
288
+ .unwrap();
289
+ let required_checks = source_change_type
290
+ .get("requiredChecks")
291
+ .and_then(Value::as_array)
292
+ .unwrap();
293
+
294
+ assert!(changed);
295
+ assert!(required_checks
296
+ .iter()
297
+ .any(|check| check.as_str() == Some("repository-quality-check")));
298
+ }
299
+
300
+ #[test]
301
+ fn cleanup_plan_and_route_turn_debt_into_agent_work() {
302
+ let repo = QualityFixture::new("quality-cleanup-plan");
303
+ repo.write_quality_config(5, 40, 4);
304
+ repo.write(
305
+ "src/large.js",
306
+ "export function legacy() {\n one();\n two();\n three();\n four();\n}\n",
307
+ );
308
+ repo.commit_all("legacy baseline");
309
+
310
+ let plan = plan_quality_cleanup(repo.path()).unwrap();
311
+ let route = route_quality_cleanup(repo.path(), "src/large.js").unwrap();
312
+
313
+ assert_eq!(plan.tasks[0].path, "src/large.js");
314
+ assert!(plan.tasks[0].violation_count >= 1);
315
+ assert_eq!(route.path, "src/large.js");
316
+ assert!(route.agent_instructions.contains("Reduce or split"));
317
+ assert!(route
318
+ .required_checks
319
+ .contains(&"naome quality check --changed".to_string()));
320
+ }
321
+
322
+ struct QualityFixture {
323
+ root: PathBuf,
324
+ }
325
+
326
+ impl QualityFixture {
327
+ fn new(name: &str) -> Self {
328
+ let root = std::env::temp_dir().join(format!(
329
+ "naome-{name}-{}-{}",
330
+ std::process::id(),
331
+ FIXTURE_COUNTER.fetch_add(1, Ordering::SeqCst)
332
+ ));
333
+ let _ = fs::remove_dir_all(&root);
334
+ fs::create_dir_all(root.join(".naome")).unwrap();
335
+ fs::write(root.join(".naomeignore"), ".naome/archive/\n").unwrap();
336
+ git(&root, &["init"]);
337
+ git(&root, &["config", "user.email", "test@example.com"]);
338
+ git(&root, &["config", "user.name", "Test User"]);
339
+ Self { root }
340
+ }
341
+
342
+ fn path(&self) -> &Path {
343
+ &self.root
344
+ }
345
+
346
+ fn write(&self, relative_path: &str, content: &str) {
347
+ let path = self.root.join(relative_path);
348
+ if let Some(parent) = path.parent() {
349
+ fs::create_dir_all(parent).unwrap();
350
+ }
351
+ fs::write(path, content).unwrap();
352
+ }
353
+
354
+ fn write_quality_config(
355
+ &self,
356
+ max_file_lines: usize,
357
+ max_function_lines: usize,
358
+ duplicate_block_lines: usize,
359
+ ) {
360
+ self.write_quality_config_with_adapters(
361
+ max_file_lines,
362
+ max_function_lines,
363
+ duplicate_block_lines,
364
+ &[],
365
+ );
366
+ }
367
+
368
+ fn write_quality_config_with_adapters(
369
+ &self,
370
+ max_file_lines: usize,
371
+ max_function_lines: usize,
372
+ duplicate_block_lines: usize,
373
+ enabled_adapters: &[&str],
374
+ ) {
375
+ let config = json!({
376
+ "schema": "naome.repository-quality.v1",
377
+ "version": 1,
378
+ "status": "ready",
379
+ "limits": {
380
+ "maxFileLines": max_file_lines,
381
+ "maxNewFileLines": max_file_lines,
382
+ "maxDiffAddedLines": 200,
383
+ "maxFunctionLines": max_function_lines,
384
+ "maxTopLevelSymbols": 30,
385
+ "duplicateBlockLines": duplicate_block_lines,
386
+ "nearDuplicateSimilarity": 0.9
387
+ },
388
+ "enabledAdapters": enabled_adapters,
389
+ "disabledChecks": [],
390
+ "ignoredPaths": [],
391
+ "generatedPaths": [],
392
+ "pathRules": []
393
+ });
394
+ self.write(
395
+ ".naome/repository-quality.json",
396
+ &format!("{}\n", serde_json::to_string_pretty(&config).unwrap()),
397
+ );
398
+ }
399
+
400
+ fn commit_all(&self, message: &str) {
401
+ git(&self.root, &["add", "."]);
402
+ git(&self.root, &["commit", "-m", message]);
403
+ }
404
+ }
405
+
406
+ impl Drop for QualityFixture {
407
+ fn drop(&mut self) {
408
+ let _ = fs::remove_dir_all(&self.root);
409
+ }
410
+ }
411
+
412
+ fn git(root: &Path, args: &[&str]) {
413
+ let output = Command::new("git")
414
+ .args(args)
415
+ .current_dir(root)
416
+ .output()
417
+ .unwrap();
418
+ assert!(
419
+ output.status.success(),
420
+ "git {} failed\nstdout: {}\nstderr: {}",
421
+ args.join(" "),
422
+ String::from_utf8_lossy(&output.stdout),
423
+ String::from_utf8_lossy(&output.stderr)
424
+ );
425
+ }