@lamentis/naome 1.3.9 → 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.
Files changed (41) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +10 -0
  3. package/bin/naome.js +1 -1
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/architecture_commands.rs +127 -0
  6. package/crates/naome-cli/src/cli_args.rs +4 -0
  7. package/crates/naome-cli/src/dispatcher.rs +2 -0
  8. package/crates/naome-cli/src/main.rs +6 -0
  9. package/crates/naome-core/Cargo.toml +1 -1
  10. package/crates/naome-core/src/architecture/config/parser/scalar.rs +26 -0
  11. package/crates/naome-core/src/architecture/config/parser/sections.rs +154 -0
  12. package/crates/naome-core/src/architecture/config/parser.rs +97 -0
  13. package/crates/naome-core/src/architecture/config.rs +126 -0
  14. package/crates/naome-core/src/architecture/model.rs +80 -0
  15. package/crates/naome-core/src/architecture/output.rs +178 -0
  16. package/crates/naome-core/src/architecture/rules.rs +212 -0
  17. package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +118 -0
  18. package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +87 -0
  19. package/crates/naome-core/src/architecture/scan/graph_builder.rs +211 -0
  20. package/crates/naome-core/src/architecture/scan/imports/extractors.rs +407 -0
  21. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +334 -0
  22. package/crates/naome-core/src/architecture/scan/imports.rs +59 -0
  23. package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
  24. package/crates/naome-core/src/architecture/scan.rs +95 -0
  25. package/crates/naome-core/src/architecture.rs +31 -0
  26. package/crates/naome-core/src/install_plan.rs +2 -0
  27. package/crates/naome-core/src/lib.rs +16 -8
  28. package/crates/naome-core/tests/architecture.rs +548 -0
  29. package/crates/naome-core/tests/harness_health.rs +1 -0
  30. package/installer/harness-files.js +3 -0
  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/bin/check-harness-health.js +7 -7
  35. package/templates/naome-root/.naome/bin/check-task-state.js +7 -7
  36. package/templates/naome-root/.naome/bin/naome.js +2 -2
  37. package/templates/naome-root/.naome/manifest.json +10 -8
  38. package/templates/naome-root/.naome/verification.json +15 -1
  39. package/templates/naome-root/docs/naome/architecture-fitness.md +109 -0
  40. package/templates/naome-root/docs/naome/index.md +4 -3
  41. package/templates/naome-root/docs/naome/testing.md +6 -3
