@@ -148,6 +148,54 @@ _filter_crosshair_dirs() {
148148 echo " ${filtered[@]} "
149149}
150150
151+ # Convert source directory paths to Python module names for CrossHair
152+ # CrossHair expects module names (e.g., "sqlalchemy") not paths (e.g., "lib/sqlalchemy")
153+ # This function extracts the module name and ensures PYTHONPATH includes the parent directory
154+ _path_to_module () {
155+ local source_path=" $1 "
156+ local repo_path=" ${2:- ${REPO_PATH} } "
157+
158+ # Remove trailing slash
159+ source_path=" ${source_path%/ } "
160+
161+ # If it's an absolute path, make it relative to repo
162+ if [[ " $source_path " == /* ]]; then
163+ source_path=" ${source_path# ${repo_path} / } "
164+ fi
165+
166+ # Handle common patterns: lib/pkg, src/pkg, backend/app, pkg
167+ # Extract the module name (last component that's a valid Python package)
168+ local module_name=" "
169+ local parent_dir=" "
170+
171+ if [[ " $source_path " == * " /" * ]]; then
172+ # Path has directory structure (e.g., lib/sqlalchemy, src/mypackage)
173+ parent_dir=" ${source_path%/* } " # Everything before last /
174+ module_name=" ${source_path##*/ } " # Last component
175+
176+ # Check if the module directory has __init__.py (is a package)
177+ local full_module_path=" ${repo_path} /${source_path} "
178+ if [[ -f " ${full_module_path} /__init__.py" ]]; then
179+ # It's a package - return module name and parent dir
180+ echo " ${module_name} |${repo_path} /${parent_dir} "
181+ return 0
182+ fi
183+
184+ # Check subdirectories for packages (e.g., lib/sqlalchemy where sqlalchemy is the package)
185+ for subdir in " ${full_module_path} " /* ; do
186+ if [[ -d " $subdir " ]] && [[ -f " ${subdir} /__init__.py" ]]; then
187+ # Found a package subdirectory
188+ echo " $( basename " $subdir " ) |${full_module_path} "
189+ return 0
190+ fi
191+ done
192+ fi
193+
194+ # No directory structure or no package found - use the path as-is
195+ # This handles cases like "mypackage" where PYTHONPATH is already set correctly
196+ echo " ${source_path} |"
197+ }
198+
151199run_with_timeout () {
152200 local timeout_secs=" $1 "
153201 shift
@@ -427,36 +475,68 @@ if [[ "${RUN_CROSSHAIR}" == "1" ]] && command -v crosshair >/dev/null 2>&1; then
427475 if [[ -z " ${CROSSHAIR_FILTERED_DIRS} " ]]; then
428476 echo " [sidecar] warning: all source directories filtered out (contain tests), skipping source code analysis"
429477 else
430- if [[ " ${FRAMEWORK_TYPE} " == " django" ]]; then
431- # Use Django-aware wrapper for source code analysis
432- CROSSHAIR_WRAPPER=" ${SIDECAR_DIR} /../frameworks/django/crosshair_django_wrapper.py"
433- if [[ -f " ${CROSSHAIR_WRAPPER} " ]]; then
434- echo " [sidecar] using Django-aware CrossHair wrapper for source analysis"
435- # Export environment variables for Django initialization
436- CROSSHAIR_ENV=" "
437- if [[ -n " ${DJANGO_SETTINGS_MODULE:- } " ]]; then
438- CROSSHAIR_ENV=" DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} "
478+ # Convert source paths to module names for CrossHair
479+ # CrossHair expects module names (e.g., "sqlalchemy") not paths (e.g., "lib/sqlalchemy")
480+ CROSSHAIR_MODULES=" "
481+ CROSSHAIR_EXTRA_PYTHONPATH=" "
482+ for src_dir in ${CROSSHAIR_FILTERED_DIRS} ; do
483+ MODULE_INFO=$( _path_to_module " $src_dir " " ${REPO_PATH} " )
484+ MODULE_NAME=" ${MODULE_INFO%% |* } "
485+ MODULE_PARENT=" ${MODULE_INFO##* |} "
486+
487+ if [[ -n " $MODULE_NAME " ]]; then
488+ CROSSHAIR_MODULES=" ${CROSSHAIR_MODULES} ${MODULE_NAME} "
489+ if [[ -n " $MODULE_PARENT " ]] && [[ " :${CROSSHAIR_EXTRA_PYTHONPATH} :" != * " :${MODULE_PARENT} :" * ]]; then
490+ CROSSHAIR_EXTRA_PYTHONPATH=" ${CROSSHAIR_EXTRA_PYTHONPATH} :${MODULE_PARENT} "
491+ fi
439492 fi
440- if [[ -n " ${REPO_PATH:- } " ]]; then
441- CROSSHAIR_ENV=" ${CROSSHAIR_ENV} REPO_PATH=${REPO_PATH} "
493+ done
494+ CROSSHAIR_MODULES=" ${CROSSHAIR_MODULES# } " # Trim leading space
495+ CROSSHAIR_EXTRA_PYTHONPATH=" ${CROSSHAIR_EXTRA_PYTHONPATH#: } " # Trim leading colon
496+
497+ if [[ -z " ${CROSSHAIR_MODULES} " ]]; then
498+ echo " [sidecar] warning: could not convert source directories to modules, skipping source code analysis"
499+ else
500+ echo " [sidecar] analyzing modules: ${CROSSHAIR_MODULES} "
501+ if [[ -n " ${CROSSHAIR_EXTRA_PYTHONPATH} " ]]; then
502+ echo " [sidecar] extra PYTHONPATH: ${CROSSHAIR_EXTRA_PYTHONPATH} "
442503 fi
443- if [[ -n " ${PYTHONPATH:- } " ]]; then
444- CROSSHAIR_ENV=" ${CROSSHAIR_ENV} PYTHONPATH=${PYTHONPATH} "
504+
505+ # Build PYTHONPATH for CrossHair (include extra paths for module resolution)
506+ CROSSHAIR_PYTHONPATH=" ${PYTHONPATH:- } "
507+ if [[ -n " ${CROSSHAIR_EXTRA_PYTHONPATH} " ]]; then
508+ CROSSHAIR_PYTHONPATH=" ${CROSSHAIR_EXTRA_PYTHONPATH} :${CROSSHAIR_PYTHONPATH} "
509+ fi
510+
511+ if [[ " ${FRAMEWORK_TYPE} " == " django" ]]; then
512+ # Use Django-aware wrapper for source code analysis
513+ CROSSHAIR_WRAPPER=" ${SIDECAR_DIR} /../frameworks/django/crosshair_django_wrapper.py"
514+ if [[ -f " ${CROSSHAIR_WRAPPER} " ]]; then
515+ echo " [sidecar] using Django-aware CrossHair wrapper for source analysis"
516+ # Export environment variables for Django initialization
517+ CROSSHAIR_ENV=" "
518+ if [[ -n " ${DJANGO_SETTINGS_MODULE:- } " ]]; then
519+ CROSSHAIR_ENV=" DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} "
520+ fi
521+ if [[ -n " ${REPO_PATH:- } " ]]; then
522+ CROSSHAIR_ENV=" ${CROSSHAIR_ENV} REPO_PATH=${REPO_PATH} "
523+ fi
524+ CROSSHAIR_ENV=" ${CROSSHAIR_ENV} PYTHONPATH=${CROSSHAIR_PYTHONPATH} "
525+ run_and_log " ${TIMEOUT_CROSSHAIR} " \
526+ " ${SIDECAR_REPORTS_DIR} /${TIMESTAMP} -crosshair-source.log" \
527+ env ${CROSSHAIR_ENV} " ${PYTHON_CMD} " " ${CROSSHAIR_WRAPPER} " check " ${CROSSHAIR_ARGS[@]} " ${CROSSHAIR_MODULES}
528+ else
529+ echo " [sidecar] warning: Django wrapper not found, using standard CrossHair (may fail)"
530+ run_and_log " ${TIMEOUT_CROSSHAIR} " \
531+ " ${SIDECAR_REPORTS_DIR} /${TIMESTAMP} -crosshair-source.log" \
532+ env PYTHONPATH=" ${CROSSHAIR_PYTHONPATH} " " ${PYTHON_CMD} " -m crosshair check " ${CROSSHAIR_ARGS[@]} " ${CROSSHAIR_MODULES}
533+ fi
534+ else
535+ # Standard CrossHair for non-Django projects
536+ run_and_log " ${TIMEOUT_CROSSHAIR} " \
537+ " ${SIDECAR_REPORTS_DIR} /${TIMESTAMP} -crosshair-source.log" \
538+ env PYTHONPATH=" ${CROSSHAIR_PYTHONPATH} " " ${PYTHON_CMD} " -m crosshair check " ${CROSSHAIR_ARGS[@]} " ${CROSSHAIR_MODULES}
445539 fi
446- run_and_log " ${TIMEOUT_CROSSHAIR} " \
447- " ${SIDECAR_REPORTS_DIR} /${TIMESTAMP} -crosshair-source.log" \
448- env ${CROSSHAIR_ENV} " ${PYTHON_CMD} " " ${CROSSHAIR_WRAPPER} " check " ${CROSSHAIR_ARGS[@]} " ${CROSSHAIR_FILTERED_DIRS}
449- else
450- echo " [sidecar] warning: Django wrapper not found, using standard CrossHair (may fail)"
451- run_and_log " ${TIMEOUT_CROSSHAIR} " \
452- " ${SIDECAR_REPORTS_DIR} /${TIMESTAMP} -crosshair-source.log" \
453- " ${PYTHON_CMD} " -m crosshair check " ${CROSSHAIR_ARGS[@]} " ${CROSSHAIR_FILTERED_DIRS}
454- fi
455- else
456- # Standard CrossHair for non-Django projects
457- run_and_log " ${TIMEOUT_CROSSHAIR} " \
458- " ${SIDECAR_REPORTS_DIR} /${TIMESTAMP} -crosshair-source.log" \
459- " ${PYTHON_CMD} " -m crosshair check " ${CROSSHAIR_ARGS[@]} " ${CROSSHAIR_FILTERED_DIRS}
460540 fi
461541 fi
462542 fi
@@ -466,21 +546,30 @@ if [[ "${RUN_CROSSHAIR}" == "1" ]] && command -v crosshair >/dev/null 2>&1; then
466546 # This is the primary analysis method for frameworks without decorators (Django, etc.)
467547 if [[ -f " ${HARNESS_PATH} " ]]; then
468548 echo " [sidecar] crosshair (harness - external contracts)..."
549+
550+ # Build PYTHONPATH for harness analysis:
551+ # 1. Sidecar directory (for harness imports like 'common.adapters')
552+ # 2. Original PYTHONPATH (for repo modules)
553+ HARNESS_DIR=" $( cd " $( dirname " ${HARNESS_PATH} " ) " && pwd) "
554+ HARNESS_FILE=" $( basename " ${HARNESS_PATH} " ) "
555+ HARNESS_MODULE=" ${HARNESS_FILE% .py} " # Remove .py extension
556+
557+ # Build PYTHONPATH: sidecar dir + original PYTHONPATH
558+ HARNESS_PYTHONPATH=" ${HARNESS_DIR} "
559+ if [[ -n " ${PYTHONPATH:- } " ]]; then
560+ HARNESS_PYTHONPATH=" ${HARNESS_PYTHONPATH} :${PYTHONPATH} "
561+ fi
562+
469563 # Export environment variables for CrossHair subprocess
470- CROSSHAIR_ENV=" "
564+ CROSSHAIR_ENV=" PYTHONPATH= ${HARNESS_PYTHONPATH} "
471565 if [[ -n " ${DJANGO_SETTINGS_MODULE:- } " ]]; then
472- CROSSHAIR_ENV=" DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} "
566+ CROSSHAIR_ENV=" ${CROSSHAIR_ENV} DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} "
473567 fi
474568 if [[ -n " ${REPO_PATH:- } " ]]; then
475569 CROSSHAIR_ENV=" ${CROSSHAIR_ENV} REPO_PATH=${REPO_PATH} "
476570 fi
477- if [[ -n " ${PYTHONPATH:- } " ]]; then
478- CROSSHAIR_ENV=" ${CROSSHAIR_ENV} PYTHONPATH=${PYTHONPATH} "
479- fi
571+
480572 # Change to harness directory to ensure valid module name (avoids hyphenated directory names in module path)
481- HARNESS_DIR=" $( dirname " ${HARNESS_PATH} " ) "
482- HARNESS_FILE=" $( basename " ${HARNESS_PATH} " ) "
483- HARNESS_MODULE=" ${HARNESS_FILE% .py} " # Remove .py extension
484573 run_and_log " ${TIMEOUT_CROSSHAIR} " \
485574 " ${SIDECAR_REPORTS_DIR} /${TIMESTAMP} -crosshair-harness.log" \
486575 bash -c " cd '${HARNESS_DIR} ' && env ${CROSSHAIR_ENV}${PYTHON_CMD} -m crosshair check ${CROSSHAIR_ARGS[*]} ${HARNESS_MODULE} "
0 commit comments