Skip to content

Commit 71ecfe4

Browse files
Zacclaude
andcommitted
feat: close 5 remaining beads — discovery, codegen, trust config
bd-36b: Extraction codegen now emits real dsl::extract::extract() calls instead of placeholder values. Generated code constructs ExtractSection structs and passes them to the extract function. bd-1zj: Content hash codegen now emits dsl::content_hash::content_hash() calls over extracted data. Generated code passes the extracted HashMap and the configured 'over' field names. bd-1kv: Generated Cargo.toml uses version-based dependency (fingerprint = { version = "0.1.0" }) instead of path = "../.." so compiled crates build from any directory. bd-28w: discover_installed() scans ~/.fingerprint/definitions/ for .fp.yaml files and wraps each in a DslFingerprint that implements the Fingerprint trait via evaluate_named_assertions + extract + content_hash at runtime. Wired into build_registry(). Override scan directory with FINGERPRINT_DEFINITIONS env var. bd-3g1: Trust allowlist loaded from ~/.fingerprint/trust.yaml (user) and .fingerprint/trust.yaml (project). Supports wildcard matching (e.g. "installed:*"). Refusal message now shows the exact trust.yaml entry to add. Override with FINGERPRINT_TRUST env var. All 204 unit tests + all integration/smoke/golden tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 20be354 commit 71ecfe4

File tree

6 files changed

+420
-105
lines changed

6 files changed

+420
-105
lines changed

.beads/issues.jsonl

Lines changed: 71 additions & 71 deletions
Large diffs are not rendered by default.

src/compile/codegen.rs

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -203,36 +203,79 @@ fn generate_extracted_code(extract: &[ExtractSection]) -> String {
203203
return " None".to_string();
204204
}
205205

206-
let mut code_lines = vec![" let mut extracted = HashMap::new();".to_string()];
207-
206+
let mut section_inits = Vec::new();
208207
for section in extract {
209-
code_lines.push(format!(
210-
r#" // Extract section: {}
211-
// TODO: Implement extraction for type: {}
212-
extracted.insert("{}.to_owned(), json!("extracted_placeholder"));"#,
213-
section.name, section.r#type, section.name
208+
section_inits.push(format!(
209+
r#" fingerprint::dsl::parser::ExtractSection {{
210+
name: {name}.to_owned(),
211+
r#type: {typ}.to_owned(),
212+
sheet: {sheet},
213+
range: {range},
214+
anchor_heading: {anchor_heading},
215+
index: {index},
216+
anchor: {anchor},
217+
pattern: {pattern},
218+
within_chars: {within_chars},
219+
}}"#,
220+
name = format_args!("{:?}", section.name),
221+
typ = format_args!("{:?}", section.r#type),
222+
sheet = codegen_option_string(section.sheet.as_deref()),
223+
range = codegen_option_string(section.range.as_deref()),
224+
anchor_heading = codegen_option_string(section.anchor_heading.as_deref()),
225+
index = codegen_option_display(section.index),
226+
anchor = codegen_option_string(section.anchor.as_deref()),
227+
pattern = codegen_option_string(section.pattern.as_deref()),
228+
within_chars = codegen_option_display(section.within_chars),
214229
));
215230
}
216231

217-
code_lines.push(" Some(extracted)".to_string());
218-
code_lines.join("\n")
232+
format!(
233+
r#" let sections = vec![
234+
{sections}
235+
];
236+
match fingerprint::dsl::extract::extract(doc, &sections) {{
237+
Ok(map) => Some(map),
238+
Err(_) => None,
239+
}}"#,
240+
sections = section_inits.join(",\n"),
241+
)
219242
}
220243

221244
/// Generate code for content hash computation.
222245
fn generate_content_hash_code(content_hash: &Option<ContentHashConfig>) -> String {
223246
match content_hash {
224247
Some(config) => {
248+
let over_entries: Vec<String> = config
249+
.over
250+
.iter()
251+
.map(|name| format!("{:?}.to_owned()", name))
252+
.collect();
225253
format!(
226-
r#" // Content hash over fields: {:?}
227-
// TODO: Implement {} content hash computation
228-
Some("{}:placeholder".to_owned())"#,
229-
config.over, config.algorithm, config.algorithm
254+
r#" let over = vec![{over}];
255+
extracted.as_ref().map(|ext| fingerprint::dsl::content_hash::content_hash(ext, &over))"#,
256+
over = over_entries.join(", "),
230257
)
231258
}
232259
None => " None".to_string(),
233260
}
234261
}
235262

