@lamentis/naome 1.3.10 → 1.3.12

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 (35) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +5 -0
  3. package/crates/naome-cli/Cargo.toml +1 -1
  4. package/crates/naome-cli/src/architecture_commands.rs +3 -3
  5. package/crates/naome-cli/tests/architecture_cli.rs +60 -0
  6. package/crates/naome-core/Cargo.toml +1 -1
  7. package/crates/naome-core/src/architecture/config/parser/sections.rs +61 -1
  8. package/crates/naome-core/src/architecture/config/parser.rs +2 -0
  9. package/crates/naome-core/src/architecture/config.rs +47 -0
  10. package/crates/naome-core/src/architecture/output.rs +15 -1
  11. package/crates/naome-core/src/architecture/rules/budgets.rs +179 -0
  12. package/crates/naome-core/src/architecture/rules/context.rs +138 -0
  13. package/crates/naome-core/src/architecture/rules/cycles.rs +39 -0
  14. package/crates/naome-core/src/architecture/rules/external.rs +185 -0
  15. package/crates/naome-core/src/architecture/rules/graph.rs +177 -0
  16. package/crates/naome-core/src/architecture/rules/transitive.rs +89 -0
  17. package/crates/naome-core/src/architecture/rules.rs +73 -27
  18. package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +63 -1
  19. package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +2 -3
  20. package/crates/naome-core/src/architecture/scan/graph_builder.rs +78 -1
  21. package/crates/naome-core/src/architecture/scan/imports/extractors.rs +404 -0
  22. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +316 -0
  23. package/crates/naome-core/src/architecture/scan/imports.rs +75 -0
  24. package/crates/naome-core/src/architecture/scan.rs +20 -0
  25. package/crates/naome-core/src/architecture.rs +1 -1
  26. package/crates/naome-core/src/lib.rs +1 -0
  27. package/crates/naome-core/tests/architecture.rs +380 -73
  28. package/crates/naome-core/tests/architecture_rules.rs +498 -0
  29. package/crates/naome-core/tests/architecture_support/mod.rs +78 -0
  30. package/installer/harness-files.js +3 -3
  31. package/native/darwin-arm64/naome +0 -0
  32. package/native/linux-x64/naome +0 -0
  33. package/package.json +1 -1
  34. package/templates/naome-root/.naome/manifest.json +2 -2
  35. package/templates/naome-root/docs/naome/architecture-fitness.md +62 -7
