svarupa 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- svarupa-0.1.0/.github/workflows/ci.yml +128 -0
- svarupa-0.1.0/.github/workflows/release.yml +38 -0
- svarupa-0.1.0/.github/workflows/svarupa.yml +61 -0
- svarupa-0.1.0/.gitignore +18 -0
- svarupa-0.1.0/.svarupa/architecture.lock +38 -0
- svarupa-0.1.0/LICENSE +21 -0
- svarupa-0.1.0/PKG-INFO +185 -0
- svarupa-0.1.0/README.md +152 -0
- svarupa-0.1.0/docs/images/demo-architecture.png +0 -0
- svarupa-0.1.0/docs/images/demo-deploy-topology.png +0 -0
- svarupa-0.1.0/docs/lockfile-grammar.md +248 -0
- svarupa-0.1.0/docs/research/2026-09-06-archify-teardown.md +79 -0
- svarupa-0.1.0/docs/research/2026-09-06-graphify-teardown.md +126 -0
- svarupa-0.1.0/docs/reviews/2026-08-30-p1-0-review.md +89 -0
- svarupa-0.1.0/docs/reviews/2026-08-30-spike-0-results.md +149 -0
- svarupa-0.1.0/docs/reviews/2026-08-30-spike-0-review.md +119 -0
- svarupa-0.1.0/docs/reviews/2026-08-30-spike-0b-call-resolution.md +139 -0
- svarupa-0.1.0/docs/reviews/2026-08-30-spike-0c-typescript.md +121 -0
- svarupa-0.1.0/docs/reviews/2026-08-31-p1-1-detect-review.md +79 -0
- svarupa-0.1.0/docs/reviews/2026-08-31-p1-2-wave1-review.md +74 -0
- svarupa-0.1.0/docs/reviews/2026-09-01-p1-2-wave2-review.md +76 -0
- svarupa-0.1.0/docs/reviews/2026-09-01-p1-3-p1-4-review.md +84 -0
- svarupa-0.1.0/docs/reviews/2026-09-02-p1-5-derive-review.md +101 -0
- svarupa-0.1.0/docs/reviews/2026-09-02-p1-6-wave1-layout-review.md +134 -0
- svarupa-0.1.0/docs/reviews/2026-09-03-p1-6-wave2-emit-review.md +133 -0
- svarupa-0.1.0/docs/reviews/2026-09-04-p1-7-lockfile-review.md +128 -0
- svarupa-0.1.0/docs/reviews/2026-09-04-replay-corpus.md +134 -0
- svarupa-0.1.0/docs/reviews/2026-09-05-wave11-review.md +122 -0
- svarupa-0.1.0/docs/reviews/2026-09-06-express-mounts-review.md +72 -0
- svarupa-0.1.0/docs/reviews/2026-09-06-p1-8-setup-review.md +103 -0
- svarupa-0.1.0/docs/reviews/2026-09-06-semantics-review.md +76 -0
- svarupa-0.1.0/docs/reviews/2026-09-06-ts-routes-review.md +65 -0
- svarupa-0.1.0/docs/reviews/2026-09-06-wave-a-review.md +72 -0
- svarupa-0.1.0/docs/reviews/2026-09-06-wave-b-review.md +89 -0
- svarupa-0.1.0/docs/reviews/2026-09-07-wave-c-review.md +77 -0
- svarupa-0.1.0/docs/reviews/2026-09-07-wave-de-review.md +92 -0
- svarupa-0.1.0/docs/reviews/2026-09-08-review-20-product-readiness.md +202 -0
- svarupa-0.1.0/docs/reviews/2026-09-08-review-21-rereview.md +200 -0
- svarupa-0.1.0/docs/reviews/2026-09-09-review-22-ship-gate.md +85 -0
- svarupa-0.1.0/docs/reviews/2026-09-09-review-23-ship-gate-rerun.md +239 -0
- svarupa-0.1.0/docs/reviews/2026-09-09-review-24-confirmation.md +134 -0
- svarupa-0.1.0/docs/reviews/2026-09-20-review-25-environment-distinction.md +65 -0
- svarupa-0.1.0/docs/reviews/2026-09-24-review-26-scale-and-performance.md +161 -0
- svarupa-0.1.0/docs/reviews/TEMPLATE.md +100 -0
- svarupa-0.1.0/docs/superpowers/specs/2026-08-30-svarupa-design.md +835 -0
- svarupa-0.1.0/docs/superpowers/specs/2026-09-06-conceptual-diagrams-design.md +277 -0
- svarupa-0.1.0/docs/superpowers/specs/2026-09-19-distribution-onboarding-design.md +73 -0
- svarupa-0.1.0/docs/superpowers/specs/2026-09-19-environment-distinction-design.md +89 -0
- svarupa-0.1.0/pyproject.toml +97 -0
- svarupa-0.1.0/scripts/mutate_conceptual.py +287 -0
- svarupa-0.1.0/scripts/mutate_dataflow.py +341 -0
- svarupa-0.1.0/scripts/mutate_emit.py +167 -0
- svarupa-0.1.0/scripts/mutate_environments.py +152 -0
- svarupa-0.1.0/scripts/mutate_lock.py +218 -0
- svarupa-0.1.0/scripts/mutate_query.py +354 -0
- svarupa-0.1.0/scripts/mutate_review20.py +394 -0
- svarupa-0.1.0/scripts/mutate_scale.py +140 -0
- svarupa-0.1.0/scripts/mutate_semantics.py +424 -0
- svarupa-0.1.0/scripts/mutate_setup.py +193 -0
- svarupa-0.1.0/scripts/mutate_wave11.py +157 -0
- svarupa-0.1.0/scripts/mutate_waved.py +263 -0
- svarupa-0.1.0/scripts/render_fixture_lock.py +100 -0
- svarupa-0.1.0/scripts/replay_history.py +206 -0
- svarupa-0.1.0/skills/svarupa/SKILL.md +144 -0
- svarupa-0.1.0/svarupa/__init__.py +8 -0
- svarupa-0.1.0/svarupa/__main__.py +9 -0
- svarupa-0.1.0/svarupa/build.py +852 -0
- svarupa-0.1.0/svarupa/cli.py +430 -0
- svarupa-0.1.0/svarupa/cluster.py +366 -0
- svarupa-0.1.0/svarupa/derive/__init__.py +101 -0
- svarupa-0.1.0/svarupa/derive/architecture.py +1101 -0
- svarupa-0.1.0/svarupa/derive/base.py +460 -0
- svarupa-0.1.0/svarupa/derive/components.py +294 -0
- svarupa-0.1.0/svarupa/derive/dataflow.py +750 -0
- svarupa-0.1.0/svarupa/derive/erd.py +54 -0
- svarupa-0.1.0/svarupa/derive/system.py +382 -0
- svarupa-0.1.0/svarupa/detect.py +793 -0
- svarupa-0.1.0/svarupa/diagnostics.py +177 -0
- svarupa-0.1.0/svarupa/emit/__init__.py +277 -0
- svarupa-0.1.0/svarupa/emit/cards.py +291 -0
- svarupa-0.1.0/svarupa/emit/data.py +531 -0
- svarupa-0.1.0/svarupa/emit/markup.py +126 -0
- svarupa-0.1.0/svarupa/emit/report.py +337 -0
- svarupa-0.1.0/svarupa/emit/svg.py +597 -0
- svarupa-0.1.0/svarupa/emit/viewer.py +1706 -0
- svarupa-0.1.0/svarupa/extract/__init__.py +282 -0
- svarupa-0.1.0/svarupa/extract/base.py +516 -0
- svarupa-0.1.0/svarupa/extract/compose.py +408 -0
- svarupa-0.1.0/svarupa/extract/environments.py +567 -0
- svarupa-0.1.0/svarupa/extract/python.py +460 -0
- svarupa-0.1.0/svarupa/extract/rationale.py +206 -0
- svarupa-0.1.0/svarupa/extract/resolve.py +1113 -0
- svarupa-0.1.0/svarupa/extract/semantics.py +679 -0
- svarupa-0.1.0/svarupa/extract/typescript.py +787 -0
- svarupa-0.1.0/svarupa/extract/vocabulary.py +259 -0
- svarupa-0.1.0/svarupa/identity.py +94 -0
- svarupa-0.1.0/svarupa/layout/__init__.py +199 -0
- svarupa-0.1.0/svarupa/layout/compose.py +99 -0
- svarupa-0.1.0/svarupa/layout/engines.py +1699 -0
- svarupa-0.1.0/svarupa/layout/geometry.py +283 -0
- svarupa-0.1.0/svarupa/layout/sugiyama.py +200 -0
- svarupa-0.1.0/svarupa/layout/text.py +161 -0
- svarupa-0.1.0/svarupa/layout/validate.py +584 -0
- svarupa-0.1.0/svarupa/lock/__init__.py +54 -0
- svarupa-0.1.0/svarupa/lock/build.py +306 -0
- svarupa-0.1.0/svarupa/lock/diff.py +220 -0
- svarupa-0.1.0/svarupa/lock/grammar.py +375 -0
- svarupa-0.1.0/svarupa/model.py +283 -0
- svarupa-0.1.0/svarupa/py.typed +0 -0
- svarupa-0.1.0/svarupa/query/__init__.py +464 -0
- svarupa-0.1.0/svarupa/query/cli.py +185 -0
- svarupa-0.1.0/svarupa/query/mcp_server.py +127 -0
- svarupa-0.1.0/svarupa/setup/__init__.py +162 -0
- svarupa-0.1.0/svarupa/setup/base.py +28 -0
- svarupa-0.1.0/svarupa/setup/ci_github.py +119 -0
- svarupa-0.1.0/svarupa/setup/ci_gitlab.py +101 -0
- svarupa-0.1.0/svarupa/setup/skill.py +176 -0
- svarupa-0.1.0/svarupa/tsconfig.py +201 -0
- svarupa-0.1.0/tests/__init__.py +0 -0
- svarupa-0.1.0/tests/fixtures/louvain_seed_sensitive.json +414 -0
- svarupa-0.1.0/tests/js/package-lock.json +815 -0
- svarupa-0.1.0/tests/js/package.json +6 -0
- svarupa-0.1.0/tests/js/viewer_harness.js +390 -0
- svarupa-0.1.0/tests/test_build.py +626 -0
- svarupa-0.1.0/tests/test_cards.py +249 -0
- svarupa-0.1.0/tests/test_cluster.py +286 -0
- svarupa-0.1.0/tests/test_compose.py +184 -0
- svarupa-0.1.0/tests/test_conceptual.py +526 -0
- svarupa-0.1.0/tests/test_conceptual_review16.py +369 -0
- svarupa-0.1.0/tests/test_dataflow.py +667 -0
- svarupa-0.1.0/tests/test_derive.py +1010 -0
- svarupa-0.1.0/tests/test_detect.py +671 -0
- svarupa-0.1.0/tests/test_determinism.py +326 -0
- svarupa-0.1.0/tests/test_diagnostics.py +390 -0
- svarupa-0.1.0/tests/test_emit.py +1551 -0
- svarupa-0.1.0/tests/test_env_diagram.py +266 -0
- svarupa-0.1.0/tests/test_environments.py +606 -0
- svarupa-0.1.0/tests/test_extract.py +780 -0
- svarupa-0.1.0/tests/test_extract_typescript.py +565 -0
- svarupa-0.1.0/tests/test_graph_query.py +627 -0
- svarupa-0.1.0/tests/test_layout.py +1621 -0
- svarupa-0.1.0/tests/test_lock.py +1142 -0
- svarupa-0.1.0/tests/test_lock_grammar.py +368 -0
- svarupa-0.1.0/tests/test_markup_types.py +146 -0
- svarupa-0.1.0/tests/test_model.py +217 -0
- svarupa-0.1.0/tests/test_semantics.py +1244 -0
- svarupa-0.1.0/tests/test_setup.py +446 -0
- svarupa-0.1.0/tests/test_viewer_js.py +79 -0
- svarupa-0.1.0/tests/test_waved.py +493 -0
- svarupa-0.1.0/uv.lock +1510 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
env:
|
|
13
|
+
FORCE_COLOR: "1"
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: astral-sh/setup-uv@v5
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
- run: uv sync --all-extras --dev
|
|
24
|
+
- name: ruff
|
|
25
|
+
run: uv run ruff check svarupa tests
|
|
26
|
+
- name: ruff format check
|
|
27
|
+
run: uv run ruff format --check svarupa tests
|
|
28
|
+
- name: pyright
|
|
29
|
+
run: uv run pyright svarupa
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
name: test ${{ matrix.os }} py${{ matrix.python }}
|
|
33
|
+
runs-on: ${{ matrix.os }}
|
|
34
|
+
strategy:
|
|
35
|
+
fail-fast: false
|
|
36
|
+
matrix:
|
|
37
|
+
os: [ubuntu-latest, macos-latest]
|
|
38
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: astral-sh/setup-uv@v5
|
|
42
|
+
with:
|
|
43
|
+
enable-cache: true
|
|
44
|
+
- run: uv sync --dev --python ${{ matrix.python }}
|
|
45
|
+
- name: pytest
|
|
46
|
+
run: uv run pytest -q
|
|
47
|
+
|
|
48
|
+
# The determinism suite is the highest-value test in the project. If it
|
|
49
|
+
# regresses, every PR shows architecture changes that did not happen, teams
|
|
50
|
+
# learn to ignore the check, and the governance pillar is worthless.
|
|
51
|
+
determinism:
|
|
52
|
+
name: determinism ${{ matrix.os }} py${{ matrix.python }} seed=${{ matrix.hashseed }}
|
|
53
|
+
runs-on: ${{ matrix.os }}
|
|
54
|
+
strategy:
|
|
55
|
+
fail-fast: false
|
|
56
|
+
matrix:
|
|
57
|
+
os: [ubuntu-latest, macos-latest]
|
|
58
|
+
python: ["3.10", "3.13"]
|
|
59
|
+
hashseed: ["0", "12345", "random"]
|
|
60
|
+
env:
|
|
61
|
+
PYTHONHASHSEED: ${{ matrix.hashseed }}
|
|
62
|
+
LC_ALL: C
|
|
63
|
+
LANG: C
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/checkout@v4
|
|
66
|
+
- uses: astral-sh/setup-uv@v5
|
|
67
|
+
with:
|
|
68
|
+
enable-cache: true
|
|
69
|
+
- run: uv sync --dev --python ${{ matrix.python }}
|
|
70
|
+
- name: determinism suite
|
|
71
|
+
run: uv run pytest -q -m determinism
|
|
72
|
+
|
|
73
|
+
# Gate 2 from Spike 0, deferred at the time because a Docker daemon was not
|
|
74
|
+
# available locally. Renders the same corpus on Linux and macOS and asserts
|
|
75
|
+
# the bytes match. A hash mismatch here means the lockfile is not portable,
|
|
76
|
+
# which breaks per-PR diffing at the root.
|
|
77
|
+
cross-platform-bytes:
|
|
78
|
+
name: byte identity (${{ matrix.os }})
|
|
79
|
+
runs-on: ${{ matrix.os }}
|
|
80
|
+
strategy:
|
|
81
|
+
fail-fast: false
|
|
82
|
+
matrix:
|
|
83
|
+
os: [ubuntu-latest, macos-latest]
|
|
84
|
+
env:
|
|
85
|
+
LC_ALL: C
|
|
86
|
+
LANG: C
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
- uses: astral-sh/setup-uv@v5
|
|
90
|
+
with:
|
|
91
|
+
enable-cache: true
|
|
92
|
+
- run: uv sync --dev
|
|
93
|
+
# Builds from a real fixture tree on disk, not an in-memory string.
|
|
94
|
+
# An in-memory corpus is pure str manipulation and can essentially never
|
|
95
|
+
# differ across platforms, so it verified the serializer's determinism
|
|
96
|
+
# rather than the product's. NFD paths, dirent ordering and
|
|
97
|
+
# case-insensitive volumes only enter through an actual filesystem walk.
|
|
98
|
+
- name: build from a real fixture tree
|
|
99
|
+
run: uv run python scripts/render_fixture_lock.py lockfile.out
|
|
100
|
+
- name: hash
|
|
101
|
+
run: |
|
|
102
|
+
python3 -c "
|
|
103
|
+
import hashlib, pathlib
|
|
104
|
+
b = pathlib.Path('lockfile.out').read_bytes()
|
|
105
|
+
print(f'sha256={hashlib.sha256(b).hexdigest()} bytes={len(b)}')
|
|
106
|
+
"
|
|
107
|
+
- uses: actions/upload-artifact@v4
|
|
108
|
+
with:
|
|
109
|
+
name: lockfile-${{ matrix.os }}
|
|
110
|
+
path: lockfile.out
|
|
111
|
+
|
|
112
|
+
compare-bytes:
|
|
113
|
+
needs: cross-platform-bytes
|
|
114
|
+
runs-on: ubuntu-latest
|
|
115
|
+
steps:
|
|
116
|
+
- uses: actions/download-artifact@v4
|
|
117
|
+
with:
|
|
118
|
+
path: artifacts
|
|
119
|
+
- name: assert Linux and macOS bytes are identical
|
|
120
|
+
run: |
|
|
121
|
+
a=artifacts/lockfile-ubuntu-latest/lockfile.out
|
|
122
|
+
b=artifacts/lockfile-macos-latest/lockfile.out
|
|
123
|
+
if ! cmp -s "$a" "$b"; then
|
|
124
|
+
echo "::error::lockfile bytes differ between Linux and macOS"
|
|
125
|
+
diff <(cat -A "$a") <(cat -A "$b") | head -40
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
echo "byte identity confirmed: $(sha256sum "$a" | cut -d' ' -f1)"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
# id-token is what PyPI trusted publishing (OIDC) exchanges for a short-lived
|
|
9
|
+
# upload credential; no API token is stored anywhere.
|
|
10
|
+
id-token: write
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
environment: pypi
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: astral-sh/setup-uv@v5
|
|
21
|
+
|
|
22
|
+
- name: check the tag matches the package version
|
|
23
|
+
shell: bash
|
|
24
|
+
run: |
|
|
25
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
26
|
+
PKG=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
27
|
+
test "$TAG" = "$PKG" || { echo "tag v$TAG != pyproject version $PKG"; exit 1; }
|
|
28
|
+
|
|
29
|
+
- name: run the test suite
|
|
30
|
+
run: |
|
|
31
|
+
uv sync --frozen
|
|
32
|
+
uv run pytest
|
|
33
|
+
|
|
34
|
+
- name: build
|
|
35
|
+
run: uv build
|
|
36
|
+
|
|
37
|
+
- name: publish to PyPI
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: architecture
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
diff:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
with:
|
|
15
|
+
# The base branch's tree and its committed lockfile are both needed,
|
|
16
|
+
# so the fetch cannot be depth 1.
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
|
|
19
|
+
- uses: astral-sh/setup-uv@v5
|
|
20
|
+
|
|
21
|
+
- name: install svarupa
|
|
22
|
+
run: uv tool install 'svarupa==0.1.0'
|
|
23
|
+
|
|
24
|
+
- name: regenerate the base lockfile from base-branch code
|
|
25
|
+
shell: bash
|
|
26
|
+
run: |
|
|
27
|
+
git worktree add /tmp/base-src "${{ github.event.pull_request.base.sha }}"
|
|
28
|
+
svarupa /tmp/base-src --lock --out /tmp/base-artifact
|
|
29
|
+
|
|
30
|
+
# `shell: bash` turns pipefail on, so nothing here hides a failure.
|
|
31
|
+
# The delta is written to the job summary before the exit code is
|
|
32
|
+
# re-raised: a failing run is the one whose delta must be readable.
|
|
33
|
+
- name: architecture delta
|
|
34
|
+
shell: bash
|
|
35
|
+
run: |
|
|
36
|
+
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
|
37
|
+
# A base commit git cannot read is a job failure, never a silent
|
|
38
|
+
# fallback: falling back would drop the drift guard exactly when
|
|
39
|
+
# the checkout is wrong.
|
|
40
|
+
git cat-file -e "$BASE_SHA^{commit}"
|
|
41
|
+
if git cat-file -e "$BASE_SHA:.svarupa/architecture.lock" 2>/dev/null; then
|
|
42
|
+
git show "$BASE_SHA:.svarupa/architecture.lock" > /tmp/committed-base.lock
|
|
43
|
+
svarupa . --lock \
|
|
44
|
+
--diff /tmp/committed-base.lock \
|
|
45
|
+
--drift-base /tmp/base-artifact/architecture.lock \
|
|
46
|
+
> /tmp/delta.txt 2>&1 && code=0 || code=$?
|
|
47
|
+
else
|
|
48
|
+
# First adoption: the base branch has no committed lockfile yet, so
|
|
49
|
+
# the delta is taken against the regenerated base directly.
|
|
50
|
+
svarupa . --lock \
|
|
51
|
+
--diff /tmp/base-artifact/architecture.lock \
|
|
52
|
+
> /tmp/delta.txt 2>&1 && code=0 || code=$?
|
|
53
|
+
fi
|
|
54
|
+
cat /tmp/delta.txt
|
|
55
|
+
{
|
|
56
|
+
echo '## Architecture delta'
|
|
57
|
+
echo '````'
|
|
58
|
+
cat /tmp/delta.txt
|
|
59
|
+
echo '````'
|
|
60
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
61
|
+
exit "$code"
|
svarupa-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# The artifact directory is regenerated on every run, with one exception:
|
|
2
|
+
# `architecture.lock` is committed on purpose. It is the diffable fingerprint
|
|
3
|
+
# of the architecture, and a reviewer is meant to see `dep api -> auth` appear
|
|
4
|
+
# as a green line in the pull request before CI even runs. Ignoring the whole
|
|
5
|
+
# directory would ignore the one file the design says to commit.
|
|
6
|
+
.svarupa/*
|
|
7
|
+
!.svarupa/architecture.lock
|
|
8
|
+
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.pyc
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.venv/
|
|
14
|
+
.venv2/
|
|
15
|
+
__pycache__/
|
|
16
|
+
build/
|
|
17
|
+
dist/
|
|
18
|
+
tests/js/node_modules/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# svarupa 0.1.0
|
|
2
|
+
# schema 1.7
|
|
3
|
+
# grammars python@0.25.0
|
|
4
|
+
dep scripts svarupa
|
|
5
|
+
dep scripts svarupa/extract
|
|
6
|
+
dep scripts svarupa/lock
|
|
7
|
+
dep svarupa svarupa/derive
|
|
8
|
+
dep svarupa svarupa/emit
|
|
9
|
+
dep svarupa svarupa/extract
|
|
10
|
+
dep svarupa svarupa/lock
|
|
11
|
+
dep svarupa svarupa/query
|
|
12
|
+
dep svarupa svarupa/setup
|
|
13
|
+
dep svarupa/derive svarupa
|
|
14
|
+
dep svarupa/derive svarupa/extract
|
|
15
|
+
dep svarupa/emit svarupa
|
|
16
|
+
dep svarupa/emit svarupa/derive
|
|
17
|
+
dep svarupa/emit svarupa/extract
|
|
18
|
+
dep svarupa/emit svarupa/layout
|
|
19
|
+
dep svarupa/emit svarupa/lock
|
|
20
|
+
dep svarupa/extract svarupa
|
|
21
|
+
dep svarupa/layout svarupa
|
|
22
|
+
dep svarupa/layout svarupa/derive
|
|
23
|
+
dep svarupa/lock svarupa
|
|
24
|
+
dep svarupa/lock svarupa/extract
|
|
25
|
+
dep svarupa/query svarupa
|
|
26
|
+
dep svarupa/setup svarupa
|
|
27
|
+
entrypoint svarupa svarupa
|
|
28
|
+
environment pypi
|
|
29
|
+
module scripts
|
|
30
|
+
module svarupa
|
|
31
|
+
module svarupa/derive
|
|
32
|
+
module svarupa/emit
|
|
33
|
+
module svarupa/extract
|
|
34
|
+
module svarupa/layout
|
|
35
|
+
module svarupa/lock
|
|
36
|
+
module svarupa/query
|
|
37
|
+
module svarupa/setup
|
|
38
|
+
role svarupa cli
|
svarupa-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 svarupa contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
svarupa-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: svarupa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Verified architecture diagrams and a queryable knowledge graph, derived from your codebase. Every box points at a line of code.
|
|
5
|
+
Project-URL: Homepage, https://github.com/aiwithvd/svarupa
|
|
6
|
+
Project-URL: Repository, https://github.com/aiwithvd/svarupa
|
|
7
|
+
Project-URL: Issues, https://github.com/aiwithvd/svarupa/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: architecture,diagrams,knowledge-graph,static-analysis,tree-sitter
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: networkx<4,>=3.2
|
|
21
|
+
Requires-Dist: pathspec<2,>=0.12
|
|
22
|
+
Requires-Dist: pyyaml<7,>=6
|
|
23
|
+
Requires-Dist: tomli>=2; python_version < '3.11'
|
|
24
|
+
Requires-Dist: tree-sitter-python==0.25.0
|
|
25
|
+
Requires-Dist: tree-sitter-typescript==0.23.2
|
|
26
|
+
Requires-Dist: tree-sitter<0.27,>=0.26
|
|
27
|
+
Provides-Extra: leiden
|
|
28
|
+
Requires-Dist: leidenalg>=0.10; extra == 'leiden'
|
|
29
|
+
Requires-Dist: python-igraph>=0.11; extra == 'leiden'
|
|
30
|
+
Provides-Extra: mcp
|
|
31
|
+
Requires-Dist: mcp>=2; extra == 'mcp'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Svarupa
|
|
35
|
+
|
|
36
|
+
**स्वरूप** — *"its own true form."*
|
|
37
|
+
|
|
38
|
+
Svarupa reads a codebase and produces a **verified** map of it: a queryable
|
|
39
|
+
knowledge graph plus six types of architecture diagram (architecture, module
|
|
40
|
+
dependencies, data flow, request flow, deploy topology, ERD), delivered as one
|
|
41
|
+
interactive HTML artifact. A type the evidence cannot produce is named as
|
|
42
|
+
absent, never fabricated.
|
|
43
|
+
|
|
44
|
+
The defining constraint: **every node and every edge in every diagram carries
|
|
45
|
+
`file:line` evidence, or it does not render.** Not a heuristic guess, not a
|
|
46
|
+
model's plausible story. A claim you can click through to the source line that
|
|
47
|
+
proves it.
|
|
48
|
+
|
|
49
|
+
The name states the thesis. Svarupa is a thing's actual form, not its intended
|
|
50
|
+
one. Architecture documents describe what someone meant to build. Svarupa
|
|
51
|
+
renders what exists.
|
|
52
|
+
|
|
53
|
+
> **Status: alpha (v0.1.0).** The evidence contract is stable; flags and output
|
|
54
|
+
> formats may still change between minor versions.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv tool install svarupa # or: pipx install svarupa / pip install svarupa
|
|
62
|
+
svarupa . # scans the repo, writes .svarupa/
|
|
63
|
+
open .svarupa/index.html
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
That is the whole setup — no configuration file, no daemon, no account. The
|
|
67
|
+
first run of a typical repository takes seconds.
|
|
68
|
+
|
|
69
|
+
`python -m svarupa` is the same command; `--out ./map` writes the artifact
|
|
70
|
+
somewhere other than `<repo>/.svarupa/`. The artifact holds:
|
|
71
|
+
|
|
72
|
+
- `index.html`: every diagram type as a tab. Click a box for its passport
|
|
73
|
+
(kind, connections, reach, cited source lines); double-click a drillable box,
|
|
74
|
+
or click its chevron, to open it in place down to component and code level.
|
|
75
|
+
- `graph.json`: the knowledge graph. `svarupa query <dir> get_node <label>`,
|
|
76
|
+
`get_neighbors`, `shortest_path`, `affected`, `god_nodes`, `graph_stats`,
|
|
77
|
+
`query_graph`; `svarupa mcp <dir>` serves the same over MCP.
|
|
78
|
+
- `REPORT.md`: the resolution scorecard and every finding, with its code.
|
|
79
|
+
- Declared environments (production, staging, qa, development) are extracted
|
|
80
|
+
with `file:line` evidence and surface in the CLI summary, a REPORT.md
|
|
81
|
+
section, `graph.json` nodes, and a cited strip on the deploy topology.
|
|
82
|
+
- `--lock` writes the committed architecture lockfile; `--diff` prints the
|
|
83
|
+
architecture delta against a base lockfile. `svarupa setup ci_github`
|
|
84
|
+
(or `ci_gitlab`) installs the per-pull-request workflow.
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+
|
|
88
|
+

|
|
89
|
+
|
|
90
|
+
Declared environments — production, staging, qa, development — are extracted
|
|
91
|
+
with `file:line` evidence and shown as a cited strip above the topology.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Why
|
|
96
|
+
|
|
97
|
+
Every tool in this space sits on one side of a divide and cannot cross it.
|
|
98
|
+
|
|
99
|
+
| Tool | Analyzes code | Draws formal diagrams |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| Graphify | Deeply, 25+ languages | No — force-directed node-link graphs only |
|
|
102
|
+
| Understand-Anything | Yes, AST + LLM | No — force layouts only |
|
|
103
|
+
| Archify | No — reads nothing | Yes, five types, excellent quality |
|
|
104
|
+
| Cocoon-AI | No — prompt-to-diagram | Yes, architecture only |
|
|
105
|
+
|
|
106
|
+
Nobody closes the loop `code → AST → derived formal diagram`. The analyzers
|
|
107
|
+
produce blobs; the drawers produce unverifiable pictures.
|
|
108
|
+
|
|
109
|
+
### The larger thesis: continuous architecture governance
|
|
110
|
+
|
|
111
|
+
Graphify and Understand-Anything are *onboarding* tools. You run one when you
|
|
112
|
+
join a project, look at the graph, and never open it again.
|
|
113
|
+
|
|
114
|
+
Because Svarupa's output is deterministic and evidence-backed, it runs in CI on
|
|
115
|
+
every pull request: diff the architecture, comment the impact, fail the build
|
|
116
|
+
on policy violations. **Cross-language architecture linting is a category
|
|
117
|
+
nobody occupies.**
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
Architecture impact of #482
|
|
121
|
+
|
|
122
|
+
+ 2 components billing.refunds, billing.webhooks
|
|
123
|
+
+ 1 datastore redis (compose.yml:31)
|
|
124
|
+
! NEW DEPENDENCY billing -> auth.internal
|
|
125
|
+
via billing/refunds.py:44
|
|
126
|
+
+ 4 endpoints POST /refunds, GET /refunds/{id}, ...
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Design principles
|
|
132
|
+
|
|
133
|
+
**Fail-closed on evidence.** An element without a source location is not
|
|
134
|
+
emitted. When Python's dynamic dispatch means a call target cannot be pinned,
|
|
135
|
+
no edge is written and the gap is counted in the report, rather than papered
|
|
136
|
+
over with a plausible one.
|
|
137
|
+
|
|
138
|
+
**Three-bin honesty.** The resolution scorecard reports *resolved*,
|
|
139
|
+
*known-external*, and **unresolved-unknown**. Counting every failure as
|
|
140
|
+
"external" would launder resolver bugs into a number that looks like honesty.
|
|
141
|
+
|
|
142
|
+
**Deterministic by contract.** Same commit, same bytes, on any machine.
|
|
143
|
+
Canonical ordering, `==`-pinned grammars, NFC-normalized paths, no timestamps,
|
|
144
|
+
no locale-dependent formatting. Verified in CI across Linux and macOS on Python
|
|
145
|
+
3.10 through 3.13 with `PYTHONHASHSEED` varied.
|
|
146
|
+
|
|
147
|
+
**Identity comes from structure, not from clustering.** Lockfile modules are
|
|
148
|
+
directories, packages, and workspace members. Community detection is
|
|
149
|
+
chaotically sensitive to input perturbation — measured: one added import flips
|
|
150
|
+
25-38% of community assignments — so it is used for visual grouping only and
|
|
151
|
+
never reaches anything committed.
|
|
152
|
+
|
|
153
|
+
**Measured, not assumed.** Function-level sequence diagrams were cut after
|
|
154
|
+
measurement showed call resolution at ~20% on service code with median chain
|
|
155
|
+
depth 0-1. Replaying this repository's own real history (15 commits of actual
|
|
156
|
+
development: license swaps, viewer work, layout fixes) produced a median of
|
|
157
|
+
**0 lockfile lines per commit** — the diff moves only when the architecture
|
|
158
|
+
moves, not when code churns. See `docs/reviews/` for the numbers and the
|
|
159
|
+
reasoning.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
uv sync
|
|
167
|
+
uv run pytest
|
|
168
|
+
uv run ruff check svarupa
|
|
169
|
+
uv run pyright svarupa
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Design and review history:
|
|
173
|
+
|
|
174
|
+
- `docs/superpowers/specs/` — the design, with a decision log carrying the
|
|
175
|
+
reasoning behind each choice
|
|
176
|
+
- `docs/reviews/` — adversarial reviews, raw and triaged. Every finding is
|
|
177
|
+
marked Accepted, Rejected, or Deferred **with a recorded why**, because a
|
|
178
|
+
rejected finding needs its reasoning kept just as much as an accepted one
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT. See `LICENSE`.
|
|
185
|
+
|
svarupa-0.1.0/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Svarupa
|
|
2
|
+
|
|
3
|
+
**स्वरूप** — *"its own true form."*
|
|
4
|
+
|
|
5
|
+
Svarupa reads a codebase and produces a **verified** map of it: a queryable
|
|
6
|
+
knowledge graph plus six types of architecture diagram (architecture, module
|
|
7
|
+
dependencies, data flow, request flow, deploy topology, ERD), delivered as one
|
|
8
|
+
interactive HTML artifact. A type the evidence cannot produce is named as
|
|
9
|
+
absent, never fabricated.
|
|
10
|
+
|
|
11
|
+
The defining constraint: **every node and every edge in every diagram carries
|
|
12
|
+
`file:line` evidence, or it does not render.** Not a heuristic guess, not a
|
|
13
|
+
model's plausible story. A claim you can click through to the source line that
|
|
14
|
+
proves it.
|
|
15
|
+
|
|
16
|
+
The name states the thesis. Svarupa is a thing's actual form, not its intended
|
|
17
|
+
one. Architecture documents describe what someone meant to build. Svarupa
|
|
18
|
+
renders what exists.
|
|
19
|
+
|
|
20
|
+
> **Status: alpha (v0.1.0).** The evidence contract is stable; flags and output
|
|
21
|
+
> formats may still change between minor versions.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv tool install svarupa # or: pipx install svarupa / pip install svarupa
|
|
29
|
+
svarupa . # scans the repo, writes .svarupa/
|
|
30
|
+
open .svarupa/index.html
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That is the whole setup — no configuration file, no daemon, no account. The
|
|
34
|
+
first run of a typical repository takes seconds.
|
|
35
|
+
|
|
36
|
+
`python -m svarupa` is the same command; `--out ./map` writes the artifact
|
|
37
|
+
somewhere other than `<repo>/.svarupa/`. The artifact holds:
|
|
38
|
+
|
|
39
|
+
- `index.html`: every diagram type as a tab. Click a box for its passport
|
|
40
|
+
(kind, connections, reach, cited source lines); double-click a drillable box,
|
|
41
|
+
or click its chevron, to open it in place down to component and code level.
|
|
42
|
+
- `graph.json`: the knowledge graph. `svarupa query <dir> get_node <label>`,
|
|
43
|
+
`get_neighbors`, `shortest_path`, `affected`, `god_nodes`, `graph_stats`,
|
|
44
|
+
`query_graph`; `svarupa mcp <dir>` serves the same over MCP.
|
|
45
|
+
- `REPORT.md`: the resolution scorecard and every finding, with its code.
|
|
46
|
+
- Declared environments (production, staging, qa, development) are extracted
|
|
47
|
+
with `file:line` evidence and surface in the CLI summary, a REPORT.md
|
|
48
|
+
section, `graph.json` nodes, and a cited strip on the deploy topology.
|
|
49
|
+
- `--lock` writes the committed architecture lockfile; `--diff` prints the
|
|
50
|
+
architecture delta against a base lockfile. `svarupa setup ci_github`
|
|
51
|
+
(or `ci_gitlab`) installs the per-pull-request workflow.
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
Declared environments — production, staging, qa, development — are extracted
|
|
58
|
+
with `file:line` evidence and shown as a cited strip above the topology.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Why
|
|
63
|
+
|
|
64
|
+
Every tool in this space sits on one side of a divide and cannot cross it.
|
|
65
|
+
|
|
66
|
+
| Tool | Analyzes code | Draws formal diagrams |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Graphify | Deeply, 25+ languages | No — force-directed node-link graphs only |
|
|
69
|
+
| Understand-Anything | Yes, AST + LLM | No — force layouts only |
|
|
70
|
+
| Archify | No — reads nothing | Yes, five types, excellent quality |
|
|
71
|
+
| Cocoon-AI | No — prompt-to-diagram | Yes, architecture only |
|
|
72
|
+
|
|
73
|
+
Nobody closes the loop `code → AST → derived formal diagram`. The analyzers
|
|
74
|
+
produce blobs; the drawers produce unverifiable pictures.
|
|
75
|
+
|
|
76
|
+
### The larger thesis: continuous architecture governance
|
|
77
|
+
|
|
78
|
+
Graphify and Understand-Anything are *onboarding* tools. You run one when you
|
|
79
|
+
join a project, look at the graph, and never open it again.
|
|
80
|
+
|
|
81
|
+
Because Svarupa's output is deterministic and evidence-backed, it runs in CI on
|
|
82
|
+
every pull request: diff the architecture, comment the impact, fail the build
|
|
83
|
+
on policy violations. **Cross-language architecture linting is a category
|
|
84
|
+
nobody occupies.**
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Architecture impact of #482
|
|
88
|
+
|
|
89
|
+
+ 2 components billing.refunds, billing.webhooks
|
|
90
|
+
+ 1 datastore redis (compose.yml:31)
|
|
91
|
+
! NEW DEPENDENCY billing -> auth.internal
|
|
92
|
+
via billing/refunds.py:44
|
|
93
|
+
+ 4 endpoints POST /refunds, GET /refunds/{id}, ...
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Design principles
|
|
99
|
+
|
|
100
|
+
**Fail-closed on evidence.** An element without a source location is not
|
|
101
|
+
emitted. When Python's dynamic dispatch means a call target cannot be pinned,
|
|
102
|
+
no edge is written and the gap is counted in the report, rather than papered
|
|
103
|
+
over with a plausible one.
|
|
104
|
+
|
|
105
|
+
**Three-bin honesty.** The resolution scorecard reports *resolved*,
|
|
106
|
+
*known-external*, and **unresolved-unknown**. Counting every failure as
|
|
107
|
+
"external" would launder resolver bugs into a number that looks like honesty.
|
|
108
|
+
|
|
109
|
+
**Deterministic by contract.** Same commit, same bytes, on any machine.
|
|
110
|
+
Canonical ordering, `==`-pinned grammars, NFC-normalized paths, no timestamps,
|
|
111
|
+
no locale-dependent formatting. Verified in CI across Linux and macOS on Python
|
|
112
|
+
3.10 through 3.13 with `PYTHONHASHSEED` varied.
|
|
113
|
+
|
|
114
|
+
**Identity comes from structure, not from clustering.** Lockfile modules are
|
|
115
|
+
directories, packages, and workspace members. Community detection is
|
|
116
|
+
chaotically sensitive to input perturbation — measured: one added import flips
|
|
117
|
+
25-38% of community assignments — so it is used for visual grouping only and
|
|
118
|
+
never reaches anything committed.
|
|
119
|
+
|
|
120
|
+
**Measured, not assumed.** Function-level sequence diagrams were cut after
|
|
121
|
+
measurement showed call resolution at ~20% on service code with median chain
|
|
122
|
+
depth 0-1. Replaying this repository's own real history (15 commits of actual
|
|
123
|
+
development: license swaps, viewer work, layout fixes) produced a median of
|
|
124
|
+
**0 lockfile lines per commit** — the diff moves only when the architecture
|
|
125
|
+
moves, not when code churns. See `docs/reviews/` for the numbers and the
|
|
126
|
+
reasoning.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uv sync
|
|
134
|
+
uv run pytest
|
|
135
|
+
uv run ruff check svarupa
|
|
136
|
+
uv run pyright svarupa
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Design and review history:
|
|
140
|
+
|
|
141
|
+
- `docs/superpowers/specs/` — the design, with a decision log carrying the
|
|
142
|
+
reasoning behind each choice
|
|
143
|
+
- `docs/reviews/` — adversarial reviews, raw and triaged. Every finding is
|
|
144
|
+
marked Accepted, Rejected, or Deferred **with a recorded why**, because a
|
|
145
|
+
rejected finding needs its reasoning kept just as much as an accepted one
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT. See `LICENSE`.
|
|
152
|
+
|
|
Binary file
|
|
Binary file
|