invar-tools 1.17.23__py3-none-any.whl → 1.17.25__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
invar/shell/config.py CHANGED
@@ -435,24 +435,42 @@ _DEFAULT_EXCLUDE_PATHS = [
435
435
  ]
436
436
 
437
437
 
438
+ # @shell_complexity: Config fallthrough requires checking multiple sources
438
439
  def _get_classification_config(project_root: Path) -> Result[dict[str, Any], str]:
439
- """Get classification-related config (paths and patterns)."""
440
- find_result = _find_config_source(project_root)
441
- if isinstance(find_result, Failure):
442
- return Success({}) # Return empty on error
443
- config_path, source = find_result.unwrap()
440
+ """Get classification-related config (paths and patterns).
444
441
 
445
- if source == "default":
446
- return Success({})
442
+ Uses fallthrough logic: if pyproject.toml exists but has no [tool.invar.guard],
443
+ continues to check invar.toml and .invar/config.toml.
444
+ """
445
+ # Build list of config sources to try (same order as load_config)
446
+ sources_to_try: list[tuple[Path, ConfigSource]] = []
447
+
448
+ pyproject = project_root / "pyproject.toml"
449
+ if pyproject.exists():
450
+ sources_to_try.append((pyproject, "pyproject"))
447
451
 
448
- assert config_path is not None
449
- result = _read_toml(config_path)
452
+ invar_toml = project_root / "invar.toml"
453
+ if invar_toml.exists():
454
+ sources_to_try.append((invar_toml, "invar"))
450
455
 
451
- if isinstance(result, Failure):
452
- return Success({}) # Return empty on error
456
+ invar_config = project_root / ".invar" / "config.toml"
457
+ if invar_config.exists():
458
+ sources_to_try.append((invar_config, "invar_dir"))
459
+
460
+ # Try each source, fallback if no guard config found
461
+ for config_path, source in sources_to_try:
462
+ result = _read_toml(config_path)
463
+ if isinstance(result, Failure):
464
+ continue # Skip unreadable files
465
+
466
+ data = result.unwrap()
467
+ guard_config = extract_guard_section(data, source)
468
+
469
+ if guard_config: # Found valid guard config
470
+ return Success(guard_config)
453
471
 
454
- data = result.unwrap()
455
- return Success(extract_guard_section(data, source))
472
+ # No config found in any source, return empty dict
473
+ return Success({})
456
474
 
457
475
 
458
476
  def get_path_classification(project_root: Path) -> Result[tuple[list[str], list[str]], str]:
