@lamentis/naome 1.3.15 → 1.3.17

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 +5 -3
  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 +1 -0
  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 +59 -36
  18. package/crates/naome-core/src/architecture/rules.rs +5 -1
  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 +6 -2
  25. package/crates/naome-core/src/lib.rs +8 -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 +154 -0
  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 +1 -1
  39. package/templates/naome-root/.naome/verification.json +6 -1
  40. package/templates/naome-root/docs/naome/architecture-fitness.md +68 -51
@@ -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.3.17", "architecture-cache-v1.3.14");
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,
@@ -0,0 +1,154 @@
1
+ use naome_core::{format_architecture_validation, validate_architecture, ArchitectureScanOptions};
2
+
3
+ mod architecture_support;
4
+
5
+ use architecture_support::FixtureRepo;
6
+
7
+ #[test]
8
+ fn validation_reports_risky_broad_architecture_config_without_failing() {
9
+ let repo = FixtureRepo::new();
10
+ repo.write(
11
+ "naome.arch.yaml",
12
+ r#"
13
+ layers:
14
+ application:
15
+ paths:
16
+ - "src/**"
17
+ domain:
18
+ paths:
19
+ - "src/domain/**"
20
+ infrastructure:
21
+ paths:
22
+ - "src/infrastructure/**"
23
+ allowed_dependencies:
24
+ application:
25
+ - domain
26
+ - infrastructure
27
+ domain:
28
+ infrastructure:
29
+ - domain
30
+ contexts:
31
+ default:
32
+ paths:
33
+ - "src/**"
34
+ public_api:
35
+ - "src/index.ts"
36
+ billing:
37
+ paths:
38
+ - "src/billing/**"
39
+ public_api:
40
+ - "src/billing/index.ts"
41
+ rules:
42
+ no_forbidden_layer_dependencies:
43
+ enabled: true
44
+ severity: error
45
+ "#,
46
+ );
47
+ repo.write("src/domain/event.ts", "export const event = 1;\n");
48
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
49
+ repo.write("src/billing/index.ts", "export const billing = 1;\n");
50
+
51
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
52
+ let output = format_architecture_validation(&report);
53
+
54
+ assert_eq!(report.status, "pass");
55
+ assert_eq!(report.summary.warnings, 0);
56
+ assert!(report.config_findings.iter().any(|finding| {
57
+ finding.id == "arch.config.broad_layer_overlap"
58
+ && finding.subject == "layer:application"
59
+ && finding.severity == "warning"
60
+ }));
61
+ assert!(report.config_findings.iter().any(|finding| {
62
+ finding.id == "arch.config.catch_all_context_with_specific_contexts"
63
+ && finding.subject == "context:default"
64
+ && finding.severity == "warning"
65
+ }));
66
+ assert!(output.contains("configuration findings: 2"));
67
+ }
68
+
69
+ #[test]
70
+ fn validation_reports_ineffective_layers_contexts_and_unresolved_imports() {
71
+ let repo = FixtureRepo::new();
72
+ repo.write(
73
+ "naome.arch.yaml",
74
+ r#"
75
+ layers:
76
+ domain:
77
+ paths:
78
+ - "src/domain/**"
79
+ missing:
80
+ paths:
81
+ - "src/missing/**"
82
+ contexts:
83
+ billing:
84
+ paths:
85
+ - "src/billing/**"
86
+ public_api:
87
+ - "src/billing/index.ts"
88
+ empty:
89
+ paths:
90
+ - "src/empty/**"
91
+ rules:
92
+ no_forbidden_layer_dependencies:
93
+ enabled: true
94
+ severity: error
95
+ "#,
96
+ );
97
+ repo.write(
98
+ "src/domain/event.ts",
99
+ "import missing from './missing';\nexport const event = missing;\n",
100
+ );
101
+ repo.write("src/billing/index.ts", "export const billing = 1;\n");
102
+
103
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
104
+
105
+ assert_eq!(report.status, "pass");
106
+ assert!(report.config_findings.iter().any(|finding| {
107
+ finding.id == "arch.config.layer_matches_no_files" && finding.subject == "layer:missing"
108
+ }));
109
+ assert!(report.config_findings.iter().any(|finding| {
110
+ finding.id == "arch.config.context_matches_no_files" && finding.subject == "context:empty"
111
+ }));
112
+ assert!(report.config_findings.iter().any(|finding| {
113
+ finding.id == "arch.import.unresolved"
114
+ && finding.subject == "file:src/domain/event.ts"
115
+ && finding.agent_instruction.contains("./missing")
116
+ }));
117
+ assert!(report.agent_feedback.iter().any(|feedback| {
118
+ feedback.files == vec!["src/domain/event.ts"]
119
+ && feedback
120
+ .repair
121
+ .contains("Resolve or remove import ./missing")
122
+ }));
123
+ }
124
+
125
+ #[test]
126
+ fn validation_reports_unresolved_repo_absolute_imports() {
127
+ let repo = FixtureRepo::new();
128
+ repo.write(
129
+ "src/domain/event.ts",
130
+ "import missing from 'src/domain/missing';\nimport app from '@/app/missing';\n",
131
+ );
132
+
133
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
134
+ let unresolved = report
135
+ .config_findings
136
+ .iter()
137
+ .filter(|finding| finding.id == "arch.import.unresolved")
138
+ .map(|finding| finding.message.as_str())
139
+ .collect::<Vec<_>>();
140
+
141
+ assert!(unresolved
142
+ .iter()
143
+ .any(|message| message.contains("src/domain/missing")));
144
+ assert!(unresolved
145
+ .iter()
146
+ .any(|message| message.contains("@/app/missing")));
147
+ assert!(report.agent_feedback.iter().any(|feedback| {
148
+ feedback.files == vec!["src/domain/event.ts"]
149
+ && feedback
150
+ .must_not_do
151
+ .iter()
152
+ .any(|item| item.contains("unresolved imports"))
153
+ }));
154
+ }
@@ -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();