Phase 10: Rich catalog v2 — metadata as product
- DatasetManifest expanded: description, owner, sensitivity, columns,
lineage, freshness contract, tags, row_count
- All new fields use #[serde(default)] for backward compatibility
- PII auto-detection: scans column names for email, phone, SSN, salary,
address, DOB, medical terms — flags as PII/PHI/Financial
- Column-level metadata: name, type, sensitivity, is_pii flag
- Lineage tracking: source_system, source_file, ingest_job, timestamp
- Ingest pipeline auto-populates: PII scan, column meta, lineage, row count
- PATCH /catalog/datasets/by-name/{name}/metadata — update metadata
- Catalog responses now include all rich fields
- 25 unit tests passing (5 new PII detection tests)
Per ADR-013: datasets without metadata become mystery files.
This makes every ingested file self-describing from day one.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ pub fn router(registry: Registry) -> Router {
|
||||
.route("/datasets", get(list_datasets))
|
||||
.route("/datasets/{id}", get(get_dataset))
|
||||
.route("/datasets/by-name/{name}", get(get_dataset_by_name))
|
||||
.route("/datasets/by-name/{name}/metadata", post(update_metadata))
|
||||
.with_state(registry)
|
||||
}
|
||||
|
||||
@@ -47,6 +48,15 @@ struct DatasetResponse {
|
||||
objects: Vec<ObjectRefResponse>,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
// Rich metadata
|
||||
description: String,
|
||||
owner: String,
|
||||
sensitivity: Option<shared::types::Sensitivity>,
|
||||
columns: Vec<shared::types::ColumnMeta>,
|
||||
lineage: Option<shared::types::Lineage>,
|
||||
freshness: Option<shared::types::FreshnessContract>,
|
||||
tags: Vec<String>,
|
||||
row_count: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -71,6 +81,14 @@ impl From<&shared::types::DatasetManifest> for DatasetResponse {
|
||||
}).collect(),
|
||||
created_at: m.created_at.to_rfc3339(),
|
||||
updated_at: m.updated_at.to_rfc3339(),
|
||||
description: m.description.clone(),
|
||||
owner: m.owner.clone(),
|
||||
sensitivity: m.sensitivity.clone(),
|
||||
columns: m.columns.clone(),
|
||||
lineage: m.lineage.clone(),
|
||||
freshness: m.freshness.clone(),
|
||||
tags: m.tags.clone(),
|
||||
row_count: m.row_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,3 +141,14 @@ async fn get_dataset_by_name(
|
||||
None => Err((StatusCode::NOT_FOUND, format!("dataset not found: {name}"))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_metadata(
|
||||
State(registry): State<Registry>,
|
||||
Path(name): Path<String>,
|
||||
Json(updates): Json<crate::registry::MetadataUpdate>,
|
||||
) -> impl IntoResponse {
|
||||
match registry.update_metadata(&name, updates).await {
|
||||
Ok(manifest) => Ok(Json(DatasetResponse::from(&manifest))),
|
||||
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user