263+
/// Generate Rust source for `Option<String>` — `Some("value".to_owned())` or `None`.
264+
fn codegen_option_string(value: Option<&str>) -> String {
265+
match value {
266+
Some(s) => format!("Some({:?}.to_owned())", s),
267+
None => "None".to_owned(),
268+
}
269+
}
270+
271+
/// Generate Rust source for `Option<T: Display>` — `Some(42)` or `None`.
272+
fn codegen_option_display<T: std::fmt::Display>(value: Option<T>) -> String {
273+
match value {
274+
Some(v) => format!("Some({})", v),
275+
None => "None".to_owned(),
276+
}
277+
}
278+
236279
#[cfg(test)]
237280
mod tests {
238281
use super::*;

src/compile/crate_gen.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ description = "{description}"
5959
# Source: DSL definition for {fingerprint_id}
6060
6161
[dependencies]
62-
fingerprint = {{ path = "../.." }}
62+
fingerprint = {{ version = "{compiler_version}" }}
6363
serde_json = "1.0"
6464
6565
[lib]

src/lib.rs

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,57 @@ fn handle_run_mode(cli: cli::Cli) -> u8 {
413413
outcome.exit_code()
414414
}
415415

416+
/// Trust configuration file format (YAML).
417+
#[derive(serde::Deserialize, Default)]
418+
struct TrustConfig {
419+
#[serde(default)]
420+
trust: Vec<String>,
421+
}
422+
423+
/// Load trust allowlist from config files.
424+
///
425+
/// Searches (in order, merging entries):
426+
/// 1. `~/.fingerprint/trust.yaml` (user config)
427+
/// 2. `.fingerprint/trust.yaml` (project config)
428+
///
429+
/// Override location with `FINGERPRINT_TRUST` environment variable.
430+
fn load_trust_config() -> Vec<String> {
431+
let mut entries = Vec::new();
432+
433+
if let Ok(path) = std::env::var("FINGERPRINT_TRUST") {
434+
load_trust_file(&std::path::PathBuf::from(path), &mut entries);
435+
return entries;
436+
}
437+
438+
// User config: ~/.fingerprint/trust.yaml
439+
let home_config = std::env::var("HOME")
440+
.map(std::path::PathBuf::from)
441+
.unwrap_or_else(|_| std::path::PathBuf::from("."))
442+
.join(".fingerprint")
443+
.join("trust.yaml");
444+
load_trust_file(&home_config, &mut entries);
445+
446+
// Project config: .fingerprint/trust.yaml
447+
let project_config = std::path::PathBuf::from(".fingerprint").join("trust.yaml");
448+
load_trust_file(&project_config, &mut entries);
449+
450+
entries
451+
}
452+
453+
fn load_trust_file(path: &std::path::Path, entries: &mut Vec<String>) {
454+
let Ok(contents) = std::fs::read_to_string(path) else {
455+
return;
456+
};
457+
let Ok(config) = serde_yaml::from_str::<TrustConfig>(&contents) else {
458+
eprintln!(
459+
"Warning: failed to parse trust config '{}', skipping",
460+
path.display()
461+
);
462+
return;
463+
};
464+
entries.extend(config.trust);
465+
}
466+
416467
/// Build the fingerprint registry with builtin fingerprints.
417468
#[allow(clippy::result_large_err)]
418469
fn build_registry() -> Result<registry::FingerprintRegistry, refusal::codes::RefusalEnvelope> {
@@ -436,10 +487,13 @@ fn build_registry() -> Result<registry::FingerprintRegistry, refusal::codes::Ref
436487
registry.register_with_info(builtin, info);
437488
}
438489

439-
// TODO: Discover and register installed fingerprint crates
490+
// Discover and register installed fingerprint definitions
491+
for (installed, info) in registry::installed::discover_installed() {
492+
registry.register_with_info(installed, info);
493+
}
440494

441495
// Validate registry (check for duplicates and trust policy)
442-
let allowlist: Vec<String> = vec![]; // TODO: Load from config
496+
let allowlist = load_trust_config();
443497
registry
444498
.validate(&allowlist)
445499
.map_err(|validation_error| match validation_error {
@@ -459,16 +513,22 @@ fn build_registry() -> Result<registry::FingerprintRegistry, refusal::codes::Ref
459513
fingerprint_id,
460514
provider,
461515
policy,
462-
} => build_envelope(
463-
RefusalCode::UntrustedFp,
464-
"Untrusted fingerprint provider",
465-
RefusalDetail::UntrustedFp(refusal::codes::UntrustedFpDetail {
466-
fingerprint_id,
467-
provider,
468-
policy,
469-
}),
470-
Some("Add provider to allowlist or use --trust-all".to_owned()),
471-
),
516+
} => {
517+
let next_command = format!(
518+
"Add to ~/.fingerprint/trust.yaml:\ntrust:\n - \"{}\"",
519+
provider
520+
);
521+
build_envelope(
522+
RefusalCode::UntrustedFp,
523+
"Untrusted fingerprint provider",
524+
RefusalDetail::UntrustedFp(refusal::codes::UntrustedFpDetail {
525+
fingerprint_id,
526+
provider,
527+
policy,
528+
}),
529+
Some(next_command),
530+
)
531+
}
472532
})?;
473533

474534
Ok(registry)

src/registry/core.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,13 @@ impl std::error::Error for RegistryValidationError {}
229229
fn is_trusted_source(source: &str, allowlist: &[String]) -> bool {
230230
source == "builtin"
231231
|| source.starts_with("builtin:")
232-
|| allowlist.iter().any(|entry| entry == source)
232+
|| allowlist.iter().any(|entry| {
233+
if let Some(prefix) = entry.strip_suffix('*') {
234+
source.starts_with(prefix)
235+
} else {
236+
entry == source
237+
}
238+
})
233239
}
234240

235241
#[cfg(test)]

0 commit comments

Comments
 (0)