@lamentis/naome 1.3.17 → 1.4.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.
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.3.17"
79
+ version = "1.4.0"
80
80
  dependencies = [
81
81
  "naome-core",
82
82
  "serde_json",
@@ -84,7 +84,7 @@ dependencies = [
84
84
 
85
85
  [[package]]
86
86
  name = "naome-core"
87
- version = "1.3.17"
87
+ version = "1.4.0"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.3.17"
3
+ version = "1.4.0"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -2,9 +2,9 @@ use std::fs;
2
2
  use std::path::{Path, PathBuf};
3
3
 
4
4
  use naome_core::{
5
- config_findings_for, format_architecture_explain, format_architecture_scan,
6
- format_architecture_validation, scan_architecture, validate_architecture,
7
- ArchitectureScanOptions, ARCHITECTURE_RULE_IDS,
5
+ architecture_validation_sarif_with_root, config_findings_for, format_architecture_explain,
6
+ format_architecture_scan, format_architecture_validation, scan_architecture,
7
+ validate_architecture, ArchitectureScanOptions, ARCHITECTURE_RULE_IDS,
8
8
  };
9
9
 
10
10
  use crate::architecture_init::architecture_init_config_text;
@@ -88,6 +88,19 @@ fn run_arch_validate(root: &Path, args: &[String]) -> Result<(), Box<dyn std::er
88
88
  root,
89
89
  scan_options(root, args, has_flag(args, "--changed-only")),
90
90
  )?;
91
+ if has_flag(args, "--sarif") {
92
+ let output =
93
+ serde_json::to_string_pretty(&architecture_validation_sarif_with_root(&report, root))?;
94
+ if let Some(path) = option_value(args, "--output") {
95
+ fs::write(root.join(path), output)?;
96
+ } else {
97
+ println!("{output}");
98
+ }
99
+ if report.status == "fail" {
100
+ std::process::exit(1);
101
+ }
102
+ return Ok(());
103
+ }
91
104
  let json = has_flag(args, "--json") || has_flag(args, "--agent-feedback");
92
105
  if json {
93
106
  if has_flag(args, "--agent-feedback") {
@@ -62,7 +62,7 @@ const HELP: &str = r#"Usage:
62
62
  naome arch init [--config <path>] [--json]
63
63
  naome arch explain [--config <path>] [--json]
64
64
  naome arch scan [--config <path>] [--changed-only] [--write] [--output <path>] [--json]
65
- naome arch validate [--config <path>] [--changed-only] [--agent-feedback] [--json]
65
+ naome arch validate [--config <path>] [--changed-only] [--agent-feedback] [--json|--sarif] [--output <path>]
66
66
  naome workflow search-profile [--json]
67
67
  naome workflow agent-plan [--json]
68
68
  naome workflow context-delta [--read-path <path>...] [--json]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.3.17"
3
+ version = "1.4.0"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -1,8 +1,14 @@
1
1
  use serde::{Deserialize, Serialize};
2
+ use serde_json::{json, Value};
3
+ use std::collections::BTreeSet;
4
+ use std::path::Path;
2
5
 
3
6
  use super::model::{Severity, SourceRange};
4
7
  use super::scan::ArchitectureScanReport;
5
8
 
9
+ const SARIF_REPO_ROOT_BASE_ID: &str = "REPO_ROOT";
10
+ const ARCHITECTURE_CONFIG_PATH: &str = "naome.arch.yaml";
11
+
6
12
  #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7
13
  #[serde(rename_all = "camelCase")]
8
14
  pub struct ViolationSummary {
@@ -216,3 +222,193 @@ pub fn format_architecture_scan(report: &ArchitectureScanReport) -> String {
216
222
  }
217
223
  output
218
224
  }
225
+
226
+ pub fn architecture_validation_sarif(report: &ArchitectureValidation) -> Value {
227
+ architecture_validation_sarif_with_root(report, Path::new("/"))
228
+ }
229
+
230
+ pub fn architecture_validation_sarif_with_root(
231
+ report: &ArchitectureValidation,
232
+ repo_root: &Path,
233
+ ) -> Value {
234
+ let mut rule_ids = ARCHITECTURE_RULE_IDS
235
+ .iter()
236
+ .map(|rule| rule.to_string())
237
+ .collect::<BTreeSet<_>>();
238
+ for finding in &report.config_findings {
239
+ rule_ids.insert(finding.id.clone());
240
+ }
241
+ for violation in &report.violations {
242
+ rule_ids.insert(violation.id.clone());
243
+ }
244
+
245
+ let rules = rule_ids
246
+ .iter()
247
+ .map(|id| {
248
+ json!({
249
+ "id": id,
250
+ "name": id,
251
+ "shortDescription": {
252
+ "text": id
253
+ },
254
+ "helpUri": "https://github.com/Lamentis-O/naome/blob/main/docs/naome/architecture-fitness.md"
255
+ })
256
+ })
257
+ .collect::<Vec<_>>();
258
+
259
+ let mut results = Vec::new();
260
+ for violation in &report.violations {
261
+ results.push(sarif_result_for_violation(violation));
262
+ }
263
+ for finding in &report.config_findings {
264
+ results.push(sarif_result_for_config_finding(finding));
265
+ }
266
+
267
+ json!({
268
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
269
+ "version": "2.1.0",
270
+ "runs": [
271
+ {
272
+ "tool": {
273
+ "driver": {
274
+ "name": "NAOME Architecture Fitness",
275
+ "informationUri": "https://github.com/Lamentis-O/naome",
276
+ "rules": rules
277
+ }
278
+ },
279
+ "originalUriBaseIds": {
280
+ "REPO_ROOT": {
281
+ "uri": file_uri_for_directory(repo_root)
282
+ }
283
+ },
284
+ "results": results,
285
+ "properties": {
286
+ "status": report.status,
287
+ "filesScanned": report.files_scanned,
288
+ "graphNodes": report.graph_nodes,
289
+ "graphEdges": report.graph_edges,
290
+ "changedOnlyRequested": report.changed_only_requested,
291
+ "changedOnlyMode": report.changed_only_mode,
292
+ "changedOnlyDegradedToFullScan": report.changed_only_degraded_to_full_scan,
293
+ "changedOnlyDegradationReason": report.changed_only_degradation_reason
294
+ }
295
+ }
296
+ ]
297
+ })
298
+ }
299
+
300
+ fn sarif_result_for_violation(violation: &ArchitectureViolation) -> Value {
301
+ json!({
302
+ "ruleId": violation.id,
303
+ "level": sarif_level_for_severity(violation.severity),
304
+ "message": {
305
+ "text": violation.message
306
+ },
307
+ "locations": sarif_locations(
308
+ violation.path.as_deref().or_else(|| violation_file_endpoint(violation)),
309
+ violation.source_range.as_ref()
310
+ ),
311
+ "properties": {
312
+ "type": violation.violation_type,
313
+ "from": violation.from,
314
+ "to": violation.to,
315
+ "suggestion": violation.suggestion,
316
+ "agentInstruction": violation.agent_instruction
317
+ }
318
+ })
319
+ }
320
+
321
+ fn sarif_result_for_config_finding(finding: &ArchitectureConfigFinding) -> Value {
322
+ let path = config_finding_path(finding);
323
+ json!({
324
+ "ruleId": finding.id,
325
+ "level": sarif_level_for_config_finding(&finding.severity),
326
+ "message": {
327
+ "text": finding.message
328
+ },
329
+ "locations": sarif_locations(Some(path.as_str()), None),
330
+ "properties": {
331
+ "subject": finding.subject,
332
+ "suggestion": finding.suggestion,
333
+ "agentInstruction": finding.agent_instruction
334
+ }
335
+ })
336
+ }
337
+
338
+ fn sarif_locations(path: Option<&str>, source_range: Option<&SourceRange>) -> Vec<Value> {
339
+ let Some(path) = path else {
340
+ return Vec::new();
341
+ };
342
+ let mut physical_location = json!({
343
+ "artifactLocation": {
344
+ "uri": path,
345
+ "uriBaseId": SARIF_REPO_ROOT_BASE_ID
346
+ }
347
+ });
348
+ if let Some(range) = source_range {
349
+ physical_location["region"] = json!({
350
+ "startLine": range.start_line,
351
+ "startColumn": range.start_column,
352
+ "endLine": range.end_line,
353
+ "endColumn": range.end_column
354
+ });
355
+ }
356
+ vec![json!({
357
+ "physicalLocation": physical_location
358
+ })]
359
+ }
360
+
361
+ fn violation_file_endpoint(violation: &ArchitectureViolation) -> Option<&str> {
362
+ [&violation.from, &violation.to]
363
+ .into_iter()
364
+ .flatten()
365
+ .find_map(|endpoint| endpoint.strip_prefix("file:"))
366
+ }
367
+
368
+ fn config_finding_path(finding: &ArchitectureConfigFinding) -> String {
369
+ finding
370
+ .subject
371
+ .strip_prefix("file:")
372
+ .unwrap_or(ARCHITECTURE_CONFIG_PATH)
373
+ .to_string()
374
+ }
375
+
376
+ fn sarif_level_for_severity(severity: Severity) -> &'static str {
377
+ match severity {
378
+ Severity::Error => "error",
379
+ Severity::Warning => "warning",
380
+ Severity::Info => "note",
381
+ }
382
+ }
383
+
384
+ fn sarif_level_for_config_finding(severity: &str) -> &'static str {
385
+ match severity {
386
+ "error" => "error",
387
+ "warning" => "warning",
388
+ _ => "note",
389
+ }
390
+ }
391
+
392
+ fn file_uri_for_directory(path: &Path) -> String {
393
+ let mut normalized = path.to_string_lossy().replace('\\', "/");
394
+ if !normalized.starts_with('/') {
395
+ normalized.insert(0, '/');
396
+ }
397
+ while normalized.ends_with('/') && normalized.len() > 1 {
398
+ normalized.pop();
399
+ }
400
+ format!("file://{}/", encode_uri_path(&normalized))
401
+ }
402
+
403
+ fn encode_uri_path(path: &str) -> String {
404
+ let mut encoded = String::new();
405
+ for byte in path.as_bytes() {
406
+ match *byte {
407
+ b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'/' | b'-' | b'.' | b'_' | b'~' => {
408
+ encoded.push(*byte as char)
409
+ }
410
+ value => encoded.push_str(&format!("%{value:02X}")),
411
+ }
412
+ }
413
+ encoded
414
+ }
@@ -9,7 +9,7 @@ use super::FileFact;
9
9
  use crate::models::NaomeError;
10
10
 
11
11
  const CACHE_SCHEMA: &str = "naome.architecture-cache.v1";
12
- const EXTRACTOR_VERSION: &str = "architecture-cache-v1.3.17";
12
+ const EXTRACTOR_VERSION: &str = "architecture-cache-v1.4.0";
13
13
  const CACHE_PATH: &str = ".naome/cache/architecture/cache.json";
14
14
 
15
15
  #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -20,6 +20,7 @@ pub use model::{
20
20
  ArchitectureNode, ArchitectureNodeKind, Severity, SourceRange,
21
21
  };
