← Files Selective IntelligenceARCHIVED FILE

skills/selective-intelligence/scripts/project_index.py

23.4 KB · Sep 30, 2026 · 23:14 UTC

↓ Download file

#!/usr/bin/env python3
"""Build and validate one durable architecture index for a project.

The index is deliberately dependency-free. It inventories directories, source
files, exported and top-level symbols, UI primitives, raw interactive elements,
and duplicate ownership risks. Refreshing preserves the project's explicit
canonical-owner declarations and reuse decisions while replacing generated
inventory with facts from the current repository.
"""

from __future__ import annotations

import argparse
import ast
import datetime as dt
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any, Iterable

sys.dont_write_bytecode = True

SCHEMA_VERSION = 1
INDEX_VERSION = "0.1.0"
DEFAULT_INDEX = Path(".selective-intelligence/project-index.json")
SOURCE_EXTENSIONS = {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".py"}
UI_EXTENSIONS = {".jsx", ".tsx"}
SKIP_DIRECTORIES = {
    ".git",
    ".hg",
    ".svn",
    ".selective-intelligence",
    ".next",
    ".nuxt",
    ".turbo",
    ".venv",
    "__pycache__",
    "build",
    "coverage",
    "dist",
    "node_modules",
    "target",
    "vendor",
}
PRIMITIVE_NAMES = {
    "Button",
    "Card",
    "Checkbox",
    "Dialog",
    "Drawer",
    "Field",
    "Form",
    "Input",
    "Modal",
    "Radio",
    "Select",
    "Table",
    "Tabs",
    "Textarea",
    "Toast",
    "Tooltip",
}
RAW_UI_RE = re.compile(r"<\s*(button|input|select|textarea|form)\b", re.IGNORECASE)
JS_DECLARATIONS = (
    ("class", re.compile(r"(?m)^\s*(export\s+(?:default\s+)?)?class\s+([A-Za-z_$][\w$]*)")),
    ("function", re.compile(r"(?m)^\s*(export\s+(?:default\s+)?)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(")),
    ("function", re.compile(r"(?m)^\s*(export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^\n]*\)|[A-Za-z_$][\w$]*)\s*=>")),
    ("type", re.compile(r"(?m)^\s*(export\s+)?(?:interface|type|enum)\s+([A-Za-z_$][\w$]*)")),
)


def utc_now() -> str:
    return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def sha256_bytes(payload: bytes) -> str:
    return hashlib.sha256(payload).hexdigest()


def json_digest(value: object) -> str:
    payload = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    return sha256_bytes(payload)


def safe_relative(root: Path, path: Path) -> str:
    return path.relative_to(root).as_posix()


def source_files(root: Path) -> Iterable[Path]:
    for current, directories, files in os.walk(root, followlinks=False):
        current_path = Path(current)
        directories[:] = sorted(
            name
            for name in directories
            if name not in SKIP_DIRECTORIES and not (current_path / name).is_symlink()
        )
        for name in sorted(files):
            path = current_path / name
            if path.suffix.lower() in SOURCE_EXTENSIONS and not path.is_symlink():
                yield path


def line_number(text: str, offset: int) -> int:
    return text.count("\n", 0, offset) + 1


def classify_symbol(name: str, declared_kind: str, suffix: str) -> str:
    if name.startswith("use") and len(name) > 3 and name[3].isupper():
        return "hook"
    if suffix in UI_EXTENSIONS and name[:1].isupper() and declared_kind in {"class", "function"}:
        return "component"
    return declared_kind


def javascript_symbols(relative: str, text: str, suffix: str) -> list[dict[str, Any]]:
    symbols: list[dict[str, Any]] = []
    seen: set[tuple[str, int]] = set()
    for declared_kind, pattern in JS_DECLARATIONS:
        for match in pattern.finditer(text):
            name = match.group(2)
            line = line_number(text, match.start())
            key = (name, line)
            if key in seen:
                continue
            seen.add(key)
            symbols.append(
                {
                    "name": name,
                    "kind": classify_symbol(name, declared_kind, suffix),
                    "path": relative,
                    "line": line,
                    "exported": bool(match.group(1)),
                }
            )
    return symbols


