@lamentis/naome 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +108 -47
  3. package/bin/naome-node.js +2 -1579
  4. package/bin/naome.js +34 -5
  5. package/crates/naome-cli/Cargo.toml +1 -1
  6. package/crates/naome-cli/src/dispatcher.rs +7 -2
  7. package/crates/naome-cli/src/main.rs +37 -22
  8. package/crates/naome-cli/src/quality_commands.rs +317 -10
  9. package/crates/naome-cli/src/workflow_commands.rs +21 -1
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/decision/checks.rs +64 -0
  12. package/crates/naome-core/src/decision/idle.rs +67 -0
  13. package/crates/naome-core/src/decision/json.rs +36 -0
  14. package/crates/naome-core/src/decision/states.rs +165 -0
  15. package/crates/naome-core/src/decision.rs +131 -353
  16. package/crates/naome-core/src/git.rs +4 -2
  17. package/crates/naome-core/src/install_plan.rs +4 -0
  18. package/crates/naome-core/src/lib.rs +12 -6
  19. package/crates/naome-core/src/paths.rs +3 -1
  20. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  21. package/crates/naome-core/src/quality/adapters.rs +20 -67
  22. package/crates/naome-core/src/quality/baseline.rs +8 -0
  23. package/crates/naome-core/src/quality/cache.rs +153 -0
  24. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +25 -11
  25. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  26. package/crates/naome-core/src/quality/checks.rs +7 -8
  27. package/crates/naome-core/src/quality/cleanup.rs +48 -3
  28. package/crates/naome-core/src/quality/config.rs +8 -15
  29. package/crates/naome-core/src/quality/config_support.rs +24 -0
  30. package/crates/naome-core/src/quality/mod.rs +72 -6
  31. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  32. package/crates/naome-core/src/quality/scanner/analysis.rs +160 -0
  33. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  34. package/crates/naome-core/src/quality/scanner.rs +200 -215
  35. package/crates/naome-core/src/quality/semantic/checks.rs +134 -0
  36. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  37. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  38. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  39. package/crates/naome-core/src/quality/semantic.rs +68 -0
  40. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  41. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  42. package/crates/naome-core/src/quality/structure/checks/directory.rs +134 -0
  43. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  44. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  45. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  46. package/crates/naome-core/src/quality/structure/classify.rs +146 -0
  47. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  48. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  49. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  50. package/crates/naome-core/src/quality/structure/model.rs +131 -0
  51. package/crates/naome-core/src/quality/types.rs +43 -2
  52. package/crates/naome-core/src/route/builtin_checks.rs +141 -0
  53. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  54. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  55. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  56. package/crates/naome-core/src/route/context.rs +180 -0
  57. package/crates/naome-core/src/route/execution.rs +96 -0
  58. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  59. package/crates/naome-core/src/route/execution_support.rs +57 -0
  60. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  61. package/crates/naome-core/src/route/git_ops.rs +72 -0
  62. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  63. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  64. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  65. package/crates/naome-core/src/route/worktree.rs +75 -0
  66. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  67. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  68. package/crates/naome-core/src/route.rs +44 -1217
  69. package/crates/naome-core/src/verification.rs +1 -0
  70. package/crates/naome-core/src/workflow/doctor.rs +144 -0
  71. package/crates/naome-core/src/workflow/mod.rs +2 -0
  72. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  73. package/crates/naome-core/tests/decision.rs +24 -118
  74. package/crates/naome-core/tests/harness_health.rs +2 -0
  75. package/crates/naome-core/tests/install_plan.rs +2 -0
  76. package/crates/naome-core/tests/quality.rs +26 -123
  77. package/crates/naome-core/tests/quality_performance.rs +231 -0
  78. package/crates/naome-core/tests/quality_structure.rs +116 -0
  79. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  80. package/crates/naome-core/tests/quality_structure_policy.rs +144 -0
  81. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  82. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  83. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  84. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  85. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  86. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  87. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  88. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  89. package/crates/naome-core/tests/route.rs +1 -1376
  90. package/crates/naome-core/tests/route_baseline.rs +86 -0
  91. package/crates/naome-core/tests/route_completion.rs +141 -0
  92. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  93. package/crates/naome-core/tests/route_user_diff.rs +202 -0
  94. package/crates/naome-core/tests/route_worktree.rs +54 -0
  95. package/crates/naome-core/tests/semantic_legacy.rs +140 -0
  96. package/crates/naome-core/tests/task_state.rs +60 -432
  97. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  98. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  99. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  100. package/crates/naome-core/tests/verification.rs +4 -45
  101. package/crates/naome-core/tests/verification_contract.rs +22 -78
  102. package/crates/naome-core/tests/workflow_doctor.rs +24 -0
  103. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  104. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  105. package/installer/agents.js +90 -0
  106. package/installer/context.js +67 -0
  107. package/installer/filesystem.js +166 -0
  108. package/installer/flows.js +84 -0
  109. package/installer/git-boundary.js +171 -0
  110. package/installer/git-hook-content.js +36 -0
  111. package/installer/git-hooks.js +134 -0
  112. package/installer/git-local.js +2 -0
  113. package/installer/git-shared.js +35 -0
  114. package/installer/harness-file-ops.js +140 -0
  115. package/installer/harness-files.js +56 -0
  116. package/installer/harness-verification.js +123 -0
  117. package/installer/install-plan.js +66 -0
  118. package/installer/main.js +25 -0
  119. package/installer/manifest-state.js +167 -0
  120. package/installer/native-build.js +24 -0
  121. package/installer/native-format.js +6 -0
  122. package/installer/native.js +162 -0
  123. package/installer/output.js +131 -0
  124. package/installer/version.js +32 -0
  125. package/native/darwin-arm64/naome +0 -0
  126. package/native/linux-x64/naome +0 -0
  127. package/package.json +2 -1
  128. package/templates/naome-root/.naome/bin/check-harness-health.js +3 -3
  129. package/templates/naome-root/.naome/bin/check-task-state.js +3 -3
  130. package/templates/naome-root/.naome/bin/naome.js +32 -21
  131. package/templates/naome-root/.naome/manifest.json +5 -3
  132. package/templates/naome-root/.naome/repository-structure.json +90 -0
  133. package/templates/naome-root/.naome/verification.json +1 -0
  134. package/templates/naome-root/.naomeignore +1 -0
  135. package/templates/naome-root/docs/naome/agent-workflow.md +16 -14
  136. package/templates/naome-root/docs/naome/index.md +4 -3
  137. package/templates/naome-root/docs/naome/repository-quality.md +66 -4
  138. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  139. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -0,0 +1,89 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use crate::models::NaomeError;
5
+
6
+ use super::adapters::{
7
+ apply_structure_adapters, detected_structure_adapter_ids, validate_structure_adapter_ids,
8
+ };
9
+ use crate::quality::config_support::validate_ready_schema;
10
+
11
+ use super::model::RepositoryStructureConfig;
12
+
13
+ const CONFIG_RELATIVE_PATH: &str = ".naome/repository-structure.json";
14
+
15
+ pub fn structure_config_relative_path() -> &'static str {
16
+ CONFIG_RELATIVE_PATH
17
+ }
18
+
19
+ pub fn read_structure_config(
20
+ root: &Path,
21
+ paths: &[String],
22
+ ) -> Result<RepositoryStructureConfig, NaomeError> {
23
+ let path = root.join(CONFIG_RELATIVE_PATH);
24
+ let config = if path.is_file() {
25
+ serde_json::from_str(&fs::read_to_string(path)?)?
26
+ } else {
27
+ generated_structure_config(paths)
28
+ };
29
+ validate_structure_config(&config)?;
30
+ apply_structure_adapters(config)
31
+ }
32
+
33
+ pub fn write_default_structure_config_if_missing(
34
+ root: &Path,
35
+ paths: &[String],
36
+ ) -> Result<bool, NaomeError> {
37
+ let path = root.join(CONFIG_RELATIVE_PATH);
38
+ if path.exists() {
39
+ return Ok(false);
40
+ }
41
+ if let Some(parent) = path.parent() {
42
+ fs::create_dir_all(parent)?;
43
+ }
44
+ let content = serde_json::to_string_pretty(&generated_structure_config(paths))?;
45
+ fs::write(path, format!("{content}\n"))?;
46
+ Ok(true)
47
+ }
48
+
49
+ fn generated_structure_config(paths: &[String]) -> RepositoryStructureConfig {
50
+ let mut config = RepositoryStructureConfig::default();
51
+ config.enabled_adapters = detected_structure_adapter_ids(paths);
52
+ config
53
+ }
54
+
55
+ fn validate_structure_config(config: &RepositoryStructureConfig) -> Result<(), NaomeError> {
56
+ validate_ready_schema(
57
+ CONFIG_RELATIVE_PATH,
58
+ &config.schema,
59
+ "naome.repository-structure.v1",
60
+ config.version,
61
+ &config.status,
62
+ )?;
63
+ validate_structure_adapter_ids(&config.enabled_adapters)?;
64
+ if config.limits.max_directory_files == 0 {
65
+ return Err(NaomeError::new(
66
+ ".naome/repository-structure.json maxDirectoryFiles must be greater than 0.",
67
+ ));
68
+ }
69
+ if config.limits.max_path_depth == 0 {
70
+ return Err(NaomeError::new(
71
+ ".naome/repository-structure.json maxPathDepth must be greater than 0.",
72
+ ));
73
+ }
74
+ for rule in &config.directory_role_rules {
75
+ if rule.id.trim().is_empty() || rule.paths.is_empty() {
76
+ return Err(NaomeError::new(
77
+ ".naome/repository-structure.json directoryRoleRules require id and paths.",
78
+ ));
79
+ }
80
+ }
81
+ for rule in &config.layer_rules {
82
+ if rule.id.trim().is_empty() || rule.paths.is_empty() || rule.layer.trim().is_empty() {
83
+ return Err(NaomeError::new(
84
+ ".naome/repository-structure.json layerRules require id, paths, and layer.",
85
+ ));
86
+ }
87
+ }
88
+ Ok(())
89
+ }
@@ -0,0 +1,107 @@
1
+ use super::model::{RepositoryStructureConfig, StructureLimits};
2
+
3
+ pub fn default_structure_config() -> RepositoryStructureConfig {
4
+ RepositoryStructureConfig {
5
+ schema: "naome.repository-structure.v1".to_string(),
6
+ version: 1,
7
+ status: "ready".to_string(),
8
+ enabled_adapters: Vec::new(),
9
+ source_roots: string_list(DEFAULT_SOURCE_ROOTS),
10
+ test_roots: string_list(DEFAULT_TEST_ROOTS),
11
+ docs_roots: string_list(DEFAULT_DOCS_ROOTS),
12
+ generated_roots: string_list(DEFAULT_GENERATED_ROOTS),
13
+ artifact_roots: string_list(DEFAULT_ARTIFACT_ROOTS),
14
+ module_roots: string_list(DEFAULT_MODULE_ROOTS),
15
+ allowed_root_files: string_list(DEFAULT_ALLOWED_ROOT_FILES),
16
+ directory_role_rules: Vec::new(),
17
+ layer_rules: Vec::new(),
18
+ ignored_paths: string_list(DEFAULT_IGNORED_PATHS),
19
+ disabled_checks: Vec::new(),
20
+ changed_code_policy: default_changed_policy(),
21
+ debt_policy: default_debt_policy(),
22
+ limits: StructureLimits::default(),
23
+ }
24
+ }
25
+
26
+ pub fn default_changed_policy() -> String {
27
+ "block".to_string()
28
+ }
29
+
30
+ pub fn default_debt_policy() -> String {
31
+ "report".to_string()
32
+ }
33
+
34
+ const DEFAULT_SOURCE_ROOTS: &[&str] = &[
35
+ "src/**",
36
+ "app/**",
37
+ "apps/*/src/**",
38
+ "packages/*/src/**",
39
+ "crates/*/src/**",
40
+ "**/src/**",
41
+ "lib/**",
42
+ ];
43
+ const DEFAULT_TEST_ROOTS: &[&str] = &[
44
+ "test/**",
45
+ "tests/**",
46
+ "__tests__/**",
47
+ "packages/*/test/**",
48
+ "packages/*/tests/**",
49
+ "crates/*/tests/**",
50
+ "**/tests/**",
51
+ "scripts/*.test.js",
52
+ ];
53
+ const DEFAULT_DOCS_ROOTS: &[&str] = &["docs/**", "doc/**"];
54
+ const DEFAULT_GENERATED_ROOTS: &[&str] = &[
55
+ "**/generated/**",
56
+ "**/__generated__/**",
57
+ "**/codegen/**",
58
+ "**/*.generated.*",
59
+ ];
60
+ const DEFAULT_ARTIFACT_ROOTS: &[&str] = &[
61
+ "dist/**",
62
+ "build/**",
63
+ "coverage/**",
64
+ ".next/**",
65
+ "target/**",
66
+ "**/dist/**",
67
+ "**/build/**",
68
+ ];
69
+ const DEFAULT_MODULE_ROOTS: &[&str] = &[
70
+ "src/**",
71
+ "app/**",
72
+ "packages/*/src/**",
73
+ "crates/*/src/**",
74
+ "**/src/**",
75
+ ];
76
+ const DEFAULT_ALLOWED_ROOT_FILES: &[&str] = &[
77
+ "README.md",
78
+ "LICENSE",
79
+ "NOTICE",
80
+ "CHANGELOG.md",
81
+ "CONTRIBUTING.md",
82
+ "SECURITY.md",
83
+ "AGENTS.md",
84
+ "package.json",
85
+ "Cargo.toml",
86
+ "pyproject.toml",
87
+ "go.mod",
88
+ "go.sum",
89
+ "Makefile",
90
+ ".gitignore",
91
+ ".naomeignore",
92
+ ];
93
+ const DEFAULT_IGNORED_PATHS: &[&str] = &[
94
+ ".git/**",
95
+ "node_modules/**",
96
+ "vendor/**",
97
+ "target/**",
98
+ "**/target/**",
99
+ "dist/**",
100
+ "build/**",
101
+ "coverage/**",
102
+ ".next/**",
103
+ ];
104
+
105
+ fn string_list(values: &[&str]) -> Vec<String> {
106
+ values.iter().map(|value| (*value).to_string()).collect()
107
+ }
@@ -0,0 +1,77 @@
1
+ mod adapters;
2
+ mod checks;
3
+ mod classify;
4
+ mod config;
5
+ mod defaults;
6
+ mod model;
7
+
8
+ use std::collections::HashSet;
9
+ use std::path::Path;
10
+
11
+ use crate::models::NaomeError;
12
+
13
+ use super::scanner::QualityContext;
14
+ use super::types::QualityViolation;
15
+ use checks::run_structure_checks;
16
+ use classify::build_structure_model;
17
+ use config::read_structure_config;
18
+ pub use config::{structure_config_relative_path, write_default_structure_config_if_missing};
19
+ pub use model::{RepositoryStructureConfig, StructurePathExplanation};
20
+
21
+ pub fn run_repository_structure_checks(
22
+ root: &Path,
23
+ context: &QualityContext,
24
+ baseline_fingerprints: &HashSet<String>,
25
+ ) -> Result<Vec<QualityViolation>, NaomeError> {
26
+ let model = structure_model(root, context, baseline_fingerprints)?;
27
+ let mut violations = run_structure_checks(&model, context.mode);
28
+ violations.retain(|violation| {
29
+ violation.check_id != "test-source-pairing"
30
+ || context
31
+ .files
32
+ .iter()
33
+ .find(|file| file.path == violation.path)
34
+ .is_some_and(|file| !file.symbols.is_empty())
35
+ });
36
+ if !context.mode.is_changed() {
37
+ for violation in &mut violations {
38
+ violation.baseline = baseline_fingerprints.contains(&violation.fingerprint);
39
+ }
40
+ }
41
+ Ok(violations)
42
+ }
43
+
44
+ pub fn explain_repository_structure(
45
+ root: &Path,
46
+ path: impl AsRef<str>,
47
+ ) -> Result<StructurePathExplanation, NaomeError> {
48
+ let repo_paths = super::scanner::collect_repo_paths(root)?;
49
+ let changed_paths = crate::git::changed_paths(root)?;
50
+ let config = read_structure_config(root, &repo_paths)?;
51
+ let model = build_structure_model(config, &repo_paths, &changed_paths, &HashSet::new());
52
+ let normalized = path.as_ref().replace('\\', "/");
53
+ model
54
+ .paths
55
+ .into_iter()
56
+ .find(|entry| entry.explanation.path == normalized)
57
+ .map(|entry| entry.explanation)
58
+ .ok_or_else(|| {
59
+ NaomeError::new(format!(
60
+ "{normalized} is not visible to structure analysis."
61
+ ))
62
+ })
63
+ }
64
+
65
+ fn structure_model(
66
+ root: &Path,
67
+ context: &QualityContext,
68
+ baseline_fingerprints: &HashSet<String>,
69
+ ) -> Result<model::RepositoryStructureModel, NaomeError> {
70
+ let config = read_structure_config(root, &context.repo_paths)?;
71
+ Ok(build_structure_model(
72
+ config,
73
+ &context.repo_paths,
74
+ &context.changed_paths,
75
+ baseline_fingerprints,
76
+ ))
77
+ }
@@ -0,0 +1,131 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+
3
+ use serde::{Deserialize, Serialize};
4
+
5
+ use super::defaults::{default_changed_policy, default_debt_policy, default_structure_config};
6
+
7
+ #[derive(Debug, Clone, Serialize, Deserialize)]
8
+ #[serde(rename_all = "camelCase")]
9
+ pub struct StructureLimits {
10
+ pub max_directory_files: usize,
11
+ pub max_path_depth: usize,
12
+ pub max_directory_roles: usize,
13
+ pub max_dumping_ground_files: usize,
14
+ }
15
+
16
+ impl Default for StructureLimits {
17
+ fn default() -> Self {
18
+ Self {
19
+ max_directory_files: 40,
20
+ max_path_depth: 10,
21
+ max_directory_roles: 2,
22
+ max_dumping_ground_files: 12,
23
+ }
24
+ }
25
+ }
26
+
27
+ #[derive(Debug, Clone, Serialize, Deserialize)]
28
+ #[serde(rename_all = "camelCase")]
29
+ pub struct DirectoryRoleRule {
30
+ pub id: String,
31
+ pub paths: Vec<String>,
32
+ #[serde(default)]
33
+ pub allowed_roles: Vec<String>,
34
+ #[serde(default)]
35
+ pub max_roles: Option<usize>,
36
+ }
37
+
38
+ #[derive(Debug, Clone, Serialize, Deserialize)]
39
+ #[serde(rename_all = "camelCase")]
40
+ pub struct LayerRule {
41
+ pub id: String,
42
+ pub paths: Vec<String>,
43
+ pub layer: String,
44
+ }
45
+
46
+ #[derive(Debug, Clone, Serialize, Deserialize)]
47
+ #[serde(rename_all = "camelCase")]
48
+ pub struct RepositoryStructureConfig {
49
+ pub schema: String,
50
+ pub version: u32,
51
+ pub status: String,
52
+ #[serde(default)]
53
+ pub enabled_adapters: Vec<String>,
54
+ #[serde(default)]
55
+ pub source_roots: Vec<String>,
56
+ #[serde(default)]
57
+ pub test_roots: Vec<String>,
58
+ #[serde(default)]
59
+ pub docs_roots: Vec<String>,
60
+ #[serde(default)]
61
+ pub generated_roots: Vec<String>,
62
+ #[serde(default)]
63
+ pub artifact_roots: Vec<String>,
64
+ #[serde(default)]
65
+ pub module_roots: Vec<String>,
66
+ #[serde(default)]
67
+ pub allowed_root_files: Vec<String>,
68
+ #[serde(default)]
69
+ pub directory_role_rules: Vec<DirectoryRoleRule>,
70
+ #[serde(default)]
71
+ pub layer_rules: Vec<LayerRule>,
72
+ #[serde(default)]
73
+ pub ignored_paths: Vec<String>,
74
+ #[serde(default)]
75
+ pub disabled_checks: Vec<String>,
76
+ #[serde(default = "default_changed_policy")]
77
+ pub changed_code_policy: String,
78
+ #[serde(default = "default_debt_policy")]
79
+ pub debt_policy: String,
80
+ #[serde(default)]
81
+ pub limits: StructureLimits,
82
+ }
83
+
84
+ impl Default for RepositoryStructureConfig {
85
+ fn default() -> Self {
86
+ default_structure_config()
87
+ }
88
+ }
89
+
90
+ #[derive(Debug, Clone, Serialize)]
91
+ #[serde(rename_all = "camelCase")]
92
+ pub struct StructurePathExplanation {
93
+ pub path: String,
94
+ pub role: String,
95
+ pub module: Option<String>,
96
+ pub layer: String,
97
+ pub language: Option<String>,
98
+ pub generated: bool,
99
+ pub debt: bool,
100
+ pub changed: bool,
101
+ pub directory: String,
102
+ }
103
+
104
+ #[derive(Debug, Clone)]
105
+ pub struct StructurePath {
106
+ pub explanation: StructurePathExplanation,
107
+ pub segments: Vec<String>,
108
+ }
109
+
110
+ #[derive(Debug, Clone)]
111
+ pub struct StructureDirectory {
112
+ pub path: String,
113
+ pub roles: BTreeSet<String>,
114
+ pub modules: BTreeSet<String>,
115
+ pub file_count: usize,
116
+ pub direct_changed_paths: Vec<String>,
117
+ }
118
+
119
+ #[derive(Debug, Clone)]
120
+ pub struct RepositoryStructureModel {
121
+ pub config: RepositoryStructureConfig,
122
+ pub paths: Vec<StructurePath>,
123
+ pub directories: Vec<StructureDirectory>,
124
+ pub module_paths: BTreeMap<String, Vec<String>>,
125
+ #[allow(dead_code)]
126
+ pub directory_paths: BTreeMap<String, Vec<String>>,
127
+ #[allow(dead_code)]
128
+ pub role_paths: BTreeMap<String, Vec<String>>,
129
+ pub lowercase_paths: BTreeMap<String, Vec<String>>,
130
+ pub changed_paths: BTreeSet<String>,
131
+ }
@@ -4,15 +4,45 @@ use crate::paths;
4
4
 
