@lamentis/naome 1.3.8 → 1.3.10
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.
- package/Cargo.lock +2 -2
- package/README.md +5 -0
- package/bin/naome.js +1 -1
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/architecture_commands.rs +123 -0
- package/crates/naome-cli/src/cli_args.rs +4 -0
- package/crates/naome-cli/src/dispatcher.rs +2 -0
- package/crates/naome-cli/src/install_bridge.rs +56 -8
- package/crates/naome-cli/src/main.rs +6 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/config/parser/scalar.rs +26 -0
- package/crates/naome-core/src/architecture/config/parser/sections.rs +137 -0
- package/crates/naome-core/src/architecture/config/parser.rs +96 -0
- package/crates/naome-core/src/architecture/config.rs +114 -0
- package/crates/naome-core/src/architecture/model.rs +80 -0
- package/crates/naome-core/src/architecture/output.rs +178 -0
- package/crates/naome-core/src/architecture/rules.rs +140 -0
- package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +56 -0
- package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +88 -0
- package/crates/naome-core/src/architecture/scan/graph_builder.rs +134 -0
- package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
- package/crates/naome-core/src/architecture/scan.rs +75 -0
- package/crates/naome-core/src/architecture.rs +31 -0
- package/crates/naome-core/src/harness_health/integrity.rs +41 -23
- package/crates/naome-core/src/harness_health/manifest.rs +97 -0
- package/crates/naome-core/src/harness_health.rs +58 -106
- package/crates/naome-core/src/install_plan.rs +2 -0
- package/crates/naome-core/src/lib.rs +16 -8
- package/crates/naome-core/src/quality/cache.rs +122 -19
- package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
- package/crates/naome-core/src/quality/scanner.rs +5 -2
- package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
- package/crates/naome-core/tests/architecture.rs +209 -0
- package/crates/naome-core/tests/harness_health.rs +150 -0
- package/crates/naome-core/tests/quality_performance.rs +63 -2
- package/installer/filesystem.js +38 -0
- package/installer/flows.js +6 -1
- package/installer/harness-file-ops.js +36 -8
- package/installer/harness-files.js +3 -0
- package/installer/manifest-state.js +2 -2
- package/installer/native.js +63 -18
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +23 -19
- package/templates/naome-root/.naome/bin/check-task-state.js +33 -40
- package/templates/naome-root/.naome/bin/naome.js +2 -2
- package/templates/naome-root/.naome/manifest.json +8 -6
- package/templates/naome-root/.naome/verification.json +15 -1
- package/templates/naome-root/docs/naome/architecture-fitness.md +97 -0
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/testing.md +6 -3
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
ArchitectureScanOptions,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
13
|
+
|
|
14
|
+
#[test]
|
|
15
|
+
fn parses_starter_architecture_config() {
|
|
16
|
+
let config = ArchitectureConfig::parse(default_architecture_config_text(), "test").unwrap();
|
|
17
|
+
|
|
18
|
+
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
|
+
);
|
|
24
|
+
assert_eq!(
|
|
25
|
+
config.ignore[0].reason,
|
|
26
|
+
"Generated code is not architecture-owned."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[test]
|
|
31
|
+
fn path_extractor_builds_language_agnostic_graph_for_mixed_repo() {
|
|
32
|
+
let repo = FixtureRepo::new();
|
|
33
|
+
repo.write("src/domain/event.ts", "export const event = 1;\n");
|
|
34
|
+
repo.write("src/infrastructure/db.py", "client = object()\n");
|
|
35
|
+
repo.write("cmd/server/main.go", "package main\n");
|
|
36
|
+
repo.write(
|
|
37
|
+
"naome.arch.yaml",
|
|
38
|
+
r#"
|
|
39
|
+
layers:
|
|
40
|
+
domain:
|
|
41
|
+
paths:
|
|
42
|
+
- "src/domain/**"
|
|
43
|
+
infrastructure:
|
|
44
|
+
paths:
|
|
45
|
+
- "src/infrastructure/**"
|
|
46
|
+
contexts:
|
|
47
|
+
app:
|
|
48
|
+
paths:
|
|
49
|
+
- "src/**"
|
|
50
|
+
public_api:
|
|
51
|
+
- "src/index.ts"
|
|
52
|
+
rules:
|
|
53
|
+
max_file_lines:
|
|
54
|
+
enabled: true
|
|
55
|
+
value: 400
|
|
56
|
+
severity: warning
|
|
57
|
+
"#,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
|
|
61
|
+
|
|
62
|
+
assert!(scan
|
|
63
|
+
.graph
|
|
64
|
+
.nodes
|
|
65
|
+
.iter()
|
|
66
|
+
.any(|node| node.id == "file:src/domain/event.ts"));
|
|
67
|
+
assert!(scan
|
|
68
|
+
.graph
|
|
69
|
+
.nodes
|
|
70
|
+
.iter()
|
|
71
|
+
.any(|node| node.id == "layer:domain"));
|
|
72
|
+
assert!(scan
|
|
73
|
+
.graph
|
|
74
|
+
.edges
|
|
75
|
+
.iter()
|
|
76
|
+
.any(|edge| { edge.from == "layer:domain" && edge.to == "file:src/domain/event.ts" }));
|
|
77
|
+
assert_eq!(
|
|
78
|
+
scan.file_facts
|
|
79
|
+
.get("cmd/server/main.go")
|
|
80
|
+
.unwrap()
|
|
81
|
+
.language
|
|
82
|
+
.as_deref(),
|
|
83
|
+
Some("go")
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn validates_file_size_budget_with_stable_json_shape() {
|
|
89
|
+
let repo = FixtureRepo::new();
|
|
90
|
+
repo.write(
|
|
91
|
+
"naome.arch.yaml",
|
|
92
|
+
r#"
|
|
93
|
+
rules:
|
|
94
|
+
max_file_lines:
|
|
95
|
+
enabled: true
|
|
96
|
+
value: 2
|
|
97
|
+
severity: error
|
|
98
|
+
"#,
|
|
99
|
+
);
|
|
100
|
+
repo.write("src/too_big.rs", "one\ntwo\nthree\n");
|
|
101
|
+
|
|
102
|
+
let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
|
|
103
|
+
let json = serde_json::to_string(&report).unwrap();
|
|
104
|
+
|
|
105
|
+
assert_eq!(report.status, "fail");
|
|
106
|
+
assert!(json.contains("\"schema\":\"naome.arch.validation.v1\""));
|
|
107
|
+
assert!(json.contains("\"id\":\"arch.max_file_lines\""));
|
|
108
|
+
let violation = report
|
|
109
|
+
.violations
|
|
110
|
+
.iter()
|
|
111
|
+
.find(|violation| violation.path.as_deref() == Some("src/too_big.rs"))
|
|
112
|
+
.expect("expected src/too_big.rs file-size violation");
|
|
113
|
+
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.");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn changed_only_generated_boundary_reports_changed_generated_file() {
|
|
118
|
+
let repo = FixtureRepo::new();
|
|
119
|
+
repo.init_git();
|
|
120
|
+
repo.write(
|
|
121
|
+
"naome.arch.yaml",
|
|
122
|
+
r#"
|
|
123
|
+
rules:
|
|
124
|
+
generated_manual_boundary:
|
|
125
|
+
enabled: true
|
|
126
|
+
severity: error
|
|
127
|
+
ignore:
|
|
128
|
+
- path: "generated/**"
|
|
129
|
+
reason: "Generated code is not manually edited."
|
|
130
|
+
"#,
|
|
131
|
+
);
|
|
132
|
+
repo.write("generated/client.ts", "export const generated = true;\n");
|
|
133
|
+
|
|
134
|
+
let report = validate_architecture(
|
|
135
|
+
repo.path(),
|
|
136
|
+
ArchitectureScanOptions {
|
|
137
|
+
changed_only: true,
|
|
138
|
+
config_path: None,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
.unwrap();
|
|
142
|
+
|
|
143
|
+
assert_eq!(report.status, "fail");
|
|
144
|
+
assert!(report.changed_only_degraded_to_full_scan);
|
|
145
|
+
assert_eq!(report.violations[0].id, "arch.generated_manual_boundary");
|
|
146
|
+
assert_eq!(
|
|
147
|
+
report.violations[0].path.as_deref(),
|
|
148
|
+
Some("generated/client.ts")
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
struct FixtureRepo {
|
|
153
|
+
root: PathBuf,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
impl FixtureRepo {
|
|
157
|
+
fn new() -> Self {
|
|
158
|
+
let nonce = SystemTime::now()
|
|
159
|
+
.duration_since(UNIX_EPOCH)
|
|
160
|
+
.unwrap()
|
|
161
|
+
.as_nanos();
|
|
162
|
+
let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
163
|
+
let root = std::env::temp_dir().join(format!(
|
|
164
|
+
"naome-arch-fixture-{}-{nonce}-{counter}",
|
|
165
|
+
std::process::id()
|
|
166
|
+
));
|
|
167
|
+
fs::create_dir_all(&root).unwrap();
|
|
168
|
+
fs::write(
|
|
169
|
+
root.join(".naomeignore"),
|
|
170
|
+
".naome/archive/\n.naome/tasks/\n",
|
|
171
|
+
)
|
|
172
|
+
.unwrap();
|
|
173
|
+
Self { root }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn path(&self) -> &Path {
|
|
177
|
+
&self.root
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn write(&self, relative_path: &str, content: &str) {
|
|
181
|
+
let path = self.root.join(relative_path);
|
|
182
|
+
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
183
|
+
fs::write(path, content).unwrap();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn init_git(&self) {
|
|
187
|
+
run_git(&self.root, &["init"]);
|
|
188
|
+
run_git(&self.root, &["config", "user.email", "naome@example.com"]);
|
|
189
|
+
run_git(&self.root, &["config", "user.name", "NAOME Test"]);
|
|
190
|
+
self.write("README.md", "# Fixture\n");
|
|
191
|
+
run_git(&self.root, &["add", "."]);
|
|
192
|
+
run_git(&self.root, &["commit", "-m", "baseline"]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn run_git(root: &Path, args: &[&str]) {
|
|
197
|
+
let result = Command::new("git")
|
|
198
|
+
.args(args)
|
|
199
|
+
.current_dir(root)
|
|
200
|
+
.output()
|
|
201
|
+
.unwrap();
|
|
202
|
+
assert!(
|
|
203
|
+
result.status.success(),
|
|
204
|
+
"git {:?} failed: {}{}",
|
|
205
|
+
args,
|
|
206
|
+
String::from_utf8_lossy(&result.stdout),
|
|
207
|
+
String::from_utf8_lossy(&result.stderr)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -19,11 +19,17 @@ 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",
|
|
25
26
|
];
|
|
26
27
|
|
|
28
|
+
#[cfg(windows)]
|
|
29
|
+
const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust.exe";
|
|
30
|
+
#[cfg(not(windows))]
|
|
31
|
+
const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust";
|
|
32
|
+
|
|
27
33
|
const PROJECT_OWNED_PATHS: &[&str] = &[
|
|
28
34
|
".naomeignore",
|
|
29
35
|
".naome/init-state.json",
|
|
@@ -78,6 +84,99 @@ fn rejects_drifted_machine_owned_files() {
|
|
|
78
84
|
assert!(joined.contains("docs/naome/execution.md"));
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
#[test]
|
|
88
|
+
fn accepts_native_decision_binary_with_manifest_ownership_and_integrity() {
|
|
89
|
+
let mut repo = HarnessFixture::new();
|
|
90
|
+
repo.install_native_decision_binary("native decision fixture\n");
|
|
91
|
+
|
|
92
|
+
let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
|
|
93
|
+
|
|
94
|
+
assert!(errors.is_empty(), "{errors:#?}");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn rejects_native_decision_binary_when_manifest_ownership_is_removed() {
|
|
99
|
+
let mut repo = HarnessFixture::new();
|
|
100
|
+
repo.install_native_decision_binary("native decision fixture\n");
|
|
101
|
+
repo.remove_native_manifest_entry();
|
|
102
|
+
|
|
103
|
+
let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
|
|
104
|
+
let joined = errors.join("\n");
|
|
105
|
+
|
|
106
|
+
assert!(
|
|
107
|
+
joined.contains(&format!(
|
|
108
|
+
".naome/manifest.json machineOwned must include {NATIVE_BINARY_PATH}."
|
|
109
|
+
)),
|
|
110
|
+
"{joined}"
|
|
111
|
+
);
|
|
112
|
+
assert!(
|
|
113
|
+
joined.contains(&format!(
|
|
114
|
+
".naome/manifest.json integrity missing {NATIVE_BINARY_PATH}."
|
|
115
|
+
)),
|
|
116
|
+
"{joined}"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[test]
|
|
121
|
+
fn rejects_checker_declared_native_integrity_without_native_manifest_entry() {
|
|
122
|
+
let mut repo = HarnessFixture::new();
|
|
123
|
+
let checker_path = ".naome/bin/check-task-state.js";
|
|
124
|
+
let native_hash = format!("sha256:{}", "a".repeat(64));
|
|
125
|
+
repo.write(
|
|
126
|
+
checker_path,
|
|
127
|
+
&format!("const expectedNativeBinaryIntegrity = \"{native_hash}\";\n"),
|
|
128
|
+
);
|
|
129
|
+
repo.integrity.insert(
|
|
130
|
+
checker_path.to_string(),
|
|
131
|
+
format!(
|
|
132
|
+
"sha256:{}",
|
|
133
|
+
sha256("const expectedNativeBinaryIntegrity = \"sha256:generated\";\n")
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
write_file(
|
|
137
|
+
repo.path(),
|
|
138
|
+
".naome/manifest.json",
|
|
139
|
+
&pretty_json(manifest_fixture(&repo.integrity)),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
|
|
143
|
+
let joined = errors.join("\n");
|
|
144
|
+
|
|
145
|
+
assert!(
|
|
146
|
+
joined.contains(&format!(
|
|
147
|
+
".naome/manifest.json machineOwned must include {NATIVE_BINARY_PATH}."
|
|
148
|
+
)),
|
|
149
|
+
"{joined}"
|
|
150
|
+
);
|
|
151
|
+
assert!(
|
|
152
|
+
joined.contains(&format!("{NATIVE_BINARY_PATH} is missing.")),
|
|
153
|
+
"{joined}"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn rejects_native_integrity_assignment_with_appended_code() {
|
|
159
|
+
let mut repo = HarnessFixture::new();
|
|
160
|
+
repo.install_native_decision_binary("native decision fixture\n");
|
|
161
|
+
let native_hash = repo.integrity.get(NATIVE_BINARY_PATH).unwrap().clone();
|
|
162
|
+
repo.write(
|
|
163
|
+
".naome/bin/check-harness-health.js",
|
|
164
|
+
&format!("const expectedNativeBinaryIntegrity = \"{native_hash}\"; process.exit(0);\n"),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
|
|
168
|
+
let joined = errors.join("\n");
|
|
169
|
+
|
|
170
|
+
assert!(
|
|
171
|
+
joined.contains(".naome/bin/check-harness-health.js integrity mismatch"),
|
|
172
|
+
"{joined}"
|
|
173
|
+
);
|
|
174
|
+
assert!(
|
|
175
|
+
joined.contains(".naome/bin/check-harness-health.js native binary integrity does not match .naome/manifest.json."),
|
|
176
|
+
"{joined}"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
81
180
|
#[test]
|
|
82
181
|
fn rejects_missing_archive_ignore_boundary() {
|
|
83
182
|
let repo = HarnessFixture::new();
|
|
@@ -175,6 +274,53 @@ impl HarnessFixture {
|
|
|
175
274
|
fn write(&self, relative_path: &str, content: &str) {
|
|
176
275
|
write_file(&self.root, relative_path, content);
|
|
177
276
|
}
|
|
277
|
+
|
|
278
|
+
fn install_native_decision_binary(&mut self, content: &str) {
|
|
279
|
+
let native_hash = format!("sha256:{}", sha256(content));
|
|
280
|
+
write_file(&self.root, NATIVE_BINARY_PATH, content);
|
|
281
|
+
self.integrity
|
|
282
|
+
.insert(NATIVE_BINARY_PATH.to_string(), native_hash.clone());
|
|
283
|
+
|
|
284
|
+
for relative_path in [
|
|
285
|
+
".naome/bin/naome.js",
|
|
286
|
+
".naome/bin/check-harness-health.js",
|
|
287
|
+
".naome/bin/check-task-state.js",
|
|
288
|
+
] {
|
|
289
|
+
let command_content =
|
|
290
|
+
format!("const expectedNativeBinaryIntegrity = \"{native_hash}\";\n");
|
|
291
|
+
let normalized_command_content =
|
|
292
|
+
"const expectedNativeBinaryIntegrity = \"sha256:generated\";\n";
|
|
293
|
+
write_file(&self.root, relative_path, &command_content);
|
|
294
|
+
self.integrity.insert(
|
|
295
|
+
relative_path.to_string(),
|
|
296
|
+
format!("sha256:{}", sha256(normalized_command_content)),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
self.write_manifest_with_native_entry();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn remove_native_manifest_entry(&self) {
|
|
304
|
+
let mut manifest = read_json_value(&self.root, ".naome/manifest.json");
|
|
305
|
+
manifest["machineOwned"]
|
|
306
|
+
.as_array_mut()
|
|
307
|
+
.unwrap()
|
|
308
|
+
.retain(|entry| entry.as_str() != Some(NATIVE_BINARY_PATH));
|
|
309
|
+
manifest["integrity"]
|
|
310
|
+
.as_object_mut()
|
|
311
|
+
.unwrap()
|
|
312
|
+
.remove(NATIVE_BINARY_PATH);
|
|
313
|
+
write_file(&self.root, ".naome/manifest.json", &pretty_json(manifest));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
fn write_manifest_with_native_entry(&self) {
|
|
317
|
+
let mut manifest = manifest_fixture(&self.integrity);
|
|
318
|
+
manifest["machineOwned"]
|
|
319
|
+
.as_array_mut()
|
|
320
|
+
.unwrap()
|
|
321
|
+
.push(json!(NATIVE_BINARY_PATH));
|
|
322
|
+
write_file(&self.root, ".naome/manifest.json", &pretty_json(manifest));
|
|
323
|
+
}
|
|
178
324
|
}
|
|
179
325
|
|
|
180
326
|
fn manifest_fixture(integrity: &HashMap<String, String>) -> serde_json::Value {
|
|
@@ -217,6 +363,10 @@ fn machine_content(relative_path: &str) -> String {
|
|
|
217
363
|
}
|
|
218
364
|
}
|
|
219
365
|
|
|
366
|
+
fn read_json_value(root: &Path, relative_path: &str) -> serde_json::Value {
|
|
367
|
+
serde_json::from_str(&fs::read_to_string(root.join(relative_path)).unwrap()).unwrap()
|
|
368
|
+
}
|
|
369
|
+
|
|
220
370
|
fn write_file(root: &Path, relative_path: &str, content: &str) {
|
|
221
371
|
let path = root.join(relative_path);
|
|
222
372
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
@@ -4,8 +4,8 @@ use std::fs;
|
|
|
4
4
|
|
|
5
5
|
use naome_core::{
|
|
6
6
|
check_repository_quality, check_repository_quality_paths, check_semantic_legacy,
|
|
7
|
-
init_repository_quality, init_repository_quality_with_mode,
|
|
8
|
-
QualityInitMode, QualityMode,
|
|
7
|
+
clear_quality_cache, init_repository_quality, init_repository_quality_with_mode,
|
|
8
|
+
quality_cache_status, QualityInitMode, QualityMode,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
use repo_support::TestRepo;
|
|
@@ -140,6 +140,67 @@ fn second_report_uses_file_analysis_cache() {
|
|
|
140
140
|
assert_eq!(second.summary.cache_misses, 0);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
#[cfg(unix)]
|
|
144
|
+
#[test]
|
|
145
|
+
fn report_skips_repository_symlinks_without_caching_target_contents() {
|
|
146
|
+
let repo = quality_repo("quality-cache-skips-file-symlink");
|
|
147
|
+
let victim = repo.path().join("../naome-victim-secret.txt");
|
|
148
|
+
fs::write(&victim, "VALIDATION_SECRET_raw_lines_12345\n").unwrap();
|
|
149
|
+
std::os::unix::fs::symlink(&victim, repo.path().join("leak.txt")).unwrap();
|
|
150
|
+
repo.git(&["add", "leak.txt"]);
|
|
151
|
+
repo.git(&["commit", "-m", "tracked symlink"]);
|
|
152
|
+
|
|
153
|
+
let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
|
|
154
|
+
let cache = quality_cache_status(repo.path()).unwrap();
|
|
155
|
+
|
|
156
|
+
assert!(!report.scanned_paths.contains(&"leak.txt".to_string()));
|
|
157
|
+
assert_eq!(cache.entry_count, 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[cfg(unix)]
|
|
161
|
+
#[test]
|
|
162
|
+
fn path_budget_skips_symlinks_before_reading_target_metadata() {
|
|
163
|
+
let repo = quality_repo("quality-budget-skips-symlink");
|
|
164
|
+
let victim = repo.path().join("../naome-large-victim.txt");
|
|
165
|
+
fs::write(&victim, "x".repeat(1024 * 1024 + 1)).unwrap();
|
|
166
|
+
std::os::unix::fs::symlink(&victim, repo.path().join("large-link.txt")).unwrap();
|
|
167
|
+
repo.git(&["add", "large-link.txt"]);
|
|
168
|
+
repo.git(&["commit", "-m", "tracked large symlink"]);
|
|
169
|
+
|
|
170
|
+
let report = check_repository_quality_paths(repo.path(), &["large-link.txt"]).unwrap();
|
|
171
|
+
|
|
172
|
+
assert!(!report.scanned_paths.contains(&"large-link.txt".to_string()));
|
|
173
|
+
assert!(!report
|
|
174
|
+
.summary
|
|
175
|
+
.reason_codes
|
|
176
|
+
.contains(&"max_file_bytes".to_string()));
|
|
177
|
+
fs::remove_file(victim).unwrap();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[cfg(unix)]
|
|
181
|
+
#[test]
|
|
182
|
+
fn cache_operations_reject_symlinked_cache_path_components() {
|
|
183
|
+
let repo = quality_repo("quality-cache-rejects-cache-symlink");
|
|
184
|
+
repo.write_file("src/a.js", "export const value = 1;\n");
|
|
185
|
+
repo.commit_all("baseline");
|
|
186
|
+
let outside = repo.path().join("../naome-outside-cache");
|
|
187
|
+
fs::create_dir_all(outside.join("quality")).unwrap();
|
|
188
|
+
fs::remove_dir_all(repo.path().join(".naome/cache")).ok();
|
|
189
|
+
std::os::unix::fs::symlink(&outside, repo.path().join(".naome/cache")).unwrap();
|
|
190
|
+
|
|
191
|
+
let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
|
|
192
|
+
let clear_error = clear_quality_cache(repo.path()).unwrap_err();
|
|
193
|
+
|
|
194
|
+
assert_eq!(report.summary.cache_hits, 0);
|
|
195
|
+
assert!(outside.join("quality").is_dir());
|
|
196
|
+
assert!(!fs::read_dir(outside.join("quality"))
|
|
197
|
+
.unwrap()
|
|
198
|
+
.any(|entry| entry.is_ok()));
|
|
199
|
+
assert!(clear_error
|
|
200
|
+
.to_string()
|
|
201
|
+
.contains("must not contain symlinks"));
|
|
202
|
+
}
|
|
203
|
+
|
|
143
204
|
#[test]
|
|
144
205
|
fn report_budget_marks_truncated_reports() {
|
|
145
206
|
let repo = quality_repo("quality-report-budget");
|
package/installer/filesystem.js
CHANGED
|
@@ -64,6 +64,44 @@ export function archiveUpgradePath(ctx, archiveDirName, relativePath) {
|
|
|
64
64
|
return join(ctx.targetRoot, ".naome", "archive", archiveDirName, relativePath);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export function assertWritableArchivePath(ctx, archiveDirName, relativePath) {
|
|
68
|
+
const archiveRelativePath = join(".naome", "archive", archiveDirName, relativePath);
|
|
69
|
+
const parts = archiveRelativePath.split(/[\\/]+/);
|
|
70
|
+
let current = ctx.targetRoot;
|
|
71
|
+
|
|
72
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
73
|
+
const part = parts[index];
|
|
74
|
+
const isLeaf = index === parts.length - 1;
|
|
75
|
+
current = join(current, part);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const stats = lstatSync(current);
|
|
79
|
+
if (stats.isSymbolicLink()) {
|
|
80
|
+
printError(ctx, `NAOME cannot archive ${relativePath} safely.`);
|
|
81
|
+
console.error(`${archiveRelativePath} must not contain symbolic links.`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!isLeaf && !stats.isDirectory()) {
|
|
86
|
+
printError(ctx, `NAOME cannot archive ${relativePath} because the archive path is not a directory.`);
|
|
87
|
+
console.error(`${join(...parts.slice(0, index + 1))} must be a regular directory or must not exist.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isLeaf && existsSync(current) && !stats.isFile()) {
|
|
92
|
+
printError(ctx, `NAOME cannot archive ${relativePath} because the archive path is not a file.`);
|
|
93
|
+
console.error(`${archiveRelativePath} must be a regular file or must not exist.`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code === "ENOENT") {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
67
105
|
export function ensureArchiveDirectory(ctx) {
|
|
68
106
|
const archivePath = ".naome/archive";
|
|
69
107
|
const targetPath = join(ctx.targetRoot, archivePath);
|
package/installer/flows.js
CHANGED
|
@@ -31,6 +31,8 @@ const legacyOptionalHookPaths = [
|
|
|
31
31
|
"docs/naome/codex-hooks.md",
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
+
const trustedRetiredMachineOwnedPaths = new Set(legacyOptionalHookPaths);
|
|
35
|
+
|
|
34
36
|
export async function runFreshInstall(ctx) {
|
|
35
37
|
await confirmAgentsTakeover(ctx);
|
|
36
38
|
|
|
@@ -110,8 +112,11 @@ function removeRetiredMachineOwnedFiles(ctx, manifest, archiveDirName, options =
|
|
|
110
112
|
...ctx.localOnlyMachineOwnedPaths,
|
|
111
113
|
ctx.nativeBinaryRelativePath,
|
|
112
114
|
].filter(Boolean));
|
|
115
|
+
const manifestRetiredPaths = Array.isArray(manifest?.machineOwned)
|
|
116
|
+
? manifest.machineOwned.filter((path) => trustedRetiredMachineOwnedPaths.has(path))
|
|
117
|
+
: [];
|
|
113
118
|
const retiredPaths = [
|
|
114
|
-
...
|
|
119
|
+
...manifestRetiredPaths,
|
|
115
120
|
...(Array.isArray(options.extraPaths) ? options.extraPaths : []),
|
|
116
121
|
];
|
|
117
122
|
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
} from "node:fs";
|
|
10
10
|
import { dirname, join, relative } from "node:path";
|
|
11
11
|
|
|
12
|
-
import { archiveUpgradePath, hasSymlinkInTargetPath } from "./filesystem.js";
|
|
13
|
-
import {
|
|
12
|
+
import { archiveUpgradePath, assertWritableArchivePath, hasSymlinkInTargetPath } from "./filesystem.js";
|
|
13
|
+
import { machineFileHash } from "./native.js";
|
|
14
14
|
import { printError } from "./output.js";
|
|
15
15
|
|
|
16
16
|
export function ensureTemplateFile(ctx, relativePath) {
|
|
@@ -55,8 +55,13 @@ export function removeLegacyHarnessFile(ctx, relativePath, archiveDirName) {
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const archivePath =
|
|
58
|
+
const archivePath = safeArchivePath(ctx, archiveDirName, relativePath);
|
|
59
|
+
if (archivePath === null) {
|
|
60
|
+
failUnsafeArchivePath(ctx, archiveDirName, relativePath);
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
mkdirSync(dirname(archivePath), { recursive: true });
|
|
64
|
+
assertWritableArchivePath(ctx, archiveDirName, relativePath);
|
|
60
65
|
copyFileSync(targetPath, archivePath);
|
|
61
66
|
unlinkSync(targetPath);
|
|
62
67
|
ctx.updated.push(relativePath);
|
|
@@ -128,17 +133,40 @@ function replaceChangedHarnessFile(ctx, relativePath, archiveDirName, sourcePath
|
|
|
128
133
|
return;
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
const archivePath =
|
|
136
|
+
const archivePath = safeArchivePath(ctx, archiveDirName, relativePath);
|
|
137
|
+
if (archivePath === null) {
|
|
138
|
+
failUnsafeArchivePath(ctx, archiveDirName, relativePath);
|
|
139
|
+
}
|
|
140
|
+
|
|
132
141
|
mkdirSync(dirname(archivePath), { recursive: true });
|
|
142
|
+
assertWritableArchivePath(ctx, archiveDirName, relativePath);
|
|
133
143
|
copyFileSync(targetPath, archivePath);
|
|
134
144
|
writeFileSync(targetPath, nextContent);
|
|
135
145
|
ctx.updated.push(relativePath);
|
|
136
146
|
ctx.archived.push({ from: relativePath, to: relative(ctx.targetRoot, archivePath) });
|
|
137
147
|
}
|
|
138
148
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
149
|
+
function safeArchivePath(ctx, archiveDirName, relativePath) {
|
|
150
|
+
const archivePath = archiveUpgradePath(ctx, archiveDirName, relativePath);
|
|
151
|
+
const archiveParent = relative(ctx.targetRoot, dirname(archivePath));
|
|
152
|
+
if (hasSymlinkInTargetPath(ctx, archiveParent)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (existsSync(archivePath)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return archivePath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function failUnsafeArchivePath(ctx, archiveDirName, relativePath) {
|
|
163
|
+
printError(ctx, `NAOME cannot archive ${relativePath} safely.`);
|
|
164
|
+
console.error(
|
|
165
|
+
`${relative(ctx.targetRoot, archiveUpgradePath(ctx, archiveDirName, relativePath))} must not contain symlinks or pre-existing files.`
|
|
143
166
|
);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function unchangedMachineHash(ctx, relativePath, currentContent, nextContent) {
|
|
171
|
+
return machineFileHash(ctx, relativePath, currentContent) === machineFileHash(ctx, relativePath, nextContent);
|
|
144
172
|
}
|
|
@@ -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
|
}
|
|
@@ -2,7 +2,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "n
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { hasSymlinkInTargetPath } from "./filesystem.js";
|
|
5
|
-
import {
|
|
5
|
+
import { installedMachineOwnedIntegrity, installedNativeBinaryHash, usesSourceNativeFallback } from "./native.js";
|
|
6
6
|
import { printError } from "./output.js";
|
|
7
7
|
import { isVersion } from "./version.js";
|
|
8
8
|
|
|
@@ -118,7 +118,7 @@ function applyManifestHealthMetadata(ctx, manifest) {
|
|
|
118
118
|
manifest.harnessVersion = ctx.packageVersion;
|
|
119
119
|
manifest.machineOwned = [...ctx.machineOwnedPaths];
|
|
120
120
|
manifest.projectOwned = ctx.projectOwnedPaths;
|
|
121
|
-
manifest.integrity =
|
|
121
|
+
manifest.integrity = installedMachineOwnedIntegrity(ctx);
|
|
122
122
|
|
|
123
123
|
const nativeHash = installedNativeBinaryHash(ctx);
|
|
124
124
|
if (!usesSourceNativeFallback(ctx) && nativeHash) {
|