{"id":50472415,"url":"https://github.com/semcod/swop","last_synced_at":"2026-06-01T11:03:25.883Z","repository":{"id":353099667,"uuid":"1217976232","full_name":"semcod/swop","owner":"semcod","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-13T15:10:37.000Z","size":4199,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-13T17:18:06.404Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/semcod.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-22T12:10:17.000Z","updated_at":"2026-05-13T15:30:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/semcod/swop","commit_stats":null,"previous_names":["semcod/swop"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/semcod/swop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/semcod%2Fswop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/semcod%2Fswop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/semcod%2Fswop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/semcod%2Fswop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/semcod","download_url":"https://codeload.github.com/semcod/swop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/semcod%2Fswop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33771630,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-01T11:03:25.805Z","updated_at":"2026-06-01T11:03:25.866Z","avatar_url":"https://github.com/semcod.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# swop\n\n**Bi-directional runtime reconciler and drift-aware state graph for full-stack systems.**\n\n[![Version](https://img.shields.io/badge/version-0.2.16-blue)](VERSION)\n[![Python](https://img.shields.io/badge/python-3.8+-blue)](pyproject.toml)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green)](LICENSE)\n\n\n## AI Cost Tracking\n\n![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.16-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)\n![AI Cost](https://img.shields.io/badge/AI%20Cost-$3.45-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-8.8h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)\n\n- 🤖 **LLM usage:** $3.4500 (23 commits)\n- 👤 **Human dev:** ~$877 (8.8h @ $100/h, 30min dedup)\n\nGenerated on 2026-04-26 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)\n\n---\n\nSwop is a Python toolkit for inspecting, reconciling, and maintaining the\narchitecture of full-stack CQRS projects. It scans Python source for\ncommands, queries, events, and handlers; generates deterministic manifests;\ndetects schema drift; and exports the runtime state graph to multiple formats.\n\n---\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [CQRS Decorators](#cqrs-decorators)\n- [CLI Reference](#cli-reference)\n- [Python API](#python-api)\n- [Configuration](#configuration)\n- [Manifest Generation](#manifest-generation)\n- [Watch Mode](#watch-mode)\n- [Drift Detection \u0026 Resolution](#drift-detection--resolution)\n- [Refactoring](#refactoring)\n- [Registry Generation](#registry-generation)\n- [Markpact Generation](#markpact-generation)\n- [Development](#development)\n- [License](#license)\n\n---\n\n## Installation\n\n```bash\npip install swop\n```\n\nDevelopment install:\n\n```bash\npip install -e \".[dev]\"\n```\n\nRequires Python 3.8+ and PyYAML.\n\n---\n\n## Quick Start\n\n### 1. Initialise a project\n\n```bash\nswop init\n```\n\nScaffolds `swop.yaml` and the `.swop/` state directory in the current folder.\n\n### 2. Annotate your domain code\n\n```python\nfrom dataclasses import dataclass\nfrom swop import command, handler\n\n@command(\"billing\")\n@dataclass\nclass IssueInvoice:\n    customer_id: int\n    amount: float\n\n@handler(IssueInvoice)\nclass IssueInvoiceHandler:\n    def handle(self, cmd: IssueInvoice) -\u003e int:\n        return cmd.customer_id\n```\n\n### 3. Scan and generate manifests\n\n```bash\nswop scan --format json\nswop gen manifests\n```\n\n### 4. Watch for changes\n\n```bash\nswop watch\n```\n\nRe-runs the scan and regenerates manifests automatically when any `.py` file\nchanges.\n\n---\n\n## CQRS Decorators\n\n`swop` provides lightweight, no-op decorators that register decorated classes\nin a module-global registry. They do not change runtime behaviour, so existing\ncode continues to work unchanged.\n\n| Decorator | Purpose | Example |\n|---|---|---|\n| `@command(context)` | Register a command | `@command(\"billing\") @dataclass class IssueInvoice: ...` |\n| `@query(context)` | Register a query | `@query(\"catalog\") @dataclass class ListProducts: ...` |\n| `@event(context, emits=[...])` | Register a domain event | `@event(\"billing\", emits=[\"InvoiceIssued\"]) class PaymentReceived: ...` |\n| `@handler(Target)` | Register a command/query handler | `@handler(IssueInvoice) class IssueInvoiceHandler: ...` |\n\nAll decorators expose a `__swop_cqrs__` attribute on the decorated class with\nmetadata including `kind`, `context`, `source_file`, and `source_line`.\n\n---\n\n## CLI Reference\n\n```\nswop [--mode {STRICT,SOFT,OBSERVE,AUTO_HEAL}] \u003ccommand\u003e\n```\n\n| Command | Description |\n|---|---|\n| `swop init` | Scaffold `swop.yaml` and `.swop/` state dir |\n| `swop scan [--format {text,json,html}] [--json-out FILE] [--html-out FILE] [--strict-heuristics] [--strict-errors]` | Walk source roots and classify CQRS artifacts |\n| `swop gen manifests` | Generate per-context YAML manifests |\n| `swop gen proto [--out PATH]` | Generate `.proto` from manifests |\n| `swop gen grpc-python` | Compile Python gRPC bindings |\n| `swop gen grpc-ts` | Compile TypeScript gRPC bindings |\n| `swop gen services [--bus TYPE] [--base-image IMG] [--grpc-port N]` | Generate service stubs + docker-compose.cqrs.yml from manifests |\n| `swop watch [--once]` | Watch source files and rebuild on change |\n| `swop sync` | Run one reconciliation pass |\n| `swop diff` | Compute drift and exit non-zero if drift exists |\n| `swop state` | Dump current runtime state as YAML |\n| `swop inspect backend\\|frontend` | Introspect actual runtime state |\n| `swop resolve` | Diff current scan against stored manifests |\n| `swop gen registry [--contracts DIR] [--check]` | Generate `registry.json` + `REGISTRY.md` from `contracts/*.json` files |\n| `swop generate --from-markpact FILE.md [--sync] [--sync-files] [--check-files] [--output-yaml PATH] [--output-docker PATH]` | Build a ProjectGraph from a Markpact manifest |\n| `swop refactor --frontend PATH [--backend PATH] [--db PATH] [--route /path] [--strategy {seeded,louvain}] --out \u003cdir\u003e` | Extract modules into a new directory |\n| `swop doctor [--deep]` | Verify the local swop environment |\n| `swop hook install\\|uninstall\\|status` | Manage the git pre-commit hook |\n\n### Reconciliation modes\n\n| Mode | Behaviour |\n|---|---|\n| `STRICT` | Fail on any drift |\n| `SOFT` | Log drift, continue (default) |\n| `OBSERVE` | Read-only, never modify |\n| `AUTO_HEAL` | Apply detected fixes automatically |\n\n---\n\n## Python API\n\n### Scan a project\n\n```python\nfrom swop import scan_project, load_config\n\ncfg = load_config(\"swop.yaml\")\nreport = scan_project(cfg)\n\nfor det in report.detections:\n    print(f\"{det.kind:8} {det.name:20} ({det.confidence:.1f} via {det.via})\")\n```\n\n### Generate manifests\n\n```python\nfrom swop import generate_manifests\n\nmanifests = generate_manifests(report, cfg)\nfor mf in manifests.files:\n    print(mf.path)\n```\n\n### Watch programmatically\n\n```python\nfrom swop import WatchEngine, load_config\n\ncfg = load_config(\"swop.yaml\")\nengine = WatchEngine(config=cfg, interval=0.5, debounce=0.3)\n\n# Single-shot rebuild\nfrom swop import rebuild_once\nresult = rebuild_once(cfg, incremental=True)\nprint(result.format())\n```\n\n### Runtime graph\n\n```python\nfrom swop import SwopRuntime\n\nrt = SwopRuntime(mode=\"SOFT\")\nrt.add_model(\"Pressure\", [\"temp\", \"pressure_low\", \"pressure_high\"])\nrt.add_service(\"api\", [\"/pressure\", \"/status\"])\nrt.add_ui_binding(\"#sensor-temp\", \"temp\")\n\ndrift = rt.run_sync()\nprint(rt.state_yaml())\n```\n\n---\n\n## Configuration\n\n`swop.yaml` describes the project structure:\n\n```yaml\nversion: 1\nproject: my-service\nsource_roots: [src]\nexclude: [\"tests/*\", \"__pycache__/*\"]\nbounded_contexts:\n  - name: billing\n    source: src/billing\n  - name: catalog\n    source: src/catalog\n    external: false\nbus:\n  type: rabbitmq\n  url: amqp://localhost\nread_models:\n  engine: postgresql\n  url: postgresql://localhost/mydb\nstate_dir: .swop\n```\n\n| Key | Description |\n|---|---|\n| `source_roots` | Directories to scan (relative to project root) |\n| `bounded_contexts` | Named contexts with source paths |\n| `exclude` | Glob patterns to skip |\n| `bus` | Message-bus configuration |\n| `read_models` | Read-model store configuration |\n| `state_dir` | Local state / cache directory |\n\n---\n\n## Manifest Generation\n\nFor each bounded context swop generates three manifest files under\n`.swop/manifests/\u003ccontext\u003e/`:\n\n- `commands.yml` — all detected commands with fields\n- `queries.yml` — all detected queries with fields\n- `events.yml` — all detected events with fields\n\nExample output (`billing/commands.yml`):\n\n```yaml\nversion: 1\ncontext: billing\ncommands:\n  IssueInvoice:\n    module: billing.ops\n    fields:\n      - name: customer_id\n        type: int\n        required: true\n      - name: amount\n        type: float\n        required: true\n```\n\nThese manifests are the single source of truth for downstream generators\n(proto, gRPC, service stubs).\n\n---\n\n## Watch Mode\n\nThe watcher uses stdlib-only `mtime` polling — no extra dependencies.\n\n```bash\n# Continuous watch\nswop watch\n\n# One-shot (CI friendly)\nswop watch --once --no-incremental\n```\n\nThe watcher automatically:\n- Skips the state directory (`.swop/`) so regenerated manifests do not\n  re-trigger a rebuild.\n- Debounces bursts of changes into a single rebuild pass.\n- Tracks file creation, modification, and deletion.\n\n---\n\n## Drift Detection \u0026 Resolution\n\nSwop compares the *expected* state (from manifests) with the *actual* state\n(introspected from running backend/frontend) and reports drift:\n\n```bash\nswop diff\nswop resolve [--json] [--apply] [--strict] [--no-incremental]\n```\n\nDrift categories:\n- `schema` — field additions, removals, type changes\n- `missing` — expected artifacts not found in runtime\n- `unexpected` — runtime artifacts not in manifests\n\nUse `swop sync --mode AUTO_HEAL` to apply fixes automatically.\n\n---\n\n## Refactoring\n\nExtract modules from a full-stack project into a clean output directory:\n\n```bash\nswop refactor --out ./refactored\n```\n\nThe refactor pipeline clusters related code, builds a composed module graph,\nand generates new file layouts while preserving behaviour.\n\n---\n\n## Registry Generation\n\nGenerate a `registry.json` and `REGISTRY.md` from JSON contract files in a `contracts/` directory:\n\n```bash\nswop gen registry [--contracts DIR] [--check]\n```\n\n| Flag | Description |\n|---|---|\n| `--contracts DIR` | Contracts directory (default: `\u003croot\u003e/contracts`) |\n| `--check` | Validate only; do not write output files |\n\n---\n\n## Markpact Generation\n\nBuild a `SwopRuntime` graph directly from a Markpact manifest (`.md` file with `markpact:*` blocks):\n\n```bash\nswop generate --from-markpact manifest.md \\\n  [--strict] [--sync] [--sync-files] [--sync-files-dry-run] \\\n  [--check-files] [--from-disk] [--from-disk-dry-run] \\\n  [--output-yaml PATH] [--output-docker PATH]\n```\n\n| Flag | Description |\n|---|---|\n| `--from-markpact FILE` | Path to Markpact manifest (required) |\n| `--strict` | Fail fast on any DOQL parse error |\n| `--sync` | Run sync engine after building the graph |\n| `--sync-files` | Materialise `markpact:file` blocks to their declared paths |\n| `--sync-files-dry-run` | Report which files would be written without writing |\n| `--check-files` | Report drift between `markpact:file` blocks and filesystem |\n| `--from-disk` | Reverse sync: rewrite blocks with disk content |\n| `--from-disk-dry-run` | Report which blocks would be updated without writing |\n| `--output-yaml PATH` | Write runtime state YAML to this path |\n| `--output-docker PATH` | Write docker-compose YAML to this path |\n\n---\n\n## Development\n\n### Run tests\n\n```bash\npytest\n```\n\n160 tests, all passing.\n\n### Project structure\n\n```\nswop/\n├── cli.py              # CLI entry point\n├── commands.py         # Command implementations\n├── config.py           # swop.yaml loader\n├── core.py             # SwopRuntime orchestrator\n├── cqrs/               # @command, @query, @event, @handler decorators\n├── graph.py            # ProjectGraph, DataModel, Service\n├── introspect/         # Backend \u0026 frontend state introspection\n├── manifests/          # YAML manifest generator\n├── markpact/           # Manifest parsing and sync engine\n├── proto/              # Protobuf generation \u0026 compilation\n├── reconcile.py        # Drift detection \u0026 resync\n├── refactor/           # Code clustering \u0026 module extraction\n├── resolve.py          # Schema-evolution resolution\n├── scan/               # AST scanner for CQRS artifacts\n├── services/           # Service stub generator\n├── sync.py             # Sync engine\n├── tools/              # Project init, doctor, git hooks\n├── versioning.py       # Graph versioning\n└── watch/              # mtime-polling file watcher\n```\n\n---\n\n## License\n\nLicensed under Apache-2.0.\n## Status\n\n_Last updated by [taskill](https://github.com/oqlos/taskill) at 2026-04-25 13:39 UTC_\n\n| Metric | Value |\n|---|---|\n| HEAD | `f4f020e` |\n| Coverage | — |\n| Failing tests | — |\n| Commits in last cycle | 25 |\n\n\u003e Added registry validation features (directional subset checks and enum/Literal cross-checks), plus many documentation updates (markdown output, changelog generation) and test/configuration improvements for the test harness and CLI.\n\n\u003c!-- taskill:status:end --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsemcod%2Fswop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsemcod%2Fswop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsemcod%2Fswop/lists"}