@xenonbyte/req-2-plan 0.4.2 → 0.4.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenonbyte/req-2-plan",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Requirement-to-PLAN workflow CLI and agent integration installer.",
5
5
  "bin": {
6
6
  "r2p": "bin/r2p.js"
@@ -6,6 +6,11 @@ import os
6
6
  from dataclasses import asdict, dataclass, field
7
7
  from pathlib import Path
8
8
 
9
+ try:
10
+ import tomllib # Python 3.11+
11
+ except ImportError: # pragma: no cover - older interpreters: pyproject parsing degrades to a no-op
12
+ tomllib = None
13
+
9
14
  from tools.workflow_cli.repo_baseline import SKIP_DIRS, scan_repo_baseline
10
15
 
11
16
  _CONFIG_NAMES = {
@@ -39,6 +44,41 @@ def _append_npm_dependencies(pack: ProjectContextPack, dependencies: object, *,
39
44
  pack.dependencies.append(dep)
40
45
 
41
46
 
47
+ def _append_pyproject_facts(pack: ProjectContextPack, repo_path: Path) -> None:
48
+ """Collect PEP 621 [project] dependencies and a pytest signal from pyproject.toml.
49
+
50
+ Poetry/PDM private tables are out of scope; without tomllib (Python < 3.11)
51
+ this is a no-op, matching the pack's best-effort scan semantics."""
52
+ pyproject = repo_path / "pyproject.toml"
53
+ if tomllib is None or not pyproject.exists():
54
+ return
55
+ try:
56
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
57
+ except (tomllib.TOMLDecodeError, OSError, UnicodeDecodeError):
58
+ return
59
+ project = data.get("project")
60
+ project = project if isinstance(project, dict) else {}
61
+ raw_deps = project.get("dependencies")
62
+ specs = [s for s in raw_deps if isinstance(s, str)] if isinstance(raw_deps, list) else []
63
+ optional = project.get("optional-dependencies")
64
+ optional_specs: list[str] = []
65
+ if isinstance(optional, dict):
66
+ for group in optional.values():
67
+ if isinstance(group, list):
68
+ optional_specs.extend(s for s in group if isinstance(s, str))
69
+ if (specs or optional_specs) and "pip" not in pack.package_managers:
70
+ pack.package_managers.append("pip")
71
+ for spec in specs:
72
+ pack.dependencies.append({"name": spec, "version": "", "ecosystem": "pip"})
73
+ for spec in optional_specs:
74
+ pack.dependencies.append({"name": spec, "version": "", "ecosystem": "pip", "dev": True})
75
+ tool = data.get("tool")
76
+ has_pytest_config = isinstance(tool, dict) and isinstance(tool.get("pytest"), dict)
77
+ mentions_pytest = any(s.lower().startswith("pytest") for s in specs + optional_specs)
78
+ if (has_pytest_config or mentions_pytest) and "python -m pytest" not in pack.test_commands:
79
+ pack.test_commands.append("python -m pytest")
80
+
81
+
42
82
  def build_context_pack(repo_path: Path) -> ProjectContextPack:
43
83
  repo_path = Path(repo_path).resolve()
44
84
  baseline = scan_repo_baseline(repo_path)
@@ -68,6 +108,8 @@ def build_context_pack(repo_path: Path) -> ProjectContextPack:
68
108
  if not pack.test_commands:
69
109
  pack.test_commands.append("python -m pytest")
70
110
 
111
+ _append_pyproject_facts(pack, repo_path)
112
+
71
113
  for root, dirs, files in os.walk(repo_path):
72
114
  dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
73
115
  rel_root = Path(root).relative_to(repo_path)
@@ -431,13 +431,14 @@ def _check_plan_context_pack(run_dir: Path) -> list[str]:
431
431
 
432
432
  def _check_plan_file_refs(run_dir: Path, content: str) -> list[str]:
433
433
  """Hard-check Files paths against the Context Pack repo_root. create-type tasks
434
- are exempt; the part after '::' (a symbol) is advisory and not checked (no AST pack yet)."""
434
+ must target paths that do not exist yet (R19); the part after '::' (a symbol) is
435
+ advisory and not checked (no AST pack yet)."""
435
436
  repo_root = _context_pack_repo_root(run_dir)
436
437
  if repo_root is None:
437
438
  return [] # no usable ground truth; standard tier blocks via _check_plan_context_pack
438
439
  issues: list[str] = []
439
440
  for body in _iter_plan_task_bodies(content):
440
- skip_missing_path = _normalized_change_type(_task_change_type(body)) == "create"
441
+ is_create = _normalized_change_type(_task_change_type(body)) == "create"
441
442
  files_field = _plan_task_field_body(body, "Files")
442
443
  for path_part in _plan_task_file_paths(files_field):
443
444
  path = Path(path_part)
@@ -453,11 +454,18 @@ def _check_plan_file_refs(run_dir: Path, content: str) -> list[str]:
453
454
  )
454
455
  continue
455
456
  if not resolved.exists():
456
- if not skip_missing_path:
457
+ if not is_create:
457
458
  issues.append(
458
459
  f"PLAN-TASK Files references missing path {path_part!r} "
459
460
  "(mark the task 'Change Type: create' if it is a new file)."
460
461
  )
462
+ elif is_create:
463
+ # R19: create must not silently mean "overwrite an existing file".
464
+ issues.append(
465
+ f"PLAN-TASK Files references path {path_part!r} that already exists "
466
+ "under 'Change Type: create'; use 'modify' for existing files or "
467
+ "split the task."
468
+ )
461
469
  return issues
462
470
 
463
471
 
@@ -1 +1 @@
1
- R2P_VERSION = "0.4.2"
1
+ R2P_VERSION = "0.4.3"