@xenonbyte/req-2-plan 0.2.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/README.zh-CN.md +158 -0
  4. package/bin/r2p.js +38 -0
  5. package/docs/req-to-plan-design.md +277 -0
  6. package/package.json +47 -0
  7. package/requirements.txt +1 -0
  8. package/tools/r2p +10 -0
  9. package/tools/r2p-continue +10 -0
  10. package/tools/r2p-gap-open +10 -0
  11. package/tools/r2p-gap-resolve +10 -0
  12. package/tools/r2p-reopen +10 -0
  13. package/tools/r2p-start +10 -0
  14. package/tools/r2p-status +10 -0
  15. package/tools/r2p-switch +10 -0
  16. package/tools/r2p-tier-lock +10 -0
  17. package/tools/workflow_cli/__init__.py +0 -0
  18. package/tools/workflow_cli/__main__.py +5 -0
  19. package/tools/workflow_cli/agent_shortcuts.py +778 -0
  20. package/tools/workflow_cli/agent_templates/claude/SKILL.md +34 -0
  21. package/tools/workflow_cli/agent_templates/claude/commands/r2p-continue.md +16 -0
  22. package/tools/workflow_cli/agent_templates/claude/commands/r2p-gap-open.md +8 -0
  23. package/tools/workflow_cli/agent_templates/claude/commands/r2p-gap-resolve.md +8 -0
  24. package/tools/workflow_cli/agent_templates/claude/commands/r2p-reopen.md +8 -0
  25. package/tools/workflow_cli/agent_templates/claude/commands/r2p-start.md +10 -0
  26. package/tools/workflow_cli/agent_templates/claude/commands/r2p-status.md +8 -0
  27. package/tools/workflow_cli/agent_templates/claude/commands/r2p-switch.md +8 -0
  28. package/tools/workflow_cli/agent_templates/claude/commands/r2p-tier-lock.md +8 -0
  29. package/tools/workflow_cli/agent_templates/codex/skills/r2p-continue/SKILL.md +12 -0
  30. package/tools/workflow_cli/agent_templates/codex/skills/r2p-gap-open/SKILL.md +12 -0
  31. package/tools/workflow_cli/agent_templates/codex/skills/r2p-gap-resolve/SKILL.md +12 -0
  32. package/tools/workflow_cli/agent_templates/codex/skills/r2p-reopen/SKILL.md +12 -0
  33. package/tools/workflow_cli/agent_templates/codex/skills/r2p-start/SKILL.md +14 -0
  34. package/tools/workflow_cli/agent_templates/codex/skills/r2p-status/SKILL.md +12 -0
  35. package/tools/workflow_cli/agent_templates/codex/skills/r2p-switch/SKILL.md +12 -0
  36. package/tools/workflow_cli/agent_templates/codex/skills/r2p-tier-lock/SKILL.md +12 -0
  37. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-continue.toml +4 -0
  38. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-gap-open.toml +4 -0
  39. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-gap-resolve.toml +4 -0
  40. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-reopen.toml +4 -0
  41. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-start.toml +4 -0
  42. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-status.toml +4 -0
  43. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-switch.toml +4 -0
  44. package/tools/workflow_cli/agent_templates/gemini/commands/r2p-tier-lock.toml +4 -0
  45. package/tools/workflow_cli/artifact.py +228 -0
  46. package/tools/workflow_cli/cli.py +1779 -0
  47. package/tools/workflow_cli/gates.py +471 -0
  48. package/tools/workflow_cli/install.py +900 -0
  49. package/tools/workflow_cli/install_cli.py +158 -0
  50. package/tools/workflow_cli/link_expander.py +102 -0
  51. package/tools/workflow_cli/models.py +504 -0
  52. package/tools/workflow_cli/output.py +91 -0
  53. package/tools/workflow_cli/repo_baseline.py +137 -0
  54. package/tools/workflow_cli/state.py +621 -0
  55. package/tools/workflow_cli/tier.py +201 -0
  56. package/tools/workflow_cli/tier_keywords.yaml +45 -0
  57. package/tools/workflow_cli/version.py +1 -0
