diff --git a/plugins/agent-plugin-skills/.codex-plugin/plugin.json b/plugins/agent-plugin-skills/.codex-plugin/plugin.json index 649cb0c1..a7ff786d 100644 --- a/plugins/agent-plugin-skills/.codex-plugin/plugin.json +++ b/plugins/agent-plugin-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-plugin-skills", - "version": "6.5.1", + "version": "6.6.0", "description": "Installable maintainer skills for skills-export repositories.", "author": { "name": "Gale", diff --git a/plugins/agent-plugin-skills/pyproject.toml b/plugins/agent-plugin-skills/pyproject.toml index 252813ba..ba11d60a 100644 --- a/plugins/agent-plugin-skills/pyproject.toml +++ b/plugins/agent-plugin-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-plugin-skills-maintenance" -version = "6.5.1" +version = "6.6.0" description = "Maintainer-only Python tooling baseline for agent-plugin-skills." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/agent-plugin-skills/uv.lock b/plugins/agent-plugin-skills/uv.lock index 50ae2de4..6ab57c43 100644 --- a/plugins/agent-plugin-skills/uv.lock +++ b/plugins/agent-plugin-skills/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "agent-plugin-skills-maintenance" -version = "6.5.1" +version = "6.6.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/apple-dev-skills/.codex-plugin/plugin.json b/plugins/apple-dev-skills/.codex-plugin/plugin.json index 3463610a..e7e0982b 100644 --- a/plugins/apple-dev-skills/.codex-plugin/plugin.json +++ b/plugins/apple-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "apple-dev-skills", - "version": "6.5.1", + "version": "6.6.0", "description": "Apple development workflows for Codex and Claude Code, including SwiftUI architecture and DocC authoring guidance.", "author": { "name": "Gale", diff --git a/plugins/apple-dev-skills/pyproject.toml b/plugins/apple-dev-skills/pyproject.toml index 53cea8b0..d6cd975d 100644 --- a/plugins/apple-dev-skills/pyproject.toml +++ b/plugins/apple-dev-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "apple-dev-skills-maintainer" -version = "6.5.1" +version = "6.6.0" description = "Maintainer tooling for the apple-dev-skills repository" requires-python = ">=3.9" dependencies = [] diff --git a/plugins/apple-dev-skills/uv.lock b/plugins/apple-dev-skills/uv.lock index 0677ecb0..1b9d8ae7 100644 --- a/plugins/apple-dev-skills/uv.lock +++ b/plugins/apple-dev-skills/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "apple-dev-skills-maintainer" -version = "6.5.1" +version = "6.6.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/cardhop-app/.codex-plugin/plugin.json b/plugins/cardhop-app/.codex-plugin/plugin.json index a0220f77..c9810359 100644 --- a/plugins/cardhop-app/.codex-plugin/plugin.json +++ b/plugins/cardhop-app/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cardhop-app", - "version": "6.5.1", + "version": "6.6.0", "description": "Cardhop.app workflow guidance plus a bundled local MCP server for contact capture and updates on macOS.", "author": { "name": "Gale", diff --git a/plugins/cardhop-app/mcp/pyproject.toml b/plugins/cardhop-app/mcp/pyproject.toml index 9a6de855..4e7484d4 100644 --- a/plugins/cardhop-app/mcp/pyproject.toml +++ b/plugins/cardhop-app/mcp/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cardhop-app-mcp" -version = "6.5.1" +version = "6.6.0" requires-python = ">=3.13" dependencies = [ "fastmcp>=3.0.2", diff --git a/plugins/cardhop-app/mcp/uv.lock b/plugins/cardhop-app/mcp/uv.lock index 04c4331a..a2d7c6b7 100644 --- a/plugins/cardhop-app/mcp/uv.lock +++ b/plugins/cardhop-app/mcp/uv.lock @@ -93,7 +93,7 @@ wheels = [ [[package]] name = "cardhop-app-mcp" -version = "6.5.1" +version = "6.6.0" source = { virtual = "." } dependencies = [ { name = "fastmcp" }, diff --git a/plugins/dotnet-skills/.codex-plugin/plugin.json b/plugins/dotnet-skills/.codex-plugin/plugin.json index 5c3198cc..e65c7de1 100644 --- a/plugins/dotnet-skills/.codex-plugin/plugin.json +++ b/plugins/dotnet-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dotnet-skills", - "version": "6.5.1", + "version": "6.6.0", "description": "Standalone plugin repository for future .NET-focused Codex skills.", "author": { "name": "Gale", diff --git a/plugins/productivity-skills/.codex-plugin/plugin.json b/plugins/productivity-skills/.codex-plugin/plugin.json index 5f17c74f..c191cf5f 100644 --- a/plugins/productivity-skills/.codex-plugin/plugin.json +++ b/plugins/productivity-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "productivity-skills", - "version": "6.5.1", + "version": "6.6.0", "description": "Broadly useful productivity workflows for Codex and Claude Code.", "author": { "name": "Gale", diff --git a/plugins/productivity-skills/README.md b/plugins/productivity-skills/README.md index baad2db3..fa140c00 100644 --- a/plugins/productivity-skills/README.md +++ b/plugins/productivity-skills/README.md @@ -43,7 +43,7 @@ uv sync --dev Use this repository when the work is about: - explaining code paths in plain language -- maintaining README, AGENTS, CONTRIBUTING, ACCESSIBILITY, or ROADMAP docs +- maintaining README, AGENTS, CONTRIBUTING, ACCESSIBILITY, ARCHITECTURE, SLICES, or ROADMAP docs - keeping a general-purpose repository-maintenance baseline aligned - describing when Codex subagents are useful for bounded docs pulling, repo scans, triage, or summarization before the main workflow edits or reports - describing when OpenAI Codex Hooks belong in repo-local agent guidance or maintainer-tooling docs @@ -85,6 +85,7 @@ See [LICENSE](./LICENSE). - `maintain-project-agents` - `maintain-project-accessibility` - `maintain-project-api` +- `maintain-project-architecture` - `maintain-project-contributing` - `maintain-project-readme` - `maintain-project-repo` diff --git a/plugins/productivity-skills/ROADMAP.md b/plugins/productivity-skills/ROADMAP.md index 75578c5b..66bfdf0e 100644 --- a/plugins/productivity-skills/ROADMAP.md +++ b/plugins/productivity-skills/ROADMAP.md @@ -165,6 +165,7 @@ Planned ## History +- Added `maintain-project-architecture` as the baseline `docs/architecture/ARCHITECTURE.md`, `SLICES.md`, and `architecture.json` maintenance skill, with first-pass SwiftPM product/target detection and stale model checks. - Added first-pass OpenAI Codex Hooks guidance for AGENTS and repo-maintenance workflows, plus a planned `maintain-project-hooks` baseline skill for future deterministic hook audits. - Added maintainer guidance for optional Codex subagent use in documentation, maintenance, and explanation skills, keeping delegation explicitly user-triggered and read-heavy by default. - Fixed `maintain-project-repo` standard release handling so new release PRs wait for initial GitHub checks before watching CI, approval-only reviews no longer count as unresolved review comments, and release tags are created only after CI and the review-comment gate pass. diff --git a/plugins/productivity-skills/pyproject.toml b/plugins/productivity-skills/pyproject.toml index 2643afa0..b3364d40 100644 --- a/plugins/productivity-skills/pyproject.toml +++ b/plugins/productivity-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "productivity-skills-maintenance" -version = "6.5.1" +version = "6.6.0" description = "Maintainer-only Python tooling baseline for productivity-skills." requires-python = ">=3.11" dependencies = [] @@ -16,6 +16,7 @@ testpaths = [ "skills/maintain-project-agents/tests", "skills/maintain-project-accessibility/tests", "skills/maintain-project-api/tests", + "skills/maintain-project-architecture/tests", "skills/maintain-project-readme/tests", "skills/maintain-project-contributing/tests", "skills/maintain-project-roadmap/tests", diff --git a/plugins/productivity-skills/skills/explain-code-slice/SKILL.md b/plugins/productivity-skills/skills/explain-code-slice/SKILL.md index 0a108547..6ea3b107 100644 --- a/plugins/productivity-skills/skills/explain-code-slice/SKILL.md +++ b/plugins/productivity-skills/skills/explain-code-slice/SKILL.md @@ -10,6 +10,7 @@ Use this skill when the user wants one bounded walkthrough of how part of a syst ## Purpose - Explain one slice end to end without dropping meaningful steps. +- When the user wants the slice recorded durably, update `docs/architecture/SLICES.md` after the explanation. - Start with the incoming data shape, what it represents, who sends it, and why it enters the flow. - Walk the execution path in order, including boundaries, branch points, shared versus specialized steps, and data transformations. - End with the final output shape, who receives it, and what purpose it serves. @@ -115,12 +116,31 @@ Return a structured narrative in this order: The writing should stay conversational and narrative-first. Avoid sterile dumps, but do not skip steps for brevity. +## Persistent Slice Records + +Use this when the user asks to save, record, maintain, update, or add a slice to repository architecture docs. + +1. Explain the slice normally first. +2. Ensure `docs/architecture/SLICES.md` exists. If it is missing, use the `maintain-project-architecture` structure: title, summary, slice index, and slices sections. +3. Add or refresh one `## Slice: ` section. +4. Include: + - `### Trigger` + - `### Data Shapes` + - `### Step Trace` + - `### Boundaries` + - `### Outputs` + - `### Evidence` +5. Every recorded step must include a file path and symbol when known. Include line numbers when available. +6. Do not record a slice that cannot be proven from code. Say what evidence is missing instead. +7. Do not use Mermaid or generic graph diagrams as the persistent slice format. + ## Guardrails - Never silently collapse or omit meaningful steps in the requested slice. - Do not replace the end-to-end walkthrough with only a component map or only a high-level summary. - If the path is ambiguous, say where the ambiguity starts and explain the most likely path plus the alternate branch. - If a step cannot be proven from the code, say that plainly instead of guessing. +- Do not persist unproven slice claims into `SLICES.md`. - Prefer concrete file/function references when available. - Keep branch and data-shape notes short and move clutter out of the main narrative when a marker note will do. diff --git a/plugins/productivity-skills/skills/explain-code-slice/agents/openai.yaml b/plugins/productivity-skills/skills/explain-code-slice/agents/openai.yaml index 7028008d..b00fcf9d 100644 --- a/plugins/productivity-skills/skills/explain-code-slice/agents/openai.yaml +++ b/plugins/productivity-skills/skills/explain-code-slice/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Explain Code Slice" short_description: "Explain one code path or slice step by step." - default_prompt: "Use $explain-code-slice to explain or compare a code path from trigger to final output. Start with incoming data shape and purpose, then walk every meaningful step in order, including branches, boundaries, transformations, and a simple diagram with notes." + default_prompt: "Use $explain-code-slice to explain or compare a code path from trigger to final output. Start with incoming data shape and purpose, then walk every meaningful step in order, including branches, boundaries, transformations, and a compact step view with notes. When the user asks to save or record the slice, update docs/architecture/SLICES.md with a provable slice record instead of using generic diagrams." diff --git a/plugins/productivity-skills/skills/maintain-project-architecture/SKILL.md b/plugins/productivity-skills/skills/maintain-project-architecture/SKILL.md new file mode 100644 index 00000000..b9835e5a --- /dev/null +++ b/plugins/productivity-skills/skills/maintain-project-architecture/SKILL.md @@ -0,0 +1,96 @@ +--- +name: maintain-project-architecture +description: Maintain docs/architecture/ARCHITECTURE.md, SLICES.md, and architecture.json for a repository's product/module architecture and provable code slices. Use when a repo needs architecture docs that explain Swift products, modules, construction, ownership, evidence, stale claims, and slice inventory without generic diagrams or ungrounded architecture claims. +--- + +# Maintain Project Architecture + +Maintain the repo-local architecture documentation under `docs/architecture/`. + +This skill is related to `explain-code-slice`, but it owns the durable architecture files. `explain-code-slice` explains or records one end-to-end path. This skill keeps the repo-wide product/module map and the slice index coherent. + +## Inputs + +- Required: `--project-root ` +- Required: `--run-mode ` +- Optional: `--architecture-dir ` + +## Workflow + +1. Resolve the project root and architecture directory. +2. Detect Swift package products and targets when `Package.swift` exists: + - prefer `swift package dump-package` when it succeeds + - fall back to conservative `Package.swift` text scanning +3. Build or refresh `docs/architecture/architecture.json` with product, target, evidence, and slice placeholders. +4. In `check-only`, audit required files, required sections, and stale product or target facts in `architecture.json`. +5. In `apply`, create missing architecture files and refresh generated model facts without inventing symbols, data flows, or ownership claims. +6. Leave `SLICES.md` present even when no provable slices have been recorded yet. +7. Re-run the same audit and report remaining findings. + +## Required Files + +- `docs/architecture/ARCHITECTURE.md` +- `docs/architecture/SLICES.md` +- `docs/architecture/architecture.json` + +## Writing Expectations + +- `ARCHITECTURE.md` is descriptive. Put preferences, constraints, and "do not" rules in `AGENTS.md`, not here. +- Use fixed section names so Gale can ask for a specific section without ambiguity. +- Treat Swift Package Manager products and targets as first-class architecture facts when available. +- Explain products/modules in terms of what they do, who creates or consumes them, what they own, and what code evidence proves that claim. +- Do not use Mermaid, generic graph diagrams, unlabeled arrows, curved-line diagrams, centered text, or diagram labels that interrupt connector lines. +- Keep visual claims in structured `architecture.json` until a purpose-built viewer can render them with Gale-readable layout. +- Every generated claim should have evidence: a file path, manifest entry, symbol, command output, or explicit "unverified" marker. + +## Visual Grammar + +Use `references/visual-grammar.md` when shaping any generated visual model or future viewer output. + +Core rules: + +- vertical flow dominates +- left/top start position, never center-first reading +- no center-aligned text +- no unlabeled or ambiguous connectors +- connectors must represent one explicit relationship kind such as `creates`, `passes`, `stores`, `calls`, `returns`, `owns`, or `depends-on` +- data models appear before slice steps +- code nodes must include symbol names and file anchors when known + +## Slices + +`SLICES.md` is always created. It may remain mostly empty until provable flows are discovered. + +When the user asks for a new slice explanation, use `explain-code-slice` for the walkthrough and update `docs/architecture/SLICES.md` with the slice if the path is provable from code. + +## Codex Subagent Fit + +When the user explicitly asks for subagents or parallel agent work, use subagents only for read-heavy discovery. Good jobs include scanning package manifests, listing products and targets, finding entrypoints, or tracing one candidate slice. Keep writes to `ARCHITECTURE.md`, `SLICES.md`, and `architecture.json` in the main thread so the architecture story stays coherent. + +## Output Contract + +- Return Markdown plus JSON with: + - `run_context` + - `detected_model` + - `schema_violations` + - `stale_claims` + - `fixes_applied` + - `post_fix_status` + - `errors` +- If there are no issues and no errors, output exactly `No findings.` + +## Guardrails + +- Never auto-commit, auto-push, or open a PR. +- Never invent products, modules, slices, symbols, data models, or code relationships. +- Never edit files outside the architecture directory. +- Never use generic architecture diagrams as filler. +- Treat stale product or target facts as audit failures. + +## References + +- `assets/ARCHITECTURE.template.md` +- `assets/SLICES.template.md` +- `references/visual-grammar.md` +- `references/architecture-json.md` +- `scripts/maintain_project_architecture.py` diff --git a/plugins/productivity-skills/skills/maintain-project-architecture/agents/openai.yaml b/plugins/productivity-skills/skills/maintain-project-architecture/agents/openai.yaml new file mode 100644 index 00000000..7dfeea68 --- /dev/null +++ b/plugins/productivity-skills/skills/maintain-project-architecture/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Maintain Project Architecture" + short_description: "Maintain architecture docs and slice inventory from repo evidence." + default_prompt: "Use $maintain-project-architecture to create or refresh docs/architecture/ARCHITECTURE.md, SLICES.md, and architecture.json. Detect Swift products and targets from repo evidence, keep architecture descriptive rather than prescriptive, reject stale claims, avoid generic diagrams, and only record products, modules, symbols, data flows, or slices that are provable from code." diff --git a/plugins/productivity-skills/skills/maintain-project-architecture/assets/ARCHITECTURE.template.md b/plugins/productivity-skills/skills/maintain-project-architecture/assets/ARCHITECTURE.template.md new file mode 100644 index 00000000..1f339848 --- /dev/null +++ b/plugins/productivity-skills/skills/maintain-project-architecture/assets/ARCHITECTURE.template.md @@ -0,0 +1,43 @@ +# Architecture + +## Summary + +This document explains the repository's product and module architecture from code evidence. Add repo-specific notes here when the layout has unusual constraints or naming. + +See [SLICES.md](./SLICES.md) for provable end-to-end code paths. + +## Product Map + + + +No products have been recorded yet. + + + +## Module Architecture + + + +No modules have been recorded yet. + + + +## Construction And Ownership + +Document who creates the important runtime objects, what inputs they receive, where those inputs come from, and which module owns the responsibility. Leave unverified relationships out until they can be proven from code. + +## Visual Model + +The structured visual model lives in [architecture.json](./architecture.json). It is intended for a purpose-built viewer rather than generic Markdown diagrams. + +## Architecture Evidence + + + +- No architecture evidence has been recorded yet. + + + +## Staleness Checks + +Refresh this document when products, targets, module boundaries, important construction paths, or recorded slices change. diff --git a/plugins/productivity-skills/skills/maintain-project-architecture/assets/SLICES.template.md b/plugins/productivity-skills/skills/maintain-project-architecture/assets/SLICES.template.md new file mode 100644 index 00000000..2f86de44 --- /dev/null +++ b/plugins/productivity-skills/skills/maintain-project-architecture/assets/SLICES.template.md @@ -0,0 +1,13 @@ +# Slices + +## Summary + +This file records provable end-to-end code slices for this repository. A slice should name the trigger, data shapes, code steps, boundaries, outputs, and evidence. + +## Slice Index + +No provable slices have been recorded yet. + +## Slices + +Add new slices here when they have code evidence. diff --git a/plugins/productivity-skills/skills/maintain-project-architecture/references/architecture-json.md b/plugins/productivity-skills/skills/maintain-project-architecture/references/architecture-json.md new file mode 100644 index 00000000..fde7b743 --- /dev/null +++ b/plugins/productivity-skills/skills/maintain-project-architecture/references/architecture-json.md @@ -0,0 +1,29 @@ +# Architecture JSON + +`architecture.json` is the structured source for future architecture viewers. + +## Top-level Shape + +```json +{ + "schemaVersion": 1, + "generatedBy": "maintain-project-architecture", + "projectRoot": "/path/to/repo", + "detectedAt": "2026-05-03T00:00:00Z", + "products": [], + "targets": [], + "relationships": [], + "slices": [], + "evidence": [] +} +``` + +## Records + +Product records include `name`, `kind`, `targets`, and `evidence`. + +Target records include `name`, `kind`, `dependencies`, `path`, and `evidence`. + +Relationship records include `kind`, `from`, `to`, `label`, and `evidence`. + +Do not create relationship records without evidence. diff --git a/plugins/productivity-skills/skills/maintain-project-architecture/references/visual-grammar.md b/plugins/productivity-skills/skills/maintain-project-architecture/references/visual-grammar.md new file mode 100644 index 00000000..ecbef833 --- /dev/null +++ b/plugins/productivity-skills/skills/maintain-project-architecture/references/visual-grammar.md @@ -0,0 +1,40 @@ +# Visual Grammar + +The architecture viewer should use a custom visual language, not Mermaid as the primary surface. + +## Hard Rules + +- No center-aligned text. +- No large decorative whitespace. +- No curved-line diagrams. +- No labels that interrupt connector lines. +- No unlabeled arrows. +- No ambiguous arrows whose direction does not say what relationship is represented. +- No generic boxes such as "Runtime" or "API" unless those are real repo symbols, products, targets, or filenames. + +## Preferred Layout + +- Use a clear top-to-bottom reading path. +- Use left/top as the starting point. +- Put data models at the top of slice views. +- Use stacked dense panels for slice steps. +- Use familiar icons or symbols to distinguish products, modules, types, functions, properties, storage, external systems, and generated artifacts. +- Use color for category and relationship meaning, not decoration. + +## Relationship Kinds + +Use explicit relationship kinds in structured data: + +- `creates` +- `initializes` +- `passes` +- `stores` +- `reads` +- `writes` +- `calls` +- `returns` +- `owns` +- `depends-on` +- `exposes` + +Every relationship should carry a source anchor, target anchor, and evidence when possible. diff --git a/plugins/productivity-skills/skills/maintain-project-architecture/scripts/maintain_project_architecture.py b/plugins/productivity-skills/skills/maintain-project-architecture/scripts/maintain_project_architecture.py new file mode 100644 index 00000000..1cdf590d --- /dev/null +++ b/plugins/productivity-skills/skills/maintain-project-architecture/scripts/maintain_project_architecture.py @@ -0,0 +1,380 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# /// +"""Maintain docs/architecture files from repo evidence.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_ARCHITECTURE_SECTIONS = [ + "Summary", + "Product Map", + "Module Architecture", + "Construction And Ownership", + "Visual Model", + "Architecture Evidence", + "Staleness Checks", +] +REQUIRED_SLICES_SECTIONS = ["Summary", "Slice Index", "Slices"] + + +@dataclass +class Issue: + issue_id: str + category: str + severity: str + file: str + evidence: str + recommended_fix: str + auto_fixable: bool + fixed: bool = False + + def to_dict(self) -> dict[str, Any]: + return { + "issue_id": self.issue_id, + "category": self.category, + "severity": self.severity, + "file": self.file, + "evidence": self.evidence, + "recommended_fix": self.recommended_fix, + "auto_fixable": self.auto_fixable, + "fixed": self.fixed, + } + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--project-root", required=True) + parser.add_argument("--run-mode", required=True, choices=["check-only", "apply"]) + parser.add_argument("--architecture-dir") + parser.add_argument("--json-out") + parser.add_argument("--print-json", action="store_true") + return parser.parse_args() + + +def skill_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def write_text(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text.rstrip() + "\n", encoding="utf-8") + + +def render_template(name: str) -> str: + return read_text(skill_root() / "assets" / name) + + +def slugify(text: str) -> str: + return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") + + +def h2_headings(markdown: str) -> list[str]: + return [match.group(1).strip() for match in re.finditer(r"^##\s+(.+?)\s*$", markdown, re.MULTILINE)] + + +def run_dump_package(project_root: Path) -> dict[str, Any] | None: + if not (project_root / "Package.swift").is_file(): + return None + proc = subprocess.run( + ["swift", "package", "dump-package"], + cwd=project_root, + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0 or not proc.stdout.strip(): + return None + try: + data = json.loads(proc.stdout) + except json.JSONDecodeError: + return None + return data if isinstance(data, dict) else None + + +def fallback_parse_package(project_root: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + manifest = project_root / "Package.swift" + if not manifest.is_file(): + return [], [] + text = read_text(manifest) + products = [ + { + "name": match.group(2), + "kind": match.group(1), + "targets": [], + "evidence": [{"kind": "manifest-regex", "path": "Package.swift"}], + } + for match in re.finditer(r"\.(library|executable)\s*\(\s*name:\s*\"([^\"]+)\"", text) + ] + targets = [] + for match in re.finditer(r"\.(target|executableTarget|testTarget)\s*\(\s*name:\s*\"([^\"]+)\"", text): + name = match.group(2) + targets.append( + { + "name": name, + "kind": match.group(1), + "dependencies": [], + "path": f"Sources/{name}" if (project_root / "Sources" / name).exists() else None, + "evidence": [{"kind": "manifest-regex", "path": "Package.swift"}], + } + ) + return products, targets + + +def normalize_dump_package(project_root: Path, data: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + products = [] + for product in data.get("products", []): + if not isinstance(product, dict): + continue + product_type = product.get("type") + kind = product_type.get("executable") if isinstance(product_type, dict) and "executable" in product_type else product_type + if isinstance(kind, dict): + kind = next(iter(kind.keys()), "library") + products.append( + { + "name": product.get("name"), + "kind": kind or "product", + "targets": product.get("targets", []), + "evidence": [{"kind": "swift-package-dump", "path": "Package.swift"}], + } + ) + + targets = [] + for target in data.get("targets", []): + if not isinstance(target, dict): + continue + name = target.get("name") + path = target.get("path") + if not path and isinstance(name, str) and (project_root / "Sources" / name).exists(): + path = f"Sources/{name}" + dependencies = [] + for dependency in target.get("dependencies", []): + if isinstance(dependency, str): + dependencies.append(dependency) + elif isinstance(dependency, dict): + dependencies.extend(str(value) for value in dependency.values() if isinstance(value, str)) + targets.append( + { + "name": name, + "kind": target.get("type", "target"), + "dependencies": dependencies, + "path": path, + "evidence": [{"kind": "swift-package-dump", "path": "Package.swift"}], + } + ) + return products, targets + + +def detect_model(project_root: Path) -> dict[str, Any]: + package_data = run_dump_package(project_root) + if package_data: + products, targets = normalize_dump_package(project_root, package_data) + source = "swift-package-dump" + else: + products, targets = fallback_parse_package(project_root) + source = "package-swift-regex" if (project_root / "Package.swift").is_file() else "filesystem" + + relationships = [] + for target in targets: + for dependency in target.get("dependencies", []): + relationships.append( + { + "kind": "depends-on", + "from": f"target:{target['name']}", + "to": f"target-or-product:{dependency}", + "label": "declared target dependency", + "evidence": target.get("evidence", []), + } + ) + + return { + "schemaVersion": 1, + "generatedBy": "maintain-project-architecture", + "projectRoot": str(project_root), + "detectedAt": datetime.now(timezone.utc).isoformat(), + "detectionSource": source, + "products": [item for item in products if item.get("name")], + "targets": [item for item in targets if item.get("name")], + "relationships": relationships, + "slices": [], + "evidence": [{"kind": source, "path": "Package.swift"}] if (project_root / "Package.swift").is_file() else [], + } + + +def product_inventory(model: dict[str, Any]) -> str: + products = model.get("products", []) + if not products: + return "No products have been recorded yet." + lines = [] + for product in products: + targets = ", ".join(product.get("targets", [])) or "no targets recorded" + kind = product.get("kind") or "product" + lines.append(f"- `{product['name']}` ({kind}) uses targets: {targets}.") + return "\n".join(lines) + + +def target_inventory(model: dict[str, Any]) -> str: + targets = model.get("targets", []) + if not targets: + return "No modules have been recorded yet." + lines = [] + for target in targets: + dependencies = ", ".join(target.get("dependencies", [])) or "no declared dependencies" + path = target.get("path") or "path not recorded" + lines.append(f"- `{target['name']}` at `{path}` depends on: {dependencies}.") + return "\n".join(lines) + + +def evidence_inventory(model: dict[str, Any]) -> str: + source = model.get("detectionSource", "unknown") + if not model.get("evidence"): + return "- No architecture evidence has been recorded yet." + return f"- Product and target inventory detected with `{source}` from `Package.swift`." + + +def replace_generated_block(text: str, start: str, end: str, body: str) -> str: + pattern = re.compile(re.escape(start) + r".*?" + re.escape(end), re.DOTALL) + replacement = f"{start}\n\n{body.strip()}\n\n{end}" + if pattern.search(text): + return pattern.sub(replacement, text) + return text.rstrip() + f"\n\n{replacement}\n" + + +def render_architecture(model: dict[str, Any]) -> str: + text = render_template("ARCHITECTURE.template.md") + text = replace_generated_block(text, "", "", product_inventory(model)) + text = replace_generated_block(text, "", "", target_inventory(model)) + text = replace_generated_block(text, "", "", evidence_inventory(model)) + return text + + +def load_json(path: Path) -> dict[str, Any] | None: + if not path.is_file(): + return None + try: + data = json.loads(read_text(path)) + except json.JSONDecodeError: + return None + return data if isinstance(data, dict) else None + + +def names(items: list[dict[str, Any]]) -> set[str]: + return {str(item.get("name")) for item in items if item.get("name")} + + +def audit(architecture_dir: Path, model: dict[str, Any]) -> tuple[list[Issue], list[Issue], list[str]]: + architecture_path = architecture_dir / "ARCHITECTURE.md" + slices_path = architecture_dir / "SLICES.md" + json_path = architecture_dir / "architecture.json" + schema_violations: list[Issue] = [] + stale_claims: list[Issue] = [] + errors: list[str] = [] + + for path, issue_id in [(architecture_path, "missing-architecture-md"), (slices_path, "missing-slices-md"), (json_path, "missing-architecture-json")]: + if not path.is_file(): + schema_violations.append(Issue(issue_id, "schema", "high", str(path), f"{path.name} is missing.", f"Create {path.name} from the architecture template.", True)) + + if architecture_path.is_file(): + headings = set(h2_headings(read_text(architecture_path))) + for section in REQUIRED_ARCHITECTURE_SECTIONS: + if section not in headings: + schema_violations.append(Issue(f"missing-architecture-section-{slugify(section)}", "schema", "high", str(architecture_path), f"ARCHITECTURE.md is missing '## {section}'.", f"Add '## {section}'.", True)) + + if slices_path.is_file(): + headings = set(h2_headings(read_text(slices_path))) + for section in REQUIRED_SLICES_SECTIONS: + if section not in headings: + schema_violations.append(Issue(f"missing-slices-section-{slugify(section)}", "schema", "high", str(slices_path), f"SLICES.md is missing '## {section}'.", f"Add '## {section}'.", True)) + + existing_model = load_json(json_path) + if json_path.is_file() and existing_model is None: + errors.append(f"{json_path} is not valid JSON.") + if existing_model: + for kind in ["products", "targets"]: + existing_names = names(existing_model.get(kind, [])) + current_names = names(model.get(kind, [])) + stale = existing_names - current_names + missing = current_names - existing_names + if stale: + stale_claims.append(Issue(f"stale-{kind}", "stale-claim", "high", str(json_path), f"architecture.json records stale {kind}: {', '.join(sorted(stale))}.", "Refresh architecture.json from current repo evidence.", True)) + if missing: + stale_claims.append(Issue(f"missing-current-{kind}", "stale-claim", "medium", str(json_path), f"architecture.json is missing current {kind}: {', '.join(sorted(missing))}.", "Refresh architecture.json from current repo evidence.", True)) + + return schema_violations, stale_claims, errors + + +def apply_fixes(architecture_dir: Path, model: dict[str, Any]) -> list[dict[str, str]]: + fixes = [] + architecture_path = architecture_dir / "ARCHITECTURE.md" + slices_path = architecture_dir / "SLICES.md" + json_path = architecture_dir / "architecture.json" + write_text(architecture_path, render_architecture(model)) + fixes.append({"action": "refresh-architecture-md", "file": str(architecture_path)}) + if not slices_path.exists(): + write_text(slices_path, render_template("SLICES.template.md")) + fixes.append({"action": "create-slices-md", "file": str(slices_path)}) + existing_model = load_json(json_path) or {} + model["slices"] = existing_model.get("slices", []) + write_text(json_path, json.dumps(model, indent=2, sort_keys=True)) + fixes.append({"action": "refresh-architecture-json", "file": str(json_path)}) + return fixes + + +def run(args: argparse.Namespace) -> dict[str, Any]: + project_root = Path(args.project_root).expanduser().resolve() + architecture_dir = Path(args.architecture_dir).expanduser().resolve() if args.architecture_dir else project_root / "docs" / "architecture" + model = detect_model(project_root) + fixes: list[dict[str, str]] = [] + errors: list[str] = [] + if not project_root.is_dir(): + errors.append(f"Project root does not exist or is not a directory: {project_root}") + elif args.run_mode == "apply": + fixes = apply_fixes(architecture_dir, model) + schema_violations, stale_claims, audit_errors = audit(architecture_dir, model) + errors.extend(audit_errors) + post_fix_status = [] + if args.run_mode == "apply": + post_schema, post_stale, post_errors = audit(architecture_dir, model) + post_fix_status = [issue.to_dict() for issue in [*post_schema, *post_stale]] + errors.extend(post_errors) + return { + "run_context": {"project_root": str(project_root), "architecture_dir": str(architecture_dir), "run_mode": args.run_mode}, + "detected_model": model, + "schema_violations": [issue.to_dict() for issue in schema_violations], + "stale_claims": [issue.to_dict() for issue in stale_claims], + "fixes_applied": fixes, + "post_fix_status": post_fix_status, + "errors": errors, + } + + +def main() -> int: + args = parse_args() + report = run(args) + if args.json_out: + write_text(Path(args.json_out), json.dumps(report, indent=2, sort_keys=True)) + if args.print_json: + print(json.dumps(report, indent=2, sort_keys=True)) + elif not report["schema_violations"] and not report["stale_claims"] and not report["errors"]: + print("No findings.") + else: + print(json.dumps(report, indent=2, sort_keys=True)) + return 1 if report["errors"] else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/productivity-skills/skills/maintain-project-architecture/tests/test_maintain_project_architecture.py b/plugins/productivity-skills/skills/maintain-project-architecture/tests/test_maintain_project_architecture.py new file mode 100644 index 00000000..ea6ac11d --- /dev/null +++ b/plugins/productivity-skills/skills/maintain-project-architecture/tests/test_maintain_project_architecture.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import argparse +import importlib.util +import sys +from pathlib import Path + + +SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "maintain_project_architecture.py" +SPEC = importlib.util.spec_from_file_location("maintain_project_architecture", SCRIPT_PATH) +MODULE = importlib.util.module_from_spec(SPEC) +assert SPEC and SPEC.loader +sys.modules["maintain_project_architecture"] = MODULE +SPEC.loader.exec_module(MODULE) + + +def run(project_root: Path, run_mode: str = "check-only"): + args = argparse.Namespace(project_root=str(project_root), run_mode=run_mode, architecture_dir=None, json_out=None, print_json=False) + return MODULE.run(args) + + +def write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def package_manifest() -> str: + return """ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "Demo", + products: [ + .library(name: "DemoCore", targets: ["DemoCore"]), + .executable(name: "demo-tool", targets: ["DemoTool"]), + ], + targets: [ + .target(name: "DemoCore"), + .executableTarget(name: "DemoTool", dependencies: ["DemoCore"]), + ] +) +""".strip() + + +def test_check_only_reports_missing_architecture_files(tmp_path: Path) -> None: + write(tmp_path / "Package.swift", package_manifest()) + report = run(tmp_path) + issue_ids = {issue["issue_id"] for issue in report["schema_violations"]} + assert "missing-architecture-md" in issue_ids + assert "missing-slices-md" in issue_ids + assert "missing-architecture-json" in issue_ids + + +def test_apply_creates_architecture_files_and_detects_products(tmp_path: Path) -> None: + write(tmp_path / "Package.swift", package_manifest()) + report = run(tmp_path, run_mode="apply") + architecture_dir = tmp_path / "docs" / "architecture" + assert report["errors"] == [] + assert report["post_fix_status"] == [] + assert (architecture_dir / "ARCHITECTURE.md").is_file() + assert (architecture_dir / "SLICES.md").is_file() + assert (architecture_dir / "architecture.json").is_file() + architecture = (architecture_dir / "ARCHITECTURE.md").read_text(encoding="utf-8") + assert "## Product Map" in architecture + assert "`DemoCore`" in architecture + assert "`demo-tool`" in architecture + slices = (architecture_dir / "SLICES.md").read_text(encoding="utf-8") + assert "No provable slices have been recorded yet." in slices + + +def test_stale_architecture_json_is_reported(tmp_path: Path) -> None: + write(tmp_path / "Package.swift", package_manifest()) + run(tmp_path, run_mode="apply") + write(tmp_path / "docs" / "architecture" / "architecture.json", '{"schemaVersion":1,"products":[{"name":"OldProduct"}],"targets":[{"name":"OldTarget"}]}') + report = run(tmp_path) + issue_ids = {issue["issue_id"] for issue in report["stale_claims"]} + assert "stale-products" in issue_ids + assert "stale-targets" in issue_ids + + +def test_apply_preserves_existing_slices_in_architecture_json(tmp_path: Path) -> None: + write(tmp_path / "Package.swift", package_manifest()) + run(tmp_path, run_mode="apply") + write(tmp_path / "docs" / "architecture" / "architecture.json", '{"schemaVersion":1,"products":[],"targets":[],"slices":[{"name":"Launch"}]}') + run(tmp_path, run_mode="apply") + model = (tmp_path / "docs" / "architecture" / "architecture.json").read_text(encoding="utf-8") + assert '"name": "Launch"' in model diff --git a/plugins/productivity-skills/uv.lock b/plugins/productivity-skills/uv.lock index ab9df770..b75b8ecd 100644 --- a/plugins/productivity-skills/uv.lock +++ b/plugins/productivity-skills/uv.lock @@ -40,7 +40,7 @@ wheels = [ [[package]] name = "productivity-skills-maintenance" -version = "6.5.1" +version = "6.6.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/python-skills/.codex-plugin/plugin.json b/plugins/python-skills/.codex-plugin/plugin.json index e4f25d4b..71211045 100644 --- a/plugins/python-skills/.codex-plugin/plugin.json +++ b/plugins/python-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "python-skills", - "version": "6.5.1", + "version": "6.6.0", "description": "Bundled Python-focused Codex skills for uv bootstrapping, FastAPI and FastMCP scaffolding, FastAPI/FastMCP integration, and pytest workflows.", "author": { "name": "Gale", diff --git a/plugins/python-skills/pyproject.toml b/plugins/python-skills/pyproject.toml index 3aedc3e1..7fbbb2be 100644 --- a/plugins/python-skills/pyproject.toml +++ b/plugins/python-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-skills-maintainer" -version = "6.5.1" +version = "6.6.0" description = "Maintainer tooling for the python-skills repository" requires-python = ">=3.11" dependencies = [] diff --git a/plugins/python-skills/uv.lock b/plugins/python-skills/uv.lock index 40a63f8e..ca5434c5 100644 --- a/plugins/python-skills/uv.lock +++ b/plugins/python-skills/uv.lock @@ -206,7 +206,7 @@ wheels = [ [[package]] name = "python-skills-maintainer" -version = "6.5.1" +version = "6.6.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/rust-skills/.codex-plugin/plugin.json b/plugins/rust-skills/.codex-plugin/plugin.json index 6936128f..192a78a5 100644 --- a/plugins/rust-skills/.codex-plugin/plugin.json +++ b/plugins/rust-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rust-skills", - "version": "6.5.1", + "version": "6.6.0", "description": "Standalone plugin repository for future Rust-focused Codex skills.", "author": { "name": "Gale", diff --git a/plugins/spotify/.codex-plugin/plugin.json b/plugins/spotify/.codex-plugin/plugin.json index 5099d5d0..1361d7a7 100644 --- a/plugins/spotify/.codex-plugin/plugin.json +++ b/plugins/spotify/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "spotify", - "version": "6.5.1", + "version": "6.6.0", "description": "Placeholder plugin repository for future Spotify-focused Codex workflows.", "author": { "name": "Gale", diff --git a/plugins/swiftasb-skills/.codex-plugin/plugin.json b/plugins/swiftasb-skills/.codex-plugin/plugin.json index bb8a0295..b85ce35d 100644 --- a/plugins/swiftasb-skills/.codex-plugin/plugin.json +++ b/plugins/swiftasb-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "swiftasb-skills", - "version": "6.5.1", + "version": "6.6.0", "description": "Codex skills for explaining SwiftASB and building SwiftUI, AppKit, and Swift package integrations on top of it.", "author": { "name": "Gale", diff --git a/plugins/things-app/.codex-plugin/plugin.json b/plugins/things-app/.codex-plugin/plugin.json index 65706042..893a3d3e 100644 --- a/plugins/things-app/.codex-plugin/plugin.json +++ b/plugins/things-app/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "things-app", - "version": "6.5.1", + "version": "6.6.0", "description": "Things.app skills and a bundled local MCP server for reminders, planning digests, and structured task workflows.", "author": { "name": "Gale", diff --git a/plugins/things-app/mcp/pyproject.toml b/plugins/things-app/mcp/pyproject.toml index 9893737b..019b5fe0 100644 --- a/plugins/things-app/mcp/pyproject.toml +++ b/plugins/things-app/mcp/pyproject.toml @@ -7,7 +7,7 @@ packages = ["app"] [project] name = "things-mcp" -version = "6.5.1" +version = "6.6.0" requires-python = ">=3.13" dependencies = [ "fastmcp>=3.0.2", diff --git a/plugins/things-app/mcp/uv.lock b/plugins/things-app/mcp/uv.lock index 1a03c522..0045e030 100644 --- a/plugins/things-app/mcp/uv.lock +++ b/plugins/things-app/mcp/uv.lock @@ -1118,7 +1118,7 @@ wheels = [ [[package]] name = "things-mcp" -version = "6.5.1" +version = "6.6.0" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/plugins/things-app/pyproject.toml b/plugins/things-app/pyproject.toml index 983f47da..b12c9ea7 100644 --- a/plugins/things-app/pyproject.toml +++ b/plugins/things-app/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "things-app-maintenance" -version = "6.5.1" +version = "6.6.0" description = "Maintainer-only Python tooling baseline for things-app skills and plugin packaging." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/things-app/uv.lock b/plugins/things-app/uv.lock index cfbb77b3..182624eb 100644 --- a/plugins/things-app/uv.lock +++ b/plugins/things-app/uv.lock @@ -120,7 +120,7 @@ wheels = [ [[package]] name = "things-app-maintenance" -version = "6.5.1" +version = "6.6.0" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/web-dev-skills/.codex-plugin/plugin.json b/plugins/web-dev-skills/.codex-plugin/plugin.json index 48045021..75691da7 100644 --- a/plugins/web-dev-skills/.codex-plugin/plugin.json +++ b/plugins/web-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "web-dev-skills", - "version": "6.5.1", + "version": "6.6.0", "description": "Standalone plugin repository for future web-focused Codex skills.", "author": { "name": "Gale", diff --git a/pyproject.toml b/pyproject.toml index 27d492dc..2cac8d1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "socket-maintenance" -version = "6.5.1" +version = "6.6.0" description = "Root uv tooling baseline for the socket superproject." requires-python = ">=3.11" dependencies = [] diff --git a/uv.lock b/uv.lock index cec0a4e0..93e83ffb 100644 --- a/uv.lock +++ b/uv.lock @@ -286,7 +286,7 @@ wheels = [ [[package]] name = "socket-maintenance" -version = "6.5.1" +version = "6.6.0" source = { virtual = "." } [package.dev-dependencies]