1717import click
1818
1919from roam .capability import roam_capability
20+ from roam .catalog ._shared import is_test_path as _is_test_path
2021from roam .commands .changed_files import get_changed_files , resolve_changed_to_db
2122from roam .commands .resolve import ensure_index
2223from roam .db .connection import batched_in , find_project_root , open_db
@@ -268,15 +269,36 @@ def _check_anti_patterns(conn, changed_file_ids, status=None):
268269
269270 changed_fids = set (changed_file_ids )
270271
272+ # W1259 dogfood fix (CHALLENGE 57 HIGH loop-query): the original loop ran
273+ # one ``SELECT file_id FROM symbols WHERE id = ?`` per finding. On
274+ # roam-code itself ``run_detectors`` emits ~7900 findings, producing
275+ # ~7900 SQL round-trips here just to filter to the small set of changed
276+ # files. Pre-fetch the symbol->file_id map for ALL referenced symbols in
277+ # one batched query, then filter in Python.
278+ candidate_sids = {f .get ("symbol_id" ) for f in findings if f .get ("symbol_id" )}
279+ sym_to_file : dict [int , int ] = {}
280+ if candidate_sids :
281+ try :
282+ rows = batched_in (
283+ conn ,
284+ "SELECT id, file_id FROM symbols WHERE id IN ({ph})" ,
285+ list (candidate_sids ),
286+ )
287+ sym_to_file = {r ["id" ]: r ["file_id" ] for r in rows }
288+ except Exception as exc : # noqa: BLE001
289+ # Lineage: degrade loudly. If the batched lookup fails we
290+ # cannot safely scope findings to changed files, so mark the
291+ # check as errored rather than emit a misleading clean result.
292+ if status is not None :
293+ status ["anti_patterns" ] = f"errored:symbol_file_lookup:{ type (exc ).__name__ } "
294+ return challenges
295+
271296 for f in findings :
272297 sym_id = f .get ("symbol_id" )
273298 if not sym_id :
274299 continue
275- try :
276- row = conn .execute ("SELECT file_id FROM symbols WHERE id = ?" , (sym_id ,)).fetchone ()
277- except Exception :
278- continue
279- if not row or row ["file_id" ] not in changed_fids :
300+ file_id = sym_to_file .get (sym_id )
301+ if file_id is None or file_id not in changed_fids :
280302 continue
281303
282304 confidence = f .get ("confidence" , "medium" )
@@ -437,9 +459,15 @@ def _check_orphaned_symbols(conn, changed_sym_ids, status=None):
437459 file_path = (sym ["file_path" ] or "" ).replace ("\\ " , "/" )
438460 name = sym ["name" ] or ""
439461
440- # Skip test files and private symbols
441- is_test = file_path .startswith ("test" ) or "tests/" in file_path or "test/" in file_path or "spec/" in file_path
442- if is_test :
462+ # W1259 dogfood fix (W907 cargo-cult guard + parity): the original
463+ # ad-hoc check (``startswith("test")`` + ``"tests/" in``) missed
464+ # ``_test.go`` / ``_test.py`` suffix files, ``__tests__/``
465+ # directories, and camelCase ``UserTest.java`` / ``UserSpec.scala``
466+ # / ``UserTests.cs`` basenames — all of which the canonical
467+ # ``roam.catalog._shared.is_test_path`` detects. Delegate to the
468+ # canonical helper so multi-language repos don't see test
469+ # symbols flagged as orphans here.
470+ if _is_test_path (file_path ):
443471 continue
444472 if name .startswith ("_" ):
445473 continue
@@ -758,16 +786,28 @@ def adversarial(ctx, staged, commit_range, severity, fail_on_critical, fmt):
758786 # ------------------------------------------------------------------
759787 # Gather symbol IDs and file IDs for changed files
760788 # ------------------------------------------------------------------
789+ # W1259 dogfood fix (CHALLENGE 71/77/88 silent-swallow at line 769):
790+ # the original loop ran one ``SELECT id FROM symbols WHERE file_id =
791+ # ?`` per changed file and silently swallowed any failure. A SQLite
792+ # error here would leave ``changed_sym_ids`` partial, making every
793+ # downstream check (cycles / layers / cross-cluster / orphaned /
794+ # fan-out) emit degraded results indistinguishable from a clean
795+ # pass — the canonical Pattern-2 silent-fallback hole. Batch the
796+ # lookup into one query AND degrade loudly via ``check_status``
797+ # when it fails.
761798 changed_sym_ids : set [int ] = set ()
762- changed_file_ids : set [int ] = set ()
763-
764- for path , fid in file_map .items ():
765- changed_file_ids .add (fid )
799+ changed_file_ids : set [int ] = set (file_map .values ())
800+ sym_lookup_status = "ran"
801+ if changed_file_ids :
766802 try :
767- syms = conn .execute ("SELECT id FROM symbols WHERE file_id = ?" , (fid ,)).fetchall ()
768- changed_sym_ids .update (s ["id" ] for s in syms )
769- except Exception :
770- pass
803+ rows = batched_in (
804+ conn ,
805+ "SELECT id FROM symbols WHERE file_id IN ({ph})" ,
806+ list (changed_file_ids ),
807+ )
808+ changed_sym_ids = {r ["id" ] for r in rows }
809+ except Exception as exc : # noqa: BLE001
810+ sym_lookup_status = f"errored:symbol_lookup:{ type (exc ).__name__ } "
771811
772812 # ------------------------------------------------------------------
773813 # Run all challenge generators
@@ -777,6 +817,10 @@ def adversarial(ctx, staged, commit_range, severity, fail_on_critical, fmt):
777817 # "changes look clean" when any check errored. Same shape as the
778818 # W832 cmd_critique guard and the X4 cmd_pr_prep guard.
779819 check_status : dict [str , str ] = {}
820+ # W1259 dogfood: also surface the changed-symbol lookup status so a
821+ # SQL failure here cannot silently produce empty downstream results.
822+ if sym_lookup_status != "ran" :
823+ check_status ["symbol_lookup" ] = sym_lookup_status
780824 challenges : list [dict ] = []
781825 challenges .extend (_check_new_cycles (conn , changed_sym_ids , status = check_status ))
782826 challenges .extend (_check_layer_violations (conn , changed_sym_ids , status = check_status ))
@@ -810,6 +854,13 @@ def adversarial(ctx, staged, commit_range, severity, fail_on_critical, fmt):
810854 errored_checks = sorted (name for name , s in check_status .items () if s .startswith ("errored:" ))
811855 partial_success = bool (errored_checks )
812856
857+ # W1259 dogfood fix (LAW 4): the original verdicts ended on
858+ # ``critical`` / ``severity`` / ``info`` — none of which are in
859+ # ``_CONCRETE_NOUN_ANCHORS``. Static lint missed it because
860+ # ``facts = [verdict]`` is a Name reference, not a literal. At
861+ # runtime the verdict (which fact[0] copies) failed LAW 4
862+ # anchoring. Rephrase each verdict to terminate on ``challenges``
863+ # (anchored).
813864 if not challenges :
814865 if partial_success :
815866 verdict = (
@@ -820,13 +871,13 @@ def adversarial(ctx, staged, commit_range, severity, fail_on_critical, fmt):
820871 else :
821872 verdict = "No architectural challenges found -- changes look clean"
822873 elif critical > 0 :
823- verdict = f"{ len (challenges )} challenge(s), { critical } critical "
874+ verdict = f"{ critical } critical of { len (challenges )} challenges "
824875 elif high > 0 :
825- verdict = f"{ len (challenges )} challenge(s), { high } high severity "
876+ verdict = f"{ high } high-severity of { len (challenges )} challenges "
826877 elif warning > 0 :
827- verdict = f"{ len ( challenges ) } challenge (s), { warning } warning(s) "
878+ verdict = f"{ warning } warning (s) across { len ( challenges ) } challenges "
828879 else :
829- verdict = f"{ len (challenges )} challenge(s), { info } info "
880+ verdict = f"{ info } info-level of { len (challenges )} challenges "
830881 if partial_success and challenges :
831882 # Append partial qualifier so consumers see BOTH the findings
832883 # count AND the cascade. Matches the W832 cmd_critique shape.
0 commit comments