@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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/README.zh-CN.md +158 -0
- package/bin/r2p.js +38 -0
- package/docs/req-to-plan-design.md +277 -0
- package/package.json +47 -0
- package/requirements.txt +1 -0
- package/tools/r2p +10 -0
- package/tools/r2p-continue +10 -0
- package/tools/r2p-gap-open +10 -0
- package/tools/r2p-gap-resolve +10 -0
- package/tools/r2p-reopen +10 -0
- package/tools/r2p-start +10 -0
- package/tools/r2p-status +10 -0
- package/tools/r2p-switch +10 -0
- package/tools/r2p-tier-lock +10 -0
- package/tools/workflow_cli/__init__.py +0 -0
- package/tools/workflow_cli/__main__.py +5 -0
- package/tools/workflow_cli/agent_shortcuts.py +778 -0
- package/tools/workflow_cli/agent_templates/claude/SKILL.md +34 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-continue.md +16 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-gap-open.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-gap-resolve.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-reopen.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-start.md +10 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-status.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-switch.md +8 -0
- package/tools/workflow_cli/agent_templates/claude/commands/r2p-tier-lock.md +8 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-continue/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-gap-open/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-gap-resolve/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-reopen/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-start/SKILL.md +14 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-status/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-switch/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/codex/skills/r2p-tier-lock/SKILL.md +12 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-continue.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-gap-open.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-gap-resolve.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-reopen.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-start.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-status.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-switch.toml +4 -0
- package/tools/workflow_cli/agent_templates/gemini/commands/r2p-tier-lock.toml +4 -0
- package/tools/workflow_cli/artifact.py +228 -0
- package/tools/workflow_cli/cli.py +1779 -0
- package/tools/workflow_cli/gates.py +471 -0
- package/tools/workflow_cli/install.py +900 -0
- package/tools/workflow_cli/install_cli.py +158 -0
- package/tools/workflow_cli/link_expander.py +102 -0
- package/tools/workflow_cli/models.py +504 -0
- package/tools/workflow_cli/output.py +91 -0
- package/tools/workflow_cli/repo_baseline.py +137 -0
- package/tools/workflow_cli/state.py +621 -0
- package/tools/workflow_cli/tier.py +201 -0
- package/tools/workflow_cli/tier_keywords.yaml +45 -0
- 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"
|