22
22
  pub use output::{
23
+ architecture_validation_sarif, architecture_validation_sarif_with_root,
23
24
  format_architecture_scan, format_architecture_validation, ArchitectureAgentFeedback,
24
25
  ArchitectureConfigFinding, ArchitectureValidation, ArchitectureViolation, ViolationSummary,
25
26
  ARCHITECTURE_RULE_IDS,
@@ -21,13 +21,14 @@ mod verification_contract_policy;
21
21
  mod workflow;
22
22
 
23
23
  pub use architecture::{
24
- config_findings_for, default_architecture_config_text, format_architecture_explain,
25
- format_architecture_scan, format_architecture_validation, scan_architecture,
26
- validate_architecture, ArchitectureAgentFeedback, ArchitectureConfig,
27
- ArchitectureConfigFinding, ArchitectureEdge, ArchitectureEdgeKind, ArchitectureGraph,
28
- ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind, ArchitectureScanOptions,
29
- ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation, ContextConfig,
30
- LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary, ARCHITECTURE_RULE_IDS,
24
+ architecture_validation_sarif, architecture_validation_sarif_with_root, config_findings_for,
25
+ default_architecture_config_text, format_architecture_explain, format_architecture_scan,
26
+ format_architecture_validation, scan_architecture, validate_architecture,
27
+ ArchitectureAgentFeedback, ArchitectureConfig, ArchitectureConfigFinding, ArchitectureEdge,
28
+ ArchitectureEdgeKind, ArchitectureGraph, ArchitectureMetadata, ArchitectureNode,
29
+ ArchitectureNodeKind, ArchitectureScanOptions, ArchitectureScanReport, ArchitectureValidation,
30
+ ArchitectureViolation, ContextConfig, LayerConfig, RuleConfig, Severity, SourceRange,
31
+ ViolationSummary, ARCHITECTURE_RULE_IDS,
31
32
  };
32
33
  pub use context::{
33
34
  select_context_for_changed_paths, select_context_for_prompt, ContextBudgetLedger,
@@ -210,7 +210,7 @@ fn changed_only_degrades_when_cache_extractor_version_is_stale() {
210
210
  let cache_path = repo.path().join(".naome/cache/architecture/cache.json");
211
211
  let stale_cache = std::fs::read_to_string(&cache_path)
212
212
  .unwrap()
213
- .replace("architecture-cache-v1.3.17", "architecture-cache-v1.3.14");
213
+ .replace("architecture-cache-v1.4.0", "architecture-cache-v1.3.17");
214
214
  std::fs::write(cache_path, stale_cache).unwrap();
215
215
 
216
216
  let report = validate_architecture(repo.path(), changed_only()).unwrap();
@@ -1,4 +1,7 @@
1
- use naome_core::{format_architecture_validation, validate_architecture, ArchitectureScanOptions};
1
+ use naome_core::{
2
+ architecture_validation_sarif_with_root, format_architecture_validation, validate_architecture,
3
+ ArchitectureScanOptions,
4
+ };
2
5
 
3
6
  mod architecture_support;
4
7
 
@@ -152,3 +155,67 @@ fn validation_reports_unresolved_repo_absolute_imports() {
152
155
  .any(|item| item.contains("unresolved imports"))
153
156
  }));
154
157
  }
