chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:35:03 +08:00
commit 4050a9af7b
678 changed files with 174935 additions and 0 deletions
+288
View File
@@ -0,0 +1,288 @@
#!/usr/bin/env python3
"""
Common utilities for ArcKit scripts (plugin version).
Looks for projects/ directory as repo root indicator.
"""
import os
import re
import sys
import json
import shutil
import subprocess
from pathlib import Path
# ANSI color codes
RED = "\033[0;31m"
GREEN = "\033[0;32m"
YELLOW = "\033[1;33m"
BLUE = "\033[0;34m"
NC = "\033[0m" # No Color
# ============================================================================
# Logging Functions
# ============================================================================
def log_info(msg):
print(f"{BLUE}[INFO]{NC} {msg}", file=sys.stderr)
def log_success(msg):
print(f"{GREEN}[SUCCESS]{NC} {msg}", file=sys.stderr)
def log_warning(msg):
print(f"{YELLOW}[WARNING]{NC} {msg}", file=sys.stderr)
def log_error(msg):
print(f"{RED}[ERROR]{NC} {msg}", file=sys.stderr)
# ============================================================================
# Repository Root Detection
# ============================================================================
def find_repo_root(start_dir=None):
"""Find the repository root by looking for projects/ directory."""
current = Path(start_dir or os.getcwd()).resolve()
while current != current.parent:
if (current / "projects").is_dir():
return str(current)
current = current.parent
log_error("Not in an ArcKit project (no projects/ directory found)")
sys.exit(1)
# ============================================================================
# Project Management
# ============================================================================
def get_next_project_number(repo_root):
"""Get the next available project number (zero-padded to 3 digits)."""
projects_dir = Path(repo_root) / "projects"
if not projects_dir.is_dir():
return "001"
max_num = 0
for entry in projects_dir.iterdir():
if entry.is_dir():
m = re.match(r"^(\d{3})-", entry.name)
if m:
num = int(m.group(1))
if num > max_num:
max_num = num
return f"{max_num + 1:03d}"
def slugify(text):
"""Convert text to kebab-case slug."""
text = text.lower()
text = re.sub(r"[^a-z0-9]+", "-", text)
text = text.strip("-")
return text
def create_project_dir(project_dir):
"""Create project directory structure with all required subdirectories."""
subdirs = [
"", "vendors", "external", "final",
"decisions", "diagrams", "wardley-maps",
"data-contracts", "reviews",
]
for sub in subdirs:
(Path(project_dir) / sub).mkdir(parents=True, exist_ok=True)
log_success(f"Created project directory: {project_dir}")
# ============================================================================
# Project Finding
# ============================================================================
def find_project_dir_by_prefix(prefix, repo_root=None):
"""Find project directory by number or prefix (exact then fuzzy match)."""
if repo_root is None:
repo_root = find_repo_root()
projects_dir = Path(repo_root) / "projects"
if not projects_dir.is_dir():
log_error("No projects directory found")
return None
# Exact match first
for entry in sorted(projects_dir.iterdir()):
if entry.is_dir():
name = entry.name
if name == prefix or name.startswith(f"{prefix}-"):
return str(entry)
# Fuzzy match
for entry in sorted(projects_dir.iterdir()):
if entry.is_dir() and prefix in entry.name:
return str(entry)
log_error(f"No project found matching: {prefix}")
return None
def get_project_number_from_dir(dir_path):
"""Extract 3-digit project number from directory name."""
name = Path(dir_path).name
m = re.match(r"^(\d{3})-", name)
return m.group(1) if m else None
def list_projects(repo_root=None):
"""List all projects."""
if repo_root is None:
repo_root = find_repo_root()
projects_dir = Path(repo_root) / "projects"
if not projects_dir.is_dir():
print("No projects found")
return
dirs = sorted(d for d in projects_dir.iterdir() if d.is_dir())
if not dirs:
print("No projects found")
return
print("Available projects:")
for d in dirs:
print(f" - {d.name}")
# ============================================================================
# Git Integration
# ============================================================================
def has_git():
"""Check if git is available."""
return shutil.which("git") is not None
def get_repo_root():
"""Get repository root using git (fallback to find_repo_root)."""
if has_git():
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True, timeout=5,
)
if result.returncode == 0:
return result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return find_repo_root()
def get_current_branch():
"""Get current git branch."""
if has_git():
try:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True, text=True, timeout=5,
)
if result.returncode == 0:
return result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return "main"
# ============================================================================
# Validation Helpers
# ============================================================================
def check_file(file_path, description=None):
"""Check if file exists and print status."""
if description is None:
description = Path(file_path).name
if Path(file_path).is_file():
print(f" \u2713 {description}")
return True
print(f" \u2717 {description}")
return False
def check_dir(dir_path, description=None):
"""Check if directory exists and is not empty."""
if description is None:
description = Path(dir_path).name
p = Path(dir_path)
if p.is_dir() and any(p.iterdir()):
print(f" \u2713 {description}")
return True
print(f" \u2717 {description}")
return False
def require_file(file_path, description=None):
"""Require file to exist."""
if description is None:
description = Path(file_path).name
if not Path(file_path).is_file():
log_error(f"Required file not found: {description}")
log_error(f" Path: {file_path}")
return False
log_success(f"Found: {description}")
return True
def require_dir(dir_path, description=None):
"""Require directory to exist."""
if description is None:
description = Path(dir_path).name
if not Path(dir_path).is_dir():
log_error(f"Required directory not found: {description}")
log_error(f" Path: {dir_path}")
return False
log_success(f"Found: {description}")
return True
# ============================================================================
# JSON Helpers
# ============================================================================
def json_escape(s):
"""Escape string for JSON embedding."""
return json.dumps(s)[1:-1] # Strip surrounding quotes
def output_json_array(items):
"""Output a JSON array string from a list."""
return json.dumps(items)
# ============================================================================
# Path Helpers
# ============================================================================
def get_arckit_dir(repo_root=None):
"""Get .arckit directory path."""
if repo_root is None:
repo_root = find_repo_root()
return os.path.join(repo_root, ".arckit")
def get_templates_dir(repo_root=None):
"""Get templates directory path."""
if repo_root is None:
repo_root = find_repo_root()
return os.path.join(repo_root, ".arckit", "templates")
def get_projects_dir(repo_root=None):
"""Get projects directory path."""
if repo_root is None:
repo_root = find_repo_root()
return os.path.join(repo_root, "projects")
def get_memory_dir(repo_root=None):
"""Get memory directory path (000-global)."""
if repo_root is None:
repo_root = find_repo_root()
return os.path.join(repo_root, "projects", "000-global")
+373
View File
@@ -0,0 +1,373 @@
#!/usr/bin/env python3
"""
Create a new ArcKit project for architecture governance.
Usage:
python3 create-project.py [OPTIONS]
Options:
--name "PROJECT_NAME" Name of the project
--json Output JSON for AI agent consumption
--force Skip prerequisites check
"""
import argparse
import glob
import json
import os
import sys
from datetime import date
from pathlib import Path
# Add parent directory to path for common imports
sys.path.insert(0, os.path.dirname(__file__))
from common import (
find_repo_root, get_next_project_number, slugify, create_project_dir,
get_arckit_dir, get_memory_dir, get_templates_dir,
log_info, log_success, log_error, output_json_array,
)
def has_doc(project_dir, project_number, type_code):
"""Check if a document with the given type code exists."""
pattern = os.path.join(project_dir, f"ARC-{project_number}-{type_code}-v*.md")
return len(glob.glob(pattern)) > 0
def main():
parser = argparse.ArgumentParser(description="Create a new ArcKit project")
parser.add_argument("--name", default="", help="Name of the project")
parser.add_argument("--json", dest="output_json", action="store_true", help="Output JSON for AI agent consumption")
parser.add_argument("--force", action="store_true", help="Skip prerequisites check")
args = parser.parse_args()
# Find repository root
repo_root = find_repo_root()
# Check prerequisites (unless --force)
if not args.force:
global_dir = get_memory_dir(repo_root)
principles_files = glob.glob(os.path.join(global_dir, "ARC-000-PRIN-*.md"))
if not principles_files or not os.path.isfile(principles_files[0]):
log_error("Prerequisites not met: Architecture principles not found")
log_error("Expected: projects/000-global/ARC-000-PRIN-v*.md")
log_error("")
log_error("Before creating a project, you must define architecture principles")
log_error("Run: /arckit:principles")
log_error("")
log_error("Or use --force to skip this check (not recommended)")
sys.exit(1)
log_success("Prerequisites check passed")
# Get project name
project_name = args.name
if not project_name:
if args.output_json:
log_error("Project name is required in JSON mode")
print('{"error": "Project name is required", "success": false}')
sys.exit(1)
log_info("Interactive mode: Creating a new ArcKit project")
print()
try:
project_name = input("Enter project name: ").strip()
except (EOFError, KeyboardInterrupt):
print()
sys.exit(1)
if not project_name:
log_error("Project name cannot be empty")
sys.exit(1)
# Get next project number
project_number = get_next_project_number(repo_root)
log_info(f"Project number: {project_number}")
# Create project slug and directory
project_slug = slugify(project_name)
project_dir_name = f"{project_number}-{project_slug}"
project_dir = os.path.join(repo_root, "projects", project_dir_name)
log_info(f"Creating project: {project_dir_name}")
# Create project directory structure
create_project_dir(project_dir)
# Create README for external documents directory
external_readme = os.path.join(project_dir, "external", "README.md")
Path(external_readme).write_text("""\
# External Documents
Place external reference documents here for ArcKit commands to read as context.
## Supported File Types
- PDF (.pdf)
- Word (.docx)
- Markdown (.md)
- Images (.png, .jpg) - for diagrams and screenshots
- CSV (.csv) - for data exports
- Subtitles/transcripts (.srt, .vtt) - for meetings, talks, and recorded walkthroughs
- SQL (.sql) - for database schemas
## What to Put Here
- RFP/ITT documents
- Legacy system specifications
- User research reports
- Meeting/video transcripts and subtitle exports
- Previous assessments and audits
- Database schemas and ERD diagrams
- Compliance evidence and certificates
- Vendor proposals and technical responses
- Performance benchmarks and test results
## How It Works
ArcKit commands automatically scan this directory when generating artifacts.
External documents enhance output quality but are never blocking.
## See Also
- `projects/000-global/policies/` - Organization-wide standards and governance documents
""")
# Ensure 000-global/policies exists and has a README
global_dir_path = os.path.join(repo_root, "projects", "000-global")
if os.path.isdir(global_dir_path):
policies_dir = os.path.join(global_dir_path, "policies")
os.makedirs(policies_dir, exist_ok=True)
policies_readme = os.path.join(policies_dir, "README.md")
if not os.path.isfile(policies_readme):
Path(policies_readme).write_text("""\
# Organization Policies
Place organization-wide governance documents here. These are read by commands across ALL projects.
## Supported File Types
- PDF (.pdf), Word (.docx), Markdown (.md)
## What to Put Here
- Architecture principles and TOGAF standards
- Security policies and compliance frameworks
- Risk appetite statements and threat assessments
- Technology standards and approved platforms
- Procurement policies and spending thresholds
- Cloud-first mandates and approved supplier lists
- AI governance frameworks and ethical guidelines
- MOD/Defence security policies (JSP 440, CAAT)
## How It Works
Commands like /arckit:principles, /arckit:risk, /arckit:secure, and /arckit:sobc
automatically scan this directory for organizational context.
""")
# Ensure 000-global/external exists and has a README
if os.path.isdir(global_dir_path):
global_ext_dir = os.path.join(global_dir_path, "external")
os.makedirs(global_ext_dir, exist_ok=True)
global_ext_readme = os.path.join(global_ext_dir, "README.md")
if not os.path.isfile(global_ext_readme):
Path(global_ext_readme).write_text("""\
# Global External Documents
Place organization-wide reference documents here. These are read by commands across ALL projects.
## Supported File Types
- PDF (.pdf), Word (.docx), Markdown (.md)
- Images (.png, .jpg) - for diagrams and screenshots
- CSV (.csv) - for data exports
- Subtitles/transcripts (.srt, .vtt) - for meetings, talks, and recorded walkthroughs
- SQL (.sql) - for database schemas
## What to Put Here
- Enterprise architecture blueprints and reference models
- Organization-wide technology standards documents
- Shared compliance evidence and audit reports
- Cross-project strategy and transformation documents
- Cross-project meeting/video transcripts and subtitle exports
- Industry benchmarks and analyst reports
## How It Works
ArcKit commands automatically scan this directory alongside project-level
external documents when generating artifacts.
## See Also
- `projects/000-global/policies/` - Governance policies (risk appetite, security, procurement)
- `projects/{NNN}-{name}/external/` - Project-specific reference documents
""")
# Create project README
today = date.today().isoformat()
project_readme = os.path.join(project_dir, "README.md")
Path(project_readme).write_text(f"""\
# {project_name}
Project ID: {project_number}
Created: {today}
## Overview
[Project description to be added]
## Workflow
Use ArcKit commands to generate project artifacts in the recommended order:
### Discovery Phase
1. `/arckit:stakeholders` - Analyze stakeholder drivers and goals
2. `/arckit:risk` - Create risk register
3. `/arckit:sobc` - Create Strategic Outline Business Case
### Alpha Phase
4. `/arckit:requirements` - Define comprehensive requirements
5. `/arckit:data-model` - Design data model and GDPR compliance
6. `/arckit:wardley` - Create Wardley maps for strategic planning
7. `/arckit:research` - Research technology options (if needed)
8. `/arckit:sow` - Generate Statement of Work for vendor procurement (if needed)
9. `/arckit:evaluate` - Create vendor evaluation framework (if needed)
### Beta Phase
10. `/arckit:hld-review` - Review High-Level Design
11. `/arckit:dld-review` - Review Detailed Design
12. `/arckit:traceability` - Generate requirements traceability matrix
### Compliance (as needed)
- `/arckit:secure` - UK Government Secure by Design review
- `/arckit:tcop` - Technology Code of Practice assessment
- `/arckit:ai-playbook` - AI Playbook compliance (for AI systems)
## Project Structure
Documents use standardized naming: `ARC-{{PROJECT_ID}}-{{TYPE}}-v{{VERSION}}.md`
```
{project_dir_name}/
\u251c\u2500\u2500 README.md (this file)
\u2502
\u251c\u2500\u2500 # Core Documents
\u251c\u2500\u2500 ARC-{project_number}-STKE-v1.0.md # Stakeholder drivers (/arckit:stakeholders)
\u251c\u2500\u2500 ARC-{project_number}-RISK-v1.0.md # Risk register (/arckit:risk)
\u251c\u2500\u2500 ARC-{project_number}-SOBC-v1.0.md # Business case (/arckit:sobc)
\u251c\u2500\u2500 ARC-{project_number}-REQ-v1.0.md # Requirements (/arckit:requirements)
\u251c\u2500\u2500 ARC-{project_number}-DATA-v1.0.md # Data model (/arckit:data-model)
\u251c\u2500\u2500 ARC-{project_number}-RSCH-v1.0.md # Research findings (/arckit:research)
\u251c\u2500\u2500 ARC-{project_number}-TRAC-v1.0.md # Traceability matrix (/arckit:traceability)
\u2502
\u251c\u2500\u2500 # Procurement
\u251c\u2500\u2500 ARC-{project_number}-SOW-v1.0.md # Statement of Work (/arckit:sow)
\u251c\u2500\u2500 ARC-{project_number}-EVAL-v1.0.md # Evaluation criteria (/arckit:evaluate)
\u2502
\u251c\u2500\u2500 # Multi-instance Documents (subdirectories)
\u251c\u2500\u2500 decisions/
\u2502 \u251c\u2500\u2500 ARC-{project_number}-ADR-001-v1.0.md # Architecture decisions (/arckit:adr)
\u2502 \u2514\u2500\u2500 ARC-{project_number}-ADR-002-v1.0.md
\u251c\u2500\u2500 diagrams/
\u2502 \u2514\u2500\u2500 ARC-{project_number}-DIAG-001-v1.0.md # Diagrams (/arckit:diagram)
\u251c\u2500\u2500 wardley-maps/
\u2502 \u2514\u2500\u2500 ARC-{project_number}-WARD-001-v1.0.md # Wardley maps (/arckit:wardley)
\u251c\u2500\u2500 reviews/
\u2502 \u251c\u2500\u2500 ARC-{project_number}-HLD-v1.0.md # HLD review (/arckit:hld-review)
\u2502 \u2514\u2500\u2500 ARC-{project_number}-DLD-v1.0.md # DLD review (/arckit:dld-review)
\u2502
\u251c\u2500\u2500 external/ # External documents (PDFs, specs, reports)
\u2514\u2500\u2500 vendors/ # Vendor proposals
```
## Document Type Codes
| Code | Document Type |
|------|---------------|
| REQ | Requirements |
| STKE | Stakeholder Analysis |
| RISK | Risk Register |
| SOBC | Strategic Outline Business Case |
| DATA | Data Model |
| ADR | Architecture Decision Record |
| RSCH | Research Findings |
| SOW | Statement of Work |
| EVAL | Evaluation Criteria |
| HLD | High-Level Design Review |
| DLD | Detailed-Level Design Review |
| TRAC | Traceability Matrix |
| DIAG | Architecture Diagram |
| WARD | Wardley Map |
| TCOP | Technology Code of Practice |
| SECD | Secure by Design |
## Status
Track your progress through the workflow:
**Discovery Phase:**
- [ ] Stakeholder analysis complete
- [ ] Risk register created
- [ ] Business case approved
**Alpha Phase:**
- [ ] Requirements defined
- [ ] Data model designed
- [ ] Vendor procurement started (if needed)
**Beta Phase:**
- [ ] HLD reviewed and approved
- [ ] DLD reviewed and approved
- [ ] Traceability matrix validated
**Live Phase:**
- [ ] Implementation complete
- [ ] Production deployment
""")
log_success("Project created successfully")
# Determine next steps
next_steps = []
if not has_doc(project_dir, project_number, "STKE"):
next_steps.append("/arckit:stakeholders - Analyze stakeholder drivers and goals")
elif not has_doc(project_dir, project_number, "RISK"):
next_steps.append("/arckit:risk - Create risk register")
elif not has_doc(project_dir, project_number, "SOBC"):
next_steps.append("/arckit:sobc - Create Strategic Outline Business Case")
elif not has_doc(project_dir, project_number, "REQ"):
next_steps.append("/arckit:requirements - Define business and technical requirements")
elif not has_doc(project_dir, project_number, "DATA"):
next_steps.append("/arckit:data-model - Design data model")
elif not os.path.isdir(os.path.join(project_dir, "wardley-maps")) or \
not any(Path(os.path.join(project_dir, "wardley-maps")).iterdir()):
next_steps.append("/arckit:research - Research technology options")
next_steps.append("/arckit:wardley - Create Wardley maps")
elif not has_doc(project_dir, project_number, "SOW"):
next_steps.append("/arckit:sow - Generate Statement of Work for RFP")
else:
next_steps.append("/arckit:evaluate - Create vendor evaluation framework")
# Output
if args.output_json:
output = {
"success": True,
"project_dir": project_dir,
"project_number": project_number,
"project_name": project_name,
"requirements_file": os.path.join(project_dir, f"ARC-{project_number}-REQ-v1.0.md"),
"stakeholders_file": os.path.join(project_dir, f"ARC-{project_number}-STKE-v1.0.md"),
"risk_file": os.path.join(project_dir, f"ARC-{project_number}-RISK-v1.0.md"),
"sobc_file": os.path.join(project_dir, f"ARC-{project_number}-SOBC-v1.0.md"),
"sow_file": os.path.join(project_dir, f"ARC-{project_number}-SOW-v1.0.md"),
"evaluation_file": os.path.join(project_dir, f"ARC-{project_number}-EVAL-v1.0.md"),
"traceability_file": os.path.join(project_dir, f"ARC-{project_number}-TRAC-v1.0.md"),
"decisions_dir": os.path.join(project_dir, "decisions"),
"diagrams_dir": os.path.join(project_dir, "diagrams"),
"wardley_maps_dir": os.path.join(project_dir, "wardley-maps"),
"reviews_dir": os.path.join(project_dir, "reviews"),
"vendors_dir": os.path.join(project_dir, "vendors"),
"external_dir": os.path.join(project_dir, "external"),
"global_external_dir": os.path.join(repo_root, "projects", "000-global", "external"),
"policies_dir": os.path.join(repo_root, "projects", "000-global", "policies"),
"next_steps": next_steps,
}
print(json.dumps(output, indent=2))
else:
log_info(f"Project directory: {project_dir}")
print()
log_info("Next steps:")
for i, step in enumerate(next_steps, 1):
log_info(f" {i}. {step}")
if __name__ == "__main__":
main()
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
Generate standardized ArcKit document IDs.
Usage:
python3 generate-document-id.py PROJECT_ID DOC_TYPE [VERSION] [OPTIONS]
Options:
--filename Return ID with .md extension
--next-num DIR For multi-instance types, find next sequence number
Examples:
python3 generate-document-id.py 001 REQ -> ARC-001-REQ-v1.0
python3 generate-document-id.py 042 HLD 2.1 -> ARC-042-HLD-v2.1
python3 generate-document-id.py 001 REQ 1.0 --filename -> ARC-001-REQ-v1.0.md
python3 generate-document-id.py 001 ADR 1.0 --filename --next-num ./decisions -> ARC-001-ADR-001-v1.0.md
"""
import argparse
import glob
import os
import re
import sys
# Multi-instance document types that require sequence numbers
# Keep in sync with arckit-claude/config/doc-types.mjs MULTI_INSTANCE_TYPES
MULTI_INSTANCE_TYPES = {"ADR", "DIAG", "DFD", "WARD", "DMC", "RSCH", "AWRS", "AZRS", "GCRS", "DSCT", "WGAM", "WCLM", "WVCH", "GOVR", "GCSR", "GLND"}
def is_multi_instance(doc_type):
return doc_type in MULTI_INSTANCE_TYPES
def main():
parser = argparse.ArgumentParser(
description="Generate standardized ArcKit document IDs",
add_help=True,
)
parser.add_argument("project_id", help="Project ID (e.g., 001)")
parser.add_argument("doc_type", help="Document type code (e.g., REQ, ADR)")
parser.add_argument("version", nargs="?", default="1.0", help="Version (default: 1.0)")
parser.add_argument("--filename", action="store_true", help="Return ID with .md extension")
parser.add_argument("--next-num", dest="next_num_dir", metavar="DIR",
help="For multi-instance types, scan DIR for next sequence number")
args = parser.parse_args()
# Normalize project ID to 3-digit zero-padded
try:
pid_clean = int(args.project_id.lstrip("0") or "0")
except ValueError:
print(f"Error: Invalid PROJECT_ID: {args.project_id}", file=sys.stderr)
sys.exit(1)
padded_pid = f"{pid_clean:03d}"
doc_type = args.doc_type
version = args.version
if is_multi_instance(doc_type):
if args.next_num_dir:
next_num_dir = args.next_num_dir
if os.path.isdir(next_num_dir):
# Scan directory for existing files to find next sequence number
pattern_prefix = f"ARC-{padded_pid}-{doc_type}-"
last_num = 0
for fname in os.listdir(next_num_dir):
if not fname.endswith(".md"):
continue
m = re.match(
rf"ARC-{padded_pid}-{re.escape(doc_type)}-(\d+)-",
fname,
)
if m:
num = int(m.group(1))
if num > last_num:
last_num = num
next_num = f"{last_num + 1:03d}"
else:
# Directory doesn't exist yet, start at 001
next_num = "001"
doc_id = f"ARC-{padded_pid}-{doc_type}-{next_num}-v{version}"
else:
print(f"Error: Multi-instance type '{doc_type}' requires --next-num DIR option", file=sys.stderr)
print(f"Usage: {sys.argv[0]} {args.project_id} {doc_type} {version} --next-num ./directory", file=sys.stderr)
sys.exit(1)
else:
# Single-instance document type
doc_id = f"ARC-{padded_pid}-{doc_type}-v{version}"
if args.filename:
doc_id += ".md"
print(doc_id)
if __name__ == "__main__":
main()
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""
List all ArcKit projects with status indicators.
Usage:
python3 list-projects.py [OPTIONS]
Options:
--json Output in JSON format
--verbose, -v Show detailed artifact status
--help, -h Show help message
"""
import argparse
import json
import os
import sys
from pathlib import Path
# Add parent directory to path for common imports
sys.path.insert(0, os.path.dirname(__file__))
from common import (
find_repo_root, get_projects_dir, get_project_number_from_dir,
log_warning,
)
def check_artifact(project_dir, artifact):
"""Check if an artifact exists (file or non-empty directory)."""
if artifact.endswith("/"):
d = os.path.join(project_dir, artifact.rstrip("/"))
return os.path.isdir(d) and bool(os.listdir(d))
return os.path.isfile(os.path.join(project_dir, artifact))
def count_vendors(project_dir):
"""Count vendor proposal directories."""
vendors_dir = os.path.join(project_dir, "vendors")
if not os.path.isdir(vendors_dir):
return 0
return sum(1 for d in os.listdir(vendors_dir)
if os.path.isdir(os.path.join(vendors_dir, d)))
def count_external_docs(project_dir):
"""Count external documents (excluding README.md)."""
external_dir = os.path.join(project_dir, "external")
if not os.path.isdir(external_dir):
return 0
extensions = {".pdf", ".docx", ".md", ".csv", ".sql", ".png", ".jpg"}
count = 0
for fname in os.listdir(external_dir):
if fname == "README.md":
continue
fpath = os.path.join(external_dir, fname)
if os.path.isfile(fpath) and Path(fname).suffix.lower() in extensions:
count += 1
return count
def calculate_completion(project_dir):
"""Calculate completion percentage based on standard artifacts."""
total = 10
completed = 0
artifacts = [
"stakeholder-drivers.md", "risk-register.md", "sobc.md",
"requirements.md", "data-model.md", "research-findings.md",
"sow.md", "evaluation-criteria.md",
]
for artifact in artifacts:
if os.path.isfile(os.path.join(project_dir, artifact)):
completed += 1
# Wardley maps
wm_dir = os.path.join(project_dir, "wardley-maps")
if os.path.isdir(wm_dir) and os.listdir(wm_dir):
completed += 1
# Vendors
vendors_dir = os.path.join(project_dir, "vendors")
if os.path.isdir(vendors_dir) and os.listdir(vendors_dir):
completed += 1
return completed * 100 // total
def get_status_emoji(percentage):
"""Get status indicator based on completion."""
if percentage == 100:
return "\u2705"
elif percentage >= 75:
return "\U0001f7e2"
elif percentage >= 50:
return "\U0001f7e1"
elif percentage >= 25:
return "\U0001f7e0"
else:
return "\U0001f534"
def main():
parser = argparse.ArgumentParser(description="List all ArcKit projects with status indicators")
parser.add_argument("--json", dest="json_mode", action="store_true", help="Output in JSON format")
parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed artifact status")
args = parser.parse_args()
repo_root = find_repo_root()
projects_dir = get_projects_dir(repo_root)
if not os.path.isdir(projects_dir):
if args.json_mode:
print('{"projects": []}')
else:
log_warning("No projects directory found")
print()
print("Run: /arckit:init to initialize an ArcKit repository")
sys.exit(0)
# Get sorted project directories
project_dirs = sorted(
d for d in Path(projects_dir).iterdir() if d.is_dir()
)
project_count = len(project_dirs)
if project_count == 0:
if args.json_mode:
print('{"projects": []}')
else:
print("No projects found")
print()
print("Run: /arckit:create to create a new project")
sys.exit(0)
# JSON output mode
if args.json_mode:
projects = []
for pd in project_dirs:
pdir = str(pd)
project_name = pd.name
project_number = get_project_number_from_dir(pdir) or ""
vendor_count = count_vendors(pdir)
external_doc_count = count_external_docs(pdir)
completion = calculate_completion(pdir)
projects.append({
"name": project_name,
"number": project_number,
"path": pdir,
"completion_percentage": completion,
"vendor_count": vendor_count,
"external_doc_count": external_doc_count,
"artifacts": {
"stakeholder_drivers": check_artifact(pdir, "stakeholder-drivers.md"),
"risk_register": check_artifact(pdir, "risk-register.md"),
"sobc": check_artifact(pdir, "sobc.md"),
"requirements": check_artifact(pdir, "requirements.md"),
"data_model": check_artifact(pdir, "data-model.md"),
"research_findings": check_artifact(pdir, "research-findings.md"),
"wardley_maps": check_artifact(pdir, "wardley-maps/"),
"sow": check_artifact(pdir, "sow.md"),
"evaluation_criteria": check_artifact(pdir, "evaluation-criteria.md"),
"vendors": check_artifact(pdir, "vendors/"),
},
})
output = {
"repository_root": repo_root,
"projects_dir": projects_dir,
"project_count": project_count,
"projects": projects,
}
print(json.dumps(output, indent=2))
sys.exit(0)
# Text output mode
print("ArcKit Projects")
print("===============")
print()
print(f"Repository: {repo_root}")
print(f"Projects found: {project_count}")
print()
for pd in project_dirs:
pdir = str(pd)
project_name = pd.name
project_number = get_project_number_from_dir(pdir) or ""
vendor_count = count_vendors(pdir)
external_doc_count = count_external_docs(pdir)
completion = calculate_completion(pdir)
status = get_status_emoji(completion)
print(f"{status} [{project_number}] {project_name} ({completion}% complete)")
if args.verbose:
print(f" Path: {pdir}")
print(" Artifacts:")
checks = [
("stakeholder-drivers.md", "Stakeholder Drivers"),
("risk-register.md", "Risk Register"),
("sobc.md", "Strategic Outline Business Case"),
("requirements.md", "Requirements"),
("data-model.md", "Data Model"),
("research-findings.md", "Research Findings"),
("wardley-maps/", "Wardley Maps"),
("sow.md", "Statement of Work"),
("evaluation-criteria.md", "Evaluation Criteria"),
]
for artifact, label in checks:
mark = "\u2713" if check_artifact(pdir, artifact) else "\u2717"
print(f" {mark} {label}")
if vendor_count > 0:
print(f" \u2713 Vendor Proposals ({vendor_count})")
else:
print(" \u2717 Vendor Proposals")
if external_doc_count > 0:
print(f" \u2713 External Documents ({external_doc_count})")
else:
print(" \u2717 External Documents")
print()
print()
print("Legend:")
print(" \u2705 Complete (100%)")
print(" \U0001f7e2 Mostly complete (75-99%)")
print(" \U0001f7e1 In progress (50-74%)")
print(" \U0001f7e0 Started (25-49%)")
print(" \U0001f534 Not started (0-24%)")
if __name__ == "__main__":
main()