@lamentis/naome 1.3.16 → 1.4.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 (40) hide show
  1. package/Cargo.lock +2 -2
  2. package/crates/naome-cli/Cargo.toml +1 -1
  3. package/crates/naome-cli/src/architecture_commands.rs +16 -2
  4. package/crates/naome-cli/src/architecture_init/infer.rs +131 -0
  5. package/crates/naome-cli/src/architecture_init/render.rs +56 -0
  6. package/crates/naome-cli/src/architecture_init/repository.rs +59 -0
  7. package/crates/naome-cli/src/architecture_init.rs +17 -0
  8. package/crates/naome-cli/src/main.rs +2 -1
  9. package/crates/naome-cli/tests/architecture_cli.rs +75 -0
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/architecture/config_findings/configuration/coverage.rs +81 -0
  12. package/crates/naome-core/src/architecture/config_findings/configuration/overlap.rs +117 -0
  13. package/crates/naome-core/src/architecture/config_findings/configuration.rs +12 -0
  14. package/crates/naome-core/src/architecture/config_findings/imports.rs +30 -0
  15. package/crates/naome-core/src/architecture/config_findings.rs +50 -0
  16. package/crates/naome-core/src/architecture/explain.rs +45 -0
  17. package/crates/naome-core/src/architecture/output.rs +211 -155
  18. package/crates/naome-core/src/architecture/rules.rs +4 -3
  19. package/crates/naome-core/src/architecture/scan/cache.rs +1 -1
  20. package/crates/naome-core/src/architecture/scan/imports/resolver/candidates.rs +71 -0
  21. package/crates/naome-core/src/architecture/scan/imports/resolver/js_ts_alias.rs +241 -0
  22. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +162 -91
  23. package/crates/naome-core/src/architecture/scan.rs +20 -6
  24. package/crates/naome-core/src/architecture.rs +8 -3
  25. package/crates/naome-core/src/lib.rs +9 -7
  26. package/crates/naome-core/tests/architecture.rs +30 -0
  27. package/crates/naome-core/tests/architecture_acceptance.rs +304 -0
  28. package/crates/naome-core/tests/architecture_aliases.rs +101 -0
  29. package/crates/naome-core/tests/architecture_cache.rs +57 -0
  30. package/crates/naome-core/tests/architecture_config.rs +155 -1
  31. package/crates/naome-core/tests/architecture_rules.rs +32 -0
  32. package/crates/naome-core/tests/architecture_unresolved.rs +36 -0
  33. package/native/darwin-arm64/naome +0 -0
  34. package/native/linux-x64/naome +0 -0
  35. package/package.json +1 -1
  36. package/templates/naome-root/.naome/bin/check-harness-health.js +1 -0
  37. package/templates/naome-root/.naome/bin/check-task-state.js +1 -0
  38. package/templates/naome-root/.naome/manifest.json +2 -2
  39. package/templates/naome-root/.naome/verification.json +6 -1
  40. package/templates/naome-root/docs/naome/architecture-fitness.md +76 -59