5
5
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
6
6
  pub enum QualityMode {
7
- Changed,
7
+ ChangedFast,
8
8
  Report,
9
+ DeepReport,
9
10
  }
10
11
 
11
12
  impl QualityMode {
13
+ #[allow(non_upper_case_globals)]
14
+ pub const Changed: Self = Self::ChangedFast;
15
+
12
16
  pub fn as_str(self) -> &'static str {
13
17
  match self {
14
- Self::Changed => "changed",
18
+ Self::ChangedFast => "changed",
15
19
  Self::Report => "report",
20
+ Self::DeepReport => "deep-report",
21
+ }
22
+ }
23
+
24
+ pub fn is_changed(self) -> bool {
25
+ self == Self::ChangedFast
26
+ }
27
+
28
+ pub fn is_deep(self) -> bool {
29
+ self == Self::DeepReport
30
+ }
31
+ }
32
+
33
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
34
+ pub enum QualityInitMode {
35
+ SeedOnly,
36
+ Baseline,
37
+ DeepBaseline,
38
+ }
39
+
40
+ impl QualityInitMode {
41
+ pub fn as_str(self) -> &'static str {
42
+ match self {
43
+ Self::SeedOnly => "init",
44
+ Self::Baseline => "baseline",
45
+ Self::DeepBaseline => "deep-baseline",
16
46
  }
17
47
  }