@@ -0,0 +1,201 @@
1
+ # tools/workflow_cli/tier.py
2
+ from __future__ import annotations
3
+ from pathlib import Path
4
+ from typing import Any
5
+ import re
6
+ import yaml
7
+
8
+ from tools.workflow_cli.models import (
9
+ TierBase, TierModifier, TierEstimate, EvidenceBlock,
10
+ )
11
+ from tools.workflow_cli.repo_baseline import RepoBaseline
12
+ from tools.workflow_cli.link_expander import LinkExpansionResult, LinkStatus
13
+
14
+ _KEYWORDS_PATH = Path(__file__).parent / "tier_keywords.yaml"
15
+ _keywords_cache: dict | None = None
16
+
17
+
18
+ def _load_keywords() -> dict:
19
+ global _keywords_cache
20
+ if _keywords_cache is None:
21
+ with open(_KEYWORDS_PATH, encoding="utf-8") as f:
22
+ _keywords_cache = yaml.safe_load(f)
23
+ return _keywords_cache
24
+
25
+
26
+ def _flatten_keywords(modifier_section: dict) -> list[str]:
27
+ """Flatten all keyword lists from a modifier section into one list."""
28
+ result = []
29
+ for value in modifier_section.values():
30
+ if isinstance(value, list):
31
+ result.extend(str(k) for k in value)
32
+ elif isinstance(value, dict):
33
+ result.extend(_flatten_keywords(value))
34
+ return result
35
+
36
+
37
+ def _normalize(text: str) -> str:
38
+ """Normalize hyphens and lowercase."""
39
+ return re.sub(r'[-\s]+', ' ', text.lower()).strip()
40
+
41
+
42
+ def _strip_hyphens(text: str) -> str:
43
+ """Strip hyphens (and surrounding spaces) to produce a compact form for hyphen-variant matching."""
44
+ return re.sub(r'\s*-\s*', '', text.lower())
45
+
46
+
47
+ def _match_keyword(keyword: str, text: str, normalized_text: str) -> bool:
48
+ """Match a keyword against text. Multi-word phrases use substring; single words use word-boundary for English."""
49
+ kw_norm = _normalize(keyword)
50
+ # Chinese text: substring match
51
+ if re.search(r'[一-鿿]', keyword):
52
+ return keyword in text
53
+ # Multi-word: substring match on normalized text
54
+ if ' ' in kw_norm:
55
+ return kw_norm in normalized_text
56
+ # Single word: word boundary on normalized text
57
+ if re.search(r'\b' + re.escape(kw_norm) + r'\b', normalized_text, re.IGNORECASE):
58
+ return True
59
+ # Hyphen-variant fallback: strip all hyphens from both keyword and text and try word boundary
60
+ # This handles "reimplement" matching "re-implement" in the text
61
+ kw_compact = _strip_hyphens(keyword)
62
+ text_compact = _strip_hyphens(text)
63
+ return bool(re.search(r'\b' + re.escape(kw_compact) + r'\b', text_compact, re.IGNORECASE))
64
+
65
+
66
+ def scan_keywords(text: str) -> dict[TierModifier, list[str]]:
67
+ """L1: scan text for modifier-triggering keywords. Returns {modifier: [matched keywords]}."""
68
+ keywords = _load_keywords()
69
+ normalized = _normalize(text)
70
+ hits: dict[TierModifier, list[str]] = {}
71
+
72
+ modifier_map = {
73
+ "migration": TierModifier.MIGRATION,
74
+ "cross_project": TierModifier.CROSS_PROJECT,
75
+ "safety": TierModifier.SAFETY,
76
+ "dependency": TierModifier.DEPENDENCY,
77
+ "scope_expanding": TierModifier.SCOPE_EXPANDING,
78
+ }
79
+
80
+ for section_name, modifier in modifier_map.items():
81
+ section = keywords.get(section_name, {})
82
+ section_keywords = _flatten_keywords(section)
83
+ matched = [kw for kw in section_keywords if _match_keyword(kw, text, normalized)]
84
+ if matched:
85
+ hits[modifier] = matched
86
+
87
+ return hits
88
+
89
+
90
+ def compute_floor(
91
+ keyword_hits: dict[TierModifier, list[str]],
92
+ repo: RepoBaseline,
93
+ link_results: list[LinkExpansionResult],
94
+ requirement_text: str = "",
95
+ ) -> TierEstimate:
96
+ """Compute tier Floor from L1-L3 signals."""
97
+ # Base floor
98
+ base = TierBase.LIGHT
99
+
100
+ loc_threshold = 10_000
101
+ module_threshold = 8
102
+
103
+ if (
104
+ len(keyword_hits) > 0
105
+ or repo.loc > loc_threshold
106
+ or repo.is_monorepo
107
+ or repo.module_count > module_threshold
108
+ or _has_multi_repo_refs(requirement_text)
109
+ or any(r.status in (LinkStatus.UNREACHABLE, LinkStatus.REQUIRES_AUTH) for r in link_results)
110
+ ):
111
+ base = TierBase.STANDARD
112
+
113
+ # Modifier floor
114
+ modifiers: set[TierModifier] = set(keyword_hits.keys())
115
+
116
+ # Repo signals → cross_project modifier
117
+ if repo.cross_language_refs and len(repo.cross_language_refs) >= 2:
118
+ modifiers.add(TierModifier.CROSS_PROJECT)
119
+
120
+ # scope_expanding upgrades base to standard
121
+ if TierModifier.SCOPE_EXPANDING in modifiers and base == TierBase.LIGHT:
122
+ base = TierBase.STANDARD
123
+
124
+ return TierEstimate(base=base, modifiers=frozenset(modifiers))
125
+
126
+
127
+ def _has_multi_repo_refs(text: str) -> bool:
128
+ """Check if requirement text mentions 2+ distinct repo names."""
129
+ repo_words = re.findall(r'\b(?:repo|repository|project)\s+\w+', text.lower())
130
+ return len(set(repo_words)) >= 2
131
+
132
+
133
+ def build_evidence_block(
134
+ keyword_hits: dict[TierModifier, list[str]],
135
+ repo: RepoBaseline,
136
+ link_results: list[LinkExpansionResult],
137
+ floor: TierEstimate,
138
+ ) -> EvidenceBlock:
139
+ """Build the EvidenceBlock from all scan results."""
140
+ all_hits = [kw for hits in keyword_hits.values() for kw in hits]
141
+
142
+ repo_summary = (
143
+ f"loc={repo.loc}, modules={repo.module_count}, "
144
+ f"monorepo={repo.is_monorepo}, languages={list(repo.language_breakdown.keys())}"
145
+ )
146
+
147
+ linked_context_parts = []
148
+ for r in link_results:
149
+ if r.content_preview:
150
+ linked_context_parts.append(f"{r.url}: {r.content_preview[:100]}")
151
+ linked_context = "; ".join(linked_context_parts) if linked_context_parts else "none"
152
+
153
+ scope_signals = list(keyword_hits.get(TierModifier.SCOPE_EXPANDING, []))
154
+ escalation_candidates = [m.value for m in keyword_hits if m != TierModifier.SCOPE_EXPANDING]
155
+
156
+ return EvidenceBlock(
157
+ keywords_hit=all_hits,
158
+ repo_baseline_summary=repo_summary,
159
+ linked_context=linked_context,
160
+ scope_signals=scope_signals,
161
+ escalation_candidates=escalation_candidates,
162
+ floor=floor,
163
+ confirm_status="pending",
164
+ )
165
+
166
+
167
+ def estimate_tier(
168
+ requirement_text: str,
169
+ repo_path: Path | None = None,
170
+ link_results: list[LinkExpansionResult] | None = None,
171
+ ) -> tuple[TierEstimate, EvidenceBlock]:
172
+ """Full tier estimation (L1-L4). Returns (estimate, evidence_block).
173
+
174
+ L1: keyword scan
175
+ L2: repo baseline
176
+ L3: link expansion (if link_results provided)
177
+ L4: Evidence Block completeness (caller must call evidence.validate_for_lock())
178
+ """
179
+ from tools.workflow_cli.repo_baseline import scan_repo_baseline
180
+
181
+ # L1: keyword scan
182
+ keyword_hits = scan_keywords(requirement_text)
183
+
184
+ # L2: repo baseline
185
+ if repo_path is not None and repo_path.exists():
186
+ repo = scan_repo_baseline(repo_path)
187
+ else:
188
+ from tools.workflow_cli.repo_baseline import RepoBaseline
189
+ repo = RepoBaseline()
190
+
191
+ # L3: link expansion (use provided results or empty)
192
+ if link_results is None:
193
+ link_results = []
194
+
195
+ # Compute floor
196
+ floor = compute_floor(keyword_hits, repo, link_results, requirement_text)
197
+
198
+ # Build evidence
199
+ evidence = build_evidence_block(keyword_hits, repo, link_results, floor)
200
+
201
+ return floor, evidence
@@ -0,0 +1,45 @@
1
+ # Tier modifier triggers.
2
+ # Match rules:
3
+ # - English: word-boundary (\b), case-insensitive
4
+ # - Chinese: substring match (no word boundary)
5
+ # - Hyphen variants normalized: "re-write" matches "rewrite"
6
+
7
+ migration:
8
+ verbs:
9
+ zh: [重写, 改写, 改成, 改用, 换成, 换用, 重做, 迁移, 移植, 转换, 转写, 升级到, 替代, 替换, 推倒重来]
10
+ en: [rewrite, rewriting, rewritten, migrate, migration, migrating, port to, porting, convert to, translate to, re-implement, reimplement, rebuild, replatform, modernize, refactor to]
11
+ nouns:
12
+ zh: [大版本升级, 技术栈迁移, 重写计划]
13
+ en: [major version upgrade, tech stack migration, rewrite plan]
14
+
15
+ cross_project:
16
+ languages: [rust, go, golang, typescript, javascript, python, java, kotlin, swift, "c", "c++", cpp, "c#", csharp, ruby, php, scala, elixir, erlang, haskell, ocaml, lua, dart, zig, nim, clojure, r, julia]
17
+ frontend_frameworks: [react, vue, angular, svelte, solid, qwik, "next.js", nextjs, nuxt, remix, astro, ember]
18
+ backend_frameworks: [express, fastify, nestjs, django, flask, fastapi, rails, spring, laravel, gin, echo, axum, actix]
19
+ databases: [mysql, postgresql, postgres, mariadb, sqlite, mongodb, redis, cassandra, dynamodb, elasticsearch, clickhouse, timescaledb, neo4j, etcd, cockroachdb]
20
+ runtimes: ["node.js", nodejs, deno, bun, jvm, ".net", dotnet, wasm]
21
+ cloud: [aws, gcp, azure, aliyun, "alibaba cloud", "tencent cloud", cloudflare, vercel, netlify]
22
+ architecture: [monolith, microservices, monorepo, multirepo, serverless, edge, "service mesh"]
23
+ signals_zh: [项目, 仓库, 跨项目, 跨仓库, 两个项目, 多个项目, 多个仓库]
24
+ signals_en: [project, repo, repository, "across repos", "cross-project", monorepo]
25
+
26
+ safety:
27
+ verbs:
28
+ zh: [删除, 删掉, 删表, 删库, 清理, 清除, 清空, 截断, 销毁, 下线, 关闭]
29
+ en: [delete, drop, truncate, purge, wipe, "remove data", destroy, retire, deprecate, "shut down"]
30
+ domains:
31
+ zh: [生产, 线上, 实时, 用户数据, 客户数据, 个人信息, 隐私, 敏感, 认证, 鉴权, 登录, 权限, 角色, 密钥, 凭证, 令牌, token, 支付, 计费, 订单, 财务, 退款, 不可逆, 破坏性]
32
+ en: [production, prod, live, "customer data", "user data", pii, privacy, sensitive, auth, authentication, authorization, login, permission, role, secret, credential, "api key", token, payment, billing, refund, order, financial, irreversible, "breaking change"]
33
+
34
+ dependency:
35
+ verbs:
36
+ zh: [接入, 集成, 调用, 对接, 依赖]
37
+ en: [integrate, integrating, "call out to", "depend on", "external service"]
38
+ nouns:
39
+ zh: [第三方, 外部 api, sdk, mcp]
40
+ en: [third-party, "external api", sdk, mcp, "external service"]
41
+
42
+ scope_expanding:
43
+ signals:
44
+ zh: [整个, 全部, 所有, 全套, 全部模块, 所有模块, 整套系统, 系统级, 全栈, 统一改造, 批量]
45
+ en: [entire, "all of", whole, full, everything, "system-wide", "across the board", comprehensive, sweeping]
@@ -0,0 +1 @@
1
+ R2P_VERSION = "0.2.3"