feat: establish first-class lexical language parity #2
5 changed files with 59 additions and 6 deletions
feat(model): retain all dependency evidence locations
commit
5f3b2484cc
|
|
@ -26,7 +26,10 @@ Every edge has:
|
||||||
- lane: syntactic, symbolic, imported, or heuristic;
|
- lane: syntactic, symbolic, imported, or heuristic;
|
||||||
- backend: producer and mode;
|
- backend: producer and mode;
|
||||||
- confidence: producer-calibrated value;
|
- confidence: producer-calibrated value;
|
||||||
- provenance: source location or report artifact.
|
- provenance: the first source location or report artifact, retained for v0.1
|
||||||
|
consumers;
|
||||||
|
- locations: every distinct source location contributing to a deduplicated
|
||||||
|
edge.
|
||||||
|
|
||||||
## Health indicator
|
## Health indicator
|
||||||
|
|
||||||
|
|
@ -50,4 +53,3 @@ should map Fold function/block/operation/value fact IDs directly to Brick
|
||||||
identities and retain source/binary lanes. Source proved, latent, heuristic, and
|
identities and retain source/binary lanes. Source proved, latent, heuristic, and
|
||||||
concrete-witness confidence must remain distinguishable. Binary evidence must
|
concrete-witness confidence must remain distinguishable. Binary evidence must
|
||||||
not be promoted to symbolic evidence without corroboration.
|
not be promoted to symbolic evidence without corroboration.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ All extractors must satisfy these rules before first-class parity is claimed:
|
||||||
or calls.
|
or calls.
|
||||||
5. Duplicate names use stable qualified identities and do not resolve
|
5. Duplicate names use stable qualified identities and do not resolve
|
||||||
ambiguously.
|
ambiguously.
|
||||||
6. Every edge retains backend, confidence, and source evidence. If multiple
|
6. Every edge retains backend, confidence, its first provenance value, and all
|
||||||
source locations are collapsed, the evidence must say so.
|
distinct source locations contributing to a deduplicated relationship.
|
||||||
|
|
||||||
## Parity test contract
|
## Parity test contract
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ pub fn import_findings(building: &mut Building, report: &Path) -> Result<usize>
|
||||||
lines_of_code: 0,
|
lines_of_code: 0,
|
||||||
attributes,
|
attributes,
|
||||||
});
|
});
|
||||||
|
let provenance = format!("{}:{}", report.display(), finding.line);
|
||||||
building.edges.push(Edge {
|
building.edges.push(Edge {
|
||||||
id: stable_id("edge", &format!("{node_id}:flags:{file_id}")),
|
id: stable_id("edge", &format!("{node_id}:flags:{file_id}")),
|
||||||
source: node_id,
|
source: node_id,
|
||||||
|
|
@ -70,7 +71,8 @@ pub fn import_findings(building: &mut Building, report: &Path) -> Result<usize>
|
||||||
},
|
},
|
||||||
backend: format!("fold:discover:{}", finding.backend),
|
backend: format!("fold:discover:{}", finding.backend),
|
||||||
confidence: confidence_value(&finding.confidence),
|
confidence: confidence_value(&finding.confidence),
|
||||||
provenance: format!("{}:{}", report.display(), finding.line),
|
locations: vec![provenance.clone()],
|
||||||
|
provenance,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
imported += 1;
|
imported += 1;
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ pub struct Evidence {
|
||||||
pub backend: String,
|
pub backend: String,
|
||||||
pub confidence: f64,
|
pub confidence: f64,
|
||||||
pub provenance: String,
|
pub provenance: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub locations: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -142,10 +144,12 @@ pub fn stable_id(kind: &str, identity: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn evidence(backend: &str, confidence: f64, provenance: impl Into<String>) -> Evidence {
|
pub fn evidence(backend: &str, confidence: f64, provenance: impl Into<String>) -> Evidence {
|
||||||
|
let provenance = provenance.into();
|
||||||
Evidence {
|
Evidence {
|
||||||
lane: EvidenceLane::Syntactic,
|
lane: EvidenceLane::Syntactic,
|
||||||
backend: backend.into(),
|
backend: backend.into(),
|
||||||
confidence,
|
confidence,
|
||||||
provenance: provenance.into(),
|
locations: vec![provenance.clone()],
|
||||||
|
provenance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
src/scan.rs
45
src/scan.rs
|
|
@ -531,8 +531,17 @@ impl ScanState {
|
||||||
confidence: f64,
|
confidence: f64,
|
||||||
provenance: impl Into<String>,
|
provenance: impl Into<String>,
|
||||||
) {
|
) {
|
||||||
|
let provenance = provenance.into();
|
||||||
let key = (source.clone(), target.clone(), kind);
|
let key = (source.clone(), target.clone(), kind);
|
||||||
if !self.edge_keys.insert(key) {
|
if !self.edge_keys.insert(key) {
|
||||||
|
if let Some(edge) = self
|
||||||
|
.edges
|
||||||
|
.iter_mut()
|
||||||
|
.find(|edge| edge.source == source && edge.target == target && edge.kind == kind)
|
||||||
|
&& !edge.evidence.locations.contains(&provenance)
|
||||||
|
{
|
||||||
|
edge.evidence.locations.push(provenance);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let identity = format!("{source}:{}:{target}", edge_kind_name(kind));
|
let identity = format!("{source}:{}:{target}", edge_kind_name(kind));
|
||||||
|
|
@ -1038,6 +1047,42 @@ mod tests {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retains_every_location_for_a_deduplicated_dependency_edge() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("calls.rs"),
|
||||||
|
"fn helper() {}\nfn caller() {\n helper();\n helper();\n}\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let building = scan(dir.path()).unwrap();
|
||||||
|
let caller = building
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.find(|node| node.kind == NodeKind::Function && node.name == "caller")
|
||||||
|
.unwrap();
|
||||||
|
let helper = building
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.find(|node| node.kind == NodeKind::Function && node.name == "helper")
|
||||||
|
.unwrap();
|
||||||
|
let call = building
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.find(|edge| {
|
||||||
|
edge.kind == EdgeKind::Calls && edge.source == caller.id && edge.target == helper.id
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let serialized = serde_json::to_value(&call.evidence).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(call.evidence.provenance, "calls.rs:3");
|
||||||
|
assert_eq!(
|
||||||
|
serialized["locations"],
|
||||||
|
serde_json::json!(["calls.rs:3", "calls.rs:4"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scans_dart_structure_without_promoting_control_flow_or_strings() {
|
fn scans_dart_structure_without_promoting_control_flow_or_strings() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue