use super::blast_paths::{entry_point_hit, entry_point_prefixes};
use serde::Deserialize;
use std::io::Read;
#[derive(Deserialize, Default)]
struct ToolInput {
path: Option<String>,
file_path: Option<String>,
target_file: Option<String>,
}
#[derive(Deserialize, Default)]
struct HookEvent {
tool_name: Option<String>,
#[serde(default)]
tool_input: ToolInput,
}
fn is_write_tool(name: &str) -> bool {
matches!(
name,
"write_file"
| "Write"
| "create_file"
| "edit_file"
| "Edit"
| "MultiEdit"
| "str_replace_based_edit"
| "str_replace_editor"
| "overwrite_file"
| "patch_file"
)
}
fn extract_path(input: &ToolInput) -> Option<String> {
input
.path
.clone()
.or_else(|| input.file_path.clone())
.or_else(|| input.target_file.clone())
}
fn advise_json(hit: &str, target: &str) {
let msg = format!(
"'{target}' is a registered fragile entry point (matched: '{hit}') per \
core/rules/71-entry-point-verify-law.md. A syntactic mistake here can break the \
whole tool, not just a code path — dispatch an independent verify-agent (or \
spec-verifier) to real-exec() this file (not `node file.js` / re-reading the diff / \
trusting a prior test run) before treating this change as verified."
);
let out = serde_json::json!({
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": msg
}
});
println!("{out}");
}
pub fn cmd_entry_point_check() -> i32 {
let mut buf = String::new();
if std::io::stdin().read_to_string(&mut buf).is_err() {
return 0;
}
let event: HookEvent = serde_json::from_str(&buf).unwrap_or_default();
let tool = event.tool_name.as_deref().unwrap_or("");
if !is_write_tool(tool) {
return 0;
}
let target = match extract_path(&event.tool_input) {
Some(p) if !p.is_empty() => p,
_ => return 0,
};
if let Some(hit) = entry_point_hit(&target, &entry_point_prefixes()) {
advise_json(&hit, &target);
}
0
}
#[cfg(test)]
mod tests {
use super::*;
fn make_payload(tool: &str, path: &str) -> String {
serde_json::json!({
"tool_name": tool,
"tool_input": { "path": path }
})
.to_string()
}
fn would_advise(payload: &str) -> Option<String> {
let event: HookEvent = serde_json::from_str(payload).unwrap_or_default();
let tool = event.tool_name.as_deref().unwrap_or("");
if !is_write_tool(tool) {
return None;
}
let target = extract_path(&event.tool_input)?;
entry_point_hit(&target, &entry_point_prefixes())
}
#[test]
fn advises_on_registered_entry_point_write() {
assert!(would_advise(&make_payload("Write", "scripts/yana-rt-wrapper.js")).is_some());
}
#[test]
fn advises_on_registered_entry_point_edit() {
assert!(would_advise(&make_payload("Edit", "scripts/yana-rt-wrapper.js")).is_some());
}
#[test]
fn advises_on_registered_entry_point_multi_edit() {
let payload = serde_json::json!({
"tool_name": "MultiEdit",
"tool_input": { "file_path": "scripts/yana-rt-wrapper.js" }
})
.to_string();
assert!(would_advise(&payload).is_some());
}
#[test]
fn silent_on_unrelated_file() {
assert!(would_advise(&make_payload("Write", "src/main.rs")).is_none());
}
#[test]
fn silent_on_read_tool() {
assert!(would_advise(&make_payload("Read", "scripts/yana-rt-wrapper.js")).is_none());
}
#[test]
fn silent_when_no_path_in_payload() {
let payload = serde_json::json!({ "tool_name": "Write", "tool_input": {} }).to_string();
assert!(would_advise(&payload).is_none());
}
}