def python_symbols(relative: str, text: str) -> list[dict[str, Any]]:
    try:
        tree = ast.parse(text)
    except SyntaxError:
        return []
    symbols: list[dict[str, Any]] = []
    for node in tree.body:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            kind = "class" if isinstance(node, ast.ClassDef) else classify_symbol(node.name, "function", ".py")
            symbols.append(
                {
                    "name": node.name,
                    "kind": kind,
                    "path": relative,
                    "line": int(getattr(node, "lineno", 1)),
                    "exported": not node.name.startswith("_"),
                }
            )
    return symbols


def git_facts(root: Path) -> dict[str, Any]:
    def run(*args: str) -> subprocess.CompletedProcess[str]:
        return subprocess.run(
            ["git", "-C", str(root), *args],
            check=False,
            capture_output=True,
            text=True,
            timeout=10,
        )

    inside = run("rev-parse", "--is-inside-work-tree")
    if inside.returncode != 0 or inside.stdout.strip() != "true":
        return {"kind": "directory", "revision": None, "dirty": None}
    revision = run("rev-parse", "HEAD")
    status = run("status", "--porcelain", "--untracked-files=normal")
    dirty_lines = []
    if status.returncode == 0:
        dirty_lines = [
            line
            for line in status.stdout.splitlines()
            if not line.replace("\\", "/").endswith(DEFAULT_INDEX.as_posix())
        ]
    return {
        "kind": "git",
        "revision": revision.stdout.strip() if revision.returncode == 0 else None,
        "dirty": bool(dirty_lines) if status.returncode == 0 else None,
    }


def retained_governance(existing: object) -> tuple[dict[str, list[dict[str, Any]]], list[dict[str, Any]]]:
    default = {"directories": [], "ui_primitives": [], "shared_symbols": []}
    if not isinstance(existing, dict):
        return default, []
    canonical = existing.get("canonical")
    decisions = existing.get("reuse_decisions")
    if not isinstance(canonical, dict):
        canonical = default
    clean_canonical: dict[str, list[dict[str, Any]]] = {}
    for key in default:
        value = canonical.get(key)
        clean_canonical[key] = value if isinstance(value, list) and all(isinstance(item, dict) for item in value) else []
    clean_decisions = decisions if isinstance(decisions, list) and all(isinstance(item, dict) for item in decisions) else []
    return clean_canonical, clean_decisions


def load_existing(path: Path) -> object:
    if not path.is_file():
        return None
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return None


def declared_generated_projections(root: Path) -> list[dict[str, str]]:
    """Read bounded adapter manifests that name one canonical source and projection.

    Generated adapters are intentional mirrors, not competing owners. The
    exception is accepted only when a repository manifest explicitly names
    both roots and duplicate paths have the same suffix below those roots.
    """
    projections: list[dict[str, str]] = []
    for manifest in sorted(root.glob("adapters/**/metadata/*-adapter.json")):
        try:
            data = json.loads(manifest.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            continue
        source = data.get("portable_source_path")
        destination = data.get("adapter_path")
        if not isinstance(source, str) or not isinstance(destination, str):
            continue
        source_path = Path(source)
        destination_path = Path(destination)
        if (
            source_path.is_absolute()
            or destination_path.is_absolute()
            or ".." in source_path.parts
            or ".." in destination_path.parts
        ):
            continue
        source_value = source_path.as_posix().strip("/")
        destination_value = destination_path.as_posix().strip("/")
        if not source_value or not destination_value or source_value == destination_value:
            continue
        if not (root / source_path).is_dir() or not (root / destination_path).is_dir():
            continue
        projections.append(
            {
                "canonical": source_value,
                "projection": destination_value,
                "manifest": safe_relative(root, manifest),
            }
        )
    return projections


def generated_projection_exception(paths: list[str], projections: list[dict[str, str]]) -> dict[str, str] | None:
    if len(paths) != 2:
        return None
    for projection in projections:
        canonical_prefix = projection["canonical"] + "/"
        adapter_prefix = projection["projection"] + "/"
        canonical_paths = [path for path in paths if path.startswith(canonical_prefix)]
        adapter_paths = [path for path in paths if path.startswith(adapter_prefix)]
        if len(canonical_paths) != 1 or len(adapter_paths) != 1:
            continue
        canonical_suffix = canonical_paths[0][len(canonical_prefix) :]
        adapter_suffix = adapter_paths[0][len(adapter_prefix) :]
        if canonical_suffix == adapter_suffix:
            return projection
    return None


def build_index(root: Path, existing: object = None) -> dict[str, Any]:
    root = root.resolve()
    canonical, reuse_decisions = retained_governance(existing)
    files: list[dict[str, Any]] = []
    symbols: list[dict[str, Any]] = []
    raw_ui: list[dict[str, Any]] = []
    directory_counts: Counter[str] = Counter()
    hash_paths: defaultdict[str, list[str]] = defaultdict(list)

    for path in source_files(root):
        relative = safe_relative(root, path)
        try:
            payload = path.read_bytes()
        except OSError:
            continue
        if len(payload) > 2 * 1024 * 1024:
            continue
        try:
            text = payload.decode("utf-8")
        except UnicodeDecodeError:
            continue
        digest = sha256_bytes(payload)
        suffix = path.suffix.lower()
        files.append({"path": relative, "language": suffix.removeprefix("."), "sha256": digest, "bytes": len(payload)})
        hash_paths[digest].append(relative)
        directory_counts[Path(relative).parent.as_posix()] += 1
        symbols.extend(python_symbols(relative, text) if suffix == ".py" else javascript_symbols(relative, text, suffix))
        if suffix in UI_EXTENSIONS:
            for match in RAW_UI_RE.finditer(text):
                raw_ui.append({"tag": match.group(1).lower(), "path": relative, "line": line_number(text, match.start())})

    files.sort(key=lambda item: item["path"])
    symbols.sort(key=lambda item: (item["path"], item["line"], item["name"]))
    raw_ui.sort(key=lambda item: (item["path"], item["line"], item["tag"]))
    directories = [
        {"path": path, "source_file_count": count}
        for path, count in sorted(directory_counts.items())
    ]
    duplicate_files = [
        {"sha256": digest, "paths": sorted(paths)}
        for digest, paths in sorted(hash_paths.items())
        if len(paths) > 1
    ]
    exported_by_name: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)
    for symbol in symbols:
        if symbol["exported"] and symbol["kind"] in {"component", "hook"}:
            exported_by_name[symbol["name"]].append(symbol)
    symbol_collisions = [
        {"name": name, "owners": [{"path": item["path"], "line": item["line"], "kind": item["kind"]} for item in owners]}
        for name, owners in sorted(exported_by_name.items())
        if len({item["path"] for item in owners}) > 1
    ]
    ui_candidates = [
        {"name": symbol["name"], "path": symbol["path"], "line": symbol["line"]}
        for symbol in symbols
        if symbol["kind"] == "component"
        and (
            symbol["name"] in PRIMITIVE_NAMES
            or any(part in {"ui", "primitives", "design-system"} for part in Path(symbol["path"]).parts)
        )
    ]
    raw_paths_by_tag: defaultdict[str, set[str]] = defaultdict(set)
    for item in raw_ui:
        raw_paths_by_tag[item["tag"]].add(item["path"])

    projections = declared_generated_projections(root)
    justified_duplicates: list[dict[str, Any]] = []
    findings: list[dict[str, Any]] = []
    for group in duplicate_files:
        exception = generated_projection_exception(group["paths"], projections)
        if exception is not None:
            justified_duplicates.append({**group, "exception": exception})
            continue
        findings.append({"severity": "error", "code": "PI001", "message": "Exact duplicate source files require consolidation or an explicit exception.", "paths": group["paths"]})
    for collision in symbol_collisions:
        findings.append({"severity": "error", "code": "PI002", "message": f"Exported symbol {collision['name']} has multiple candidate owners.", "paths": [item["path"] for item in collision["owners"]]})
    for tag, paths in sorted(raw_paths_by_tag.items()):
        if len(paths) >= 3:
            findings.append({"severity": "warning", "code": "PI003", "message": f"Raw <{tag}> markup is spread across {len(paths)} files; reuse or establish one canonical primitive before adding another variant.", "paths": sorted(paths)})
    known_paths = {item["path"] for item in files}
    for section in ("ui_primitives", "shared_symbols"):
        for declaration in canonical[section]:
            path = declaration.get("path")
            if not isinstance(path, str) or path not in known_paths:
                findings.append({"severity": "error", "code": "PI004", "message": f"Canonical {section} declaration points to a missing source file.", "paths": [str(path)]})
    known_directories = {item["path"] for item in directories}
    for declaration in canonical["directories"]:
        path = declaration.get("path")
        if not isinstance(path, str) or path not in known_directories:
            findings.append({"severity": "error", "code": "PI005", "message": "Canonical directory declaration points to a missing source directory.", "paths": [str(path)]})

    inventory = {
        "directories": directories,
        "files": files,
        "symbols": symbols,
        "ui": {"primitive_candidates": ui_candidates, "raw_elements": raw_ui},
        "duplicates": {
            "exact_files": duplicate_files,
            "justified_generated_projections": justified_duplicates,
            "exported_symbol_collisions": symbol_collisions,
        },
    }
    return {
        "schema_version": SCHEMA_VERSION,
        "index_version": INDEX_VERSION,
        "generated_at": utc_now(),
        "source": {**git_facts(root), "inventory_sha256": json_digest(inventory)},
        "inventory": inventory,
        "canonical": canonical,
        "reuse_decisions": reuse_decisions,
        "findings": findings,
        "summary": {
            "directories": len(directories),
            "source_files": len(files),
            "symbols": len(symbols),
            "components": sum(item["kind"] == "component" for item in symbols),
            "functions_and_hooks": sum(item["kind"] in {"function", "hook"} for item in symbols),
            "raw_ui_elements": len(raw_ui),
            "errors": sum(item["severity"] == "error" for item in findings),
            "warnings": sum(item["severity"] == "warning" for item in findings),
        },
    }


def resolve_index_path(root: Path, output: str | None) -> Path:
    root = root.resolve()
    candidate = Path(output) if output else DEFAULT_INDEX
    if not candidate.is_absolute():
        candidate = root / candidate
    resolved_parent = candidate.parent.resolve()
    if resolved_parent != root and root not in resolved_parent.parents:
        raise ValueError("project index output must stay inside the project root")
    if candidate.exists() and candidate.is_symlink():
        raise ValueError("project index output may not be a symlink")
    return candidate


def write_index(root: Path, output: Path | None = None) -> dict[str, Any]:
    root = root.resolve()
    output = output or (root / DEFAULT_INDEX)
    output.parent.mkdir(parents=True, exist_ok=True)
    index = build_index(root, load_existing(output))
    payload = json.dumps(index, indent=2, ensure_ascii=False) + "\n"
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=output.parent, delete=False, newline="\n") as handle:
        handle.write(payload)
        temporary = Path(handle.name)
    temporary.replace(output)
    return index


def doctor(root: Path, output: Path, strict: bool = False) -> tuple[dict[str, Any], int]:
    existing = load_existing(output)
    if not isinstance(existing, dict):
        result = {"ready": False, "stale": True, "errors": ["project index is missing or invalid"], "warnings": [], "summary": {}}
        return result, 1
    fresh = build_index(root, existing)
    stale = existing.get("source", {}).get("inventory_sha256") != fresh["source"]["inventory_sha256"]
    errors = [item["message"] for item in fresh["findings"] if item["severity"] == "error"]
    warnings = [item["message"] for item in fresh["findings"] if item["severity"] == "warning"]
    if stale:
        errors.insert(0, "project index is stale; refresh it before creating or moving code")
    result = {"ready": not errors and not (strict and warnings), "stale": stale, "errors": errors, "warnings": warnings, "summary": fresh["summary"]}
    return result, 1 if errors or (strict and warnings) else 0


def command_refresh(args: argparse.Namespace) -> int:
    root = Path(args.root).resolve()
    output = resolve_index_path(root, args.output)
    index = write_index(root, output)
    result = {"written": safe_relative(root, output), "source": index["source"], "summary": index["summary"]}
    print(json.dumps(result, indent=2) if args.json else f"Refreshed {result['written']}: {json.dumps(result['summary'], sort_keys=True)}")
    return 0


def command_doctor(args: argparse.Namespace) -> int:
    root = Path(args.root).resolve()
    output = resolve_index_path(root, args.output)
    result, code = doctor(root, output, args.strict)
    print(json.dumps(result, indent=2) if args.json else ("Project index is ready." if code == 0 else "Project index requires attention."))
    if not args.json:
        for item in result["errors"]:
            print(f"ERROR: {item}")
        for item in result["warnings"]:
            print(f"WARNING: {item}")
    return code


