@lamentis/naome 1.3.10 → 1.3.11

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.
@@ -6,21 +6,18 @@ use std::time::{SystemTime, UNIX_EPOCH};
6
6
 
7
7
  use naome_core::{
8
8
  default_architecture_config_text, scan_architecture, validate_architecture, ArchitectureConfig,
9
- ArchitectureScanOptions,
9
+ ArchitectureEdgeKind, ArchitectureScanOptions,
10
10
  };
11
11
 
12
12
  static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
13
+ const FORBIDDEN_DOMAIN_TO_INFRASTRUCTURE_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";
13
14
 
14
15
  #[test]
15
16
  fn parses_starter_architecture_config() {
16
17
  let config = ArchitectureConfig::parse(default_architecture_config_text(), "test").unwrap();
17
18
 
18
19
  assert!(config.layers.contains_key("application"));
19
- assert_eq!(
20
- config.rule("max_file_lines").value,
21
- Some(400),
22
- "starter config should seed a production file-size budget"
23
- );
20
+ assert_eq!(config.rule("max_file_lines").value, Some(400));
24
21
  assert_eq!(
25
22
  config.ignore[0].reason,
26
23
  "Generated code is not architecture-owned."
@@ -58,7 +55,6 @@ rules:
58
55
  );
59
56
 
60
57
  let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
61
-
62
58
  assert!(scan
63
59
  .graph
64
60
  .nodes
@@ -84,6 +80,343 @@ rules:
84
80
  );
85
81
  }
86
82
 
