@xenonbyte/req-2-plan 0.4.1 → 0.4.2

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.1",
3
+ "version": "0.4.2",
4
4
  "description": "Requirement-to-PLAN workflow CLI and agent integration installer.",
5
5
  "bin": {
6
6
  "r2p": "bin/r2p.js"
@@ -3,7 +3,7 @@ description: Start a new requirement-to-PLAN workflow run
3
3
  ---
4
4
  Run `{{R2P_BIN_DIR}}/r2p-start` with the requirement as a positional argument.
5
5
 
6
- Usage: `{{R2P_BIN_DIR}}/r2p-start [--separate] ("<raw requirement>" | --file <path>)`
6
+ Usage: `{{R2P_BIN_DIR}}/r2p-start [--separate] [--repo-path <dir>] ("<raw requirement>" | --file <path>)`
7
7
 
8
8
  To start from a requirement document, pass `--file <path>` instead of inline text — r2p reads the file contents as the requirement (the path itself is never stored as the requirement): `{{R2P_BIN_DIR}}/r2p-start [--separate] --file ./requirement.md`. `--file` and a positional requirement are mutually exclusive.
9
9
 
@@ -7,7 +7,7 @@ description: Start a new requirement-to-PLAN workflow run from a raw requirement
7
7
 
8
8
  Run `{{R2P_BIN_DIR}}/r2p-start` with the requirement as a positional argument.
9
9
 
10
- Usage: `{{R2P_BIN_DIR}}/r2p-start [--separate] ("<raw requirement>" | --file <path>)`
10
+ Usage: `{{R2P_BIN_DIR}}/r2p-start [--separate] [--repo-path <dir>] ("<raw requirement>" | --file <path>)`
11
11
 
12
12
  To start from a requirement document, pass `--file <path>` instead of inline text — r2p reads the file contents as the requirement (the path itself is never stored as the requirement): `{{R2P_BIN_DIR}}/r2p-start [--separate] --file ./requirement.md`. `--file` and a positional requirement are mutually exclusive.
13
13
 
@@ -27,6 +27,18 @@ class ProjectContextPack:
27
27
  source_dirs: list = field(default_factory=list)
28
28
 
29
29
 
30
+ def _append_npm_dependencies(pack: ProjectContextPack, dependencies: object, *, dev: bool = False) -> None:
31
+ if not isinstance(dependencies, dict):
32
+ return
33
+ for name, version in dependencies.items():
34
+ if not isinstance(name, str) or not isinstance(version, str):
35
+ continue
36
+ dep = {"name": name, "version": version, "ecosystem": "npm"}
37
+ if dev:
38
+ dep["dev"] = True
39
+ pack.dependencies.append(dep)
40
+
41
+
30
42
  def build_context_pack(repo_path: Path) -> ProjectContextPack:
31
43
  repo_path = Path(repo_path).resolve()
32
44
  baseline = scan_repo_baseline(repo_path)
@@ -41,15 +53,8 @@ def build_context_pack(repo_path: Path) -> ProjectContextPack:
41
53
  scripts = data.get("scripts")
42
54
  if isinstance(scripts, dict) and scripts.get("test"):
43
55
  pack.test_commands.append("npm test")
44
- dependencies = data.get("dependencies")
45
- if isinstance(dependencies, dict):
46
- for name, ver in dependencies.items():
47
- pack.dependencies.append({"name": name, "version": ver, "ecosystem": "npm"})
48
- dev_dependencies = data.get("devDependencies")
49
- if isinstance(dev_dependencies, dict):
50
- for name, ver in dev_dependencies.items():
51
- pack.dependencies.append(
52
- {"name": name, "version": ver, "ecosystem": "npm", "dev": True})
56
+ _append_npm_dependencies(pack, data.get("dependencies"))
57
+ _append_npm_dependencies(pack, data.get("devDependencies"), dev=True)
53
58
  except (ValueError, OSError):
54
59
  pass
55
60
 
@@ -84,6 +89,25 @@ def to_json(pack: ProjectContextPack) -> str:
84
89
  return json.dumps(asdict(pack), indent=2, ensure_ascii=False)
85
90
 
86
91
 
92
+ _DEP_DISPLAY_CAP = 20
93
+
94
+
95
+ def _dependencies_markdown(dependencies: list) -> str:
96
+ """Itemize dependencies (name, version, ecosystem, dev flag), capped to keep the seed small."""
97
+ if not dependencies:
98
+ return "- dependencies (0): none\n"
99
+ lines = [f"- dependencies ({len(dependencies)}):\n"]
100
+ for dep in dependencies[:_DEP_DISPLAY_CAP]:
101
+ label = " ".join(part for part in (dep.get("name", ""), dep.get("version", "")) if part)
102
+ ecosystem = dep.get("ecosystem", "")
103
+ suffix = f"{ecosystem}, dev" if dep.get("dev") else ecosystem
104
+ lines.append(f" - {label} ({suffix})\n")
105
+ hidden = len(dependencies) - _DEP_DISPLAY_CAP
106
+ if hidden > 0:
107
+ lines.append(f" - … and {hidden} more\n")
108
+ return "".join(lines)
109
+
110
+
87
111
  def to_markdown(pack: ProjectContextPack) -> str:
88
112
  return (
89
113
  "# Project Context Pack\n\n"
@@ -93,7 +117,7 @@ def to_markdown(pack: ProjectContextPack) -> str:
93
117
  f"- test_commands: {pack.test_commands or 'none'}\n"
94
118
  f"- entrypoints: {pack.entrypoints or 'none'}\n"
95
119
  f"- config_files: {pack.config_files or 'none'}\n"
96
- f"- dependencies: {len(pack.dependencies)} found\n"
120
+ f"{_dependencies_markdown(pack.dependencies)}"
97
121
  f"- source_dirs: {pack.source_dirs}\n"
98
122
  )
99
123
 
@@ -290,14 +290,6 @@ def _find_next_plan_task_field_start(task_body: str, after: int) -> int | None:
290
290
  return None
291
291
 
292
292
 
293
- def _plan_task_field_value(task_body: str, field: str) -> str:
294
- found = _find_plan_task_field(task_body, field)
295
- if found is None:
296
- return ""
297
- match, _ = found
298
- return match.group(1).strip()
299
-
300
-
301
293
  def _iter_plan_task_bodies(content: str):
302
294
  return heading_bounded_bodies(content, _PLAN_TASK_RE.match)
303
295
 
@@ -496,8 +488,10 @@ def _check_spec_refs_valid(run_dir: Path, content: str) -> list[str]:
496
488
 
497
489
 
498
490
  # R10: Change Type is a closed operation-kind enum; 'new' is a legacy alias.
499
- _CHANGE_TYPE_VALUES = frozenset({"create", "modify", "delete"})
491
+ _FILE_CHANGE_TYPES = frozenset({"create", "modify", "delete"})
492
+ _CHANGE_TYPE_VALUES = _FILE_CHANGE_TYPES | {"non_code"}
500
493
  _CHANGE_TYPE_ALIASES = {"new": "create"}
494
+ _TDD_APPLICABLE_VALUES = frozenset({"yes", "no"})
501
495
 
502
496
 
503
497
  def _normalized_change_type(raw: str) -> str:
@@ -510,6 +504,11 @@ def _task_change_type(body: str) -> str:
510
504
  return " ".join(_plan_task_field_body(body, "Change Type").split())
511
505
 
512
506
 
507
+ def _task_tdd_applicable(body: str) -> str:
508
+ """Whitespace-normalized TDD Applicable field body (same line + continuation lines)."""
509
+ return " ".join(_plan_task_field_body(body, "TDD Applicable").split())
510
+
511
+
513
512
  def _check_plan_task_fields(content: str) -> list[str]:
514
513
  issues: list[str] = []
515
514
  numbers: list[int] = []
@@ -523,10 +522,32 @@ def _check_plan_task_fields(content: str) -> list[str]:
523
522
  if not _plan_task_field_body(body, field).strip():
524
523
  issues.append(f"{label} is missing a non-empty '{field}:' field.")
525
524
  raw_change_type = _task_change_type(body)
526
- if raw_change_type and _normalized_change_type(raw_change_type) not in _CHANGE_TYPE_VALUES:
525
+ change_type = _normalized_change_type(raw_change_type)
526
+ if raw_change_type and change_type not in _CHANGE_TYPE_VALUES:
527
527
  issues.append(
528
528
  f"{label} has invalid 'Change Type: {raw_change_type}'; "
529
- "allowed: create|modify|delete (alias: new = create)."
529
+ "allowed: create|modify|delete|non_code (alias: new = create)."
530
+ )
531
+ # R16: Change Type and Files must agree — file-op types need a real
532
+ # path; non_code is the only legal shape for no-file tasks.
533
+ files_body = _plan_task_field_body(body, "Files")
534
+ if files_body.strip():
535
+ file_paths = _plan_task_file_paths(files_body)
536
+ if change_type in _FILE_CHANGE_TYPES and not file_paths:
537
+ issues.append(
538
+ f"{label} has 'Change Type: {raw_change_type}' but 'Files:' lists "
539
+ "no real file path; use 'Change Type: non_code' for tasks that "
540
+ "touch no files."
541
+ )
542
+ elif change_type == "non_code" and file_paths:
543
+ issues.append(
544
+ f"{label} has 'Change Type: non_code' but 'Files:' lists a file path; "
545
+ "use create|modify|delete for file-touching tasks."
546
+ )
547
+ raw_tdd = _task_tdd_applicable(body)
548
+ if raw_tdd and raw_tdd.lower() not in _TDD_APPLICABLE_VALUES:
549
+ issues.append(
550
+ f"{label} has invalid 'TDD Applicable: {raw_tdd}'; allowed: yes|no."
530
551
  )
531
552
  if numbers:
532
553
  if len(set(numbers)) != len(numbers):
@@ -575,8 +596,7 @@ def _plan_tasks_missing_code(content: str) -> bool:
575
596
  return False
576
597
  for body in bodies:
577
598
  skeleton = _plan_task_field_body(body, "Skeleton")
578
- tdd_applicable = _plan_task_field_value(body, "TDD Applicable")
579
- if tdd_applicable.lower() == "yes" and not _has_complete_code_fence(skeleton):
599
+ if _task_tdd_applicable(body).lower() == "yes" and not _has_complete_code_fence(skeleton):
580
600
  return True
581
601
  return False
582
602
 
@@ -1 +1 @@
1
- R2P_VERSION = "0.4.1"
1
+ R2P_VERSION = "0.4.2"