@@ -0,0 +1,304 @@
1
+ use naome_core::{
2
+ scan_architecture, validate_architecture, ArchitectureScanOptions, ArchitectureScanReport,
3
+ ArchitectureValidation,
4
+ };
5
+
6
+ mod architecture_support;
7
+
8
+ use architecture_support::{has_import_edge, FixtureRepo};
9
+
10
+ const LAYER_POLICY: &str = r#"
11
+ layers:
12
+ ui:
13
+ paths:
14
+ - "src/ui/**"
15
+ - "Sources/App/**"
16
+ application:
17
+ paths:
18
+ - "src/application/**"
19
+ - "cmd/**"
20
+ domain:
21
+ paths:
22
+ - "src/domain/**"
23
+ - "src/billing/domain/**"
24
+ - "crates/**/src/domain/**"
25
+ - "Sources/Core/**"
26
+ - "internal/domain/**"
27
+ infrastructure:
28
+ paths:
29
+ - "src/infrastructure/**"
30
+ - "src/billing/infrastructure/**"
31
+ - "crates/**/src/infrastructure/**"
32
+ - "Sources/Adapters/**"
33
+ - "internal/infrastructure/**"
34
+ allowed_dependencies:
35
+ ui:
36
+ - application
37
+ - domain
38
+ application:
39
+ - domain
40
+ - infrastructure
41
+ infrastructure:
42
+ - domain
43
+ domain:
44
+ rules:
45
+ no_forbidden_layer_dependencies:
46
+ enabled: true
47
+ severity: error
48
+ external_dependency_policy:
49
+ enabled: true
50
+ severity: error
51
+ external_dependencies:
52
+ domain:
53
+ allow: []
54
+ infrastructure:
55
+ allow:
56
+ - "stripe"
57
+ - "Alamofire"
58
+ ignore:
59
+ - path: "generated/**"
60
+ reason: "Generated code is not architecture-owned."
61
+ "#;
62
+
63
+ #[test]
64
+ fn typescript_rust_go_and_swift_fixture_repos_detect_layer_violations() {
65
+ let repo = FixtureRepo::new();
66
+ write_files(
67
+ &repo,
68
+ &[
69
+ ("naome.arch.yaml", LAYER_POLICY),
70
+ (
71
+ "src/domain/order.ts",
72
+ "import { connect } from '../infrastructure/sql';\n",
73
+ ),
74
+ ("src/infrastructure/sql.ts", "export const connect = 1;\n"),
75
+ (
76
+ "crates/core/src/domain/lib.rs",
77
+ "use crate::infrastructure::db::Pool;\n",
78
+ ),
79
+ ("crates/core/src/infrastructure/db.rs", "pub struct Pool;\n"),
80
+ ("go.mod", "module example.com/acme\n"),
81
+ (
82
+ "internal/domain/event.go",
83
+ "package domain\nimport \"example.com/acme/internal/infrastructure/db\"\n",
84
+ ),
85
+ ("internal/infrastructure/db/db.go", "package db\n"),
86
+ (
87
+ "Sources/Core/Event.swift",
88
+ "import Foundation\nimport Adapters\n",
89
+ ),
90
+ (
91
+ "Sources/Adapters/Adapters.swift",
92
+ "public struct Adapter {}\n",
93
+ ),
94
+ ],
95
+ );
96
+
97
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
98
+ let violation_paths = report
99
+ .violations
100
+ .iter()
101
+ .filter(|violation| violation.id == "arch.no_forbidden_layer_dependencies")
102
+ .filter_map(|violation| violation.path.as_deref())
103
+ .collect::<Vec<_>>();
104
+
105
+ assert!(violation_paths.contains(&"src/domain/order.ts"));
106
+ assert!(violation_paths.contains(&"crates/core/src/domain/lib.rs"));
107
+ assert!(violation_paths.contains(&"internal/domain/event.go"));
108
+ assert!(violation_paths.contains(&"Sources/Core/Event.swift"));
109
+ }
110
+
111
+ #[test]
112
+ fn python_package_imports_resolve_to_local_files_before_external_policy_runs() {
113
+ let repo = FixtureRepo::new();
114
+ write_files(
115
+ &repo,
116
+ &[
117
+ ("naome.arch.yaml", LAYER_POLICY),
118
+ ("src/billing/__init__.py", ""),
119
+ ("src/billing/domain/__init__.py", ""),
120
+ ("src/billing/infrastructure/__init__.py", ""),
121
+ (
122
+ "src/billing/domain/event.py",
123
+ "from billing.infrastructure.db import Session\n",
124
+ ),
125
+ (
126
+ "src/billing/infrastructure/db.py",
127
+ "class Session:\n pass\n",
128
+ ),
129
+ ],
130
+ );
131
+
132
+ let (report, scan) = validate_and_scan(&repo);
133
+
134
+ assert!(has_import_edge(
135
+ &scan,
136
+ "file:src/billing/domain/event.py",
137
+ "file:src/billing/infrastructure/db.py"
138
+ ));
139
+ assert!(report.violations.iter().any(|violation| {
140
+ violation.id == "arch.no_forbidden_layer_dependencies"
141
+ && violation.path.as_deref() == Some("src/billing/domain/event.py")
142
+ }));
143
+ assert!(!report.violations.iter().any(|violation| {
144
+ violation.id == "arch.external_dependency_policy"
145
+ && violation.to.as_deref() == Some("external:billing")
146
+ }));
147
+ }
148
+
149
+ #[test]
150
+ fn python_package_imports_prefer_importing_root_and_keep_missing_modules_unresolved() {
151
+ let repo = FixtureRepo::new();
152
+ write_files(
153
+ &repo,
154
+ &[
155
+ ("naome.arch.yaml", LAYER_POLICY),
156
+ ("services/a/src/billing/__init__.py", ""),
157
+ ("services/a/src/billing/infrastructure/__init__.py", ""),
158
+ ("services/a/src/billing/infrastructure/db.py", "A = 1\n"),
159
+ ("services/b/src/billing/__init__.py", ""),
160
+ ("services/b/src/billing/domain/__init__.py", ""),
161
+ ("services/b/src/billing/infrastructure/__init__.py", ""),
162
+ ("services/b/src/billing/infrastructure/db.py", "B = 1\n"),
163
+ (
164
+ "services/b/src/billing/domain/event.py",
165
+ "from billing.infrastructure.db import B\nfrom billing.infrastructure.missing import Thing\n",
166
+ ),
167
+ ],
168
+ );
169
+
170
+ let (report, scan) = validate_and_scan(&repo);
171
+
172
+ assert!(has_import_edge(
173
+ &scan,
174
+ "file:services/b/src/billing/domain/event.py",
175
+ "file:services/b/src/billing/infrastructure/db.py"
176
+ ));
177
+ assert!(!has_import_edge(
178
+ &scan,
179
+ "file:services/b/src/billing/domain/event.py",
180
+ "file:services/a/src/billing/infrastructure/db.py"
181
+ ));
182
+ assert!(report.config_findings.iter().any(|finding| {
183
+ finding.id == "arch.import.unresolved"
184
+ && finding.subject == "file:services/b/src/billing/domain/event.py"
185
+ && finding.message.contains("billing.infrastructure.missing")
186
+ }));
187
+ }
188
+
189
+ #[test]
190
+ fn python_from_package_missing_child_does_not_resolve_to_package_init() {
191
+ assert_missing_child_import_stays_unresolved(
192
+ "from billing.infrastructure import missing\n",
193
+ "billing.infrastructure.missing",
194
+ );
195
+ }
196
+
197
+ #[test]
198
+ fn python_relative_missing_child_does_not_resolve_to_package_init() {
199
+ assert_missing_child_import_stays_unresolved(
200
+ "from ..infrastructure import missing\n",
201
+ "..infrastructure.missing",
202
+ );
203
+ }
204
+
205
+ fn assert_missing_child_import_stays_unresolved(import_line: &str, expected_specifier: &str) {
206
+ let repo = FixtureRepo::new();
207
+ write_files(
208
+ &repo,
209
+ &[
210
+ ("naome.arch.yaml", LAYER_POLICY),
211
+ ("src/billing/__init__.py", ""),
212
+ ("src/billing/domain/__init__.py", ""),
213
+ ("src/billing/infrastructure/__init__.py", ""),
214
+ ("src/billing/domain/event.py", import_line),
215
+ ],
216
+ );
217
+
218
+ let (report, scan) = validate_and_scan(&repo);
219
+
220
+ assert!(!has_import_edge(
221
+ &scan,
222
+ "file:src/billing/domain/event.py",
223
+ "file:src/billing/infrastructure/__init__.py"
224
+ ));
225
+ assert!(report.config_findings.iter().any(|finding| {
226
+ finding.id == "arch.import.unresolved"
227
+ && finding.subject == "file:src/billing/domain/event.py"
228
+ && finding.message.contains(expected_specifier)
229
+ }));
230
+ }
231
+
232
+ #[test]
233
+ fn python_top_level_package_imports_resolve_to_local_package_files() {
234
+ let repo = FixtureRepo::new();
235
+ repo.write("naome.arch.yaml", LAYER_POLICY);
236
+ repo.write("src/domain/event.py", "import infrastructure\n");
237
+ repo.write("src/infrastructure/__init__.py", "client = object()\n");
238
+
239
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
240
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
241
+
242
+ assert!(has_import_edge(
243
+ &scan,
244
+ "file:src/domain/event.py",
245
+ "file:src/infrastructure/__init__.py"
246
+ ));
247
+ assert!(report.violations.iter().any(|violation| {
248
+ violation.id == "arch.no_forbidden_layer_dependencies"
249
+ && violation.path.as_deref() == Some("src/domain/event.py")
250
+ }));
251
+ assert!(!report.violations.iter().any(|violation| {
252
+ violation.id == "arch.external_dependency_policy"
253
+ && violation.to.as_deref() == Some("external:infrastructure")
254
+ }));
255
+ }
256
+
257
+ #[test]
258
+ fn mixed_monorepo_acceptance_keeps_local_edges_and_generated_code_out_of_findings() {
259
+ let repo = FixtureRepo::new();
260
+ write_files(
261
+ &repo,
262
+ &[
263
+ ("naome.arch.yaml", LAYER_POLICY),
264
+ (
265
+ "tsconfig.json",
266
+ r#"{"compilerOptions":{"baseUrl":".","paths":{"@domain/*":["src/domain/*"]}}}"#,
267
+ ),
268
+ (
269
+ "src/ui/View.tsx",
270
+ "import { event } from '@domain/event';\n",
271
+ ),
272
+ ("src/domain/event.ts", "export const event = 1;\n"),
273
+ ("generated/client.ts", "import Stripe from 'stripe';\n"),
274
+ (
275
+ "Package.swift",
276
+ "import PackageDescription\nlet package = Package(name: \"App\")\n",
277
+ ),
278
+ ],
279
+ );
280
+
281
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
282
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
283
+
284
+ assert!(has_import_edge(
285
+ &scan,
286
+ "file:src/ui/View.tsx",
287
+ "file:src/domain/event.ts"
288
+ ));
289
+ assert!(!scan.file_facts.contains_key("generated/client.ts"));
290
+ assert_eq!(report.summary.errors, 0, "{:?}", report.violations);
291
+ }
292
+
293
+ fn write_files(repo: &FixtureRepo, files: &[(&str, &str)]) {
294
+ for (path, content) in files {
295
+ repo.write(path, content);
296
+ }
297
+ }
298
+
299
+ fn validate_and_scan(repo: &FixtureRepo) -> (ArchitectureValidation, ArchitectureScanReport) {
300
+ (
301
+ validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap(),
302
+ scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap(),
303
+ )
304
+ }
@@ -0,0 +1,101 @@
1
+ use naome_core::{validate_architecture, ArchitectureScanOptions};
2
+
3
+ mod architecture_support;
4
+
5
+ use architecture_support::FixtureRepo;
6
+
7
+ #[test]
8
+ fn typescript_path_aliases_resolve_to_repository_files() {
9
+ let repo = FixtureRepo::new();
10
+ repo.write("naome.arch.yaml", &alias_fixture_config());
11
+ repo.write(
12
+ "tsconfig.json",
13
+ r#"{
14
+ "compilerOptions": {
15
+ "baseUrl": ".",
16
+ "paths": {
17
+ "@infra/*": ["src/infrastructure/*"]
18
+ }
19
+ }
20
+ }
21
+ "#,
22
+ );
23
+ repo.write("src/domain/event.ts", "import { db } from '@infra/db';\n");
24
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
25
+
26
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
27
+
28
+ assert_eq!(report.status, "fail");
29
+ assert!(report.violations.iter().any(|violation| {
30
+ violation.id == "arch.no_forbidden_layer_dependencies"
31
+ && violation.path.as_deref() == Some("src/domain/event.ts")
32
+ }));
33
+ }
34
+
35
+ #[test]
36
+ fn typescript_aliases_read_jsonc_parent_configs_and_report_missing_targets() {
37
+ let repo = FixtureRepo::new();
38
+ repo.write("naome.arch.yaml", &alias_fixture_config());
39
+ repo.write(
40
+ "tsconfig.base.json",
41
+ r#"{
42
+ // workspace aliases are JSONC in normal TypeScript projects
43
+ "compilerOptions": {
44
+ "baseUrl": ".",
45
+ "paths": {
46
+ "*": ["types/*"],
47
+ "@infra/*": ["src/infrastructure/*"],
48
+ },
49
+ },
50
+ }
51
+ "#,
52
+ );
53
+ repo.write(
54
+ "packages/app/tsconfig.json",
55
+ r#"{
56
+ "extends": "../../tsconfig.base.json"
57
+ }
58
+ "#,
59
+ );
60
+ repo.write(
61
+ "packages/app/src/domain/event.ts",
62
+ "import { db } from '@infra/db';\nimport { missing } from '@infra/missing';\n",
63
+ );
64
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
65
+
66
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
67
+
68
+ assert!(report.violations.iter().any(|violation| {
69
+ violation.id == "arch.no_forbidden_layer_dependencies"
70
+ && violation.path.as_deref() == Some("packages/app/src/domain/event.ts")
71
+ && violation.to.as_deref() == Some("file:src/infrastructure/db.ts")
72
+ }));
73
+ assert!(report.config_findings.iter().any(|finding| {
74
+ finding.id == "arch.import.unresolved"
75
+ && finding.subject == "file:packages/app/src/domain/event.ts"
76
+ && finding.message.contains("@infra/missing")
77
+ }));
78
+ }
79
+
80
+ fn alias_fixture_config() -> String {
81
+ [
82
+ "layers:",
83
+ " domain:",
84
+ " paths:",
85
+ " - \"src/domain/**\"",
86
+ " - \"packages/app/src/domain/**\"",
87
+ " infrastructure:",
88
+ " paths:",
89
+ " - \"src/infrastructure/**\"",
90
+ "allowed_dependencies:",
91
+ " domain:",
92
+ " infrastructure:",
93
+ " - domain",
94
+ "rules:",
95
+ " no_forbidden_layer_dependencies:",
96
+ " enabled: true",
97
+ " severity: error",
98
+ "",
99
+ ]
100
+ .join("\n")
101
+ }
@@ -165,6 +165,63 @@ fn changed_only_degrades_when_go_module_context_changes() {
165
165
  );
166
166
  }
167
167
 
168
+ #[test]
169
+ fn changed_only_degrades_when_manifest_resolver_context_changes() {
170
+ let repo = FixtureRepo::new();
171
+ repo.write(
172
+ "naome.arch.yaml",
173
+ forbidden_domain_to_infrastructure_config(),
174
+ );
175
+ repo.write("Package.swift", "// swift package\n");
176
+ repo.write(
177
+ "src/domain/event.swift",
178
+ "import Foundation\nlet event = 1\n",
179
+ );
180
+ repo.write("src/infrastructure/db.swift", "let db = 1\n");
181
+ repo.init_git();
182
+ validate_architecture(repo.path(), changed_only()).unwrap();
183
+
184
+ repo.write(
185
+ "Package.swift",
186
+ "let package = Package(name: \"Changed\")\n",
187
+ );
188
+
189
+ let report = validate_architecture(repo.path(), changed_only()).unwrap();
190
+
191
+ assert!(report.changed_only_requested);
192
+ assert!(report.changed_only_degraded_to_full_scan);
193
+ assert_eq!(report.changed_only_mode, "degraded_full_scan");
194
+ assert_eq!(
195
+ report.changed_only_degradation_reason.as_deref(),
196
+ Some("resolver_context_changed")
197
+ );
198
+ }
199
+
200
+ #[test]
201
+ fn changed_only_degrades_when_cache_extractor_version_is_stale() {
202
+ let repo = FixtureRepo::new();
203
+ repo.write(
204
+ "naome.arch.yaml",
205
+ forbidden_domain_to_infrastructure_config(),
206
+ );
207
+ repo.write("src/domain/event.ts", "export const event = 1;\n");
208
+ repo.init_git();
209
+ validate_architecture(repo.path(), changed_only()).unwrap();
210
+ let cache_path = repo.path().join(".naome/cache/architecture/cache.json");
211
+ let stale_cache = std::fs::read_to_string(&cache_path)
212
+ .unwrap()
213
+ .replace("architecture-cache-v1.4.0", "architecture-cache-v1.3.17");
214
+ std::fs::write(cache_path, stale_cache).unwrap();
215
+
216
+ let report = validate_architecture(repo.path(), changed_only()).unwrap();
217
+
218
+ assert!(report.changed_only_degraded_to_full_scan);
219
+ assert_eq!(
220
+ report.changed_only_degradation_reason.as_deref(),
221
+ Some("cache_miss")
222
+ );
223
+ }
224
+
168
225
  fn changed_only() -> ArchitectureScanOptions {
169
226
  ArchitectureScanOptions {
170
227
  changed_only: true,
@@ -1,4 +1,7 @@
1
- use naome_core::{format_architecture_validation, validate_architecture, ArchitectureScanOptions};
1
+ use naome_core::{
2
+ architecture_validation_sarif_with_root, format_architecture_validation, validate_architecture,
3
+ ArchitectureScanOptions,
4
+ };
2
5
 
3
6
  mod architecture_support;
4
7
 
@@ -65,3 +68,154 @@ rules:
65
68
  }));
