fix: accept non-DNS Common Crawl graph rows
Some checks failed
Standalone registry checks / check (push) Has been cancelled
Some checks failed
Standalone registry checks / check (push) Has been cancelled
This commit is contained in:
parent
3e0cc1bc50
commit
3b6966c81a
7 changed files with 60 additions and 21 deletions
|
|
@ -1,5 +1,13 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.6.1 - 2026-09-22
|
||||||
|
|
||||||
|
- Accept complete Common Crawl domain-rank releases containing provider rows
|
||||||
|
that are not valid DNS hostnames. Such rows remain authenticated and counted
|
||||||
|
in source coordinates but cannot match or enter the official-domain catalog.
|
||||||
|
- Keep malformed graph schemas and numeric fields fail-closed, with a regression
|
||||||
|
derived from the real `com.your_domain` provider row.
|
||||||
|
|
||||||
## 0.6.0 - 2026-09-22
|
## 0.6.0 - 2026-09-22
|
||||||
|
|
||||||
- Add a streaming Common Crawl domain Web Graph adapter for harmonic-centrality,
|
- Add a streaming Common Crawl domain Web Graph adapter for harmonic-centrality,
|
||||||
|
|
|
||||||
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -78,14 +78,14 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argand-atomic"
|
name = "argand-atomic"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argand-site-registry"
|
name = "argand-site-registry"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argand-atomic",
|
"argand-atomic",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ resolver = "3"
|
||||||
members = ["crates/argand-atomic", "crates/argand-site-registry"]
|
members = ["crates/argand-atomic", "crates/argand-site-registry"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
authors = ["Nic Weyand"]
|
authors = ["Nic Weyand"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,15 @@ impl SourceAdapter for DomainRanks {
|
||||||
let harmonic_value = nonnegative_finite(fields[1], "harmonic value")?;
|
let harmonic_value = nonnegative_finite(fields[1], "harmonic value")?;
|
||||||
let pagerank_rank = positive_integer(fields[2], "PageRank rank")?;
|
let pagerank_rank = positive_integer(fields[2], "PageRank rank")?;
|
||||||
let pagerank_value = nonnegative_finite(fields[3], "PageRank value")?;
|
let pagerank_value = nonnegative_finite(fields[3], "PageRank value")?;
|
||||||
let target = reverse_domain(fields[4])?;
|
|
||||||
let member_hosts = positive_integer(fields[5], "member host count")?;
|
let member_hosts = positive_integer(fields[5], "member host count")?;
|
||||||
source_row += 1;
|
source_row += 1;
|
||||||
|
let Some(target) = reverse_domain(fields[4]) else {
|
||||||
|
// The provider graph contains a small amount of underscore and
|
||||||
|
// otherwise non-DNS host material. It cannot match the registry's
|
||||||
|
// normalized public identity domains, but it remains part of the
|
||||||
|
// authenticated input stream and source-row coordinate space.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
if !self.targets.contains(&target) {
|
if !self.targets.contains(&target) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -118,18 +124,13 @@ fn nonnegative_finite(value: &str, field: &str) -> anyhow::Result<f64> {
|
||||||
Ok(parsed)
|
Ok(parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reverse_domain(value: &str) -> anyhow::Result<String> {
|
fn reverse_domain(value: &str) -> Option<String> {
|
||||||
ensure!(
|
if value.is_empty() || value.len() > 253 || value != value.trim() {
|
||||||
!value.is_empty() && value.len() <= 253 && value == value.trim(),
|
return None;
|
||||||
"invalid reversed domain"
|
}
|
||||||
);
|
|
||||||
let labels = value.split('.').collect::<Vec<_>>();
|
let labels = value.split('.').collect::<Vec<_>>();
|
||||||
ensure!(
|
if labels.len() < 2
|
||||||
labels.len() >= 2,
|
|| !labels.iter().all(|label| {
|
||||||
"reversed domain needs at least two labels"
|
|
||||||
);
|
|
||||||
ensure!(
|
|
||||||
labels.iter().all(|label| {
|
|
||||||
!label.is_empty()
|
!label.is_empty()
|
||||||
&& label.len() <= 63
|
&& label.len() <= 63
|
||||||
&& label
|
&& label
|
||||||
|
|
@ -143,8 +144,9 @@ fn reverse_domain(value: &str) -> anyhow::Result<String> {
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
.last()
|
.last()
|
||||||
.is_some_and(u8::is_ascii_alphanumeric)
|
.is_some_and(u8::is_ascii_alphanumeric)
|
||||||
}),
|
})
|
||||||
"invalid reversed domain label"
|
{
|
||||||
);
|
return None;
|
||||||
Ok(labels.into_iter().rev().collect::<Vec<_>>().join("."))
|
}
|
||||||
|
Some(labels.into_iter().rev().collect::<Vec<_>>().join("."))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,6 @@ fn domain_rank_parser_rejects_schema_and_value_drift() -> anyhow::Result<()> {
|
||||||
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n\n",
|
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n\n",
|
||||||
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n0\t1\t1\t1\tcom.example\t1\n",
|
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n0\t1\t1\t1\tcom.example\t1\n",
|
||||||
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\tNaN\t1\t1\tcom.example\t1\n",
|
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\tNaN\t1\t1\tcom.example\t1\n",
|
||||||
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\t1\t1\t1\tcom..example\t1\n",
|
|
||||||
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\t1\t1\t1\tcom.example\t0\n",
|
"#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n1\t1\t1\t1\tcom.example\t0\n",
|
||||||
] {
|
] {
|
||||||
let root = tempfile::tempdir()?;
|
let root = tempfile::tempdir()?;
|
||||||
|
|
@ -167,6 +166,30 @@ fn domain_rank_parser_rejects_schema_and_value_drift() -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_dns_provider_rows_are_skipped_without_losing_source_coordinates() -> anyhow::Result<()> {
|
||||||
|
let root = tempfile::tempdir()?;
|
||||||
|
let mut db = store::open(&root.path().join("store.sqlite"))?;
|
||||||
|
import_identity_candidates(&mut db, root.path())?;
|
||||||
|
let input = "#harmonicc_pos\t#harmonicc_val\t#pr_pos\t#pr_val\t#host_rev\t#n_hosts\n\
|
||||||
|
1\t1\t1\t1\tcom.your_domain\t15\n\
|
||||||
|
2\t1\t2\t1\tcom.facebook\t18795\n";
|
||||||
|
import_graph(&mut db, root.path(), input.as_bytes())?;
|
||||||
|
|
||||||
|
let retained: Vec<(String, String)> = {
|
||||||
|
let mut statement = db.prepare(
|
||||||
|
"SELECT r.native_id,f.value FROM records r JOIN facts f ON f.source_id=r.source_id AND f.ordinal=r.ordinal WHERE f.predicate='popularity' ORDER BY r.native_id",
|
||||||
|
)?;
|
||||||
|
statement
|
||||||
|
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
|
||||||
|
.collect::<Result<_, _>>()?
|
||||||
|
};
|
||||||
|
assert_eq!(retained.len(), 1);
|
||||||
|
assert_eq!(retained[0].0, "row:2");
|
||||||
|
assert!(retained[0].1.contains("facebook.com"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn domain_rank_source_url_is_exactly_allowlisted() -> anyhow::Result<()> {
|
fn domain_rank_source_url_is_exactly_allowlisted() -> anyhow::Result<()> {
|
||||||
let good = "https://data.commoncrawl.org/projects/hyperlinkgraph/cc-main-2022-may-jun-aug/domain/cc-main-2022-may-jun-aug-domain-ranks.txt.gz";
|
let good = "https://data.commoncrawl.org/projects/hyperlinkgraph/cc-main-2022-may-jun-aug/domain/cc-main-2022-may-jun-aug-domain-ranks.txt.gz";
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ source attribution and application-specific malware/content policy.
|
||||||
|
|
||||||
## Current contracts
|
## Current contracts
|
||||||
|
|
||||||
Code version 0.6.0 uses writer schema 5 and `argand.site-rules/v4`.
|
Code version 0.6.1 uses writer schema 5 and `argand.site-rules/v4`.
|
||||||
New compact `COMPLETE.json` files use `argand.site-registry/v3` and bind:
|
New compact `COMPLETE.json` files use `argand.site-registry/v3` and bind:
|
||||||
|
|
||||||
- authenticated `registry.sqlite` bytes;
|
- authenticated `registry.sqlite` bytes;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
# Version 0.6.0 Web Graph and updater validation, 2026-09-22
|
# Version 0.6.0 Web Graph and updater validation, 2026-09-22
|
||||||
|
|
||||||
|
Patch release 0.6.1 additionally replays the real provider-shaped
|
||||||
|
`com.your_domain` case: the row remains in authenticated input/coordinate
|
||||||
|
accounting but is not retained as a DNS target. The following valid
|
||||||
|
`com.facebook` row is retained at its original `row:2` coordinate. Schema and
|
||||||
|
numeric corruption continue to fail closed.
|
||||||
|
|
||||||
The evaluation contract was written before implementation. The new Common Crawl
|
The evaluation contract was written before implementation. The new Common Crawl
|
||||||
domain-rank integration passed five default integration tests; its sixth test is
|
domain-rank integration passed five default integration tests; its sixth test is
|
||||||
an explicit resource benchmark. The benchmark parsed 100,000 valid provider-shaped
|
an explicit resource benchmark. The benchmark parsed 100,000 valid provider-shaped
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue