visiter 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.
@@ -0,0 +1,35 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(*)",
5
+ "Edit(*)",
6
+ "Write(*)",
7
+ "Read(*)",
8
+ "Glob(*)",
9
+ "Grep(*)",
10
+ "Agent(*)",
11
+ "NotebookEdit(*)",
12
+ "WebFetch(*)",
13
+ "WebSearch(*)",
14
+ "TodoWrite(*)"
15
+ ],
16
+ "deny": [
17
+ "Bash(git push*)"
18
+ ]
19
+ },
20
+ "hooks": {
21
+ "PreToolUse": [
22
+ {
23
+ "matcher": "Bash",
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "if": "Bash(git commit *)",
28
+ "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PreToolUse\", \"permissionDecision\": \"ask\", \"permissionDecisionReason\": \"git commit requires user approval\"}}'",
29
+ "statusMessage": "Checking git commit..."
30
+ }
31
+ ]
32
+ }
33
+ ]
34
+ }
35
+ }
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ *.swp
12
+ .DS_Store
13
+ demos/out/
14
+
@@ -0,0 +1,218 @@
1
+ # CLAUDE.md — VisIter project notes for Claude Code
2
+
3
+ This file collects project-specific conventions, design decisions, and
4
+ workflow rules that were agreed on during the initial extraction of
5
+ VisIter from the `_math` research project. Read this at the start of
6
+ every session before making non-trivial changes.
7
+
8
+ ---
9
+
10
+ ## 1. Project purpose
11
+
12
+ VisIter provides two composable pieces:
13
+
14
+ - **`iterate`** — builds an iteration graph from guard-and-op Rules via
15
+ BFS, records per-node depth, and emits pseudo-edges for structural
16
+ boundaries (`Rule.bound`, `max_depth`).
17
+ - **`to_dot`** — converts the graph dict into a `graphviz.Digraph` with
18
+ cropping, coloring, ghost stubs, and node annotations.
19
+
20
+ They are connected only by a documented dict shape (see
21
+ `docs/manual.md` §6). Either can be used without the other.
22
+
23
+ ---
24
+
25
+ ## 2. Design decisions already made
26
+
27
+ These are the results of deliberate earlier discussions; do not
28
+ re-litigate without a clear reason.
29
+
30
+ ### API shape
31
+
32
+ - `iterate(start, rules, *, default, ...)` — `default` is required
33
+ (keyword-only, no Python default value) to force the caller to make
34
+ the "no rule matched" case explicit. Pass `None` for "that value is
35
+ a leaf"; pass an `Op` for the else branch.
36
+ - `Rule(condition, op, bound=None)` — three-field namedtuple. The
37
+ `condition` asks "is this op applicable at all?" (mathematical
38
+ predicate). The optional `bound` asks "should we stop here anyway?"
39
+ (structural cutoff). When `condition` is True but `bound` is False,
40
+ a pseudo-edge is recorded and the renderer draws a ghost stub.
41
+ - `Op(func, label)` — the label is the identity key for color
42
+ assignment, ordering, and op_colors lookup. Two Op instances with
43
+ the same label are treated as the same op.
44
+ - iterate uses **BFS** (not DFS) so each node's recorded `depth` is
45
+ the minimum hop count from the nearest start.
46
+ - `on_limit` governs `max_nodes` and `time_limit` ("raise" or "stop").
47
+ `max_depth` is always a soft topological stop; it does not raise.
48
+ - The graph dict is the single data contract. `op_order` is part of it
49
+ and drives palette assignment; it follows rule-declaration order
50
+ (not traversal-first-seen). Preserve `op_order` across any
51
+ transformation that rewrites the graph (e.g. cropping in `to_dot`).
52
+
53
+ ### Rendering model
54
+
55
+ - **Two-layer palette**: each palette slot is a `(fill, edge)` pair.
56
+ Light pastel for node fills (readable labels), saturated mid-tone
57
+ for edges (visible thin lines). The caller can override via
58
+ `op_colors` with a single hex (applies to both) or a tuple (fill,
59
+ edge).
60
+ - **Highlight** (the `"highlight"` tag): darkens fills in **HSL
61
+ space** (lightness-only reduction, hue+saturation preserved), not
62
+ RGB-toward-black. RGB-toward-black desaturates; HSL preserves the
63
+ color identity. Default factor 0.45.
64
+ - **Node fill comes from outgoing op labels**, not from node identity.
65
+ 0 out-ops → no fill (leaf, default white). 1 out-op → solid fill.
66
+ N ≥ 2 → `style="wedged"` with colon-joined colors (pie segments).
67
+ - **Roots** are marked with `penwidth="3"` (bold border). Not with
68
+ `peripheries`, not with a distinct fill.
69
+ - **Ghost stubs** appear for three reasons, all rendered the same:
70
+ (a) outgoing cut (kept_src → outside_dst filtered),
71
+ (b) incoming cut (outside_src → kept_dst filtered),
72
+ (c) pseudo-edge (iterate recorded Rule.bound=False or max_depth).
73
+ All three fold into one `cut_edges` list with a `kept_is_src` flag.
74
+ Only case (a) and (c) contribute to `extra_out_ops` (fill lives on
75
+ outgoing edges only — incoming cuts don't need a fill counterpart).
76
+
77
+ ### CLI shape
78
+
79
+ - **One** entry point `visiter` with subcommands. Subcommands are
80
+ registered in `src/visiter/cli.py::SUBCOMMANDS`. Adding a new
81
+ subcommand means writing a module with a `main()` function and
82
+ adding one entry there.
83
+ - **Each subcommand takes a single positional Python expression** as
84
+ its "argstring", spliced into the corresponding function call and
85
+ eval'd in a namespace where `Op`, `Rule`, `iterate`, `to_dot`, and
86
+ `graph` (for to-dot) are pre-bound. This deliberately avoids a
87
+ per-flag DSL — any Python-API kwarg is expressible on the shell,
88
+ and future API additions cost nothing in CLI glue.
89
+ - **Pipe composition is the idiom**: `visiter iterate '...' | visiter
90
+ to-dot '...' | dot -Tsvg > out.svg`. Input/output conventions are
91
+ stdin/stdout with `--input FILE` / `-o FILE` overrides.
92
+ - `eval` is appropriate: this is a local research tool, not a
93
+ network-exposed service. Errors propagate as normal Python
94
+ exceptions; no parser front-end to misdiagnose input.
95
+
96
+ ### Why subcommands (not separate console scripts)
97
+
98
+ We explicitly chose the subcommand layout over multiple separate
99
+ `visiter-X` scripts because we expect more commands to be added
100
+ (e.g. `analyze` for jq-style queries over the graph JSON, `stats` for
101
+ depth-distribution/fan-out summaries, `diff` for comparing two
102
+ iteration graphs). One stable command name, `visiter --help` as a
103
+ single discovery point, and a minimal dispatcher makes that growth
104
+ cheap.
105
+
106
+ ---
107
+
108
+ ## 3. Workflow rules
109
+
110
+ ### Commits
111
+
112
+ - **Use conventional-commit prefixes** (`feat:`, `fix:`, `refactor:`,
113
+ `docs:`, `chore:`, `test:`). Match the ones already in the log.
114
+ - **Never add `Co-Authored-By` trailers.** Not here, not anywhere.
115
+ - **Always propose the commit message first and wait for user
116
+ approval** before executing the commit. The user says "ja" / "okay"
117
+ or asks for changes.
118
+ - **Message describes net diff, not session steps.** A mid-session
119
+ dead-end doesn't belong in the message.
120
+ - **Before every `git commit`, run `git status`** and cross-check
121
+ that the staged set matches the work. Nothing relevant should be
122
+ left Untracked or unstaged. Files referenced by the commit message
123
+ must actually be in the commit.
124
+ - **Don't ask "commit or review first?"** — the user reviews before
125
+ saying "commit". The question is a false dichotomy.
126
+ - **Never `git push`** without explicit user request. (Enforced by
127
+ the deny rule in `.claude/settings.json`.)
128
+
129
+ ### Verification
130
+
131
+ - **Always verify.** `make test` after any logic change. `make build`
132
+ after any packaging change. For CLI changes, at least one
133
+ end-to-end pipe-through-dot smoke test.
134
+ - **Don't report success without confirming.** Build output, test
135
+ status, or rendered artefact is what confirms; "I think it works"
136
+ is not.
137
+
138
+ ### Testing
139
+
140
+ - Tests live in `tests/`. They use only the public package API
141
+ (`from visiter import …`), not internal paths, so we'd notice if
142
+ re-exports drift.
143
+ - New features: add at least one test exercising the happy path and
144
+ one exercising a limit/error case (e.g. `max_nodes` raise vs stop).
145
+ - When a bug is found, the repro-first rule applies: write a failing
146
+ test, confirm it fails, then fix, then confirm it passes.
147
+
148
+ ### Dependencies
149
+
150
+ - **Never `pip install` directly**. Add to `pyproject.toml` (core
151
+ deps or `[project.optional-dependencies].dev`), then `make setup`.
152
+ - Keep the dep list minimal. Currently `graphviz` and `sympy`;
153
+ anything new needs a clear justification.
154
+
155
+ ---
156
+
157
+ ## 4. Open topics (not decided, worth revisiting)
158
+
159
+ ### Versioning of the eval-based CLI
160
+
161
+ The CLI eval contract means any shell script that embeds an argstring
162
+ is effectively coupled to the **implementation version** of the
163
+ called function — its parameter names, `Rule`/`Op` fields, default
164
+ behavior, etc. If we change a parameter name or add a required field
165
+ to `Rule`, downstream shell scripts break silently-until-executed.
166
+
167
+ Unresolved:
168
+
169
+ - Should we document a stability guarantee (semver-style, where
170
+ parameter-name / signature breakage requires a major bump)?
171
+ - Should the CLI check some form of `visiter.__version__` in the
172
+ argstring context, so scripts can assert compatibility?
173
+ - Is there a less-coupled interface worth exposing alongside (e.g. a
174
+ JSON-in / JSON-out config for the common cases) without losing the
175
+ expressivity of eval?
176
+
177
+ This should be addressed before `v1.0`.
178
+
179
+ ### Other candidates for future work
180
+
181
+ - `visiter analyze` subcommand: jq-style or xpath-like queries over
182
+ the graph JSON (e.g. "show all nodes with depth > 5 and fan-out >
183
+ 1").
184
+ - `visiter stats`: depth distribution, op frequency, average fan-out,
185
+ cycle detection summary.
186
+ - `visiter diff`: compare two graphs structurally (added/removed
187
+ nodes/edges, shifted depths).
188
+ - Depth-based rendering affordances (`to_dot` `depth_gradient=True`,
189
+ or per-depth op_colors override).
190
+ - A proper Graphviz-side timeout that's integrated rather than left
191
+ to callers (the `with_timeout` wrapper in the notebook).
192
+
193
+ ---
194
+
195
+ ## 5. Pointers
196
+
197
+ - Package source: `src/visiter/`
198
+ - Public API surface: `src/visiter/__init__.py`
199
+ - Subcommand registry: `src/visiter/cli.py`
200
+ - Tests: `tests/`
201
+ - User-facing docs: `README.md` (quickstart) and `docs/manual.md`
202
+ (full reference, including the graph-dict shape)
203
+ - Build / test / publish pipeline: `Makefile`
204
+
205
+ ---
206
+
207
+ ## 6. Things that are NOT conventions
208
+
209
+ Don't propagate from other codebases without re-litigating:
210
+
211
+ - No Python typing (`type: ignore`, `TYPE_CHECKING`, `typing.Protocol`) is
212
+ used pervasively yet. If you start adding annotations, do it
213
+ consistently and run mypy — don't sprinkle them.
214
+ - No linter/formatter is configured yet. Don't reformat existing code
215
+ on unrelated changes.
216
+ - No GitHub Actions, no pre-commit hooks, no automated release
217
+ pipeline. All of these are reasonable next steps but should be
218
+ introduced deliberately, not opportunistically.
visiter-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jakob Stemberger
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.
visiter-0.1.0/Makefile ADDED
@@ -0,0 +1,46 @@
1
+ VENV := .venv
2
+ VENV_BIN := $(VENV)/bin
3
+ VENV_STAMP := $(VENV)/.installed
4
+
5
+ .PHONY: setup test demo build publish test-publish clean help
6
+
7
+ help:
8
+ @echo "Targets:"
9
+ @echo " setup create venv and install package + dev deps"
10
+ @echo " test run pytest"
11
+ @echo " demo run all demos/*.sh (writes to demos/out/)"
12
+ @echo " build build sdist + wheel into dist/"
13
+ @echo " publish upload dist/ to PyPI (requires credentials)"
14
+ @echo " test-publish upload dist/ to TestPyPI"
15
+ @echo " clean remove build artefacts"
16
+
17
+ setup: $(VENV_STAMP)
18
+
19
+ $(VENV_STAMP): pyproject.toml
20
+ python3 -m venv $(VENV)
21
+ $(VENV_BIN)/pip install -U pip
22
+ $(VENV_BIN)/pip install -e ".[dev]"
23
+ @touch $@
24
+
25
+ test: setup
26
+ $(VENV_BIN)/pytest
27
+
28
+ demo: setup
29
+ @command -v dot >/dev/null || { echo "demos require 'dot' (Graphviz) on PATH"; exit 1; }
30
+ @for s in demos/*.sh; do \
31
+ echo "== $$s =="; \
32
+ PATH="$(CURDIR)/$(VENV_BIN):$$PATH" bash "$$s" || exit 1; \
33
+ done
34
+ @echo "all demos succeeded — see demos/out/"
35
+
36
+ build: setup clean
37
+ $(VENV_BIN)/python -m build
38
+
39
+ test-publish: build
40
+ $(VENV_BIN)/python -m twine upload --repository testpypi dist/*
41
+
42
+ publish: build
43
+ $(VENV_BIN)/python -m twine upload dist/*
44
+
45
+ clean:
46
+ rm -rf dist/ build/ src/visiter.egg-info src/*.egg-info
visiter-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: visiter
3
+ Version: 0.1.0
4
+ Summary: Build and visualize iteration graphs from rule-based state transitions.
5
+ Project-URL: Homepage, https://github.com/yaccob/visiter
6
+ Project-URL: Issues, https://github.com/yaccob/visiter/issues
7
+ Author-email: Jakob Stemberger <jakob.stemberger@gmail.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: bfs,graph,graphviz,iteration,state-transition,visualization
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Classifier: Topic :: Scientific/Engineering :: Visualization
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: graphviz>=0.20
20
+ Requires-Dist: sympy>=1.10
21
+ Provides-Extra: dev
22
+ Requires-Dist: build>=1.0; extra == 'dev'
23
+ Requires-Dist: jsonschema>=4.18; extra == 'dev'
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Requires-Dist: twine>=4; extra == 'dev'
26
+ Provides-Extra: validate
27
+ Requires-Dist: jsonschema>=4.18; extra == 'validate'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # VisIter
31
+
32
+ Build and visualize iteration graphs from rule-based state transitions.
33
+
34
+ VisIter is a small library that does two complementary things:
35
+
36
+ 1. **`iterate(start, rules, default=..., max_depth=..., ...)`** —
37
+ applies a list of guard-and-operation Rules to seed values via BFS,
38
+ producing a graph (nodes, edges, per-node depth, optional pseudo-edges
39
+ marking structural boundaries).
40
+
41
+ 2. **`to_dot(graph, ...)`** — turns the graph into a Graphviz
42
+ Digraph for SVG/HTML/PDF rendering, with anchor/radius cropping,
43
+ per-rule edge coloring, wedged-pie node fills for branching nodes,
44
+ and dashed ghost stubs at every kind of cut boundary.
45
+
46
+ The two are independent: any graph dict that fits the documented shape
47
+ can be rendered, and `iterate` can be used purely for graph construction
48
+ without ever touching the renderer.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install visiter
54
+ ```
55
+
56
+ Graphviz must be available on `PATH` for image rendering (the Python
57
+ `graphviz` package wraps the system tool).
58
+
59
+ ## Quickstart — descent with divisor rule
60
+
61
+ A rule that divides by three when applicable, with a `+2` fallback for
62
+ all other values. Every integer path eventually joins one of two small
63
+ cycles (`1 → 3 → 1`, `2 → 4 → 6 → 2`).
64
+
65
+ ```python
66
+ from visiter import iterate, Op, Rule, to_dot
67
+
68
+ graph = iterate(
69
+ start=range(1, 30),
70
+ rules=[Rule(lambda x: x % 3 == 0, Op(lambda x: x // 3, "÷3"))],
71
+ default=Op(lambda x: x + 2, "+2"),
72
+ )
73
+ dot = to_dot(graph, anchor=1, radius=8, direction="backward")
74
+ dot.render("descent", format="svg")
75
+ ```
76
+
77
+ ## Quickstart — reverse binary tree with bound
78
+
79
+ Generate every positive integer up to a ceiling as the binary tree of
80
+ `×2` / `×2+1` successors of 1. `Rule.bound` keeps the expansion inside
81
+ the ceiling; to_dot draws the frontier as dashed ghost stubs.
82
+
83
+ ```python
84
+ from visiter import iterate, Op, Rule, to_dot
85
+
86
+ ceiling = 64
87
+
88
+ graph = iterate(
89
+ start=[1],
90
+ rules=[
91
+ Rule(lambda x: True,
92
+ Op(lambda x: 2 * x, "×2"),
93
+ bound=lambda x: 2 * x <= ceiling),
94
+ Rule(lambda x: True,
95
+ Op(lambda x: 2 * x + 1, "×2+1"),
96
+ bound=lambda x: 2 * x + 1 <= ceiling),
97
+ ],
98
+ default=None,
99
+ )
100
+ dot = to_dot(graph, show_binary=True)
101
+ dot.render("binary_tree", format="svg")
102
+ ```
103
+
104
+ ## CLI
105
+
106
+ A single `visiter` command with subcommands. Each subcommand takes its
107
+ function's keyword arguments as a single Python expression that is
108
+ `eval`'d in a namespace where `Op`, `Rule`, `iterate`, and `to_dot` are
109
+ pre-bound. Output is JSON (`iterate`) and DOT (`to-dot`) on stdout, so
110
+ they pipe directly:
111
+
112
+ ```bash
113
+ visiter iterate 'range(1, 30),
114
+ [Rule(lambda x: x%3==0, Op(lambda x: x//3, "÷3"))],
115
+ default=Op(lambda x: x+2, "+2")' \
116
+ | visiter to-dot 'anchor=1, radius=8, direction="backward"' \
117
+ | dot -Tsvg > descent.svg
118
+ ```
119
+
120
+ The `validate` subcommand checks a graph JSON document against the
121
+ bundled JSON Schema (`schemas/v1/graph.schema.json`, Draft 2020-12):
122
+
123
+ ```bash
124
+ pip install visiter[validate]
125
+ visiter iterate '...' | visiter validate
126
+ ```
127
+
128
+ ## Documentation
129
+
130
+ - [docs/tutorial.md](docs/tutorial.md) — gentle introduction: what
131
+ problem the tool solves, smallest example, what each piece does,
132
+ what the dashed arrows mean. Start here.
133
+ - [docs/manual.md](docs/manual.md) — reference: every parameter,
134
+ every data field, the rendering model in full, design decisions.
135
+ - [demos/](demos/) — runnable end-to-end examples: `make demo` writes
136
+ SVG/PDF/DOT into `demos/out/`.
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,111 @@
1
+ # VisIter
2
+
3
+ Build and visualize iteration graphs from rule-based state transitions.
4
+
5
+ VisIter is a small library that does two complementary things:
6
+
7
+ 1. **`iterate(start, rules, default=..., max_depth=..., ...)`** —
8
+ applies a list of guard-and-operation Rules to seed values via BFS,
9
+ producing a graph (nodes, edges, per-node depth, optional pseudo-edges
10
+ marking structural boundaries).
11
+
12
+ 2. **`to_dot(graph, ...)`** — turns the graph into a Graphviz
13
+ Digraph for SVG/HTML/PDF rendering, with anchor/radius cropping,
14
+ per-rule edge coloring, wedged-pie node fills for branching nodes,
15
+ and dashed ghost stubs at every kind of cut boundary.
16
+
17
+ The two are independent: any graph dict that fits the documented shape
18
+ can be rendered, and `iterate` can be used purely for graph construction
19
+ without ever touching the renderer.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install visiter
25
+ ```
26
+
27
+ Graphviz must be available on `PATH` for image rendering (the Python
28
+ `graphviz` package wraps the system tool).
29
+
30
+ ## Quickstart — descent with divisor rule
31
+
32
+ A rule that divides by three when applicable, with a `+2` fallback for
33
+ all other values. Every integer path eventually joins one of two small
34
+ cycles (`1 → 3 → 1`, `2 → 4 → 6 → 2`).
35
+
36
+ ```python
37
+ from visiter import iterate, Op, Rule, to_dot
38
+
39
+ graph = iterate(
40
+ start=range(1, 30),
41
+ rules=[Rule(lambda x: x % 3 == 0, Op(lambda x: x // 3, "÷3"))],
42
+ default=Op(lambda x: x + 2, "+2"),
43
+ )
44
+ dot = to_dot(graph, anchor=1, radius=8, direction="backward")
45
+ dot.render("descent", format="svg")
46
+ ```
47
+
48
+ ## Quickstart — reverse binary tree with bound
49
+
50
+ Generate every positive integer up to a ceiling as the binary tree of
51
+ `×2` / `×2+1` successors of 1. `Rule.bound` keeps the expansion inside
52
+ the ceiling; to_dot draws the frontier as dashed ghost stubs.
53
+
54
+ ```python
55
+ from visiter import iterate, Op, Rule, to_dot
56
+
57
+ ceiling = 64
58
+
59
+ graph = iterate(
60
+ start=[1],
61
+ rules=[
62
+ Rule(lambda x: True,
63
+ Op(lambda x: 2 * x, "×2"),
64
+ bound=lambda x: 2 * x <= ceiling),
65
+ Rule(lambda x: True,
66
+ Op(lambda x: 2 * x + 1, "×2+1"),
67
+ bound=lambda x: 2 * x + 1 <= ceiling),
68
+ ],
69
+ default=None,
70
+ )
71
+ dot = to_dot(graph, show_binary=True)
72
+ dot.render("binary_tree", format="svg")
73
+ ```
74
+
75
+ ## CLI
76
+
77
+ A single `visiter` command with subcommands. Each subcommand takes its
78
+ function's keyword arguments as a single Python expression that is
79
+ `eval`'d in a namespace where `Op`, `Rule`, `iterate`, and `to_dot` are
80
+ pre-bound. Output is JSON (`iterate`) and DOT (`to-dot`) on stdout, so
81
+ they pipe directly:
82
+
83
+ ```bash
84
+ visiter iterate 'range(1, 30),
85
+ [Rule(lambda x: x%3==0, Op(lambda x: x//3, "÷3"))],
86
+ default=Op(lambda x: x+2, "+2")' \
87
+ | visiter to-dot 'anchor=1, radius=8, direction="backward"' \
88
+ | dot -Tsvg > descent.svg
89
+ ```
90
+
91
+ The `validate` subcommand checks a graph JSON document against the
92
+ bundled JSON Schema (`schemas/v1/graph.schema.json`, Draft 2020-12):
93
+
94
+ ```bash
95
+ pip install visiter[validate]
96
+ visiter iterate '...' | visiter validate
97
+ ```
98
+
99
+ ## Documentation
100
+
101
+ - [docs/tutorial.md](docs/tutorial.md) — gentle introduction: what
102
+ problem the tool solves, smallest example, what each piece does,
103
+ what the dashed arrows mean. Start here.
104
+ - [docs/manual.md](docs/manual.md) — reference: every parameter,
105
+ every data field, the rendering model in full, design decisions.
106
+ - [demos/](demos/) — runnable end-to-end examples: `make demo` writes
107
+ SVG/PDF/DOT into `demos/out/`.
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ # Generate one iteration graph, then render it three different ways:
3
+ # (a) full backward neighborhood of the cycle attractor at 1
4
+ # (b) a tight 2-hop crop around 1 (shows the ghost-stub mechanism)
5
+ # (c) full graph with custom op_colors
6
+ # All three views share the same input data (descent.json), proving
7
+ # that iterate and to_dot are decoupled by the documented graph dict.
8
+ set -euo pipefail
9
+ cd "$(dirname "$0")"
10
+ mkdir -p out
11
+
12
+ DATA="out/descent.json"
13
+ EXPR="$(cat data/descent.expr)"
14
+
15
+ visiter iterate "$EXPR" > "$DATA"
16
+ echo "wrote $DATA"
17
+
18
+ visiter to-dot 'anchor=1, radius=10, direction="backward"' \
19
+ < "$DATA" > out/descent_full.dot
20
+ dot -Tsvg out/descent_full.dot -o out/descent_full.svg
21
+ echo "wrote out/descent_full.svg"
22
+
23
+ visiter to-dot 'anchor=1, radius=2, direction="backward"' \
24
+ < "$DATA" > out/descent_tight.dot
25
+ dot -Tsvg out/descent_tight.dot -o out/descent_tight.svg
26
+ echo "wrote out/descent_tight.svg (tight crop — note the ghost stubs)"
27
+
28
+ visiter to-dot 'op_colors={"÷3": "#a83232", "+2": "#3266a8"}' \
29
+ < "$DATA" > out/descent_recolored.dot
30
+ dot -Tsvg out/descent_recolored.dot -o out/descent_recolored.svg
31
+ echo "wrote out/descent_recolored.svg (custom palette)"
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # Full pipe composition: build a graph, render to DOT, hand to `dot` for
3
+ # layout + format conversion. Two output formats demonstrate that the
4
+ # downstream stage is just a Graphviz invocation.
5
+ set -euo pipefail
6
+ cd "$(dirname "$0")"
7
+ mkdir -p out
8
+
9
+ # Reverse binary tree: from 1, doubling and double-plus-one until 64.
10
+ EXPR='start=[1], rules=[
11
+ Rule(lambda x: True, Op(lambda x: 2*x, "×2"),
12
+ bound=lambda x: 2*x <= 64),
13
+ Rule(lambda x: True, Op(lambda x: 2*x+1, "×2+1"),
14
+ bound=lambda x: 2*x+1 <= 64),
15
+ ], default=None'
16
+
17
+ visiter iterate "$EXPR" \
18
+ | visiter to-dot 'show_binary=True' \
19
+ | tee out/binary_tree.dot \
20
+ | dot -Tsvg -o out/binary_tree.svg
21
+ echo "wrote out/binary_tree.svg"
22
+
23
+ # Same data, PDF instead of SVG.
24
+ dot -Tpdf out/binary_tree.dot -o out/binary_tree.pdf
25
+ echo "wrote out/binary_tree.pdf"
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # Insert schema validation as a pipeline checkpoint. `tee` forks the
3
+ # graph JSON to `visiter validate` (which writes its verdict to stderr
4
+ # / a side file) while the main pipe continues into `to-dot`. If the
5
+ # graph ever drifted from the documented shape — say, after a future
6
+ # breaking change to iterate — this would catch it before rendering.
7
+ set -euo pipefail
8
+ cd "$(dirname "$0")"
9
+ mkdir -p out
10
+
11
+ EXPR="$(cat data/descent.expr)"
12
+
13
+ VALIDATE_LOG="out/validate.log"
14
+ visiter iterate "$EXPR" \
15
+ | tee >(visiter validate > "$VALIDATE_LOG" 2>&1) \
16
+ | visiter to-dot 'anchor=1, radius=10, direction="backward"' \
17
+ | dot -Tsvg -o out/descent_validated.svg
18
+
19
+ # Surface the validator's verdict.
20
+ echo "validator said: $(cat "$VALIDATE_LOG")"
21
+ echo "wrote out/descent_validated.svg"
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ # Iteration on strings, not numbers. Rule: if the word ends in a vowel,
3
+ # drop the last character. Default: none — words ending in a consonant
4
+ # are leaves. Demonstrates that `iterate`'s value type can be any
5
+ # hashable, str()-able Python object.
6
+ set -euo pipefail
7
+ cd "$(dirname "$0")"
8
+ mkdir -p out
9
+
10
+ EXPR='start=["banana", "garage", "queue", "iterator"],
11
+ rules=[Rule(lambda s: len(s) > 0 and s[-1] in set("aeiou"),
12
+ Op(lambda s: s[:-1], "drop-vowel"))],
13
+ default=None'
14
+
15
+ visiter iterate "$EXPR" \
16
+ | visiter to-dot '' \
17
+ | dot -Tsvg -o out/words.svg
18
+ echo "wrote out/words.svg (string-valued iteration)"