66
69
  assert!(output.contains("configuration findings: 2"));
67
70
  }
71
+
72
+ #[test]
73
+ fn validation_reports_ineffective_layers_contexts_and_unresolved_imports() {
74
+ let repo = FixtureRepo::new();
75
+ repo.write(
76
+ "naome.arch.yaml",
77
+ r#"
78
+ layers:
79
+ domain:
80
+ paths:
81
+ - "src/domain/**"
82
+ missing:
83
+ paths:
84
+ - "src/missing/**"
85
+ contexts:
86
+ billing:
87
+ paths:
88
+ - "src/billing/**"
89
+ public_api:
90
+ - "src/billing/index.ts"
91
+ empty:
92
+ paths:
93
+ - "src/empty/**"
94
+ rules:
95
+ no_forbidden_layer_dependencies:
96
+ enabled: true
97
+ severity: error
98
+ "#,
99
+ );
100
+ repo.write(
101
+ "src/domain/event.ts",
102
+ "import missing from './missing';\nexport const event = missing;\n",
103
+ );
104
+ repo.write("src/billing/index.ts", "export const billing = 1;\n");
105
+
106
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
107
+
108
+ assert_eq!(report.status, "pass");
109
+ assert!(report.config_findings.iter().any(|finding| {
110
+ finding.id == "arch.config.layer_matches_no_files" && finding.subject == "layer:missing"
111
+ }));
112
+ assert!(report.config_findings.iter().any(|finding| {
113
+ finding.id == "arch.config.context_matches_no_files" && finding.subject == "context:empty"
114
+ }));
115
+ assert!(report.config_findings.iter().any(|finding| {
116
+ finding.id == "arch.import.unresolved"
117
+ && finding.subject == "file:src/domain/event.ts"
118
+ && finding.agent_instruction.contains("./missing")
119
+ }));
120
+ assert!(report.agent_feedback.iter().any(|feedback| {
121
+ feedback.files == vec!["src/domain/event.ts"]
122
+ && feedback
123
+ .repair
124
+ .contains("Resolve or remove import ./missing")
125
+ }));
126
+ }
127
+
128
+ #[test]
129
+ fn validation_reports_unresolved_repo_absolute_imports() {
130
+ let repo = FixtureRepo::new();
131
+ repo.write(
132
+ "src/domain/event.ts",
133
+ "import missing from 'src/domain/missing';\nimport app from '@/app/missing';\n",
134
+ );
135
+
136
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
137
+ let unresolved = report
138
+ .config_findings
139
+ .iter()
140
+ .filter(|finding| finding.id == "arch.import.unresolved")
141
+ .map(|finding| finding.message.as_str())
142
+ .collect::<Vec<_>>();
143
+
144
+ assert!(unresolved
145
+ .iter()
146
+ .any(|message| message.contains("src/domain/missing")));
147
+ assert!(unresolved
148
+ .iter()
149
+ .any(|message| message.contains("@/app/missing")));
150
+ assert!(report.agent_feedback.iter().any(|feedback| {
151
+ feedback.files == vec!["src/domain/event.ts"]
152
+ && feedback
153
+ .must_not_do
154
+ .iter()
155
+ .any(|item| item.contains("unresolved imports"))
156
+ }));
157
+ }
158
+
159
+ #[test]
160
+ fn validation_sarif_includes_violations_and_config_findings() {
161
+ let repo = FixtureRepo::new();
162
+ repo.write(
163
+ "naome.arch.yaml",
164
+ r#"
165
+ layers:
166
+ domain:
167
+ paths:
168
+ - "src/domain/**"
169
+ infrastructure:
170
+ paths:
171
+ - "src/infrastructure/**"
172
+ unused:
173
+ paths:
174
+ - "src/unused/**"
175
+ allowed_dependencies:
176
+ domain:
177
+ infrastructure:
178
+ rules:
179
+ no_forbidden_layer_dependencies:
180
+ enabled: true
181
+ severity: error
182
+ "#,
183
+ );
184
+ repo.write(
185
+ "src/domain/event.ts",
186
+ "import { db } from '../infrastructure/db';\nimport missing from './missing';\n",
187
+ );
188
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
189
+
190
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
191
+ let sarif = architecture_validation_sarif_with_root(&report, repo.path());
192
+ let results = sarif["runs"][0]["results"].as_array().unwrap();
193
+
194
+ assert_eq!(sarif["version"], "2.1.0");
195
+ assert!(sarif["runs"][0]["originalUriBaseIds"]["REPO_ROOT"]["uri"]
196
+ .as_str()
197
+ .is_some_and(|uri| uri.starts_with("file://") && uri.ends_with('/')));
198
+ assert!(results.iter().any(|result| {
199
+ result["ruleId"] == "arch.no_forbidden_layer_dependencies"
200
+ && result["level"] == "error"
201
+ && result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
202
+ == "src/domain/event.ts"
203
+ && result["locations"][0]["physicalLocation"]["artifactLocation"]["uriBaseId"]
204
+ == "REPO_ROOT"
205
+ }));
206
+ assert!(results.iter().any(|result| {
207
+ result["ruleId"] == "arch.import.unresolved"
208
+ && result["level"] == "warning"
209
+ && result["properties"]["agentInstruction"]
210
+ .as_str()
211
+ .is_some_and(|instruction| instruction.contains("./missing"))
212
+ }));
213
+ assert!(results.iter().any(|result| {
214
+ result["ruleId"] == "arch.config.layer_matches_no_files"
215
+ && result["level"] == "warning"
216
+ && result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
217
+ == "naome.arch.yaml"
218
+ && result["locations"][0]["physicalLocation"]["artifactLocation"]["uriBaseId"]
219
+ == "REPO_ROOT"
220
+ }));
221
+ }
@@ -20,6 +20,7 @@ const TRANSITIVE_LAYER_POLICY_CONFIG: &str = "layers:\n domain:\n paths:\n
20
20
  const GO_EXTERNAL_POLICY_CONFIG: &str = "layers:\n domain:\n paths:\n - \"domain/**\"\nexternal_dependencies:\n domain:\n allow:\n - \"github.com/acme/payments\"\nrules:\n external_dependency_policy:\n enabled: true\n severity: error\n";