@@ -0,0 +1,498 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::sync::atomic::{AtomicU64, Ordering};
4
+ use std::time::{SystemTime, UNIX_EPOCH};
5
+
6
+ use naome_core::{validate_architecture, ArchitectureScanOptions};
7
+
8
+ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
9
+ const BILLING_INTERNAL_DB_IMPORT: &str = "import { db } from '../src/billing/internal/db';\n";
10
+ const BILLING_INTERNAL_DB_EXPORT: &str = "export const db = 1;\n";
11
+ const RUST_PARENT_MODULE_ENTRY: &str = "mod parent;\nuse parent::run;\npub fn main() { run(); }\n";
12
+ const RUST_PARENT_MODULE_SOURCE: &str =
13
+ "mod child;\nuse self::child::call;\npub fn helper() {}\npub fn run() { call(); }\n";
14
+ const RUST_CHILD_MODULE_SOURCE: &str = "use super::helper;\npub fn call() { helper(); }\n";
15
+ const RUST_CRATE_PARENT_MODULE_SOURCE: &str =
16
+ "mod child;\nuse crate::parent::child::call;\npub fn helper() {}\npub fn run() { call(); }\n";
17
+ const RUST_CRATE_CHILD_MODULE_SOURCE: &str =
18
+ "use crate::parent::helper;\npub fn call() { helper(); }\n";
19
+ const TRANSITIVE_LAYER_POLICY_CONFIG: &str = "layers:\n domain:\n paths:\n - \"src/domain/**\"\n shared:\n paths:\n - \"src/shared/**\"\n infrastructure:\n paths:\n - \"src/infrastructure/**\"\nallowed_dependencies:\n domain:\n - shared\n shared:\n infrastructure:\n - domain\n - shared\nexternal_dependencies:\n domain:\n allow: []\n infrastructure:\n allow:\n - \"stripe\"\n - \"@supabase/*\"\nrules:\n no_transitive_forbidden_layer_dependencies:\n enabled: true\n severity: error\n external_dependency_policy:\n enabled: true\n severity: error\n";
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
+ 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
+ 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
+
24
+ #[test]
25
+ fn context_public_api_cycles_and_budgets_report_deterministic_violations() {
26
+ let repo = fixture_root();
27
+ write_context_boundary_config(
28
+ &repo,
29
+ false,
30
+ " no_cycles:\n enabled: true\n severity: error\n max_imports_per_file:\n enabled: true\n value: 1\n severity: warning\n max_fan_out:\n enabled: true\n value: 1\n severity: warning\n",
31
+ );
32
+ fixture_write(
33
+ &repo,
34
+ "src/billing/use_ticket.ts",
35
+ "import { internal } from '../ticketing/internal/service';\nimport { helper } from './helper';\n",
36
+ );
37
+ fixture_write(&repo, "src/billing/helper.ts", "export const helper = 1;\n");
38
+ fixture_write(
39
+ &repo,
40
+ "src/ticketing/internal/service.ts",
41
+ "export const internal = 1;\n",
42
+ );
43
+ fixture_write(
44
+ &repo,
45
+ "src/billing/a.ts",
46
+ "import { b } from './b';\nexport const a = b;\n",
47
+ );
48
+ fixture_write(
49
+ &repo,
50
+ "src/billing/b.ts",
51
+ "import { a } from './a';\nexport const b = a;\n",
52
+ );
53
+
54
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
55
+ let ids = report
56
+ .violations
57
+ .iter()
58
+ .map(|violation| violation.id.as_str())
59
+ .collect::<Vec<_>>();
60
+
61
+ assert!(ids.contains(&"arch.no_cross_context_internal_imports"));
62
+ assert!(ids.contains(&"arch.public_api_boundary"));
63
+ assert!(ids.contains(&"arch.no_cycles"));
64
+ assert!(ids.contains(&"arch.max_imports_per_file"));
65
+ assert!(ids.contains(&"arch.max_fan_out"));
66
+ assert!(report
67
+ .violations
68
+ .iter()
69
+ .any(|violation| violation.agent_instruction.contains("public API")));
70
+ }
71
+
72
+ #[test]
73
+ fn catch_all_contexts_do_not_hide_specific_context_boundaries() {
74
+ let repo = fixture_root();
75
+ write_context_boundary_config(&repo, true, "");
76
+ fixture_write(
77
+ &repo,
78
+ "src/billing/use_ticket.ts",
79
+ "import { internal } from '../ticketing/internal/service';\n",
80
+ );
81
+ fixture_write(
82
+ &repo,
83
+ "src/ticketing/internal/service.ts",
84
+ "export const internal = 1;\n",
85
+ );
86
+
87
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
88
+
89
+ assert!(report
90
+ .violations
91
+ .iter()
92
+ .any(|violation| violation.id == "arch.no_cross_context_internal_imports"));
93
+ assert!(report
94
+ .violations
95
+ .iter()
96
+ .any(|violation| violation.id == "arch.public_api_boundary"));
97
+ }
98
+
99
+ #[test]
100
+ fn catch_all_contexts_do_not_turn_shared_helpers_into_context_targets() {
101
+ let repo = fixture_root();
102
+ write_context_boundary_config(&repo, true, "");
103
+ fixture_write(
104
+ &repo,
105
+ "src/billing/use_logger.ts",
106
+ "import { logger } from '../shared/logger';\n",
107
+ );
108
+ fixture_write(
109
+ &repo,
110
+ "src/shared/logger.ts",
111
+ "export const logger = console;\n",
112
+ );
113
+
114
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
115
+
116
+ assert!(
117
+ report.violations.iter().all(|violation| !matches!(
118
+ violation.id.as_str(),
119
+ "arch.no_cross_context_internal_imports" | "arch.public_api_boundary"
120
+ )),
121
+ "{:?}",
122
+ report.violations
123
+ );
124
+ }
125
+
126
+ fn write_context_boundary_config(repo: &Path, include_default: bool, extra_rules: &str) {
127
+ let default_context = if include_default {
128
+ " default:\n paths:\n - \"src/**\"\n public_api:\n - \"src/index.ts\"\n"
129
+ } else {
130
+ ""
131
+ };
132
+ fixture_write(
133
+ repo,
134
+ "naome.arch.yaml",
135
+ &format!(
136
+ "contexts:\n{default_context} billing:\n paths:\n - \"src/billing/**\"\n public_api:\n - \"src/billing/index.ts\"\n ticketing:\n paths:\n - \"src/ticketing/**\"\n public_api:\n - \"src/ticketing/index.ts\"\nrules:\n no_cross_context_internal_imports:\n enabled: true\n severity: error\n public_api_boundary:\n enabled: true\n severity: error\n{extra_rules}"
137
+ ),
138
+ );
139
+ }
140
+
141
+ #[test]
142
+ fn transitive_layer_and_external_dependency_policies_are_enforced() {
143
+ let repo = fixture_root();
144
+ fixture_write(&repo, "naome.arch.yaml", TRANSITIVE_LAYER_POLICY_CONFIG);
145
+ fixture_write(
146
+ &repo,
147
+ "src/domain/event.ts",
148
+ "import { shared } from '../shared/shared';\nimport Stripe from 'stripe';\n",
149
+ );
150
+ fixture_write(
151
+ &repo,
152
+ "src/shared/shared.ts",
153
+ "import { db } from '../infrastructure/db';\nexport const shared = db;\n",
154
+ );
155
+ fixture_write(&repo, "src/infrastructure/db.ts", "export const db = 1;\n");
156
+ fixture_write(
157
+ &repo,
158
+ "src/infrastructure/payments.ts",
159
+ "import Stripe from 'stripe';\nexport const payments = Stripe;\n",
160
+ );
161
+
162
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
163
+
164
+ assert!(report.violations.iter().any(|violation| {
165
+ violation.id == "arch.no_transitive_forbidden_layer_dependencies"
166
+ && violation.path.as_deref() == Some("src/domain/event.ts")
167
+ }));
168
+ assert!(report.violations.iter().any(|violation| {
169
+ violation.id == "arch.external_dependency_policy"
170
+ && violation.path.as_deref() == Some("src/domain/event.ts")
171
+ && violation.to.as_deref() == Some("external:stripe")
172
+ }));
173
+ assert!(!report.violations.iter().any(|violation| {
174
+ violation.id == "arch.external_dependency_policy"
175
+ && violation.path.as_deref() == Some("src/infrastructure/payments.ts")
176
+ }));
177
+ }
178
+
179
+ #[test]
180
+ fn go_external_dependency_policy_matches_full_module_paths() {
181
+ let repo = fixture_root();
182
+ fixture_write(&repo, "naome.arch.yaml", GO_EXTERNAL_POLICY_CONFIG);
183
+ fixture_write(
184
+ &repo,
185
+ "domain/payments.go",
186
+ "package domain\nimport \"github.com/acme/payments\"\n",
187
+ );
188
+ fixture_write(
189
+ &repo,
190
+ "domain/storage.go",
191
+ "package domain\nimport \"github.com/acme/storage\"\n",
192
+ );
193
+
194
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
195
+
196
+ assert!(!report.violations.iter().any(|violation| {
197
+ violation.id == "arch.external_dependency_policy"
198
+ && violation.path.as_deref() == Some("domain/payments.go")
199
+ }));
200
+ assert!(report.violations.iter().any(|violation| {
201
+ violation.id == "arch.external_dependency_policy"
202
+ && violation.path.as_deref() == Some("domain/storage.go")
203
+ && violation.to.as_deref() == Some("external:github.com/acme/storage")
204
+ }));
205
+ }
206
+
207
+ #[test]
208
+ fn external_dependency_policy_normalizes_python_members_and_rust_aliases() {
209
+ let repo = fixture_root();
210
+ fixture_write(&repo, "naome.arch.yaml", PYTHON_RUST_EXTERNAL_POLICY_CONFIG);
211
+ fixture_write(
212
+ &repo,
213
+ "src/domain/http.py",
214
+ "from requests import Session\n",
215
+ );
216
+ fixture_write(
217
+ &repo,
218
+ "src/domain/lib.rs",
219
+ "extern crate serde as serde_json;\n",
220
+ );
221
+
222
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
223
+
224
+ assert!(
225
+ report
226
+ .violations
227
+ .iter()
228
+ .all(|violation| violation.id != "arch.external_dependency_policy"),
229
+ "{:?}",
230
+ report.violations
231
+ );
232
+ }
233
+
234
+ #[test]
235
+ fn external_dependency_policy_ignores_language_standard_libraries() {
236
+ let repo = fixture_root();
237
+ fixture_write(&repo, "naome.arch.yaml", STDLIB_EXTERNAL_POLICY_CONFIG);
238
+ fixture_write(
239
+ &repo,
240
+ "src/domain/node.js",
241
+ "import fs from 'node:fs';\nimport express from 'express';\n",
242
+ );
243
+ fixture_write(
244
+ &repo,
245
+ "src/domain/service.py",
246
+ "from os import path\nimport requests\n",
247
+ );
248
+ fixture_write(
249
+ &repo,
250
+ "src/domain/lib.rs",
251
+ "use std::fs;\nextern crate serde;\n",
252
+ );
253
+ fixture_write(
254
+ &repo,
255
+ "domain/main.go",
256
+ "package domain\nimport (\n \"fmt\"\n \"github.com/acme/storage\"\n)\n",
257
+ );
258
+
259
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
260
+ let external_targets = report
261
+ .violations
262
+ .iter()
263
+ .filter(|violation| violation.id == "arch.external_dependency_policy")
264
+ .filter_map(|violation| violation.to.as_deref())
265
+ .collect::<Vec<_>>();
266
+
267
+ assert_eq!(
268
+ external_targets,
269
+ vec![
270
+ "external:github.com/acme/storage",
271
+ "external:serde",
272
+ "external:express",
273
+ "external:requests"
274
+ ]
275
+ );
276
+ }
277
+
278
+ #[test]
279
+ fn uncontexted_callers_must_use_context_public_apis() {
280
+ let repo = fixture_root();
281
+ fixture_write(
282
+ &repo,
283
+ "naome.arch.yaml",
284
+ r#"
285
+ contexts:
286
+ billing:
287
+ paths:
288
+ - "src/billing/**"
289
+ public_api:
290
+ - "src/billing/index.ts"
291
+ rules:
292
+ no_cross_context_internal_imports:
293
+ enabled: true
294
+ severity: error
295
+ public_api_boundary:
296
+ enabled: true
297
+ severity: error
298
+ "#,
299
+ );
300
+ fixture_write(&repo, "tests/billing_test.ts", BILLING_INTERNAL_DB_IMPORT);
301
+ fixture_write(
302
+ &repo,
303
+ "src/billing/internal/db.ts",
304
+ BILLING_INTERNAL_DB_EXPORT,
305
+ );
306
+
307
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
308
+
309
+ assert!(report
310
+ .violations
311
+ .iter()
312
+ .any(|violation| violation.id == "arch.no_cross_context_internal_imports"));
313
+ assert!(report
314
+ .violations
315
+ .iter()
316
+ .any(|violation| violation.id == "arch.public_api_boundary"));
317
+ }
318
+
319
+ #[test]
320
+ fn non_public_cross_context_imports_do_not_count_as_internal_imports() {
321
+ let repo = fixture_root();
322
+ write_context_boundary_config(&repo, false, "");
323
+ fixture_write(
324
+ &repo,
325
+ "src/billing/use_ticket.ts",
326
+ "import { helper } from '../ticketing/helpers';\n",
327
+ );
328
+ fixture_write(
329
+ &repo,
330
+ "src/ticketing/helpers.ts",
331
+ "export const helper = 1;\n",
332
+ );
333
+
334
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
335
+
336
+ assert!(!report
337
+ .violations
338
+ .iter()
339
+ .any(|violation| violation.id == "arch.no_cross_context_internal_imports"));
340
+ assert!(report
341
+ .violations
342
+ .iter()
343
+ .any(|violation| violation.id == "arch.public_api_boundary"));
344
+ }
345
+
346
+ #[test]
347
+ fn rust_module_declarations_do_not_create_import_cycles() {
348
+ let repo = fixture_root();
349
+ write_cycle_config(&repo);
350
+ fixture_write(&repo, "src/lib.rs", RUST_PARENT_MODULE_ENTRY);
351
+ fixture_write(&repo, "src/parent.rs", RUST_PARENT_MODULE_SOURCE);
352
+ fixture_write(&repo, "src/parent/child.rs", RUST_CHILD_MODULE_SOURCE);
353
+
354
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
355
+
356
+ assert!(
357
+ report
358
+ .violations
359
+ .iter()
360
+ .all(|violation| violation.id != "arch.no_cycles"),
361
+ "{:?}",
362
+ report.violations
363
+ );
364
+ }
365
+
366
+ #[test]
367
+ fn rust_crate_qualified_parent_child_modules_do_not_create_import_cycles() {
368
+ let repo = fixture_root();
369
+ write_cycle_config(&repo);
370
+ fixture_write(&repo, "src/lib.rs", "mod parent;\n");
371
+ fixture_write(&repo, "src/parent.rs", RUST_CRATE_PARENT_MODULE_SOURCE);
372
+ fixture_write(&repo, "src/parent/child.rs", RUST_CRATE_CHILD_MODULE_SOURCE);
373
+
374
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
375
+
376
+ assert!(
377
+ report
378
+ .violations
379
+ .iter()
380
+ .all(|violation| violation.id != "arch.no_cycles"),
381
+ "{:?}",
382
+ report.violations
383
+ );
384
+ }
385
+
386
+ #[test]
387
+ fn self_imports_are_reported_as_cycles() {
388
+ let repo = fixture_root();
389
+ write_cycle_config(&repo);
390
+ fixture_write(
391
+ &repo,
392
+ "src/a.ts",
393
+ "import { a } from './a';\nexport const a = 1;\n",
394
+ );
395
+
396
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
397
+
398
+ assert!(
399
+ report
400
+ .violations
401
+ .iter()
402
+ .any(|violation| violation.id == "arch.no_cycles"),
403
+ "{:?}",
404
+ report.violations
405
+ );
406
+ }
407
+
408
+ #[test]
409
+ fn rust_sibling_module_import_cycles_are_still_reported() {
410
+ let repo = rust_sibling_cycle_repo(
411
+ "use crate::parent::right::right;\npub fn left() { right(); }\n",
412
+ "use crate::parent::left::left;\npub fn right() { left(); }\n",
413
+ );
414
+
415
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
416
+
417
+ assert!(
418
+ report
419
+ .violations
420
+ .iter()
421
+ .any(|violation| violation.id == "arch.no_cycles"),
422
+ "{:?}",
423
+ report.violations
424
+ );
425
+ }
426
+
427
+ #[test]
428
+ fn rust_relative_sibling_module_references_are_module_internal() {
429
+ let repo = rust_sibling_cycle_repo(
430
+ "use super::right::right;\npub fn left() { right(); }\n",
431
+ "use super::left::left;\npub fn right() { left(); }\n",
432
+ );
433
+
434
+ let report = validate_architecture(&repo, ArchitectureScanOptions::default()).unwrap();
435
+
436
+ assert!(
437
+ report
438
+ .violations
439
+ .iter()
440
+ .any(|violation| violation.id == "arch.no_cycles"),
441
+ "{:?}",
442
+ report.violations
443
+ );
444
+ }
445
+
446
+ fn rust_sibling_cycle_repo(left_source: &str, right_source: &str) -> PathBuf {
447
+ let repo = fixture_root();
448
+ write_cycle_config(&repo);
449
+ fixture_write(&repo, "src/lib.rs", "mod parent;\n");
450
+ fixture_write(&repo, "src/parent.rs", "pub mod left;\npub mod right;\n");
451
+ fixture_write(&repo, "src/parent/left.rs", left_source);
452
+ fixture_write(&repo, "src/parent/right.rs", right_source);
453
+ repo
454
+ }
455
+
456
+ fn write_cycle_config(repo: &Path) {
457
+ fixture_write(
458
+ repo,
459
+ "naome.arch.yaml",
460
+ r#"
461
+ rules:
462
+ no_cycles:
463
+ enabled: true
464
+ severity: error
465
+ "#,
466
+ );
467
+ }
468
+
469
+ fn fixture_root() -> PathBuf {
470
+ let nonce = SystemTime::now()
471
+ .duration_since(UNIX_EPOCH)
472
+ .unwrap()
473
+ .as_nanos();
474
+ let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
475
+ let mut root = std::env::temp_dir();
476
+ root.push(format!(
477
+ "naome-arch-rules-fixture-{}-{nonce}-{counter}",
478
+ std::process::id()
479
+ ));
480
+ fs::create_dir_all(&root).unwrap();
481
+ fs::write(
482
+ root.join(".naomeignore"),
483
+ [".naome/archive/", ".naome/tasks/", ""].join("\n"),
484
+ )
485
+ .unwrap();
486
+ root
487
+ }
488
+
489
+ fn fixture_write(root: &Path, relative_path: &str, content: &str) {
490
+ let path = relative_path
491
+ .split('/')
492
+ .fold(root.to_path_buf(), |mut path, segment| {
493
+ path.push(segment);
494
+ path
495
+ });
496
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
497
+ fs::write(path, content).unwrap();
498
+ }
@@ -0,0 +1,78 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+ use std::sync::atomic::{AtomicU64, Ordering};
5
+ use std::time::{SystemTime, UNIX_EPOCH};
6
+
7
+ use naome_core::{ArchitectureEdgeKind, ArchitectureScanReport};
8
+
9
+ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
10
+
11
+ pub struct FixtureRepo {
12
+ root: PathBuf,
13
+ }
14
+
15
+ impl FixtureRepo {
16
+ pub fn new() -> Self {
17
+ let nonce = SystemTime::now()
18
+ .duration_since(UNIX_EPOCH)
19
+ .unwrap()
20
+ .as_nanos();
21
+ let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
22
+ let root = std::env::temp_dir().join(format!(
23
+ "naome-arch-fixture-{}-{nonce}-{counter}",
24
+ std::process::id()
25
+ ));
26
+ fs::create_dir_all(&root).unwrap();
27
+ fs::write(
28
+ root.join(".naomeignore"),
29
+ ".naome/archive/\n.naome/tasks/\n",
30
+ )
31
+ .unwrap();
32
+ Self { root }
33
+ }
34
+
35
+ pub fn path(&self) -> &Path {
36
+ &self.root
37
+ }
38
+
39
+ pub fn write(&self, relative_path: &str, content: &str) {
40
+ let fixture_path = self.root.join(relative_path);
41
+ if let Some(parent) = fixture_path.parent() {
42
+ if !parent.exists() {
43
+ fs::create_dir_all(parent).unwrap();
44
+ }
45
+ }
46
+ fs::write(&fixture_path, content.as_bytes()).unwrap();
47
+ }
48
+
49
+ pub fn init_git(&self) {
50
+ run_git(&self.root, &["init"]);
51
+ run_git(&self.root, &["config", "user.email", "naome@example.com"]);
52
+ run_git(&self.root, &["config", "user.name", "NAOME Test"]);
53
+ self.write("README.md", "# Fixture\n");
54
+ run_git(&self.root, &["add", "."]);
55
+ run_git(&self.root, &["commit", "-m", "baseline"]);
56
+ }
57
+ }
58
+
59
+ pub fn has_import_edge(scan: &ArchitectureScanReport, from: &str, to: &str) -> bool {
60
+ scan.graph.edges.iter().any(|edge| {
61
+ edge.kind == ArchitectureEdgeKind::Imports && edge.from == from && edge.to == to
62
+ })
63
+ }
64
+
65
+ fn run_git(root: &Path, args: &[&str]) {
66
+ let result = Command::new("git")
67
+ .args(args)
68
+ .current_dir(root)
69
+ .output()
70
+ .unwrap();
71
+ assert!(
72
+ result.status.success(),
73
+ "git {:?} failed: {}{}",
74
+ args,
75
+ String::from_utf8_lossy(&result.stdout),
76
+ String::from_utf8_lossy(&result.stderr)
77
+ );
78
+ }
@@ -22,7 +22,7 @@ export function ensureCoreHarnessFiles(ctx, archiveDirName) {
22
22
  replaceHarnessFile(ctx, "docs/naome/task-ledger.md", archiveDirName);
23
23
  ensureTemplateFile(ctx, "docs/naome/repository-model.md");
24
24
  ensureTemplateFile(ctx, "docs/naome/security.md");
25
- ensureTemplateFile(ctx, "docs/naome/architecture-fitness.md");
25
+ replaceHarnessFile(ctx, "docs/naome/architecture-fitness.md", archiveDirName);
26
26
  replaceHarnessFile(ctx, "docs/naome/upgrade.md", archiveDirName);
27
27
  }