@@ -0,0 +1,548 @@
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::{
8
+ default_architecture_config_text, scan_architecture, validate_architecture, ArchitectureConfig,
9
+ ArchitectureEdgeKind, ArchitectureScanOptions,
10
+ };
11
+
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";
14
+
15
+ #[test]
16
+ fn parses_starter_architecture_config() {
17
+ let config = ArchitectureConfig::parse(default_architecture_config_text(), "test").unwrap();
18
+
19
+ assert!(config.layers.contains_key("application"));
20
+ assert_eq!(config.rule("max_file_lines").value, Some(400));
21
+ assert_eq!(
22
+ config.ignore[0].reason,
23
+ "Generated code is not architecture-owned."
24
+ );
25
+ }
26
+
27
+ #[test]
28
+ fn path_extractor_builds_language_agnostic_graph_for_mixed_repo() {
29
+ let repo = FixtureRepo::new();
30
+ repo.write("src/domain/event.ts", "export const event = 1;\n");
31
+ repo.write("src/infrastructure/db.py", "client = object()\n");
32
+ repo.write("cmd/server/main.go", "package main\n");
33
+ repo.write(
34
+ "naome.arch.yaml",
35
+ r#"
36
+ layers:
37
+ domain:
38
+ paths:
39
+ - "src/domain/**"
40
+ infrastructure:
41
+ paths:
42
+ - "src/infrastructure/**"
43
+ contexts:
44
+ app:
45
+ paths:
46
+ - "src/**"
47
+ public_api:
48
+ - "src/index.ts"
49
+ rules:
50
+ max_file_lines:
51
+ enabled: true
52
+ value: 400
53
+ severity: warning
54
+ "#,
55
+ );
56
+
57
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
58
+ assert!(scan
59
+ .graph
60
+ .nodes
61
+ .iter()
62
+ .any(|node| node.id == "file:src/domain/event.ts"));
63
+ assert!(scan
64
+ .graph
65
+ .nodes
66
+ .iter()
67
+ .any(|node| node.id == "layer:domain"));
68
+ assert!(scan
69
+ .graph
70
+ .edges
71
+ .iter()
72
+ .any(|edge| { edge.from == "layer:domain" && edge.to == "file:src/domain/event.ts" }));
73
+ assert_eq!(
74
+ scan.file_facts
75
+ .get("cmd/server/main.go")
76
+ .unwrap()
77
+ .language
78
+ .as_deref(),
79
+ Some("go")
80
+ );
81
+ }
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
+
420
+ #[test]
421
+ fn validates_file_size_budget_with_stable_json_shape() {
422
+ let repo = FixtureRepo::new();
423
+ repo.write(
424
+ "naome.arch.yaml",
425
+ r#"
426
+ rules:
427
+ max_file_lines:
428
+ enabled: true
429
+ value: 2
430
+ severity: error
431
+ "#,
432
+ );
433
+ repo.write("src/too_big.rs", "one\ntwo\nthree\n");
434
+
435
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
436
+ let json = serde_json::to_string(&report).unwrap();
437
+
438
+ assert_eq!(report.status, "fail");
439
+ assert!(json.contains("\"schema\":\"naome.arch.validation.v1\""));
440
+ assert!(json.contains("\"id\":\"arch.max_file_lines\""));
441
+ let violation = report
442
+ .violations
443
+ .iter()
444
+ .find(|violation| violation.path.as_deref() == Some("src/too_big.rs"))
445
+ .expect("expected src/too_big.rs file-size violation");
446
+ assert_eq!(violation.agent_instruction, "Reduce src/too_big.rs below 2 lines or add a justified generated-code ignore rule if it is not manually owned.");
447
+ }
448
+
449
+ #[test]
450
+ fn changed_only_generated_boundary_reports_changed_generated_file() {
451
+ let repo = FixtureRepo::new();
452
+ repo.init_git();
453
+ repo.write(
454
+ "naome.arch.yaml",
455
+ r#"
456
+ rules:
457
+ generated_manual_boundary:
458
+ enabled: true
459
+ severity: error
460
+ ignore:
461
+ - path: "generated/**"
462
+ reason: "Generated code is not manually edited."
463
+ "#,
464
+ );
465
+ repo.write("generated/client.ts", "export const generated = true;\n");
466
+
467
+ let report = validate_architecture(
468
+ repo.path(),
469
+ ArchitectureScanOptions {
470
+ changed_only: true,
471
+ config_path: None,
472
+ },
473
+ )
474
+ .unwrap();
475
+
476
+ assert_eq!(report.status, "fail");
477
+ assert!(report.changed_only_degraded_to_full_scan);
478
+ assert_eq!(report.violations[0].id, "arch.generated_manual_boundary");
479
+ assert_eq!(
480
+ report.violations[0].path.as_deref(),
481
+ Some("generated/client.ts")
482
+ );
483
+ }
484
+
485
+ struct FixtureRepo {
486
+ root: PathBuf,
487
+ }
488
+
489
+ impl FixtureRepo {
490
+ fn new() -> Self {
491
+ let nonce = SystemTime::now()
492
+ .duration_since(UNIX_EPOCH)
493
+ .unwrap()
494
+ .as_nanos();
495
+ let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
496
+ let root = std::env::temp_dir().join(format!(
497
+ "naome-arch-fixture-{}-{nonce}-{counter}",
498
+ std::process::id()
499
+ ));
500
+ fs::create_dir_all(&root).unwrap();
501
+ fs::write(
502
+ root.join(".naomeignore"),
503
+ ".naome/archive/\n.naome/tasks/\n",
504
+ )
505
+ .unwrap();
506
+ Self { root }
507
+ }
508
+
509
+ fn path(&self) -> &Path {
510
+ &self.root
511
+ }
512
+
513
+ fn write(&self, relative_path: &str, content: &str) {
514
+ let path = self.root.join(relative_path);
515
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
516
+ fs::write(path, content).unwrap();
517
+ }
518
+
519
+ fn init_git(&self) {
520
+ run_git(&self.root, &["init"]);
521
+ run_git(&self.root, &["config", "user.email", "naome@example.com"]);
522
+ run_git(&self.root, &["config", "user.name", "NAOME Test"]);
523
+ self.write("README.md", "# Fixture\n");
524
+ run_git(&self.root, &["add", "."]);
525
+ run_git(&self.root, &["commit", "-m", "baseline"]);
526
+ }
527
+ }
528
+
529
+ fn run_git(root: &Path, args: &[&str]) {
530
+ let result = Command::new("git")
531
+ .args(args)
532
+ .current_dir(root)
533
+ .output()
534
+ .unwrap();
535
+ assert!(
536
+ result.status.success(),
537
+ "git {:?} failed: {}{}",
538
+ args,
539
+ String::from_utf8_lossy(&result.stdout),
540
+ String::from_utf8_lossy(&result.stderr)
541
+ );
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
+ }
@@ -19,6 +19,7 @@ const MACHINE_OWNED_PATHS: &[&str] = &[
19
19
  "docs/naome/first-run.md",
20
20
  "docs/naome/agent-workflow.md",
21
21
  "docs/naome/context-economy.md",
22
+ "docs/naome/architecture-fitness.md",
22
23
  "docs/naome/execution.md",
23
24
  "docs/naome/task-ledger.md",
24
25
  "docs/naome/upgrade.md",
@@ -22,6 +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
26
  replaceHarnessFile(ctx, "docs/naome/upgrade.md", archiveDirName);
26
27
  }
