diff --git a/enrichers.cpp b/enrichers.cpp index 9e440d9..18c2caf 100644 --- a/enrichers.cpp +++ b/enrichers.cpp @@ -180,6 +180,15 @@ bool parse_manifest(const std::string& path, EnricherSpec* out) { // Si fuese inline (`params: [{...}]`) — formato no usado en // los manifests actuales, lo ignoramos. } + else if (key == "auto_group_threshold") { + // Issue 0035e: override del threshold de auto-grouping. Si el + // valor no es un entero parseable, se ignora (queda en 0 = + // usar default interno del enricher). + try { + int v = std::stoi(strip_quotes(val)); + if (v > 0) out->auto_group_threshold = v; + } catch (...) { /* ignore */ } + } 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; diff --git a/enrichers.h b/enrichers.h index 26bac80..1048b0d 100644 --- a/enrichers.h +++ b/enrichers.h @@ -48,6 +48,14 @@ struct EnricherSpec { // Parametros editables por el usuario antes de lanzar el job. std::vector params; + // Threshold opcional de auto-grouping (issue 0035e). Si > 0, el + // enricher debe respetarlo al decidir cuando crear un Group con sus + // resultados (vs dejarlos sueltos). Cuando es 0 / no declarado, el + // enricher usa su default interno (DEFAULT_GROUP_THRESHOLD = 50). + // Se propaga al runtime Python via campo `auto_group_threshold` del + // JSON de stdin que jobs.cpp construye. + int auto_group_threshold = 0; + // True si lang != "" y no se pudo resolver el ejecutable // correspondiente (ej: enricher Go sin compilar). El loader deja // el spec en el registro pero marcado como deshabilitado para diff --git a/enrichers/extract_iocs_text/run.py b/enrichers/extract_iocs_text/run.py index fed3983..880bb34 100644 --- a/enrichers/extract_iocs_text/run.py +++ b/enrichers/extract_iocs_text/run.py @@ -48,6 +48,17 @@ DEFAULT_GROUP_THRESHOLD = 50 GROUP_PREVIEW_K = 10 +def _coerce_threshold(raw, default: int) -> int: + """Acepta int / str numerico / None, devuelve >0 o el default (issue 0035e).""" + if raw is None or raw == "": + return default + try: + v = int(raw) + except (TypeError, ValueError): + return default + return v if v > 0 else default + + def progress(p: float, stage: str = "") -> None: sys.stderr.write(f"PROGRESS:{p:.2f} {stage}\n") sys.stderr.flush() @@ -206,7 +217,9 @@ def main() -> int: try: has_group_col = has_group_id_column(conn) n_total = len(unique) - threshold = DEFAULT_GROUP_THRESHOLD + # Issue 0035e: respeta override del manifest si viene en ctx. + threshold = _coerce_threshold(ctx.get("auto_group_threshold"), + DEFAULT_GROUP_THRESHOLD) if n_total >= threshold and has_group_col: # Group heterogeneo (provisional, ver docstring). diff --git a/enrichers/split_sentences/run.py b/enrichers/split_sentences/run.py index e125ec9..f045f32 100644 --- a/enrichers/split_sentences/run.py +++ b/enrichers/split_sentences/run.py @@ -38,6 +38,17 @@ from datetime import datetime, timezone DEFAULT_GROUP_THRESHOLD = 50 GROUP_PREVIEW_K = 10 + +def _coerce_threshold(raw, default: int) -> int: + """Acepta int / str numerico / None, devuelve >0 o el default (issue 0035e).""" + if raw is None or raw == "": + return default + try: + v = int(raw) + except (TypeError, ValueError): + return default + return v if v > 0 else default + # Split por delimitador de oracion (.!?) seguido de whitespace seguido de # inicial de oracion en mayusculas (incluye acentos espanoles). Robusto # para texto en espanol e ingles. Casos limite (abreviaturas como "Sr.", @@ -273,7 +284,9 @@ def main() -> int: try: has_group_col = has_group_id_column(conn) n_total = len(sentences) - threshold = DEFAULT_GROUP_THRESHOLD + # Issue 0035e: respeta override del manifest si viene en ctx. + threshold = _coerce_threshold(ctx.get("auto_group_threshold"), + DEFAULT_GROUP_THRESHOLD) if n_total >= threshold and has_group_col: group_id = insert_group_entity( diff --git a/enrichers/split_words/run.py b/enrichers/split_words/run.py index 9cdaf0a..5e69323 100644 --- a/enrichers/split_words/run.py +++ b/enrichers/split_words/run.py @@ -39,6 +39,17 @@ from datetime import datetime, timezone DEFAULT_GROUP_THRESHOLD = 50 GROUP_PREVIEW_K = 10 + +def _coerce_threshold(raw, default: int) -> int: + """Acepta int / str numerico / None, devuelve >0 o el default (issue 0035e).""" + if raw is None or raw == "": + return default + try: + v = int(raw) + except (TypeError, ValueError): + return default + return v if v > 0 else default + # Tokenizer: secuencias de letras (incluye acentos espanyoles + apostrofo # interno tipo "don't"). Mas robusto que split por espacios para texto # real con puntuacion adyacente. Numeros se ignoran — pensado para @@ -264,7 +275,9 @@ def main() -> int: try: has_group_col = has_group_id_column(conn) n_total = len(words) - threshold = DEFAULT_GROUP_THRESHOLD + # Issue 0035e: respeta override del manifest si viene en ctx. + threshold = _coerce_threshold(ctx.get("auto_group_threshold"), + DEFAULT_GROUP_THRESHOLD) if n_total >= threshold and has_group_col: group_id = insert_group_entity( diff --git a/enrichers/web_search/run.py b/enrichers/web_search/run.py index 45fef87..e992362 100755 --- a/enrichers/web_search/run.py +++ b/enrichers/web_search/run.py @@ -50,6 +50,22 @@ DEFAULT_GROUP_THRESHOLD = 50 GROUP_PREVIEW_K = 10 +def _coerce_threshold(raw, default: int) -> int: + """Acepta int / str numerico / None, devuelve >0 o el default. + + Issue 0035e: el manifest puede declarar `auto_group_threshold: ` + y jobs.cpp lo propaga al subprocess. Cualquier otro valor (None, + "", 0, no parseable) cae al default global. + """ + if raw is None or raw == "": + return default + try: + v = int(raw) + except (TypeError, ValueError): + return default + return v if v > 0 else default + + def progress(p: float, stage: str = "") -> None: sys.stderr.write(f"PROGRESS:{p:.2f} {stage}\n") sys.stderr.flush() @@ -619,9 +635,11 @@ def main() -> int: try: has_group_col = has_group_id_column(conn) n_total = len(results) - # Threshold: por ahora hardcoded; la lectura del manifest - # vendra en 0035e (settings UI / overrides por enricher). - threshold = DEFAULT_GROUP_THRESHOLD + # Threshold: el manifest puede declarar `auto_group_threshold` y + # jobs.cpp lo propaga via stdin (issue 0035e). Si no viene, se + # usa el default interno del enricher. + threshold = _coerce_threshold(ctx.get("auto_group_threshold"), + DEFAULT_GROUP_THRESHOLD) if n_total >= threshold and has_group_col: # Modo Twitter/Reddit: K sueltos + Group con N-K hijos. diff --git a/jobs.cpp b/jobs.cpp index ba0d841..0bb75d1 100644 --- a/jobs.cpp +++ b/jobs.cpp @@ -391,7 +391,8 @@ std::string build_stdin_json(const std::string& job_id, const std::string& ops_db, const std::string& app_dir, const std::string& registry_root, - const std::string& lang) + const std::string& lang, + int auto_group_threshold = 0) { std::string node_type, node_name, node_metadata = "{}"; if (!node_id.empty()) { @@ -457,8 +458,14 @@ std::string build_stdin_json(const std::string& job_id, << "\"ops_db_path\":\"" << json_escape(ops_db_out) << "\"," << "\"app_dir\":\"" << json_escape(app_dir_out) << "\"," << "\"cache_dir\":\"" << json_escape(cache_dir) << "\"," - << "\"registry_root\":\"" << json_escape(root_out) << "\"" - << '}'; + << "\"registry_root\":\"" << json_escape(root_out) << "\""; + // Issue 0035e: solo emitimos el campo si el manifest declara override. + // Asi las pruebas que NO setean el campo siguen viendo defaults estables + // y los enrichers Python solo lo leen cuando viene declarado. + if (auto_group_threshold > 0) { + o << ",\"auto_group_threshold\":" << auto_group_threshold; + } + o << '}'; return o.str(); } @@ -1050,7 +1057,8 @@ void worker_loop() { } std::string stdin_payload = build_stdin_json( ctx.id, ctx.enricher_id, ctx.node_id, ctx.params_json, - ops_db, g_state->app_dir, g_state->registry_root, lang); + ops_db, g_state->app_dir, g_state->registry_root, lang, + spec->auto_group_threshold); ProcResult res = run_subprocess(job_id, run_path, lang, stdin_payload, ctrl);