18
48
  }
@@ -187,9 +217,14 @@ pub struct QualityReport {
187
217
  #[serde(rename_all = "camelCase")]
188
218
  pub struct QualitySummary {
189
219
  pub scanned_files: usize,
220
+ pub scanned_path_count: usize,
190
221
  pub violation_count: usize,
191
222
  pub baseline_violation_count: usize,
192
223
  pub blocking_violation_count: usize,
224
+ pub truncated: bool,
225
+ pub reason_codes: Vec<String>,
226
+ pub cache_hits: usize,
227
+ pub cache_misses: usize,
193
228
  }
194
229
 
195
230
  #[derive(Debug, Clone, Serialize)]
@@ -211,10 +246,14 @@ pub struct QualityViolation {
211
246
  #[serde(rename_all = "camelCase")]
212
247
  pub struct QualityInitResult {
213
248
  pub schema: String,
249
+ pub mode: String,
214
250
  pub config_written: bool,
251
+ pub structure_config_written: bool,
215
252
  pub baseline_written: bool,
253
+ pub baseline_pending: bool,
216
254
  pub baseline_violations: usize,
217
255
  pub config_path: String,
256
+ pub structure_config_path: String,
218
257
  pub baseline_path: String,
219
258
  }
220
259
 
@@ -256,9 +295,11 @@ pub fn default_generated_paths() -> Vec<String> {
256
295
  const DEFAULT_IGNORED_PATHS: &str = r#"
257
296
  .git/**
258
297
  .naome/archive/**
298
+ .naome/cache/**
259
299
  .naome/task-state.json
260
300
  .naome/task-journal.jsonl
261
301
  .naome/repository-quality.json
302
+ .naome/repository-structure.json
262
303
  .naome/repository-quality-baseline.json
263
304
  node_modules/**
264
305
  .npm/**
@@ -0,0 +1,141 @@
1
+ use std::path::Path;
2
+
3
+ use crate::harness_health::{validate_harness_health, HarnessHealthOptions};
4
+ use crate::models::NaomeError;
5
+ use crate::quality::{check_repository_quality, QualityMode};
6
+ use crate::route::git_ops::{command_output, git_output};
7
+ use crate::route::quality_gate::QualityCheck;
8
+ use crate::task_state::{validate_task_state, TaskStateMode, TaskStateOptions};
9
+ use crate::verification_contract::validate_verification_contract;
10
+
11
+ use super::builtin_context::{run_context_budget_check, template_root};
12
+ use super::builtin_integrity::packaged_harness_integrity;
13
+ use super::builtin_require::{require_builtin_quality_check, require_builtin_quality_check_any};
14
+
15
+ pub(super) fn run_quality_check(
16
+ root: &Path,
17
+ check_id: &str,
18
+ check: &QualityCheck,
19
+ ) -> Result<(), NaomeError> {
20
+ match check_id {
21
+ "diff-check" => {
22
+ require_builtin_quality_check(check_id, check, "git diff --check")?;
23
+ let output = git_output(root, &["diff", "--check"])?;
24
+
25
+ if output.status.success() {
26
+ Ok(())
27
+ } else {
28
+ Err(NaomeError::new(command_output(&output)))
29
+ }
30
+ }
31
+ "naome-harness-health" => {
32
+ require_builtin_quality_check(
33
+ check_id,
34
+ check,
35
+ "node .naome/bin/check-harness-health.js",
36
+ )?;
37
+ run_harness_health_check(root)
38
+ }
39
+ "task-state-check" => {
40
+ require_builtin_quality_check(check_id, check, "npm run check:task-state")?;
41
+ run_template_task_state_check(root)
42
+ }
43
+ "verification-contract-check" => {
44
+ require_builtin_quality_check(
45
+ check_id,
46
+ check,
47
+ "npm run check:verification-contract",
48
+ )?;
49
+ run_template_verification_contract_check(root)
50
+ }
51
+ "context-budget-check" => {
52
+ require_builtin_quality_check(check_id, check, "npm run check:context-budget")?;
53
+ run_context_budget_check(root)
54
+ }
55
+ "repository-quality-check" => {
56
+ require_builtin_quality_check_any(
57
+ check_id,
58
+ check,
59
+ &[
60
+ "naome quality check --changed",
61
+ "node .naome/bin/naome.js quality check --changed",
62
+ "npm run check:repository-quality",
63
+ ],
64
+ )?;
65
+ run_repository_quality_check(root)
66
+ }
67
+ _ => Err(NaomeError::new(format!(
68
+ "Quality check {check_id} is not a built-in safe check; NAOME will not execute repository-controlled verification commands."
69
+ ))),
70
+ }
71
+ }
72
+
73
+ fn run_repository_quality_check(root: &Path) -> Result<(), NaomeError> {
74
+ let report = check_repository_quality(root, QualityMode::ChangedFast)?;
75
+ if report.ok {
76
+ return Ok(());
77
+ }
78
+
79
+ let details = report
80
+ .violations
81
+ .iter()
82
+ .take(20)
83
+ .map(|violation| {
84
+ let location = violation
85
+ .line
86
+ .map(|line| format!("{}:{line}", violation.path))
87
+ .unwrap_or_else(|| violation.path.clone());
88
+ format!("{location} {}: {}", violation.check_id, violation.message)
89
+ })
90
+ .collect::<Vec<_>>()
91
+ .join("\n");
92
+ Err(NaomeError::new(format!(
93
+ "repository-quality-check failed with {} violation(s).\n{}",
94
+ report.violations.len(),
95
+ details
96
+ )))
97
+ }
98
+
99
+ fn run_harness_health_check(root: &Path) -> Result<(), NaomeError> {
100
+ let errors = validate_harness_health(
101
+ root,
102
+ HarnessHealthOptions {
103
+ expected_integrity: packaged_harness_integrity()?,
104
+ ..HarnessHealthOptions::default()
105
+ },
106
+ )?;
107
+ if errors.is_empty() {
108
+ Ok(())
109
+ } else {
110
+ Err(NaomeError::new(errors.join("\n")))
111
+ }
112
+ }
113
+
114
+ fn run_template_task_state_check(root: &Path) -> Result<(), NaomeError> {
115
+ let template_root = template_root(root);
116
+ let report = validate_task_state(
117
+ &template_root,
118
+ TaskStateOptions {
119
+ mode: TaskStateMode::State,
120
+ harness_health: Some(HarnessHealthOptions {
121
+ expected_integrity: packaged_harness_integrity()?,
122
+ allow_missing_archive: true,
123
+ ..HarnessHealthOptions::default()
124
+ }),
125
+ },
126
+ )?;
127
+ if report.errors.is_empty() {
128
+ Ok(())
129
+ } else {
130
+ Err(NaomeError::new(report.errors.join("\n")))
131
+ }
132
+ }
133
+
134
+ fn run_template_verification_contract_check(root: &Path) -> Result<(), NaomeError> {
135
+ let errors = validate_verification_contract(&template_root(root))?;
136
+ if errors.is_empty() {
137
+ Ok(())
138
+ } else {
139
+ Err(NaomeError::new(errors.join("\n")))
140
+ }
141
+ }
@@ -0,0 +1,73 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use crate::models::NaomeError;
5
+
6
+ pub(super) fn run_context_budget_check(root: &Path) -> Result<(), NaomeError> {
7
+ let template_root = template_root(root);
8
+ let mut context_files = vec![
9
+ template_root.join("AGENTS.md"),
10
+ template_root.join(".naomeignore"),
11
+ ];
12
+ context_files.extend(markdown_files(&template_root.join("docs").join("naome"))?);
13
+ context_files.sort();
14
+
15
+ let mut errors = Vec::new();
16
+ for path in context_files {
17
+ let content = fs::read_to_string(&path)?;
18
+ let line_count = count_lines(&content);
19
+ if line_count > 200 {
20
+ errors.push(format!(
21
+ "{}: {line_count} lines",
22
+ display_repo_path(root, &path)
23
+ ));
24
+ }
25
+ }
26
+
27
+ if errors.is_empty() {
28
+ Ok(())
29
+ } else {
30
+ Err(NaomeError::new(format!(
31
+ "NAOME context budget exceeded. Limit: 200 lines per file.\n{}",
32
+ errors.join("\n")
33
+ )))
34
+ }
35
+ }
36
+
37
+ pub(super) fn template_root(root: &Path) -> PathBuf {
38
+ root.join("packages")
39
+ .join("naome")
40
+ .join("templates")
41
+ .join("naome-root")
42
+ }
43
+
44
+ fn markdown_files(dir: &Path) -> Result<Vec<PathBuf>, NaomeError> {
45
+ let mut files = Vec::new();
46
+ for entry in fs::read_dir(dir)? {
47
+ let entry = entry?;
48
+ let path = entry.path();
49
+ if path.is_dir() {
50
+ files.extend(markdown_files(&path)?);
51
+ } else if path.is_file() && path.extension().is_some_and(|extension| extension == "md") {
52
+ files.push(path);
53
+ }
54
+ }
55
+ Ok(files)
56
+ }
57
+
58
+ fn count_lines(content: &str) -> usize {
59
+ if content.is_empty() {
60
+ 0
61
+ } else if content.ends_with('\n') {
62
+ content.split('\n').count() - 1
63
+ } else {
64
+ content.split('\n').count()
65
+ }
66
+ }
67
+
68
+ fn display_repo_path(root: &Path, path: &Path) -> String {
69
+ path.strip_prefix(root)
70
+ .unwrap_or(path)
71
+ .to_string_lossy()
72
+ .to_string()
73
+ }