27
28
 
@@ -39,6 +40,7 @@ export function ensureTaskControlHarnessFiles(ctx, archiveDirName) {
39
40
  replaceHarnessFile(ctx, "docs/naome/agent-workflow.md", archiveDirName);
40
41
  replaceHarnessFile(ctx, "docs/naome/context-economy.md", archiveDirName);
41
42
  replaceHarnessFile(ctx, "docs/naome/execution.md", archiveDirName);
43
+ ensureTemplateFile(ctx, "docs/naome/architecture-fitness.md");
42
44
  replaceHarnessFile(ctx, "docs/naome/upgrade.md", archiveDirName);
43
45
  }
44
46
 
@@ -57,6 +59,7 @@ export function ensureHarnessHealthFiles(ctx, archiveDirName) {
57
59
  replaceHarnessFile(ctx, "docs/naome/context-economy.md", archiveDirName);
58
60
  replaceHarnessFile(ctx, "docs/naome/execution.md", archiveDirName);
59
61
  ensureTemplateFile(ctx, "docs/naome/security.md");
62
+ ensureTemplateFile(ctx, "docs/naome/architecture-fitness.md");
60
63
  replaceHarnessFile(ctx, "docs/naome/upgrade.md", archiveDirName);
61
64
  ensureNaomeIgnore(ctx);
62
65
  }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.3.9",
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",
@@ -10,18 +10,18 @@ const nativeBinaryName = process.platform === "win32" ? "naome.exe" : "naome";
10
10
  const expectedNativeBinaryIntegrity = "sha256:generated";
11
11
 
12
12
  const expectedMachineOwnedIntegrity = Object.freeze({
13
- "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
14
- ".naome/package.json": "sha256:8005a3491db7d92f36ac66369861589f9c47123d3a7c71e643fc2c06168cd45a",
15
- ".naome/bin/naome.js": "sha256:a34c2e50a68d15ff2722a57590ccddc504e025d3c98ea62fd700d7ec1a789b9a",
16
- ".naome/bin/check-task-state.js": "sha256:2612577b7e4ab45d9d39dd5ac54c8e7ed749d237d78f1a8d252f4dfa0b4eaaab",
17
13
  ".naome/bin/check-harness-health.js": "sha256:802d7419774981a6af1826b3882270ff8f41259d516f98c52a02b4ddc184c467",
14
+ ".naome/bin/check-task-state.js": "sha256:2612577b7e4ab45d9d39dd5ac54c8e7ed749d237d78f1a8d252f4dfa0b4eaaab",
15
+ ".naome/bin/naome.js": "sha256:f129c580fb70b3a1d92d0ab0de9fa6c131b2fc51a8dcdebe9d6f5a812a3be61b",
16
+ ".naome/package.json": "sha256:8005a3491db7d92f36ac66369861589f9c47123d3a7c71e643fc2c06168cd45a",
18
17
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
19
- "docs/naome/index.md": "sha256:07ef776f49130319a5280bdb3ae38af22141708253f38eb983a4336fbae1b25a",
20
- "docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
18
+ "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
21
19
  "docs/naome/agent-workflow.md": "sha256:0be1c29adfbcd3fd73c4f904080ffc67237692fe413871a30243538c4db38ac7",
22
20
  "docs/naome/context-economy.md": "sha256:3ed5075815ecf4ada46a5e65438769310307c35759fcd46b13dc0b96e02bebd9",
23
- "docs/naome/task-ledger.md": "sha256:6ca7222c80079b4662fb718d3c71d686770646f1fa52b83b0e90aed1c5a1101b",
24
21
  "docs/naome/execution.md": "sha256:bfc5d55838942ec8e3d790b59e3c634ff5bf6a2298265cef3dca9788a097eafb",
22
+ "docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
23
+ "docs/naome/index.md": "sha256:cac748ed375d86d288460456fc5d606b29bd99d91148522c303d5400a083dbc5",
24
+ "docs/naome/task-ledger.md": "sha256:6ca7222c80079b4662fb718d3c71d686770646f1fa52b83b0e90aed1c5a1101b",
25
25
  "docs/naome/upgrade.md": "sha256:2c60f0441bbd98bd528d109b30a7ded4b0ad55d61ffb9f52edac9e93b7999cb1"
26
26
  });
