Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## Unreleased

- Hard-cut C# production extraction to the version-1 universal candidate with
bounded AST-backed declarations, namespaces, imports and aliases, overload
signatures, typed members and parameters, construction and call evidence,
base types, ownership, and explicit overrides. Remove the replaced C# raw
graph, namespace canonicalizer, and member type-table paths. Convert ASP.NET
MVC routing to the universal `aspnet-csharp` pack with evidence-backed
attributes, aliases, `AcceptVerbs`, controller/action token composition,
absolute templates, overload-safe handler resolution, and `[NonAction]`.
Extend universal evidence with typed override roles and relationships, and
move XAML code-behind lookup onto the same exact C# ownership facts. Keep
sourceless external import identities scoped to their exact wiring sites so
imports in different files cannot merge their provenance.

## 0.3.12 - 2026-08-13

- Remove the 2 GiB aggregate canonical-payload limit from current SQLite
Expand Down
41 changes: 25 additions & 16 deletions crates/compass-graph/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4520,6 +4520,8 @@ fn node_identity(
domain_id(kind, source_path, &positional_name)
}
NodeKind::Import | NodeKind::Export => {
let identity_source =
node_identity_source(attributes, source_path, record, identity_site);
let mut binding = match details {
Some(NodeDetails::ImportExport(details)) => format!(
"{}:{}:{}",
Expand All @@ -4535,7 +4537,7 @@ fn node_identity(
}
symbol_id(
language.unwrap_or("unknown"),
source_path,
&identity_source,
kind,
qualified_name,
&binding,
Expand All @@ -4551,20 +4553,8 @@ fn node_identity(
domain_id(kind, &namespace, qualified_name)
}
_ => {
let unresolved_scope;
let identity_source = if source_path.is_empty() {
unresolved_scope = optional_string(attributes, "external_identity_scope")
.filter(|scope| !scope.trim().is_empty())
.unwrap_or_else(|| {
identity_site.map_or_else(
|| format!("unresolved:{record}"),
|site| format!("{}#{}:{}", site.file, site.start_byte, site.end_byte),
)
});
unresolved_scope.as_str()
} else {
source_path
};
let identity_source =
node_identity_source(attributes, source_path, record, identity_site);
let overload = optional_string(attributes, "overload_discriminator");
let lexical_owner =
optional_any_string(attributes, &["lexical_owner", "declaring_scope"]);
Expand All @@ -4576,7 +4566,7 @@ fn node_identity(
};
symbol_id(
language.unwrap_or("unknown"),
identity_source,
&identity_source,
kind,
qualified_name,
&disambiguator,
Expand All @@ -4586,6 +4576,25 @@ fn node_identity(
Ok(id)
}

fn node_identity_source(
attributes: &Map<String, Value>,
source_path: &str,
record: &str,
identity_site: Option<&SourceAnchor>,
) -> String {
if !source_path.is_empty() {
return source_path.to_owned();
}
optional_string(attributes, "external_identity_scope")
.filter(|scope| !scope.trim().is_empty())
.unwrap_or_else(|| {
identity_site.map_or_else(
|| format!("unresolved:{record}"),
|site| format!("{}#{}:{}", site.file, site.start_byte, site.end_byte),
)
})
}

fn raw_markdown_heading(attributes: &Map<String, Value>) -> bool {
optional_any_string(attributes, &["language", "lang"]).as_deref() == Some("markdown")
&& optional_string(attributes, "document_kind").as_deref() == Some("heading")
Expand Down
84 changes: 84 additions & 0 deletions crates/compass-graph/tests/graph_v1_normalization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,90 @@ fn sourceless_placeholder_identity_never_merges_same_name_across_source_files()
Ok(())
}

#[test]
fn sourceless_import_placeholder_identity_never_merges_across_source_files()
-> Result<(), Box<dyn std::error::Error>> {
let directory = tempfile::tempdir()?;
let root = directory.path();
let external_name = "Microsoft.AspNetCore.Mvc";
let mut external = raw_external_node("raw:mvc", external_name);
external.attributes.extend(Map::from_iter([
("symbol_kind".to_owned(), json!("import")),
("language".to_owned(), json!("csharp")),
(
"extractor".to_owned(),
json!("compass.resolve.csharp.universal"),
),
("_origin".to_owned(), json!("ast")),
("confidence".to_owned(), json!("EXTRACTED")),
("_canonical_external_symbol".to_owned(), json!(true)),
]));
let graph = Extraction {
nodes: vec![
raw_file_node(root, "raw:first", "src/FirstController.cs"),
raw_file_node(root, "raw:second", "src/SecondController.cs"),
external,
],
edges: vec![
raw_php_edge(
root,
"src/FirstController.cs",
"raw:first",
"raw:mvc",
"imports_from",
0,
),
raw_php_edge(
root,
"src/SecondController.cs",
"raw:second",
"raw:mvc",
"imports_from",
0,
),
],
..Extraction::default()
};
let mut evidence = build_evidence(root)?;
add_inventory_file(
root,
&mut evidence,
"src/FirstController.cs",
"csharp",
b'f',
)?;
add_inventory_file(
root,
&mut evidence,
"src/SecondController.cs",
"csharp",
b's',
)?;

let document = normalize_v1(graph, evidence)?;
let external = document
.nodes
.iter()
.filter(|node| node.qualified_name == external_name)
.collect::<Vec<_>>();
assert_eq!(external.len(), 2, "nodes={:#?}", document.nodes);
assert!(external.iter().all(|node| {
node.evidence
.iter()
.filter(|evidence| evidence.rule.as_deref() == Some("external-symbol-placeholder"))
.count()
== 1
}));
let targets = document
.links
.iter()
.map(|edge| edge.target.as_str())
.collect::<std::collections::HashSet<_>>();
assert_eq!(targets.len(), 2);
assert!(document.links.iter().all(|edge| edge.deferred));
Ok(())
}

#[test]
fn sourceless_implemented_placeholder_infers_a_deferred_interface()
-> Result<(), Box<dyn std::error::Error>> {
Expand Down
26 changes: 26 additions & 0 deletions crates/compass-languages/src/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,24 @@ const GO_CAPABILITIES: &[LanguageCapability] = &[
LanguageCapability::ExternalReferences,
];

const CSHARP_CAPABILITIES: &[LanguageCapability] = &[
LanguageCapability::Declarations,
LanguageCapability::LexicalScopes,
LanguageCapability::Namespaces,
LanguageCapability::Imports,
LanguageCapability::Aliases,
LanguageCapability::Calls,
LanguageCapability::Construction,
LanguageCapability::Decorators,
LanguageCapability::TypeReferences,
LanguageCapability::BaseTypes,
LanguageCapability::HierarchyDispatch,
LanguageCapability::Members,
LanguageCapability::Ownership,
LanguageCapability::Receivers,
LanguageCapability::ExternalReferences,
];

const PYTHON_CAPABILITIES: &[LanguageCapability] = &[
LanguageCapability::Declarations,
LanguageCapability::LexicalScopes,
Expand Down Expand Up @@ -213,6 +231,14 @@ const TYPESCRIPT_CAPABILITIES: &[LanguageCapability] = &[
];

const UNIVERSAL_ADAPTERS: &[AdapterProfile] = &[
AdapterProfile {
id: "compass.csharp.candidate",
language: "csharp",
version: 1,
evidence_schema: crate::UNIVERSAL_EVIDENCE_SCHEMA,
profile: UniversalAdapterProfile::UniversalCandidate,
capabilities: CSHARP_CAPABILITIES,
},
AdapterProfile {
id: "compass.go",
language: "go",
Expand Down
Loading
Loading