feat: catch-up de decisiones previas (Webpage→Url, anti-bot, UI 2-col, tests cross-platform)
Bloque de cambios revisados y validados con el usuario en sesiones previas que no habian aterrizado en commits propios. Lista por tema: * enrichers: web_search ahora usa lite.duckduckgo.com como endpoint primario (mas tolerante con bot detection desde IP residencial), con fallback al endpoint html. Detecta pagina captcha y emite error claro si ambos fallan. Anyade _DDGLiteParser para el formato lite + auto-pick de parser por contenido. * enrichers: tipo Webpage unificado en Url (campos de cuerpo cacheado viven en metadata del Url). Manifests actualizados (applies_to: [Url]). fetch_webpage ya no convierte Url->Webpage. * enrichers/manifest: campo `params` parseado a EnricherSpec.params (name, type, default_value, description). UI puede renderizar dialog de configuracion. * jobs: fix de path conversion para Python embebido nativo Windows (no convertir a /mnt/c/... cuando el subproceso es Windows-native; solo cuando es bash o python via WSL). * main.cpp: ventana ImGui (no modal) "Run enricher" con layout 2-col (label izq, input der). Inserta job con JSON tipado. Layout clustering apretado: hijos del mismo anchor en un solo anillo alrededor del padre, sin desperdigar por anillos crecientes. * views: inspector con layout 2-col via BeginTable (Identity, Schema fields, Extras). Description full-width debajo de su label. * tests: portable conftest (auto-detecta REGISTRY_ROOT, PYTHON_BIN, ENRICHERS_DIR para WSL y Windows portable). _runner.py trampoline inyecta stub via sys.path porque embedded Python ignora PYTHONPATH. Tests bash-only (vendor_script, freeze, dispatcher bash, resolver Linux-binary) skipean en Windows. Tests existentes adaptados a Webpage->Url. Resultado actual: 32 passed WSL, 21 passed + 11 skipped Windows.
This commit is contained in:
+87
-8
@@ -59,6 +59,57 @@ std::vector<std::string> parse_inline_list(const std::string& v) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Split por comas a nivel cero, respetando comillas y nesting de [] / {}.
|
||||
// El YAML inline `{ name: limit, type: int, default: 10 }` puede contener
|
||||
// strings con comas entre comillas — un split crudo las rompería.
|
||||
std::vector<std::string> split_top_level(const std::string& s) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
int depth_b = 0, depth_c = 0;
|
||||
char quote = 0;
|
||||
for (char c : s) {
|
||||
if (quote) {
|
||||
cur.push_back(c);
|
||||
if (c == quote) quote = 0;
|
||||
continue;
|
||||
}
|
||||
if (c == '"' || c == '\'') { quote = c; cur.push_back(c); continue; }
|
||||
if (c == '[') ++depth_b;
|
||||
if (c == ']') --depth_b;
|
||||
if (c == '{') ++depth_c;
|
||||
if (c == '}') --depth_c;
|
||||
if (c == ',' && depth_b == 0 && depth_c == 0) {
|
||||
out.push_back(cur);
|
||||
cur.clear();
|
||||
continue;
|
||||
}
|
||||
cur.push_back(c);
|
||||
}
|
||||
if (!cur.empty()) out.push_back(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Parsea un objeto YAML inline `{ name: x, type: int, default: 10 }` a un
|
||||
// EnricherParam. Retorna true si al menos `name` se resolvio.
|
||||
bool parse_inline_param(const std::string& v, EnricherParam* out) {
|
||||
std::string s = strip(v);
|
||||
if (s.size() < 2 || s.front() != '{' || s.back() != '}') return false;
|
||||
s = s.substr(1, s.size() - 2);
|
||||
for (auto& kv : split_top_level(s)) {
|
||||
size_t colon = kv.find(':');
|
||||
if (colon == std::string::npos) continue;
|
||||
std::string k = strip(kv.substr(0, colon));
|
||||
std::string val = strip_quotes(strip(kv.substr(colon + 1)));
|
||||
if (k == "name") out->name = val;
|
||||
else if (k == "type") out->type = lower(val);
|
||||
else if (k == "default") out->default_value = val;
|
||||
else if (k == "description") out->description = val;
|
||||
else if (k == "desc") out->description = val;
|
||||
}
|
||||
if (out->type.empty()) out->type = "string";
|
||||
return !out->name.empty();
|
||||
}
|
||||
|
||||
// Manifest YAML soportado (subset):
|
||||
// id: fetch_webpage
|
||||
// name: "Fetch web page"
|
||||
@@ -66,16 +117,19 @@ std::vector<std::string> parse_inline_list(const std::string& v) {
|
||||
// applies_to: [Webpage, Url]
|
||||
// lang: python <- issue 0033: go|python|bash (default python)
|
||||
// exec: run <- basename del binario/script (default "run")
|
||||
// params: <- v1 ignora bloque
|
||||
// - { name: timeout_s, ... }
|
||||
// params:
|
||||
// - { name: timeout_s, type: int, default: 15 }
|
||||
// - { name: region, type: string, default: "" }
|
||||
//
|
||||
// Las claves anidadas bajo `params:` (y otros bloques con valor vacio
|
||||
// seguido de lineas indentadas) se ignoran.
|
||||
// Solo el bloque `params:` se parsea con detalle. Otros bloques con valor
|
||||
// vacio seguido de lineas indentadas (`emits:`, `relations:`,
|
||||
// `uses_functions:`) se ignoran como antes.
|
||||
bool parse_manifest(const std::string& path, EnricherSpec* out) {
|
||||
std::ifstream f(path);
|
||||
if (!f) return false;
|
||||
std::string line;
|
||||
bool in_skip_block = false;
|
||||
bool in_skip_block = false;
|
||||
bool in_params_block = false;
|
||||
while (std::getline(f, line)) {
|
||||
// Strip CR de Windows.
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
@@ -84,10 +138,27 @@ bool parse_manifest(const std::string& path, EnricherSpec* out) {
|
||||
std::string trim = strip(line);
|
||||
if (trim.empty() || trim.front() == '#') continue;
|
||||
|
||||
// Si la linea NO empieza con whitespace, salimos del bloque skip.
|
||||
// Si la linea NO empieza con whitespace, salimos de los bloques
|
||||
// anidados — el siguiente top-level reinicia el contexto.
|
||||
bool indented = !line.empty() && std::isspace((unsigned char)line.front());
|
||||
if (!indented) in_skip_block = false;
|
||||
if (!indented) {
|
||||
in_skip_block = false;
|
||||
in_params_block = false;
|
||||
}
|
||||
if (in_skip_block) continue;
|
||||
if (in_params_block) {
|
||||
// Linea esperada: ` - { name: x, type: int, default: 10 }`.
|
||||
// Tolera variaciones de indent y comilla.
|
||||
std::string body = trim;
|
||||
if (!body.empty() && body.front() == '-') {
|
||||
body = strip(body.substr(1));
|
||||
}
|
||||
EnricherParam p;
|
||||
if (parse_inline_param(body, &p)) {
|
||||
out->params.push_back(std::move(p));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t colon = trim.find(':');
|
||||
if (colon == std::string::npos) continue;
|
||||
@@ -101,9 +172,17 @@ bool parse_manifest(const std::string& path, EnricherSpec* out) {
|
||||
else if (key == "applies_to") out->applies_to = parse_inline_list(val);
|
||||
else if (key == "lang") out->lang = lower(strip_quotes(val));
|
||||
else if (key == "exec") out->exec_basename = strip_quotes(val);
|
||||
else if (key == "params" && val.empty()) in_skip_block = true;
|
||||
else if (key == "params") {
|
||||
// `params: []` — vacio explicito, nada que hacer.
|
||||
// `params:` — siguiente bloque indentado son items.
|
||||
std::string vs = strip(val);
|
||||
if (vs.empty()) in_params_block = true;
|
||||
// Si fuese inline (`params: [{...}]`) — formato no usado en
|
||||
// los manifests actuales, lo ignoramos.
|
||||
}
|
||||
else if (key == "emits" && val.empty()) in_skip_block = true;
|
||||
else if (key == "relations" && val.empty()) in_skip_block = true;
|
||||
else if (key == "uses_functions" && val.empty()) in_skip_block = true;
|
||||
}
|
||||
|
||||
// Defaults — preservan retrocompat con manifests existentes que no
|
||||
|
||||
Reference in New Issue
Block a user