27
27
 
@@ -10,18 +10,18 @@ const nativeBinaryName = process.platform === "win32" ? "naome.exe" : "naome";
10
10
  const expectedNativeBinaryIntegrity = "sha256:generated";
11
11
 
12
12
  const expectedMachineOwnedIntegrity = Object.freeze({
13
- "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
14
- ".naome/package.json": "sha256:8005a3491db7d92f36ac66369861589f9c47123d3a7c71e643fc2c06168cd45a",
15
- ".naome/bin/naome.js": "sha256:a34c2e50a68d15ff2722a57590ccddc504e025d3c98ea62fd700d7ec1a789b9a",
16
- ".naome/bin/check-task-state.js": "sha256:2612577b7e4ab45d9d39dd5ac54c8e7ed749d237d78f1a8d252f4dfa0b4eaaab",
17
13
  ".naome/bin/check-harness-health.js": "sha256:802d7419774981a6af1826b3882270ff8f41259d516f98c52a02b4ddc184c467",
14
+ ".naome/bin/check-task-state.js": "sha256:2612577b7e4ab45d9d39dd5ac54c8e7ed749d237d78f1a8d252f4dfa0b4eaaab",
15
+ ".naome/bin/naome.js": "sha256:f129c580fb70b3a1d92d0ab0de9fa6c131b2fc51a8dcdebe9d6f5a812a3be61b",
16
+ ".naome/package.json": "sha256:8005a3491db7d92f36ac66369861589f9c47123d3a7c71e643fc2c06168cd45a",
18
17
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
19
- "docs/naome/index.md": "sha256:07ef776f49130319a5280bdb3ae38af22141708253f38eb983a4336fbae1b25a",
20
- "docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
18
+ "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
21
19
  "docs/naome/agent-workflow.md": "sha256:0be1c29adfbcd3fd73c4f904080ffc67237692fe413871a30243538c4db38ac7",
22
20
  "docs/naome/context-economy.md": "sha256:3ed5075815ecf4ada46a5e65438769310307c35759fcd46b13dc0b96e02bebd9",
23
- "docs/naome/task-ledger.md": "sha256:6ca7222c80079b4662fb718d3c71d686770646f1fa52b83b0e90aed1c5a1101b",
24
21
  "docs/naome/execution.md": "sha256:bfc5d55838942ec8e3d790b59e3c634ff5bf6a2298265cef3dca9788a097eafb",
22
+ "docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
23
+ "docs/naome/index.md": "sha256:cac748ed375d86d288460456fc5d606b29bd99d91148522c303d5400a083dbc5",
24
+ "docs/naome/task-ledger.md": "sha256:6ca7222c80079b4662fb718d3c71d686770646f1fa52b83b0e90aed1c5a1101b",
25
25
  "docs/naome/upgrade.md": "sha256:2c60f0441bbd98bd528d109b30a7ded4b0ad55d61ffb9f52edac9e93b7999cb1"
26
26
  });
27
27
 
@@ -27,7 +27,7 @@ function main(argv) {
27
27
  return;
28
28
  }
29
29
 