21
21
  const PYTHON_RUST_EXTERNAL_POLICY_CONFIG: &str = "layers:\n domain:\n paths:\n - \"src/domain/**\"\nexternal_dependencies:\n domain:\n allow:\n - \"requests\"\n - \"serde\"\nrules:\n external_dependency_policy:\n enabled: true\n severity: error\n";
22
22
  const STDLIB_EXTERNAL_POLICY_CONFIG: &str = "layers:\n domain:\n paths:\n - \"src/domain/**\"\n - \"domain/**\"\nexternal_dependencies:\n domain:\n allow: []\nrules:\n external_dependency_policy:\n enabled: true\n severity: error\n";
23
+ const FORBIDDEN_LAYER_CONFIG: &str = "layers:\n domain:\n paths:\n - \"src/domain/**\"\n infrastructure:\n paths:\n - \"src/infrastructure/**\"\nallowed_dependencies:\n domain:\n infrastructure:\n - domain\nrules:\n no_forbidden_layer_dependencies:\n enabled: true\n severity: error\n";
23
24
 
24
25
  #[test]
25
26
  fn context_public_api_cycles_and_budgets_report_deterministic_violations() {
@@ -176,6 +177,37 @@ fn transitive_layer_and_external_dependency_policies_are_enforced() {
176
177
  }));