158
+
159
+ #[test]
160
+ fn validation_sarif_includes_violations_and_config_findings() {
161
+ let repo = FixtureRepo::new();
162
+ repo.write(
163
+ "naome.arch.yaml",
164
+ r#"
165
+ layers:
166
+ domain:
167
+ paths:
168
+ - "src/domain/**"
169
+ infrastructure:
170
+ paths:
171
+ - "src/infrastructure/**"
172
+ unused:
173
+ paths:
174
+ - "src/unused/**"
175
+ allowed_dependencies:
176
+ domain:
177
+ infrastructure:
178
+ rules:
179
+ no_forbidden_layer_dependencies:
180
+ enabled: true
181
+ severity: error
182
+ "#,
183
+ );
184
+ repo.write(
185
+ "src/domain/event.ts",
186
+ "import { db } from '../infrastructure/db';\nimport missing from './missing';\n",
187
+ );
188
+ repo.write("src/infrastructure/db.ts", "export const db = 1;\n");
189
+
190
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
191
+ let sarif = architecture_validation_sarif_with_root(&report, repo.path());
192
+ let results = sarif["runs"][0]["results"].as_array().unwrap();
193
+
194
+ assert_eq!(sarif["version"], "2.1.0");
195
+ assert!(sarif["runs"][0]["originalUriBaseIds"]["REPO_ROOT"]["uri"]
196
+ .as_str()
197
+ .is_some_and(|uri| uri.starts_with("file://") && uri.ends_with('/')));
198
+ assert!(results.iter().any(|result| {
199
+ result["ruleId"] == "arch.no_forbidden_layer_dependencies"
200
+ && result["level"] == "error"
201
+ && result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
202
+ == "src/domain/event.ts"
203
+ && result["locations"][0]["physicalLocation"]["artifactLocation"]["uriBaseId"]
204
+ == "REPO_ROOT"
205
+ }));
206
+ assert!(results.iter().any(|result| {
207
+ result["ruleId"] == "arch.import.unresolved"
208
+ && result["level"] == "warning"
209
+ && result["properties"]["agentInstruction"]
210
+ .as_str()
211
+ .is_some_and(|instruction| instruction.contains("./missing"))
212
+ }));
213
+ assert!(results.iter().any(|result| {
214
+ result["ruleId"] == "arch.config.layer_matches_no_files"
215
+ && result["level"] == "warning"
216
+ && result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
217
+ == "naome.arch.yaml"
218
+ && result["locations"][0]["physicalLocation"]["artifactLocation"]["uriBaseId"]
219
+ == "REPO_ROOT"
220
+ }));
221
+ }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.3.17",
3
+ "version": "1.4.0",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,7 +17,7 @@ const expectedMachineOwnedIntegrity = Object.freeze({
17
17
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
18
18
  "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
19
19
  "docs/naome/agent-workflow.md": "sha256:0be1c29adfbcd3fd73c4f904080ffc67237692fe413871a30243538c4db38ac7",
20
- "docs/naome/architecture-fitness.md": "sha256:5067f57787f0cac0be4b0d49079f03b595fdab6e4fba4b8bdde4bc3cfc0d1de1",
20
+ "docs/naome/architecture-fitness.md": "sha256:be38e0c6dea8b1ebeee3bfd4a2e4e17008829360b1b4649ef45f2ce61e7287bd",
21
21
  "docs/naome/context-economy.md": "sha256:3ed5075815ecf4ada46a5e65438769310307c35759fcd46b13dc0b96e02bebd9",
22
22
  "docs/naome/execution.md": "sha256:bfc5d55838942ec8e3d790b59e3c634ff5bf6a2298265cef3dca9788a097eafb",
23
23
  "docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
@@ -17,7 +17,7 @@ const expectedMachineOwnedIntegrity = Object.freeze({
17
17
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
18
18
  "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
19
19
  "docs/naome/agent-workflow.md": "sha256:0be1c29adfbcd3fd73c4f904080ffc67237692fe413871a30243538c4db38ac7",
20
- "docs/naome/architecture-fitness.md": "sha256:5067f57787f0cac0be4b0d49079f03b595fdab6e4fba4b8bdde4bc3cfc0d1de1",
20
+ "docs/naome/architecture-fitness.md": "sha256:be38e0c6dea8b1ebeee3bfd4a2e4e17008829360b1b4649ef45f2ce61e7287bd",
21
21
  "docs/naome/context-economy.md": "sha256:3ed5075815ecf4ada46a5e65438769310307c35759fcd46b13dc0b96e02bebd9",
22
22
  "docs/naome/execution.md": "sha256:bfc5d55838942ec8e3d790b59e3c634ff5bf6a2298265cef3dca9788a097eafb",
23
23
  "docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
@@ -1,5 +1,5 @@
1
1
  {
2
- "harnessVersion": "1.3.13",
2
+ "harnessVersion": "1.4.0",
3
3
  "installedAt": null,
4
4
  "integrity": {
5
5
  ".naome/bin/check-harness-health.js": "sha256:802d7419774981a6af1826b3882270ff8f41259d516f98c52a02b4ddc184c467",
@@ -9,7 +9,7 @@
9
9
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
10
10
  "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
11
11
  "docs/naome/agent-workflow.md": "sha256:0be1c29adfbcd3fd73c4f904080ffc67237692fe413871a30243538c4db38ac7",
12
- "docs/naome/architecture-fitness.md": "sha256:5067f57787f0cac0be4b0d49079f03b595fdab6e4fba4b8bdde4bc3cfc0d1de1",
12
+ "docs/naome/architecture-fitness.md": "sha256:be38e0c6dea8b1ebeee3bfd4a2e4e17008829360b1b4649ef45f2ce61e7287bd",
13
13
  "docs/naome/context-economy.md": "sha256:3ed5075815ecf4ada46a5e65438769310307c35759fcd46b13dc0b96e02bebd9",
14
14
  "docs/naome/execution.md": "sha256:bfc5d55838942ec8e3d790b59e3c634ff5bf6a2298265cef3dca9788a097eafb",
15
15
  "docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
@@ -14,6 +14,8 @@ facts instead of prompt text.
14
14
  - `naome arch validate` runs architecture rules with human output.
15
15
  - `naome arch validate --json` emits stable machine-readable output.
16
16
  - `naome arch validate --agent-feedback` emits compact repair instructions.
17
+ - `naome arch validate --sarif [--output architecture.sarif]` emits SARIF 2.1.0
18
+ for CI code-scanning while preserving failing exit codes.
17
19
  - `naome arch validate --changed-only` uses changed paths with the persistent
18
20
  architecture cache when available and safely degrades to a full scan when the
19
21
  cache cannot prove graph-level soundness.
@@ -110,15 +112,14 @@ edges as clean architecture.
110
112
 
111
113
  ## Configuration Findings
112
114
 
113
- Validation can pass while still reporting configuration findings. These are
114
- non-blocking warnings about weak architecture policy, such as:
115
+ Validation can pass while still reporting non-blocking configuration findings
116
+ about weak architecture policy, such as:
115
117
 
116
118
  - broad catch-all layers or contexts overlapping narrower boundaries;
117
119
  - layers or contexts whose path patterns match no files;
118
120
  - unresolved imports that could hide real dependency edges.
119
121
 
120
- Use `naome arch explain --json` or `naome arch validate --json` to inspect
121
- `configFindings` deterministically in CI or agent loops.
122
+ Use JSON output to inspect `configFindings` deterministically in CI or agents.
122
123
 
123
124
  ## Incremental Cache
124
125
 
@@ -148,7 +149,11 @@ The command is deterministic in both cache-backed and fallback modes; CI can
148
149
  inspect the JSON fields above when it needs to distinguish performance from
149
150
  soundness fallbacks.
150
151
 
151
- For v1.4 readiness, architecture fitness is a release gate whenever source,
152
+ Use `node .naome/bin/naome.js arch validate --sarif --output architecture.sarif`
153
+ when CI can upload SARIF. It contains blocking violations, configuration
154
+ findings, unresolved imports, and `agentInstruction` properties for repair loops.
155
+
156
+ For v1.4.0, architecture fitness is a release gate whenever source,
152
157
  manifest, alias, config, or structure changes can alter dependency edges. Treat
153
158
  error-level violations as blocking. Warning-level config findings are advisory
154
159
  unless the repository chooses stricter policy, but unresolved imports should be
@@ -156,29 +161,24 @@ triaged before claiming a clean architecture result.
156
161
 
157
162
  ## Language Support
158
163
 
159
- The v1.3.17 foundation classifies TypeScript, JavaScript, Rust, Python, Go,
160
- Java, Kotlin, and Swift files by path extension. It extracts import facts for
161
- TypeScript, JavaScript, Rust, Python, Go, and Swift. It resolves relative
162
- imports, repository-absolute aliases, TypeScript/JavaScript `paths` aliases
163
- from nearby `tsconfig.json` or `jsconfig.json`, Python package imports with
164
- local `__init__.py` roots, Go module imports, SwiftPM target imports, and Rust
165
- crate/self/super module paths where possible.
166
- Unresolved imports are represented explicitly as graph nodes instead of
167
- dropping them. Swift and iOS app repositories get Apple framework imports, SwiftPM
168
- manifests, and Xcode Swift package references represented in the normalized
169
- graph; Apple SDK frameworks are treated as platform APIs for external policy.
170
- It also extracts package and dependency facts from `package.json`,
171
- `Cargo.toml`, `pyproject.toml`, `go.mod`, lightweight `pom.xml` / Gradle
172
- manifests, `Package.swift`, and `.xcodeproj/project.pbxproj`, then emits
173
- manifest-identity package nodes and manifest-owned `DependsOn` edges to
174
- external dependencies.
164
+ The v1.4.0 foundation classifies TypeScript, JavaScript, Rust, Python, Go, Java,
165
+ Kotlin, and Swift files by path extension. It extracts imports for TypeScript,
166
+ JavaScript, Rust, Python, Go, and Swift, resolving relative paths,
167
+ repository-absolute aliases, TS/JS `paths`, Python package roots, Go modules,
168
+ SwiftPM targets, and Rust crate/self/super module paths where possible.
169
+ Unresolved imports become explicit graph nodes. Swift/iOS repositories get Apple
170
+ framework imports, SwiftPM manifests, and Xcode Swift package references in the
171
+ normalized graph; Apple SDK frameworks are treated as platform APIs. Manifest
172
+ facts from `package.json`, `Cargo.toml`, `pyproject.toml`, `go.mod`, lightweight
173
+ `pom.xml` / Gradle manifests, `Package.swift`, and `.xcodeproj/project.pbxproj`
174
+ emit manifest-identity package nodes and manifest-owned dependency edges.
175
175
  Architecture rules now cover layers, bounded contexts, public APIs, cycles,
176
176
  transitive layer reach, import/fan-out budgets, and external dependency
177
177
  policies.
178
178
 
179
179
  ## Acceptance Coverage
180
180
 
181
- v1.4 readiness is covered with deterministic fixture repositories for
181
+ v1.4.0 readiness is covered with deterministic fixture repositories for
182
182
  TypeScript/JavaScript aliases, Rust crate paths, Python package roots, Go
183
183
  modules, Swift/iOS targets, and mixed monorepos. These fixtures assert both
184
184
  positive local-edge resolution and negative forbidden-dependency findings.
@@ -187,7 +187,7 @@ positive local-edge resolution and negative forbidden-dependency findings.
187
187
 
188
188
  This release intentionally keeps validation file-graph based. Manifest
189
189
  extractors are dependency-owner oriented and do not yet parse every build-tool
190
- feature. Deep symbol-level call analysis and SARIF output remain follow-up work.
190
+ feature. Deep symbol-level call analysis remains follow-up work.
191
191
 
192
192
  ## v1.4 Migration Checklist
193
193