83
+ #[test]
84
+ fn import_extractors_emit_file_and_external_dependency_edges() {
85
+ let repo = FixtureRepo::new();
86
+ repo.write(
87
+ "src/domain/event.ts",
88
+ "import { db } from '../infrastructure/db';\nimport React from 'react';\n",
89
+ );
90
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
91
+ repo.write(
92
+ "src/domain/service.py",
93
+ "from ..infrastructure.client import db\nimport requests\n",
94
+ );
95
+ repo.write("src/infrastructure/client.py", "db = object()\n");
96
+ repo.write(
97
+ "src/domain/lib.rs",
98
+ "use crate::infrastructure::db;\nextern crate serde;\n",
99
+ );
100
+ repo.write("src/infrastructure/db.rs", "pub fn connect() {}\n");
101
+ repo.write("cmd/server/main.go", "package main\nimport \"fmt\"\n");
102
+
103
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
104
+
105
+ assert!(scan.graph.edges.iter().any(|edge| {
106
+ edge.kind == ArchitectureEdgeKind::Imports
107
+ && edge.from == "file:src/domain/event.ts"
108
+ && edge.to == "file:src/infrastructure/db.ts"
109
+ }));
110
+ assert!(scan
111
+ .graph
112
+ .nodes
113
+ .iter()
114
+ .any(|node| node.id == "external:react"));
115
+ assert!(scan
116
+ .graph
117
+ .nodes
118
+ .iter()
119
+ .any(|node| node.id == "external:requests"));
120
+ assert!(scan
121
+ .graph
122
+ .nodes
123
+ .iter()
124
+ .any(|node| node.id == "external:fmt"));
125
+ assert!(scan.graph.edges.iter().any(|edge| {
126
+ edge.kind == ArchitectureEdgeKind::Imports
127
+ && edge.from == "file:src/domain/service.py"
128
+ && edge.to == "file:src/infrastructure/client.py"
129
+ }));
130
+ assert!(scan.graph.edges.iter().any(|edge| {
131
+ edge.kind == ArchitectureEdgeKind::Imports
132
+ && edge.from == "file:src/domain/lib.rs"
133
+ && edge.to == "file:src/infrastructure/db.rs"
134
+ }));
135
+ }
136
+
137
+ #[test]
138
+ fn import_resolution_covers_common_language_forms_without_dropping_layer_edges() {
139
+ let repo = FixtureRepo::new();
140
+ repo.write("naome.arch.yaml", FORBIDDEN_DOMAIN_TO_INFRASTRUCTURE_CONFIG);
141
+ repo.write(
142
+ "src/domain/event.ts",
143
+ "import {\n db\n} from '../infrastructure/db';\n",
144
+ );
145
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
146
+ repo.write(
147
+ "src/domain/rust_item.rs",
148
+ "use crate::infrastructure::pool::Pool;\n",
149
+ );
150
+ repo.write("src/infrastructure/pool.rs", "pub struct Pool;\n");
151
+ repo.write(
152
+ "src/domain/python_from_relative.py",
153
+ "from ..infrastructure import client\n",
154
+ );
155
+ repo.write(
156
+ "src/domain/python_from_absolute.py",
157
+ "from src.infrastructure.client import db\n",
158
+ );
159
+ repo.write(
160
+ "src/domain/python_import_list.py",
161
+ "import requests, src.infrastructure.client\n",
162
+ );
163
+ repo.write("src/infrastructure/client.py", "db = object()\n");
164
+
165
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
166
+
167
+ assert_eq!(report.summary.errors, 5);
168
+ let violation_paths = report
169
+ .violations
170
+ .iter()
171
+ .map(|violation| violation.path.as_deref().unwrap_or(""))
172
+ .collect::<Vec<_>>();
173
+ assert!(violation_paths.contains(&"src/domain/event.ts"));
174
+ assert!(violation_paths.contains(&"src/domain/rust_item.rs"));
175
+ assert!(violation_paths.contains(&"src/domain/python_from_relative.py"));
176
+ assert!(violation_paths.contains(&"src/domain/python_from_absolute.py"));
177
+ assert!(violation_paths.contains(&"src/domain/python_import_list.py"));
178
+ }
179
+
180
+ #[test]
181
+ fn relative_import_resolution_prefers_source_language_extensions() {
182
+ let repo = FixtureRepo::new();
183
+ repo.write("src/domain/mod.rs", "mod db;\n");
184
+ repo.write("src/domain/db.ts", "export const wrong = true;\n");
185
+ repo.write("src/domain/db.rs", "pub fn right() {}\n");
186
+
187
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
188
+
189
+ assert!(scan.graph.edges.iter().any(|edge| {
190
+ edge.kind == ArchitectureEdgeKind::Imports
191
+ && edge.from == "file:src/domain/mod.rs"
192
+ && edge.to == "file:src/domain/db.rs"
193
+ }));
194
+ assert!(!scan.graph.edges.iter().any(|edge| {
195
+ edge.kind == ArchitectureEdgeKind::Imports
196
+ && edge.from == "file:src/domain/mod.rs"
197
+ && edge.to == "file:src/domain/db.ts"
198
+ }));
199
+ }
200
+
201
+ #[test]
202
+ fn import_resolver_handles_language_specific_module_roots() {
203
+ let repo = FixtureRepo::new();
204
+ repo.write(
205
+ "packages/app/src/domain/event.rs",
206
+ "use crate::infrastructure::db::Pool;\n",
207
+ );
208
+ repo.write(
209
+ "packages/app/src/infrastructure/db.rs",
210
+ "pub struct Pool;\n",
211
+ );
212
+ repo.write(
213
+ "src/domain/service.rs",
214
+ "mod helper;\nuse self::helper::Thing;\nuse super::infra;\n",
215
+ );
216
+ repo.write("src/domain/helper.rs", "pub fn wrong() {}\n");
217
+ repo.write("src/domain/service/helper.rs", "pub fn right() {}\n");
218
+ repo.write("src/infra.rs", "pub fn wrong() {}\n");
219
+ repo.write("src/domain/infra.rs", "pub fn right() {}\n");
220
+ repo.write("go.mod", "module github.com/acme/app\n");
221
+ repo.write(
222
+ "domain/event.go",
223
+ "package domain\nimport \"github.com/acme/app/infrastructure/db\"\n",
224
+ );
225
+ repo.write(
226
+ "domain/external.go",
227
+ "package domain\nimport \"github.com/other/db\"\n",
228
+ );
229
+ repo.write("infrastructure/db/db.go", "package db\n");
230
+
231
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
232
+
233
+ let has = |from, to| has_import_edge(&scan, from, to);
234
+ assert!(has(
235
+ "file:packages/app/src/domain/event.rs",
236
+ "file:packages/app/src/infrastructure/db.rs"
237
+ ));
238
+ assert!(has("file:src/domain/service.rs", "file:src/domain/service/helper.rs"));
239
+ assert!(!has("file:src/domain/service.rs", "file:src/domain/helper.rs"));
240
+ assert!(has("file:src/domain/service.rs", "file:src/domain/infra.rs"));
241
+ assert!(!has("file:src/domain/service.rs", "file:src/infra.rs"));
242
+ assert!(has("file:domain/event.go", "file:infrastructure/db/db.go"));
243
+ assert!(!has("file:domain/external.go", "file:infrastructure/db/db.go"));
244
+ }
245
+
246
+ #[test]
247
+ fn missing_layer_allowlist_is_treated_as_no_allowed_dependencies() {
248
+ let repo = FixtureRepo::new();
249
+ repo.write(
250
+ "naome.arch.yaml",
251
+ r#"
252
+ layers:
253
+ domain:
254
+ paths:
255
+ - "src/domain/**"
256
+ infrastructure:
257
+ paths:
258
+ - "src/infrastructure/**"
259
+ rules:
260
+ no_forbidden_layer_dependencies:
261
+ enabled: true
262
+ severity: error
263
+ "#,
264
+ );
265
+ repo.write(
266
+ "src/domain/event.ts",
267
+ "import { db } from '../infrastructure/db';\n",
268
+ );
269
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
270
+
271
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
272
+
273
+ assert_eq!(report.status, "fail");
274
+ assert!(report.violations.iter().any(|violation| {
275
+ violation.id == "arch.no_forbidden_layer_dependencies"
276
+ && violation.path.as_deref() == Some("src/domain/event.ts")
277
+ }));
278
+ }
279
+
280
+ #[test]
281
+ fn overlapping_target_layers_do_not_create_false_forbidden_dependency() {
282
+ let repo = FixtureRepo::new();
283
+ repo.write("naome.arch.yaml", "layers:\n application:\n paths:\n - \"src/**\"\n domain:\n paths:\n - \"src/domain/**\"\nallowed_dependencies:\n application:\n - domain\n domain:\nrules:\n no_forbidden_layer_dependencies:\n enabled: true\n severity: error\n");
284
+ repo.write("src/domain/event.ts", "import { type } from './types';\n");
285
+ repo.write("src/domain/types.ts", "export const type = 1;\n");
286
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
287
+ assert_eq!(report.status, "pass");
288
+ }
289
+
290
+ #[test]
291
+ fn late_review_import_forms_keep_forbidden_layer_edges_visible() {
292
+ let repo = FixtureRepo::new();
293
+ repo.write("naome.arch.yaml", FORBIDDEN_DOMAIN_TO_INFRASTRUCTURE_CONFIG);
294
+ repo.write("src/domain/grouped.rs", "use crate::{domain::types, infrastructure::db};\n");
295
+ repo.write("src/domain/types.rs", "pub struct Event;\n");
296
+ repo.write("src/infrastructure/db.rs", "pub fn connect() {}\n");
297
+ repo.write("src/domain/visible.rs", "pub(crate) use crate::infrastructure::db as database;\n");
298
+ repo.write("src/domain/nested.rs", "use crate::{domain::types, infrastructure::{db, cache}};\n");
299
+ repo.write("src/infrastructure/cache.rs", "pub fn cache() {}\n");
300
+ repo.write("src/domain/parenthesized.py", "from src.infrastructure import (\n client,\n)\n");
301
+ repo.write("src/infrastructure/client.py", "db = object()\n");
302
+ repo.write("src/domain/dynamic.ts", "const db = await import(\n '../infrastructure/db_client'\n);\n");
303
+ repo.write("src/infrastructure/db_client.ts", "export const db = 1;\n");
304
+ repo.write("src/domain/export_literal.ts", "export const fixture = '../infrastructure/db_client';\n");
305
+ repo.write("src/domain/commented.ts", "// await import('../infrastructure/db_client')\n");
306
+ repo.write("go.mod", "module github.com/acme/app\n");
307
+ repo.write("src/domain/commented.go", "package domain\nimport (\n // \"github.com/acme/app/src/infrastructure/dbgo\"\n)\n");
308
+ repo.write("src/infrastructure/dbgo/dbgo.go", "package dbgo\n");
309
+
310
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
311
+ let violation_paths = report
312
+ .violations
313
+ .iter()
314
+ .map(|violation| violation.path.as_deref().unwrap_or(""))
315
+ .collect::<Vec<_>>();
316
+
317
+ assert!(violation_paths.contains(&"src/domain/grouped.rs"));
318
+ assert!(violation_paths.contains(&"src/domain/visible.rs"));
319
+ assert!(violation_paths.contains(&"src/domain/nested.rs"));
320
+ assert!(violation_paths.contains(&"src/domain/parenthesized.py"));
321
+ assert!(violation_paths.contains(&"src/domain/dynamic.ts"));
322
+ assert!(!violation_paths.contains(&"src/domain/export_literal.ts"));
323
+ assert!(!violation_paths.contains(&"src/domain/commented.ts"));
324
+ assert!(!violation_paths.contains(&"src/domain/commented.go"));
325
+ }
326
+
327
+ #[test]
328
+ fn unresolved_relative_imports_are_represented_explicitly() {
329
+ let repo = FixtureRepo::new();
330
+ repo.write("src/app.ts", "import missing from './missing';\n");
331
+
332
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
333
+
334
+ assert!(scan
335
+ .graph
336
+ .nodes
337
+ .iter()
338
+ .any(|node| node.id == "unknown-import:./missing"));
339
+ assert!(scan.graph.edges.iter().any(|edge| {
340
+ edge.kind == ArchitectureEdgeKind::Imports
341
+ && edge.from == "file:src/app.ts"
342
+ && edge.to == "unknown-import:./missing"
343
+ }));
344
+ }
345
+
346
+ #[test]
347
+ fn validates_forbidden_layer_dependency_from_import_edges() {
348
+ let repo = FixtureRepo::new();
349
+ repo.write("naome.arch.yaml", FORBIDDEN_DOMAIN_TO_INFRASTRUCTURE_CONFIG);
350
+ repo.write(
351
+ "src/domain/event.ts",
352
+ "import { db } from '../infrastructure/db';\n",
353
+ );
354
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
355
+
356
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
357
+
358
+ assert_eq!(report.status, "fail");
359
+ let violation = report
360
+ .violations
361
+ .iter()
362
+ .find(|violation| violation.id == "arch.no_forbidden_layer_dependencies")
363
+ .expect("expected forbidden layer dependency violation");
364
+ assert_eq!(violation.path.as_deref(), Some("src/domain/event.ts"));
365
+ assert_eq!(violation.source_range.as_ref().unwrap().start_line, 1);
366
+ assert!(violation
367
+ .agent_instruction
368
+ .contains("Do not import infrastructure from domain"));
369
+ }
370
+
371
+ #[test]
372
+ fn exact_entrypoint_layer_paths_keep_bootstrap_out_of_cli_layer() {
373
+ let repo = FixtureRepo::new();
374
+ repo.write(
375
+ "naome.arch.yaml",
376
+ r#"
377
+ layers:
378
+ cli:
379
+ paths:
380
+ - "packages/naome/bin/naome.js"
381
+ installer:
382
+ paths:
383
+ - "packages/naome/bin/naome-node.js"
384
+ - "packages/naome/installer/**"
385
+ allowed_dependencies:
386
+ cli:
387
+ installer:
388
+ rules:
389
+ no_forbidden_layer_dependencies:
390
+ enabled: true
391
+ severity: error
392
+ "#,
393
+ );
394
+ repo.write(
395
+ "packages/naome/bin/naome.js",
396
+ "console.log('native wrapper');\n",
397
+ );
398
+ repo.write(
399
+ "packages/naome/bin/naome-node.js",
400
+ "import { runNaomeNodeCli } from '../installer/main.js';\nawait runNaomeNodeCli();\n",
401
+ );
402
+ repo.write(
403
+ "packages/naome/installer/main.js",
404
+ "export async function runNaomeNodeCli() {}\n",
405
+ );
406
+
407
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
408
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
409
+
410
+ assert_eq!(report.status, "pass");
411
+ assert_eq!(
412
+ scan.file_facts
413
+ .get("packages/naome/bin/naome-node.js")
414
+ .unwrap()
415
+ .layers,
416
+ vec!["installer"]
417
+ );
418
+ }
419
+
87
420
  #[test]