177
178
  }
178
179
 
180
+ #[test]
181
+ fn agent_feedback_includes_source_and_target_files_for_dependency_repairs() {
182
+ let repo = fixture_root();
183
+ fixture_write(&repo, "naome.arch.yaml", FORBIDDEN_LAYER_CONFIG);
184
+ fixture_write(
185
+ &repo,
186
+ "src/domain/event.ts",
187
+ "import { db } from '../infrastructure/db';\n",
188
+ );
189
+ fixture_write(&repo, "src/infrastructure/db.ts", "export const db = 1;\n");
190
+
191
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
192
+
193
+ let feedback = report
194
+ .agent_feedback
195
+ .iter()
196
+ .find(|feedback| feedback.problem.contains("forbidden layer"))
197
+ .expect("expected forbidden dependency feedback");
198
+ assert_eq!(
199
+ feedback.files,
200
+ vec![
201
+ "src/domain/event.ts".to_string(),
202
+ "src/infrastructure/db.ts".to_string()
203
+ ]
204
+ );
205
+ assert!(feedback
206
+ .must_not_do
207
+ .iter()
208
+ .any(|item| item.contains("Do not import infrastructure from domain")));
209
+ }
210
+
179
211
  #[test]
180
212
  fn go_external_dependency_policy_matches_full_module_paths() {
181
213
  let repo = fixture_root();