30
- if (["status", "next", "intent", "route", "explain", "context", "doctor", "task", "quality", "semantic", "repo", "structure", "cleanup", "refresh-integrity", "workflow"].includes(command)) {
30
+ if (["status", "next", "intent", "route", "explain", "context", "doctor", "task", "quality", "semantic", "arch", "repo", "structure", "cleanup", "refresh-integrity", "workflow"].includes(command)) {
31
31
  runNativeDecisionCommand(command, args);
32
32
  return;
33
33
  }
@@ -367,7 +367,7 @@ function findAncestorWithAnyMarker(startPath, markers) {
367
367
  }
368
368
 
369
369
  function printHelp() {
370
- const commands = "naome status [--json]|naome next [--json]|naome intent --prompt-file <path> [--json]|naome intent --prompt <text> [--json]|naome route --prompt-file <path> [--execute] [--json]|naome route --prompt <text> [--execute] [--json]|naome explain --prompt-file <path> [--json]|naome explain --prompt <text> [--json]|naome context select --changed [--json]|naome context select --prompt-file <path> [--json]|naome context select --prompt <text> [--json]|naome doctor [--json]|naome task render-state [--write] [--json]|naome task migrate-ledger [--write] [--json]|naome quality init [--baseline|--deep-baseline] [--json]|naome quality check --changed [--include-scanned-paths] [--json]|naome quality check --path <path> [--path <path>...] [--include-scanned-paths] [--json]|naome quality report [--deep] [--include-scanned-paths] [--json]|naome quality cache status [--json]|naome quality cache clear|naome semantic report [--deep] [--json]|naome semantic check --changed [--json]|naome semantic check --path <path> [--path <path>...] [--json]|naome semantic route --finding <id> [--json]|naome semantic loop [--json]|naome repo model [--write] [--json]|naome repo check [--json]|naome repo explain --path <path> [--json]|naome structure report [--json]|naome structure explain --path <path> [--json]|naome cleanup plan [--json]|naome cleanup route --path <path> [--json]|naome refresh-integrity [--json]|naome workflow agent-plan|context-delta|proof-plan|capabilities|edit-watchdog|decision-gate|digest [--json]|naome workflow search-profile|check-search|phases|processes|mutations [--json]|naome install|naome sync|naome commit -m \"type(scope): message\"|node .naome/bin/naome.js commit -m \"type(scope): message\"".split("|");
370
+ const commands = "naome status [--json]|naome next [--json]|naome intent --prompt-file <path> [--json]|naome intent --prompt <text> [--json]|naome route --prompt-file <path> [--execute] [--json]|naome route --prompt <text> [--execute] [--json]|naome explain --prompt-file <path> [--json]|naome explain --prompt <text> [--json]|naome context select --changed [--json]|naome context select --prompt-file <path> [--json]|naome context select --prompt <text> [--json]|naome doctor [--json]|naome task render-state [--write] [--json]|naome task migrate-ledger [--write] [--json]|naome quality init [--baseline|--deep-baseline] [--json]|naome quality check --changed [--include-scanned-paths] [--json]|naome quality check --path <path> [--path <path>...] [--include-scanned-paths] [--json]|naome quality report [--deep] [--include-scanned-paths] [--json]|naome quality cache status [--json]|naome quality cache clear|naome semantic report [--deep] [--json]|naome semantic check --changed [--json]|naome semantic check --path <path> [--path <path>...] [--json]|naome semantic route --finding <id> [--json]|naome semantic loop [--json]|naome arch init [--config <path>] [--json]|naome arch explain [--config <path>] [--json]|naome arch scan [--config <path>] [--changed-only] [--write] [--output <path>] [--json]|naome arch validate [--config <path>] [--changed-only] [--json|--agent-feedback]|naome repo model [--write] [--json]|naome repo check [--json]|naome repo explain --path <path> [--json]|naome structure report [--json]|naome structure explain --path <path> [--json]|naome cleanup plan [--json]|naome cleanup route --path <path> [--json]|naome refresh-integrity [--json]|naome workflow agent-plan|context-delta|proof-plan|capabilities|edit-watchdog|decision-gate|digest [--json]|naome workflow search-profile|check-search|phases|processes|mutations [--json]|naome install|naome sync|naome commit -m \"type(scope): message\"|node .naome/bin/naome.js commit -m \"type(scope): message\"".split("|");
371
371
  console.log(["Usage:", ...commands.map((command) => ` ${command}`)].join("\n"));
372
372
  }
373
373