def command_self_test(_: argparse.Namespace) -> int:
    with tempfile.TemporaryDirectory(prefix="selective-intelligence-project-index-") as temporary:
        root = Path(temporary)
        (root / "src/components/ui").mkdir(parents=True)
        (root / "src/features/checkout").mkdir(parents=True)
        (root / "src/lib").mkdir(parents=True)
        (root / "src/components/ui/Button.tsx").write_text("export function Button() { return <button>Go</button>; }\n", encoding="utf-8")
        (root / "src/features/checkout/Checkout.tsx").write_text("export const Checkout = () => <button>Pay</button>;\n", encoding="utf-8")
        (root / "src/features/checkout/Retry.tsx").write_text("export const Retry = () => <button>Retry</button>;\n", encoding="utf-8")
        (root / "src/features/checkout/Cancel.tsx").write_text("export const Cancel = () => <button>Cancel</button>;\n", encoding="utf-8")
        (root / "src/features/checkout/Button.tsx").write_text("export function Button() { return <button>Feature</button>; }\n", encoding="utf-8")
        (root / "src/lib/money.py").write_text("def format_money(value):\n    return f'${value}'\n", encoding="utf-8")
        output = root / DEFAULT_INDEX
        first = write_index(root, output)
        if first["summary"]["components"] != 5 or first["summary"]["functions_and_hooks"] != 1:
            raise AssertionError("project index did not classify representative components and functions")
        if not any(item["code"] == "PI003" for item in first["findings"]):
            raise AssertionError("project index did not flag raw UI proliferation")
        if not any(item["code"] == "PI002" for item in first["findings"]):
            raise AssertionError("project index did not flag competing component owners")
        checked, checked_code = doctor(root, output)
        if checked_code != 1 or checked["stale"]:
            raise AssertionError("fresh project index did not preserve component-owner findings")
        (root / "src/lib/money.py").write_text("def format_money(value):\n    return f'USD {value}'\n", encoding="utf-8")
        stale, stale_code = doctor(root, output)
        if stale_code != 1 or not stale["stale"]:
            raise AssertionError("project index did not detect source drift")

        projection_root = root / "projection-fixture"
        canonical = projection_root / "skills/example/scripts"
        adapter = projection_root / "adapters/chatgpt/example/scripts"
        metadata = projection_root / "adapters/chatgpt/example/metadata"
        canonical.mkdir(parents=True)
        adapter.mkdir(parents=True)
        metadata.mkdir(parents=True)
        payload = "def shared_owner():\n    return True\n"
        (canonical / "owner.py").write_text(payload, encoding="utf-8")
        (adapter / "owner.py").write_text(payload, encoding="utf-8")
        (metadata / "chatgpt-adapter.json").write_text(
            json.dumps(
                {
                    "portable_source_path": "skills/example",
                    "adapter_path": "adapters/chatgpt/example",
                }
            ),
            encoding="utf-8",
        )
        projected = build_index(projection_root)
        if any(item["code"] == "PI001" for item in projected["findings"]):
            raise AssertionError("declared generated projection was treated as a competing owner")
        if len(projected["inventory"]["duplicates"]["justified_generated_projections"]) != 1:
            raise AssertionError("generated projection exception was not recorded")
        (projection_root / "rogue.py").write_text(payload, encoding="utf-8")
        rogue = build_index(projection_root)
        if not any(item["code"] == "PI001" for item in rogue["findings"]):
            raise AssertionError("undeclared third duplicate did not fail closed")
    print(json.dumps({"passed": True, "index_version": INDEX_VERSION, "checks": ["inventory", "ui-proliferation", "freshness", "generated-projection-boundary"]}, indent=2))
    return 0


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(description="Generate one project directory, function, component, and reuse index")
    commands = root.add_subparsers(dest="command", required=True)
    refresh = commands.add_parser("refresh")
    refresh.add_argument("--root", default=".")
    refresh.add_argument("--output")
    refresh.add_argument("--json", action="store_true")
    refresh.set_defaults(func=command_refresh)
    check = commands.add_parser("doctor")
    check.add_argument("--root", default=".")
    check.add_argument("--output")
    check.add_argument("--strict", action="store_true")
    check.add_argument("--json", action="store_true")
    check.set_defaults(func=command_doctor)
    self_test = commands.add_parser("self-test")
    self_test.set_defaults(func=command_self_test)
    return root


def main() -> int:
    args = parser().parse_args()
    try:
        return args.func(args)
    except (OSError, ValueError, subprocess.SubprocessError) as exc:
        print(str(exc), file=sys.stderr)
        return 2


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: a570e648d1b7cf5aeaad57037c600cbcc65599f7e8633110f21a2eb658c94b63