/
mkudmi
/
gigaspec-kit
Обзор
Документация
Войти
/
mkudmi
/
gigaspec-kit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
scripts/bash/setup-context-init.sh
556 строк
21 KB
mkudmi
Translate tasks template to Russian and update integration tests for new command structure
18 июн 2026, 12:20
18 июн 2026, 12:20
219d58f
Код
Авторство
О чём код?
#!/usr/bin/env bash set -euo pipefail JSON_MODE=false PREPARE_ISSUES=false ISSUES_FILE="" CHUNK_SIZE=20 CONTEXT_DIR_OVERRIDE="" while [[ $# -gt 0 ]]; do case "$1" in --json) JSON_MODE=true shift ;; --prepare-issues) PREPARE_ISSUES=true ISSUES_FILE="${2:-}" shift 2 ;; --chunk-size) CHUNK_SIZE="${2:-20}" shift 2 ;; --context-dir) CONTEXT_DIR_OVERRIDE="${2:-}" shift 2 ;; --help|-h) cat <<'EOF' Usage: setup-context-init.sh [--json] [--context-dir DIR] setup-context-init.sh --prepare-issues <issues.json|issues.jsonl> [--chunk-size N] [--json] [--context-dir DIR] EOF exit 0 ;; *) echo "Unknown argument: $1" >&2 exit 1 ;; esac done SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" PYTHON_BIN="" if command -v python3 >/dev/null 2>&1; then PYTHON_BIN="python3" elif command -v python >/dev/null 2>&1 && python --version 2>&1 | grep -q "Python 3"; then PYTHON_BIN="python" fi if [[ -z "$PYTHON_BIN" ]]; then echo "ERROR: setup-context-init.sh requires Python 3" >&2 exit 1 fi REPO_ROOT="$(get_repo_root)" CONTEXT_DIR="${CONTEXT_DIR_OVERRIDE:-${SPECIFY_CONTEXT_DIR:-$REPO_ROOT/.specify/project-context}}" if [[ "$PREPARE_ISSUES" == true && -z "$ISSUES_FILE" ]]; then echo "ERROR: --prepare-issues requires a file path" >&2 exit 1 fi "$PYTHON_BIN" - "$REPO_ROOT" "$CONTEXT_DIR" "$JSON_MODE" "$PREPARE_ISSUES" "$ISSUES_FILE" "$CHUNK_SIZE" <<'PY' from __future__ import annotations import json import os import re import subprocess import sys from collections import Counter from pathlib import Path REPO_ROOT = Path(sys.argv[1]).resolve() CONTEXT_DIR = Path(sys.argv[2]).resolve() JSON_MODE = sys.argv[3].lower() == "true" PREPARE_ISSUES = sys.argv[4].lower() == "true" ISSUES_FILE = sys.argv[5] CHUNK_SIZE = max(int(sys.argv[6] or "20"), 1) SKIP_DIRS = { ".git", ".hg", ".svn", ".venv", "venv", "node_modules", "dist", "build", "target", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".next", ".turbo", ".idea", ".vscode", "coverage", } STOPWORDS = { "the", "and", "for", "with", "from", "into", "that", "this", "your", "you", "are", "was", "were", "have", "has", "had", "will", "would", "should", "can", "could", "our", "out", "not", "but", "use", "using", "used", "make", "made", "add", "adds", "added", "fix", "fixed", "update", "updated", "remove", "removed", "implement", "implemented", "support", "supported", "project", "service", "module", "task", "tasks", "issue", "issues", "story", "stories", "эпик", "задача", "задачи", "проект", "сервис", "модуль", "для", "как", "что", "или", "это", "этот", "эта", "при", "без", "над", "под", "из", "по", "на", "в", "и", "не", "но", "мы", "они", } ISSUE_KEY_RE = re.compile(r"\b([A-Z][A-Z0-9]{1,15}-\d+)\b") TOKEN_RE = re.compile(r"[A-Za-zА-Яа-я0-9][A-Za-zА-Яа-я0-9_-]{2,}") def rel(path: Path) -> str: return path.resolve().relative_to(REPO_ROOT).as_posix() def write_json(path: Path, payload: object) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") def safe_text(path: Path, limit: int = 200_000) -> str: try: return path.read_text(encoding="utf-8", errors="ignore")[:limit] except OSError: return "" def run_git(*args: str) -> str: try: result = subprocess.run( ["git", *args], cwd=REPO_ROOT, check=False, capture_output=True, text=True, ) except OSError: return "" return result.stdout if result.returncode == 0 else "" def extract_atlassian_doc(value: object) -> str: if value is None: return "" if isinstance(value, str): return value if isinstance(value, list): return "\n".join(filter(None, (extract_atlassian_doc(item) for item in value))) if isinstance(value, dict): pieces: list[str] = [] if isinstance(value.get("text"), str): pieces.append(value["text"]) for key in ("content", "items"): if key in value: nested = extract_atlassian_doc(value[key]) if nested: pieces.append(nested) return "\n".join(piece for piece in pieces if piece) return str(value) def walk_repo() -> list[Path]: files: list[Path] = [] for root, dirs, filenames in os.walk(REPO_ROOT): dirs[:] = [item for item in dirs if item not in SKIP_DIRS] root_path = Path(root) for filename in filenames: files.append(root_path / filename) return files def detect_stack(files: list[Path]) -> dict[str, object]: manifests = { "python": [], "node": [], "java": [], "go": [], "rust": [], "dotnet": [], "ruby": [], "php": [], } frameworks: list[str] = [] ext_counts = Counter(path.suffix.lower() for path in files if path.suffix) for path in files: name = path.name rel_path = rel(path) text = safe_text(path, 40_000) if name in { "pyproject.toml", "requirements.txt", "package.json", "pom.xml", "go.mod", "Cargo.toml" } else "" lowered = text.lower() if name in {"pyproject.toml", "requirements.txt", "setup.py", "manage.py"}: manifests["python"].append(rel_path) if "django" in lowered: frameworks.append("Django") if "fastapi" in lowered: frameworks.append("FastAPI") if "flask" in lowered: frameworks.append("Flask") if name in {"package.json", "pnpm-workspace.yaml", "yarn.lock", "package-lock.json"}: manifests["node"].append(rel_path) if '"next"' in lowered: frameworks.append("Next.js") if '"react"' in lowered: frameworks.append("React") if '"nestjs"' in lowered: frameworks.append("NestJS") if '"express"' in lowered: frameworks.append("Express") if name in {"pom.xml", "build.gradle", "build.gradle.kts"}: manifests["java"].append(rel_path) if "spring-boot" in lowered: frameworks.append("Spring Boot") if name == "go.mod": manifests["go"].append(rel_path) if name == "Cargo.toml": manifests["rust"].append(rel_path) if path.suffix == ".csproj" or name.endswith(".sln"): manifests["dotnet"].append(rel_path) if name == "Gemfile": manifests["ruby"].append(rel_path) if name == "composer.json": manifests["php"].append(rel_path) return { "detected_stacks": [name for name, items in manifests.items() if items], "manifest_files": manifests, "framework_hints": sorted(set(frameworks)), "top_extensions": ext_counts.most_common(12), } def find_clues(files: list[Path]) -> dict[str, object]: entrypoints: list[str] = [] interface_clues: list[str] = [] infra_clues: list[str] = [] data_clues: list[str] = [] test_clues: list[str] = [] for path in files: rel_path = rel(path) name = path.name.lower() lowered = rel_path.lower() if name in { "main.py", "manage.py", "app.py", "server.py", "main.go", "main.rs", "program.cs", "server.js", "index.js", "main.ts" }: entrypoints.append(rel_path) if "/cmd/" in f"/{rel_path}/" and name == "main.go": entrypoints.append(rel_path) if any(token in lowered for token in ("openapi", "swagger", "graphql", ".proto", "routes", "router", "controller", "handler")): interface_clues.append(rel_path) if name in {"dockerfile", "docker-compose.yml", "docker-compose.yaml"} or any( token in lowered for token in ("helm/", "k8s/", "kubernetes/", ".github/workflows/") ): infra_clues.append(rel_path) if any(token in lowered for token in ("migrations/", "alembic/", "schema.sql", "prisma/", "models/", "entities/")): data_clues.append(rel_path) if any(token in lowered for token in ("/tests/", "/test/", "_test.", "spec.")): test_clues.append(rel_path) return { "entrypoints": sorted(set(entrypoints))[:20], "interface_clues": sorted(set(interface_clues))[:30], "infra_clues": sorted(set(infra_clues))[:30], "data_clues": sorted(set(data_clues))[:30], "test_clues": sorted(set(test_clues))[:30], } def git_signals() -> dict[str, object]: messages = run_git("log", "--max-count=800", "--pretty=format:%s%n%b<<<END>>>") changed = run_git("log", "--max-count=500", "--name-only", "--pretty=format:") issue_keys = ISSUE_KEY_RE.findall(messages) project_keys = Counter(key.split("-", 1)[0] for key in issue_keys) hotspots: Counter[str] = Counter() terms: Counter[str] = Counter() for line in changed.splitlines(): path = line.strip() if not path: continue parts = path.split("/") hotspot = "/".join(parts[:2]) if len(parts) > 1 else parts[0] hotspots[hotspot] += 1 for token in TOKEN_RE.findall(messages.lower()): if token in STOPWORDS or token.isdigit(): continue terms[token] += 1 return { "issue_keys": sorted(set(issue_keys)), "project_key_candidates": [name for name, _ in project_keys.most_common(10)], "top_hotspots": hotspots.most_common(20), "commit_terms": terms.most_common(25), } def readme_excerpt(files: list[Path]) -> list[str]: candidates = [path for path in files if path.name.lower() in {"readme.md", "readme", "readme.rst"}] if not candidates: return [] lines = [line.strip() for line in safe_text(sorted(candidates)[0], 10_000).splitlines() if line.strip()] return lines[:12] def summarize_repo() -> dict[str, object]: CONTEXT_DIR.mkdir(parents=True, exist_ok=True) files = walk_repo() stack = detect_stack(files) clues = find_clues(files) git = git_signals() readme = readme_excerpt(files) repo_signals = CONTEXT_DIR / "repo-signals.json" write_json(repo_signals, { "repo_root": str(REPO_ROOT), "context_dir": str(CONTEXT_DIR), "stack": stack, "clues": clues, "git": git, "readme_excerpt": readme, }) candidate_keys = CONTEXT_DIR / "candidate-issue-keys.txt" candidate_keys.write_text("\n".join(git["issue_keys"]) + ("\n" if git["issue_keys"] else ""), encoding="utf-8") candidate_projects = CONTEXT_DIR / "candidate-project-keys.txt" candidate_projects.write_text("\n".join(git["project_key_candidates"]) + ("\n" if git["project_key_candidates"] else ""), encoding="utf-8") issue_hints = CONTEXT_DIR / "issue-hints.json" write_json(issue_hints, { "issue_keys": git["issue_keys"], "project_key_candidates": git["project_key_candidates"], "top_hotspots": git["top_hotspots"], "commit_terms": git["commit_terms"], }) repo_discovery = CONTEXT_DIR / "repo-discovery.md" lines = [ "# Repository Discovery Summary", "", "## Stack Signals", "", f"- Detected stacks: {', '.join(stack['detected_stacks']) if stack['detected_stacks'] else 'none inferred'}", f"- Framework hints: {', '.join(stack['framework_hints']) if stack['framework_hints'] else 'none inferred'}", "", "## Manifest Files", "", ] for stack_name, items in stack["manifest_files"].items(): if items: lines.append(f"- {stack_name}: {', '.join(items[:6])}") if readme: lines += ["", "## README Excerpt", ""] + [f"- {item}" for item in readme] lines += ["", "## Entrypoints", ""] + [f"- {item}" for item in clues["entrypoints"]] + (["- none found"] if not clues["entrypoints"] else []) lines += ["", "## Interface Clues", ""] + [f"- {item}" for item in clues["interface_clues"][:15]] + (["- none found"] if not clues["interface_clues"] else []) lines += ["", "## Data and Migration Clues", ""] + [f"- {item}" for item in clues["data_clues"][:15]] + (["- none found"] if not clues["data_clues"] else []) lines += ["", "## Infrastructure Clues", ""] + [f"- {item}" for item in clues["infra_clues"][:15]] + (["- none found"] if not clues["infra_clues"] else []) lines += ["", "## Git Signals", ""] lines.append(f"- Candidate project keys from commits: {', '.join(git['project_key_candidates']) if git['project_key_candidates'] else 'none found'}") lines.append(f"- Candidate issue keys from commits: {', '.join(git['issue_keys'][:20]) if git['issue_keys'] else 'none found'}") lines.append("- Top hotspots:") for hotspot, count in git["top_hotspots"][:10]: lines.append(f" - {hotspot}: {count}") repo_discovery.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") atlassian_issues = CONTEXT_DIR / "atlassian-issues.jsonl" if not atlassian_issues.exists(): atlassian_issues.write_text("", encoding="utf-8") return { "REPO_ROOT": str(REPO_ROOT), "CONTEXT_DIR": str(CONTEXT_DIR), "LEGACY_CONTEXT_FILE": str(CONTEXT_DIR / "legacy-context.md"), "REPO_DISCOVERY_MD": str(repo_discovery), "REPO_SIGNALS_JSON": str(repo_signals), "ISSUE_HINTS_JSON": str(issue_hints), "CANDIDATE_ISSUE_KEYS_FILE": str(candidate_keys), "CANDIDATE_PROJECT_KEYS_FILE": str(candidate_projects), "ATLASSIAN_ISSUES_FILE": str(atlassian_issues), "ISSUE_BATCH_DIR": str(CONTEXT_DIR / "jira-issue-batches"), "BATCH_SUMMARIES_DIR": str(CONTEXT_DIR / "jira-batch-summaries"), "PROJECT_KEY_CANDIDATES": git["project_key_candidates"], "ISSUE_KEY_CANDIDATES": git["issue_keys"][:100], "DETECTED_STACKS": stack["detected_stacks"], } def load_issues(path: Path) -> list[object]: raw = path.read_text(encoding="utf-8", errors="ignore").strip() if not raw: return [] if raw.startswith("["): payload = json.loads(raw) return list(payload) if isinstance(payload, list) else [payload] if raw.startswith("{"): try: payload = json.loads(raw) except json.JSONDecodeError: payload = None if payload is not None: if isinstance(payload, dict): for key in ("issues", "items", "results", "data"): if isinstance(payload.get(key), list): return payload[key] return [payload] return [json.loads(line) for line in raw.splitlines() if line.strip()] def normalize_issue(issue: object) -> dict[str, object]: if not isinstance(issue, dict): return {"key": "", "title": str(issue), "description": "", "issue_type": "", "status": "", "labels": [], "components": []} fields = issue.get("fields") if isinstance(issue.get("fields"), dict) else {} key = issue.get("key") or issue.get("issueKey") or issue.get("id") or fields.get("key") or "" title = issue.get("summary") or issue.get("title") or fields.get("summary") or fields.get("title") or "" description = extract_atlassian_doc(issue.get("description") or fields.get("description") or "") issue_type = issue.get("issue_type") or issue.get("type") or fields.get("issuetype", {}).get("name") or "" status = issue.get("status") or fields.get("status", {}).get("name") or "" labels = [str(item) for item in (issue.get("labels") or fields.get("labels") or []) if item] components = issue.get("components") or fields.get("components") or [] component_names = [] for item in components: if isinstance(item, dict): if item.get("name"): component_names.append(str(item["name"])) elif item: component_names.append(str(item)) return { "key": str(key), "title": str(title), "description": description.strip(), "issue_type": str(issue_type), "status": str(status), "labels": labels, "components": component_names, } def prepare_issues(path: Path) -> dict[str, object]: CONTEXT_DIR.mkdir(parents=True, exist_ok=True) issues = [normalize_issue(item) for item in load_issues(path)] normalized = CONTEXT_DIR / "atlassian-issues.normalized.json" write_json(normalized, issues) batch_dir = CONTEXT_DIR / "jira-issue-batches" batch_dir.mkdir(parents=True, exist_ok=True) summary_dir = CONTEXT_DIR / "jira-batch-summaries" summary_dir.mkdir(parents=True, exist_ok=True) labels = Counter() components = Counter() issue_types = Counter() statuses = Counter() project_keys = Counter() terms = Counter() for issue in issues: labels.update(issue["labels"]) components.update(issue["components"]) if issue["issue_type"]: issue_types[issue["issue_type"]] += 1 if issue["status"]: statuses[issue["status"]] += 1 if issue["key"] and "-" in issue["key"]: project_keys[issue["key"].split("-", 1)[0]] += 1 for token in TOKEN_RE.findall(f"{issue['title']} {issue['description']}".lower()): if token in STOPWORDS or token.isdigit(): continue terms[token] += 1 batches = [] for start in range(0, len(issues), CHUNK_SIZE): batch_no = len(batches) + 1 chunk = issues[start:start + CHUNK_SIZE] json_path = batch_dir / f"issues-batch-{batch_no:03d}.json" md_path = batch_dir / f"issues-batch-{batch_no:03d}.md" write_json(json_path, {"issues": chunk}) lines = [f"# Jira Issue Batch {batch_no}", "", f"- Issues in batch: {len(chunk)}", ""] for issue in chunk: lines += [ f"## {issue['key'] or '(no-key)'} - {issue['title'] or '(untitled)'}", "", f"- Type: {issue['issue_type'] or 'unknown'}", f"- Status: {issue['status'] or 'unknown'}", f"- Labels: {', '.join(issue['labels']) if issue['labels'] else 'none'}", f"- Components: {', '.join(issue['components']) if issue['components'] else 'none'}", "", "### Description", "", issue["description"] or "(empty)", "", ] md_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") batches.append({ "batch_number": batch_no, "issue_count": len(chunk), "json_file": str(json_path), "markdown_file": str(md_path), "summary_file": str(summary_dir / f"issues-batch-{batch_no:03d}-summary.md"), "issue_keys": [item["key"] for item in chunk if item["key"]], }) batch_index = CONTEXT_DIR / "jira-issue-batches" / "index.json" write_json(batch_index, { "issue_count": len(issues), "chunk_size": CHUNK_SIZE, "batch_count": len(batches), "batches": batches, }) overview_json = CONTEXT_DIR / "jira-issues-overview.json" write_json(overview_json, { "issue_count": len(issues), "batch_count": len(batches), "project_keys": project_keys.most_common(10), "issue_types": issue_types.most_common(10), "statuses": statuses.most_common(10), "labels": labels.most_common(15), "components": components.most_common(15), "top_terms": terms.most_common(30), }) overview_md = CONTEXT_DIR / "jira-issues-overview.md" lines = [ "# Jira Issues Overview", "", f"- Total issues: {len(issues)}", f"- Batch count: {len(batches)}", f"- Chunk size: {CHUNK_SIZE}", "", "## Top Project Keys", "", ] lines += [f"- {name}: {count}" for name, count in project_keys.most_common(10)] or ["- none"] lines += ["", "## Issue Types", ""] + ([f"- {name}: {count}" for name, count in issue_types.most_common(10)] or ["- none"]) lines += ["", "## Statuses", ""] + ([f"- {name}: {count}" for name, count in statuses.most_common(10)] or ["- none"]) lines += ["", "## Top Labels", ""] + ([f"- {name}: {count}" for name, count in labels.most_common(15)] or ["- none"]) lines += ["", "## Top Components", ""] + ([f"- {name}: {count}" for name, count in components.most_common(15)] or ["- none"]) lines += ["", "## Recurring Terms", ""] + ([f"- {name}: {count}" for name, count in terms.most_common(25)] or ["- none"]) overview_md.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") return { "ISSUE_COUNT": len(issues), "BATCH_COUNT": len(batches), "CHUNK_SIZE": CHUNK_SIZE, "CONTEXT_DIR": str(CONTEXT_DIR), "NORMALIZED_ISSUES_FILE": str(normalized), "BATCH_INDEX_FILE": str(batch_index), "ISSUE_OVERVIEW_JSON": str(overview_json), "ISSUE_OVERVIEW_MD": str(overview_md), "BATCH_DIR": str(batch_dir), "BATCH_SUMMARIES_DIR": str(summary_dir), } result = prepare_issues(Path(ISSUES_FILE).resolve()) if PREPARE_ISSUES else summarize_repo() print(json.dumps(result, ensure_ascii=False, separators=(",", ":")) if JSON_MODE else "\n".join( f"{key}: {', '.join(map(str, value)) if isinstance(value, list) else value}" for key, value in result.items() )) PY