Files
tractorjuice--arckit-gemini/hooks/scripts/hook_utils.py
T
2026-07-13 12:35:03 +08:00

258 lines
9.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
ArcKit Hook Utilities -- Shared module for all Gemini CLI hooks.
Python equivalent of arckit-claude/hooks/hook-utils.mjs.
Provides common helpers for stdin parsing, repo discovery, doc-type
metadata, and JSON output formatting.
"""
import json
import os
import re
import sys
# ── Document Type Codes (mirrors arckit-claude/config/doc-types.mjs) ──
DOC_TYPES = {
# Discovery
"REQ": {"name": "Requirements", "category": "Discovery"},
"STKE": {"name": "Stakeholder Analysis", "category": "Discovery"},
"RSCH": {"name": "Research Findings", "category": "Discovery"},
"DSCT": {"name": "Data Source Discovery", "category": "Discovery"},
# Planning
"SOBC": {"name": "Strategic Outline Business Case", "category": "Planning"},
"PLAN": {"name": "Project Plan", "category": "Planning"},
"ROAD": {"name": "Roadmap", "category": "Planning"},
"STRAT": {"name": "Architecture Strategy", "category": "Planning"},
"BKLG": {"name": "Product Backlog", "category": "Planning"},
# Architecture
"PRIN": {"name": "Architecture Principles", "category": "Architecture"},
"HLDR": {"name": "High-Level Design Review", "category": "Architecture"},
"DLDR": {"name": "Detailed Design Review", "category": "Architecture"},
"DATA": {"name": "Data Model", "category": "Architecture"},
"WARD": {"name": "Wardley Map", "category": "Architecture"},
"DIAG": {"name": "Architecture Diagrams", "category": "Architecture"},
"DFD": {"name": "Data Flow Diagram", "category": "Architecture"},
"ADR": {"name": "Architecture Decision Records", "category": "Architecture"},
"PLAT": {"name": "Platform Design", "category": "Architecture"},
# Governance
"RISK": {"name": "Risk Register", "category": "Governance"},
"TRAC": {"name": "Traceability Matrix", "category": "Governance"},
"PRIN-COMP": {"name": "Principles Compliance", "category": "Governance"},
"CONF": {"name": "Conformance Assessment", "category": "Governance"},
"PRES": {"name": "Presentation", "category": "Governance"},
"ANAL": {"name": "Analysis Report", "category": "Governance"},
"GAPS": {"name": "Gap Analysis", "category": "Governance"},
# Compliance
"TCOP": {"name": "TCoP Assessment", "category": "Compliance"},
"SECD": {"name": "Secure by Design", "category": "Compliance"},
"SECD-MOD": {"name": "MOD Secure by Design", "category": "Compliance"},
"AIPB": {"name": "AI Playbook Assessment", "category": "Compliance"},
"ATRS": {"name": "ATRS Record", "category": "Compliance"},
"DPIA": {"name": "Data Protection Impact Assessment", "category": "Compliance"},
"JSP936": {"name": "JSP 936 Assessment", "category": "Compliance"},
"SVCASS": {"name": "Service Assessment", "category": "Compliance"},
# Operations
"SNOW": {"name": "ServiceNow Design", "category": "Operations"},
"DEVOPS": {"name": "DevOps Strategy", "category": "Operations"},
"MLOPS": {"name": "MLOps Strategy", "category": "Operations"},
"FINOPS": {"name": "FinOps Strategy", "category": "Operations"},
"OPS": {"name": "Operational Readiness", "category": "Operations"},
# Procurement
"SOW": {"name": "Statement of Work", "category": "Procurement"},
"EVAL": {"name": "Evaluation Criteria", "category": "Procurement"},
"DOS": {"name": "DOS Requirements", "category": "Procurement"},
"GCLD": {"name": "G-Cloud Search", "category": "Procurement"},
"GCLC": {"name": "G-Cloud Clarifications", "category": "Procurement"},
"DMC": {"name": "Data Mesh Contract", "category": "Procurement"},
"VEND": {"name": "Vendor Evaluation", "category": "Procurement"},
# Research
"AWRS": {"name": "AWS Research", "category": "Research"},
"AZRS": {"name": "Azure Research", "category": "Research"},
"GCRS": {"name": "GCP Research", "category": "Research"},
# Other
"STORY": {"name": "Project Story", "category": "Other"},
}
# Set of all valid type codes
KNOWN_TYPES = set(DOC_TYPES.keys())
# Multi-instance types requiring sequence numbers (ADR-001, DIAG-002, etc.)
MULTI_INSTANCE_TYPES = {
"ADR", "DIAG", "DFD", "WARD", "DMC",
"RSCH", "AWRS", "AZRS", "GCRS", "DSCT",
}
# Type code -> required subdirectory
SUBDIR_MAP = {
"ADR": "decisions",
"DIAG": "diagrams",
"DFD": "diagrams",
"WARD": "wardley-maps",
"DMC": "data-contracts",
"RSCH": "research",
"AWRS": "research",
"AZRS": "research",
"GCRS": "research",
"DSCT": "research",
}
# Compound types (contain hyphens) -- checked first during extraction
COMPOUND_TYPES = [k for k in DOC_TYPES if "-" in k]
# Regex for ARC filenames: ARC-NNN-TYPE[-SEQ]-vN.N.md
ARC_PATTERN = re.compile(r"^ARC-\d{3}-.+-v\d+(\.\d+)?\.md$")
# Subdirectory name -> manifest array key
SUBDIR_TO_KEY = {}
for _dir in set(SUBDIR_MAP.values()):
# Convert kebab-case to camelCase: "wardley-maps" -> "wardleyMaps"
_key = re.sub(r"-([a-z])", lambda m: m.group(1).upper(), _dir)
SUBDIR_TO_KEY[_dir] = _key
SUBDIR_TO_KEY["reviews"] = "reviews"
# Subdirectories to scan for artifacts
ARTIFACT_SUBDIRS = list(set(SUBDIR_MAP.values())) + ["reviews"]
# ── File System Helpers ──
def is_dir(path):
"""Check if path is a directory."""
return os.path.isdir(path)
def is_file(path):
"""Check if path is a file."""
return os.path.isfile(path)
def read_text(path):
"""Read file contents as text, return None on failure."""
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except (OSError, IOError):
return None
def list_dir(path):
"""List directory contents sorted, return empty list on failure."""
try:
return sorted(os.listdir(path))
except (OSError, IOError):
return []
def list_files_recursive(root_dir):
"""List files below root_dir with stable slash-separated relative paths."""
files = []
def walk(current_dir, parts):
for entry in list_dir(current_dir):
full_path = os.path.join(current_dir, entry)
next_parts = parts + [entry]
if is_dir(full_path):
walk(full_path, next_parts)
elif is_file(full_path):
files.append({
"name": entry,
"path": full_path,
"relative_path": "/".join(next_parts),
})
if is_dir(root_dir):
walk(root_dir, [])
return files
def mtime_ms(path):
"""Get file modification time in milliseconds, 0 on failure."""
try:
return os.stat(path).st_mtime * 1000
except (OSError, IOError):
return 0
# ── Repository Discovery ──
def find_repo_root(cwd):
"""Walk up from cwd until a directory containing projects/ is found."""
current = os.path.abspath(cwd)
while True:
if is_dir(os.path.join(current, "projects")):
return current
parent = os.path.dirname(current)
if parent == current:
break
current = parent
return None
# ── Doc Type Extraction ──
def extract_doc_type(filename):
"""Extract the document type code from an ARC filename."""
m = re.match(r"^ARC-\d{3}-(.+)-v\d+(\.\d+)?\.md$", filename)
if not m:
return None
rest = m.group(1)
# Try compound types first (e.g. SECD-MOD, PRIN-COMP)
for code in COMPOUND_TYPES:
if rest.startswith(code):
return code
# Strip trailing -NNN for multi-instance types (ADR-001, DIAG-002)
rest = re.sub(r"-\d{3}$", "", rest)
return rest
def extract_version(filename):
"""Extract version string from ARC filename."""
m = re.search(r"-v(\d+(?:\.\d+)?)\.md$", filename)
return m.group(1) if m else None
def doc_type_name(code):
"""Get display name for a doc type code."""
entry = DOC_TYPES.get(code)
return entry["name"] if entry else code
# ── Hook Input/Output ──
def parse_hook_input():
"""Read JSON from stdin. Return empty dict on failure."""
try:
raw = sys.stdin.read()
except Exception:
return {}
if not raw or not raw.strip():
return {}
try:
return json.loads(raw)
except (json.JSONDecodeError, ValueError):
return {}
def output_json(data):
"""Write JSON to stdout."""
print(json.dumps(data))
def output_context(text):
"""Output additionalContext via hookSpecificOutput."""
output_json({
"hookSpecificOutput": {
"additionalContext": text,
}
})
def output_block(reason):
"""Write reason to stderr and exit with code 2 (block)."""
sys.stderr.write(reason + "\n")
sys.exit(2)