@@ -178,7 +178,24 @@ def run_crosshair_phase(
178
178
  return True, {"status": "skipped", "reason": "no files to verify"}
179
179
 
180
180
  # Only verify Core files (pure logic)
181
- core_files = [f for f in checked_files if "core" in str(f)]
181
+ # BUG-57: Use config-based core detection instead of hardcoded "core" in path
182
+ from invar.core.utils import matches_path_prefix
183
+ from invar.shell.config import get_path_classification
184
+
185
+ path_result = get_path_classification(path)
186
+ if isinstance(path_result, Success):
187
+ core_paths, _ = path_result.unwrap()
188
+ else:
189
+ core_paths = ["src/core", "core"]
190
+
191
+ def is_core_file(f: Path) -> bool:
192
+ try:
193
+ rel = str(f.relative_to(path))
194
+ except ValueError:
195
+ rel = str(f)
196
+ return matches_path_prefix(rel, core_paths)
197
+
198
+ core_files = [f for f in checked_files if is_core_file(f)]
182
199
  if not core_files:
183
200
  return True, {"status": "skipped", "reason": "no core files found"}
184
201
 
@@ -306,7 +323,24 @@ def run_property_tests_phase(
306
323
  return True, {"status": "skipped", "reason": "no files"}, None
307
324
 
308
325
  # Only test Core files (with contracts)
309
- core_files = [f for f in checked_files if "core" in str(f)]
326
+ # BUG-57: Use config-based core detection instead of hardcoded "core" in path
327
+ from invar.core.utils import matches_path_prefix
328
+ from invar.shell.config import get_path_classification
329
+
330
+ path_result = get_path_classification(project_root)
331
+ if isinstance(path_result, Success):
332
+ core_paths, _ = path_result.unwrap()
333
+ else:
334
+ core_paths = ["src/core", "core"]
335
+
336
+ def is_core_file(f: Path) -> bool:
337
+ try:
338
+ rel = str(f.relative_to(project_root))
339
+ except ValueError:
340
+ rel = str(f)
341
+ return matches_path_prefix(rel, core_paths)
342
+
343
+ core_files = [f for f in checked_files if is_core_file(f)]
310
344
  if not core_files:
311
345
  return True, {"status": "skipped", "reason": "no core files"}, None
312
346
 
@@ -97,7 +97,7 @@ def run_property_tests_on_file(
97
97
 
98
98
  root = project_root or file_path.parent
99
99
  with _inject_project_site_packages(root):
100
- module = _import_module_from_path(file_path)
100
+ module = _import_module_from_path(file_path, project_root=root)
101
101
 
102
102
  if module is None:
103
103
  return Failure(f"Could not import module: {file_path}")
@@ -218,15 +218,56 @@ def _accumulate_report(
218
218
  combined_report.errors.extend(file_report.errors)
219
219
 
220
220
 
221
- def _import_module_from_path(file_path: Path) -> object | None:
221
+ # @shell_complexity: BUG-57 fix requires package hierarchy setup for relative imports
222
+ def _import_module_from_path(file_path: Path, project_root: Path | None = None) -> object | None:
222
223
  """
223
224
  Import a Python module from a file path.
224
225
 
226
+ BUG-57: Properly handles relative imports by setting up package context.
227
+
225
228
  Returns None if import fails.
226
229
  """
227
230
  try:
228
- module_name = file_path.stem
229
- spec = importlib.util.spec_from_file_location(module_name, file_path)
231
+ # Calculate the full module name from project root
232
+ if project_root and file_path.is_relative_to(project_root):
233
+ # Convert path to module name: my_package/main.py -> my_package.main
234
+ relative = file_path.relative_to(project_root)
235
+ parts = list(relative.with_suffix("").parts)
236
+ module_name = ".".join(parts)
237
+ else:
238
+ module_name = file_path.stem
239
+
240
+ # Ensure project root is in sys.path for relative imports
241
+ if project_root:
242
+ root_str = str(project_root)
243
+ if root_str not in sys.path:
244
+ sys.path.insert(0, root_str)
245
+
246
+ # For packages with relative imports, we need to set up parent packages first
247
+ if "." in module_name:
248
+ # Import parent packages first
249
+ parts = module_name.split(".")
250
+ for i in range(1, len(parts)):
251
+ parent_name = ".".join(parts[:i])
252
+ if parent_name not in sys.modules:
253
+ parent_path = project_root / "/".join(parts[:i]) if project_root else None
254
+ if parent_path and (parent_path / "__init__.py").exists():
255
+ parent_spec = importlib.util.spec_from_file_location(
256
+ parent_name,
257
+ parent_path / "__init__.py",
258
+ submodule_search_locations=[str(parent_path)],
259
+ )
260
+ if parent_spec and parent_spec.loader:
261
+ parent_module = importlib.util.module_from_spec(parent_spec)
262
+ sys.modules[parent_name] = parent_module
263
+ parent_spec.loader.exec_module(parent_module)
264
+
265
+ # Now import the target module
266
+ spec = importlib.util.spec_from_file_location(
267
+ module_name,
268
+ file_path,
269
+ submodule_search_locations=[str(file_path.parent)],
270
+ )
230
271
  if spec is None or spec.loader is None:
231
272
  return None
232
273
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invar-tools
3
- Version: 1.17.23
3
+ Version: 1.17.25
4
4
  Summary: AI-native software engineering tools with design-by-contract verification
5
5
  Project-URL: Homepage, https://github.com/tefx/invar
6
6
  Project-URL: Documentation, https://github.com/tefx/invar#readme
@@ -2657,20 +2657,20 @@ invar/node_tools/quick-check/cli.js,sha256=dwV3hdJleFQga2cKUn3PPfQDvvujhzKdjQcIv
2657
2657
  invar/node_tools/ts-analyzer/cli.js,sha256=SvZ6HyjmobpP8NAZqXFiy8BwH_t5Hb17Ytar_18udaQ,4092887
2658
2658
  invar/shell/__init__.py,sha256=FFw1mNbh_97PeKPcHIqQpQ7mw-JoIvyLM1yOdxLw5uk,204
2659
2659
  invar/shell/claude_hooks.py,sha256=hV4DfG3cVng32f0Rxoo070tliVlYFC5v9slIWEbAD7E,18899
2660
- invar/shell/config.py,sha256=Q8HI_bYz3mwKTAuG4JYjiXt0kXCdQdWZ0hZn3xy9r-M,18570
2660
+ invar/shell/config.py,sha256=fPKY1AAqM28vcIIasvitYqqCv3g5VAjCdS8M7dNbQWA,19331
2661
2661
  invar/shell/contract_coverage.py,sha256=81OQkQqUVYUKytG5aiJyRK62gwh9UzbSG926vkvFTc8,12088
2662
2662
  invar/shell/coverage.py,sha256=m01o898IFIdBztEBQLwwL1Vt5PWrpUntO4lv4nWEkls,11344
2663
2663
  invar/shell/doc_tools.py,sha256=16gvo_ay9-_EK6lX16WkiRGg4OfTAKK_i0ucQkE7lbI,15149
2664
2664
  invar/shell/fs.py,sha256=ctqU-EX0NnKC4txudRCRpbWxWSgBZTInXMeOUnl3IM0,6196
2665
2665
  invar/shell/git.py,sha256=R-ynlYa65xtCdnNjHeu42uPyrqoo9KZDzl7BZUW0oWU,2866
2666
- invar/shell/guard_helpers.py,sha256=lpaFIe328ZISzim92TAxZHTT8jC4N0_TcQ7PV7u327w,16083
2666
+ invar/shell/guard_helpers.py,sha256=IWiQEhDXnvN7QizGFrTNgfkSRN3ZVRF66gj-CeTjuJE,17261
2667
2667
  invar/shell/guard_output.py,sha256=v3gG5P-_47nIFo8eAMKwdA_hLf2KZ0cQ-45Z6JjKp4w,12520
2668
2668
  invar/shell/mcp_config.py,sha256=-hC7Y5BGuVs285b6gBARk7ZyzVxHwPgXSyt_GoN0jfs,4580
2669
2669
  invar/shell/mutation.py,sha256=Lfyk2b8j8-hxAq-iwAgQeOhr7Ci6c5tRF1TXe3CxQCs,8914
2670
2670
  invar/shell/pattern_integration.py,sha256=pRcjfq3NvMW_tvQCnaXZnD1k5AVEWK8CYOE2jN6VTro,7842
2671
2671
  invar/shell/pi_hooks.py,sha256=ulZc1sP8mTRJTBsjwFHQzUgg-h8ajRIMp7iF1Y4UUtw,6885
2672
2672
  invar/shell/pi_tools.py,sha256=a3ACDDXykFV8fUB5UpBmgMvppwkmLvT1k_BWm0IY47k,4068
2673
- invar/shell/property_tests.py,sha256=SB-3tS5TupYQP1jpEUNpQs6fWCwLWxck0akNsgVwGGM,10533
2673
+ invar/shell/property_tests.py,sha256=qt0CP5RH9Md2ZZV64ziNsjQ_-x0onCYtZwbQfqw9gbY,12586
2674
2674
  invar/shell/py_refs.py,sha256=Vjz50lmt9prDBcBv4nkkODdiJ7_DKu5zO4UPZBjAfmM,4638
2675
2675
  invar/shell/skill_manager.py,sha256=Mr7Mh9rxPSKSAOTJCAM5ZHiG5nfUf6KQVCuD4LBNHSI,12440
2676
2676
  invar/shell/subprocess_env.py,sha256=hendEERSyAG4a8UFhYfPtOAlfspVRB03aVCYpj3uqk4,12745
@@ -2778,10 +2778,10 @@ invar/templates/skills/invar-reflect/template.md,sha256=Rr5hvbllvmd8jSLf_0ZjyKt6
2778
2778
  invar/templates/skills/investigate/SKILL.md.jinja,sha256=cp6TBEixBYh1rLeeHOR1yqEnFqv1NZYePORMnavLkQI,3231
2779
2779
  invar/templates/skills/propose/SKILL.md.jinja,sha256=6BuKiCqO1AEu3VtzMHy1QWGqr_xqG9eJlhbsKT4jev4,3463
2780
2780
  invar/templates/skills/review/SKILL.md.jinja,sha256=ET5mbdSe_eKgJbi2LbgFC-z1aviKcHOBw7J5Q28fr4U,14105
2781
- invar_tools-1.17.23.dist-info/METADATA,sha256=GEpstDi-UqTQlgkBB_WpmhT30NWcAkX3ULm9CxAklBc,28582
2782
- invar_tools-1.17.23.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
2783
- invar_tools-1.17.23.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
2784
- invar_tools-1.17.23.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
2785
- invar_tools-1.17.23.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
2786
- invar_tools-1.17.23.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
2787
- invar_tools-1.17.23.dist-info/RECORD,,
2781
+ invar_tools-1.17.25.dist-info/METADATA,sha256=t7Kc8ideD0woPDbz-PQiXfyQzlyBOPis0VZjDQHHCPU,28582
2782
+ invar_tools-1.17.25.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
2783
+ invar_tools-1.17.25.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
2784
+ invar_tools-1.17.25.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
2785
+ invar_tools-1.17.25.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
2786
+ invar_tools-1.17.25.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
2787
+ invar_tools-1.17.25.dist-info/RECORD,,