88
421
  fn validates_file_size_budget_with_stable_json_shape() {
89
422
  let repo = FixtureRepo::new();
@@ -207,3 +540,9 @@ fn run_git(root: &Path, args: &[&str]) {
207
540
  String::from_utf8_lossy(&result.stderr)
208
541
  );
209
542
  }
543
+
544
+ fn has_import_edge(scan: &naome_core::ArchitectureScanReport, from: &str, to: &str) -> bool {
545
+ scan.graph.edges.iter().any(|edge| {
546
+ edge.kind == ArchitectureEdgeKind::Imports && edge.from == from && edge.to == to
547
+ })
548
+ }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.3.10",
3
+ "version": "1.3.11",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  {
2
- "harnessVersion": "1.3.10",
2
+ "harnessVersion": "1.3.11",
3
3
  "installedAt": null,
4
4
  "integrity": {
5
5
  ".naome/bin/check-harness-health.js": "sha256:802d7419774981a6af1826b3882270ff8f41259d516f98c52a02b4ddc184c467",
@@ -9,7 +9,7 @@
9
9
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
10
10
  "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
11
11
  "docs/naome/agent-workflow.md": "sha256:0be1c29adfbcd3fd73c4f904080ffc67237692fe413871a30243538c4db38ac7",
12
- "docs/naome/architecture-fitness.md": "sha256:ec2cff9f2090d9b6bfeb2464a650cff97790c4d3e626fd40a42374d5f79c7c38",
12
+ "docs/naome/architecture-fitness.md": "sha256:a1f08dc02eff4e4c37934cf48e3d7ed2453e34d55dac475bc24ab913e03f263d",
13
13
  "docs/naome/context-economy.md": "sha256:3ed5075815ecf4ada46a5e65438769310307c35759fcd46b13dc0b96e02bebd9",
14
14
  "docs/naome/execution.md": "sha256:bfc5d55838942ec8e3d790b59e3c634ff5bf6a2298265cef3dca9788a097eafb",
15
15
  "docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
@@ -50,6 +50,9 @@ contexts:
50
50
  - "src/billing/index.ts"
51
51
 
52
52
  rules:
53
+ no_forbidden_layer_dependencies:
54
+ enabled: true
55
+ severity: error
53
56
  max_file_lines:
54
57
  enabled: true
55
58
  value: 400
@@ -58,6 +61,11 @@ rules:
58
61
  enabled: true
59
62
  severity: error
60
63
 
64
+ allowed_dependencies:
65
+ domain:
66
+ infrastructure:
67
+ - domain
68
+
61
69
  ignore:
62
70
  - path: "generated/**"
63
71
  reason: "Generated code is not architecture-owned."
@@ -69,6 +77,8 @@ ignore:
69
77
  - `arch.generated_manual_boundary` prevents changed files under explicitly
70
78
  ignored generated paths from being accepted without regeneration or a config
71
79
  change.
80
+ - `arch.no_forbidden_layer_dependencies` rejects import edges whose source
81
+ layer is not allowed to depend on the imported file's layer.
72
82
 
73
83
  ## Agent Integration
74
84
 
@@ -85,13 +95,15 @@ fails only on configured error rules.
85
95
 
86
96
  ## Language Support
87
97
 
88
- The v1.3.10 foundation classifies TypeScript, JavaScript, Rust, Python, Go,
89
- Java, Kotlin, and Swift files by path extension. Import extractors, resolver
90
- rules, cycle detection, layer dependency rules, and manifest extraction are
91
- planned follow-up slices before v1.4.0.
98
+ The v1.3.11 foundation classifies TypeScript, JavaScript, Rust, Python, Go,
99
+ Java, Kotlin, and Swift files by path extension. It extracts import facts for
100
+ TypeScript, JavaScript, Rust, Python, and Go, resolves relative imports and
101
+ simple repository-absolute aliases, and represents unresolved imports
102
+ explicitly as graph nodes instead of dropping them.
92
103
 
93
104
  ## Limitations
94
105
 
95
- This release intentionally starts with path extraction, graph construction,
96
- file budgets, generated/manual boundaries, JSON output, human output, and CLI
97
- integration. It does not yet resolve imports or validate cross-layer imports.
106
+ This release intentionally validates direct file-level import edges. Manifest
107
+ extractors, cross-context public API boundaries, cycle detection, transitive
108
+ forbidden dependencies, SARIF output, and persistent scan caching remain planned
109
+ follow-up slices before v1.4.0.