28
28
 
@@ -40,7 +40,7 @@ export function ensureTaskControlHarnessFiles(ctx, archiveDirName) {
40
40
  replaceHarnessFile(ctx, "docs/naome/agent-workflow.md", archiveDirName);
41
41
  replaceHarnessFile(ctx, "docs/naome/context-economy.md", archiveDirName);
42
42
  replaceHarnessFile(ctx, "docs/naome/execution.md", archiveDirName);
43
- ensureTemplateFile(ctx, "docs/naome/architecture-fitness.md");
43
+ replaceHarnessFile(ctx, "docs/naome/architecture-fitness.md", archiveDirName);
44
44
  replaceHarnessFile(ctx, "docs/naome/upgrade.md", archiveDirName);
45
45
  }
46
46
 
@@ -59,7 +59,7 @@ export function ensureHarnessHealthFiles(ctx, archiveDirName) {
59
59
  replaceHarnessFile(ctx, "docs/naome/context-economy.md", archiveDirName);
60
60
  replaceHarnessFile(ctx, "docs/naome/execution.md", archiveDirName);
61
61
  ensureTemplateFile(ctx, "docs/naome/security.md");
62
- ensureTemplateFile(ctx, "docs/naome/architecture-fitness.md");
62
+ replaceHarnessFile(ctx, "docs/naome/architecture-fitness.md", archiveDirName);
63
63
  replaceHarnessFile(ctx, "docs/naome/upgrade.md", archiveDirName);
64
64
  ensureNaomeIgnore(ctx);
65
65
  }
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.12",
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:3963d9a92d6481d4e72815a4dd749549320f23c45100b4821e75b7e6f3ffd00b",
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",