185 lines
6.4 KiB
Python
Executable File
185 lines
6.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import json
|
|
import sys
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
BACKLOG = ROOT / 'backlog' / 'features.json'
|
|
TYPE_CHOICES = ('feature', 'fix', 'bug', 'chore')
|
|
LEVEL_CHOICES = ('low', 'med', 'high')
|
|
|
|
|
|
def ask(prompt, default=''):
|
|
value = input(f"{prompt}{' [' + default + ']' if default else ''}: ").strip()
|
|
return value if value else default
|
|
|
|
|
|
def ask_choice(prompt, choices, default):
|
|
while True:
|
|
value = ask(prompt, default).lower()
|
|
if value in choices:
|
|
return value
|
|
print(f"Invalid value. Use one of: {', '.join(choices)}")
|
|
|
|
|
|
def ask_list(prompt, default_csv=''):
|
|
raw = ask(prompt, default_csv)
|
|
return [item.strip() for item in raw.split(',') if item.strip()]
|
|
|
|
|
|
def next_id(features):
|
|
nums = []
|
|
for feature in features:
|
|
fid = str(feature.get('id', ''))
|
|
if fid.startswith('F-') and fid[2:].isdigit():
|
|
nums.append(int(fid[2:]))
|
|
return f"F-{(max(nums) + 1) if nums else 1:03d}"
|
|
|
|
|
|
def normalize_deprecated_gates(data):
|
|
"""Replace the legacy `review` gate key without changing feature state."""
|
|
changed = 0
|
|
for feature in data.get('features', []):
|
|
gates = feature.get('gates')
|
|
if not isinstance(gates, dict) or 'review' not in gates:
|
|
continue
|
|
gates.setdefault('reviewer', gates['review'])
|
|
del gates['review']
|
|
changed += 1
|
|
return changed
|
|
|
|
|
|
def start_feature(data, feature_id):
|
|
"""Promote one pending feature to in_progress while enforcing exclusivity."""
|
|
active = [
|
|
feature.get('id')
|
|
for feature in data.get('features', [])
|
|
if feature.get('status') == 'in_progress' and feature.get('id') != feature_id
|
|
]
|
|
if active:
|
|
raise SystemExit(f"Cannot start {feature_id}; already in progress: {', '.join(active)}")
|
|
for feature in data.get('features', []):
|
|
if feature.get('id') != feature_id:
|
|
continue
|
|
if feature.get('status') not in {'pending', 'in_progress'}:
|
|
raise SystemExit(f"Cannot start {feature_id} from status {feature.get('status')}")
|
|
feature['status'] = 'in_progress'
|
|
return
|
|
raise SystemExit(f'Feature not found: {feature_id}')
|
|
|
|
|
|
def main():
|
|
data = json.loads(BACKLOG.read_text(encoding='utf-8'))
|
|
if '--normalize-gates' in sys.argv[1:]:
|
|
changed = normalize_deprecated_gates(data)
|
|
BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
|
print(f'Normalized deprecated gates in {changed} feature(s)')
|
|
return
|
|
if '--start' in sys.argv[1:]:
|
|
index = sys.argv.index('--start')
|
|
if index + 1 >= len(sys.argv):
|
|
raise SystemExit('Usage: new_ticket.py --start <feature_id>')
|
|
feature_id = sys.argv[index + 1]
|
|
start_feature(data, feature_id)
|
|
BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
|
print(f'Started {feature_id}')
|
|
return
|
|
|
|
# CLI flags (non-interactive mode)
|
|
cli_args = sys.argv[1:]
|
|
if any(a.startswith('--id') or a.startswith('--type') for a in cli_args):
|
|
import argparse
|
|
p = argparse.ArgumentParser(add_help=False)
|
|
p.add_argument('--id')
|
|
p.add_argument('--type', choices=TYPE_CHOICES, default='fix')
|
|
p.add_argument('--title', default='Fix TODO')
|
|
p.add_argument('--description', default='Need change')
|
|
p.add_argument('--priority', choices=LEVEL_CHOICES, default='med')
|
|
p.add_argument('--risk', choices=LEVEL_CHOICES, default='low')
|
|
p.add_argument('--phase', default=None)
|
|
ns, _ = p.parse_known_args(cli_args)
|
|
features = data['features']
|
|
fid = ns.id or next_id(features)
|
|
new_feature = {
|
|
'id': fid,
|
|
'type': ns.type,
|
|
'title': ns.title,
|
|
'description': ns.description,
|
|
'priority': ns.priority,
|
|
'risk': ns.risk,
|
|
'status': 'pending',
|
|
'created_at': str(date.today()),
|
|
'gates': {'reviewer': False, 'security': False, 'qa': False},
|
|
}
|
|
if ns.phase:
|
|
new_feature['phase'] = ns.phase
|
|
features.append(new_feature)
|
|
data['features'] = features
|
|
BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
|
print(f'Created {fid}: {ns.title}')
|
|
return
|
|
|
|
features = data.get('features', [])
|
|
|
|
print('Create ticket (English caveman style).')
|
|
ticket_type = ask_choice('Type (feature/fix/bug/chore)', TYPE_CHOICES, 'feature')
|
|
title = ask('Title (short EN)', f'{ticket_type.capitalize()} TODO')
|
|
problem = ask('Problem (short EN)', 'Need change')
|
|
goal = ask('Goal (short EN)', 'Make flow better')
|
|
scope_in = ask_list('Scope IN (comma list EN)', 'Core flow')
|
|
scope_out = ask_list('Scope OUT (comma list EN)', 'No redesign')
|
|
risk = ask_choice('Risk (low/med/high)', LEVEL_CHOICES, 'low')
|
|
priority = ask_choice('Priority (low/med/high)', LEVEL_CHOICES, 'med')
|
|
|
|
print('Acceptance bullets (EN caveman). Empty line to end.')
|
|
acceptance = []
|
|
while True:
|
|
line = input('- ').strip()
|
|
if not line:
|
|
break
|
|
acceptance.append(line)
|
|
|
|
if not acceptance:
|
|
acceptance = [
|
|
'Flow works end to end',
|
|
'No break old behavior',
|
|
'verify.sh is green',
|
|
]
|
|
|
|
fid = next_id(features)
|
|
desc = (
|
|
f"Problem: {problem}. "
|
|
f"Goal: {goal}. "
|
|
f"Scope IN: {', '.join(scope_in) or 'none'}. "
|
|
f"Scope OUT: {', '.join(scope_out) or 'none'}. "
|
|
f"Type: {ticket_type}. Priority: {priority}. Risk: {risk}."
|
|
)
|
|
|
|
features.append({
|
|
'id': fid,
|
|
'type': ticket_type,
|
|
'title': title,
|
|
'problem': problem,
|
|
'goal': goal,
|
|
'scope_in': scope_in,
|
|
'scope_out': scope_out,
|
|
'priority': priority,
|
|
'risk': risk,
|
|
'description': desc,
|
|
'acceptance': acceptance,
|
|
'status': 'pending',
|
|
'created_at': str(date.today()),
|
|
'gates': {'reviewer': False, 'security': False, 'qa': False},
|
|
})
|
|
|
|
data['features'] = features
|
|
rules = data.setdefault('rules', {})
|
|
rules.setdefault('valid_types', list(TYPE_CHOICES))
|
|
BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
|
print(f'Created {fid}: {title}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|