# docgen — code to diagram, and to everything else the IR can feed. # # Derived from where this file sits, so the folder can be copied anywhere and # renamed and still work. The logic lives in the Python, never here. # # make book SRC=../station the whole operation, measured at both ends # make check prove docgen, on a tree it builds itself # make check BOOK=out/book/x prove one book — its own level # make ir SRC=../station extract -> out/ir.json (one step, on its own) # make graph out/ir.json -> out/graph.svg # make index out/ir.json -> out/index.md # make self docgen's book of itself, then check it # make doctor what this machine has # # Every target below is one step of a book and still works alone — that is the # property the book spine exists to preserve, not to replace. # # The pipeline is three commands and they compose, which is the point: # # python3 -m docgen.extractors.python --root SRC -o ir.json # python3 -m docgen.ops ir.json --overview -o view.json # python3 -m docgen.emitters dot view.json -o graph.svg --theme dark HERE := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) PKG := $(notdir $(HERE)) PARENT := $(patsubst %/,%,$(dir $(HERE))) PY ?= python3 RUN := PYTHONPATH=$(PARENT) $(PY) -m OUT ?= $(HERE)/out SRC ?= SCHEMA ?= OPENAPI ?= HAR ?= STYLE ?= lucid THEME ?= DEPTH ?= 2 SCALE ?= 0.55 BOOK ?= SLUG ?= # NOT `LANG`: that is the shell's locale variable, so `?=` inherits # en_US.UTF-8 from the environment and --lang rejects it. READER ?= python OVERLAY ?= THEME_ARG := $(if $(THEME),--theme $(THEME)) SLUG_ARG := $(if $(SLUG),--slug $(SLUG)) OVER_ARG := $(if $(OVERLAY),--overlay $(OVERLAY)) .PHONY: help book check ir db code graph index site minimap explore docs view self doctor clean help: ## List every target @echo "docgen — static analysis of a tree, and the artifacts that fall out of it" @echo @grep -E '^[a-z-]+:.*?## .*$$' $(MAKEFILE_LIST) \ | awk 'BEGIN{FS=":.*?## "}{printf " \033[1m%-10s\033[0m %s\n", $$1, $$2}' @echo @echo " SRC=/path/to/tree what to read OUT=/path where output goes" @echo " SCHEMA=schema.json a database instead STYLE=lucid THEME=dark|lucid" @echo " OPENAPI=spec.yaml an API document HAR=session.har a recording" @echo " BOOK=/path where a book goes, and which book to check" @echo " READER=python|code ast, or tree-sitter SLUG=name what to call the book" @echo " OVERLAY=overlay.json hand-written notebook additions, re-applied every build" @echo " DEPTH=2 how deep to draw" @echo @echo " Three levels of test, by what they assert about:" @echo " make doctor the machine. Never fails." @echo " make check docgen. Exits 1." @echo " make check BOOK= that book. Exits 1." check: ## Prove docgen (or one book, with BOOK=) @if [ -n "$(BOOK)" ]; then \ $(RUN) $(PKG).book.checks "$(BOOK)"; \ else \ $(PY) $(HERE)/selftest.py; \ fi book: ## SRC (or SCHEMA/OPENAPI/HAR) -> one operation, measured at both ends @test -n "$(SRC)$(SCHEMA)$(OPENAPI)$(HAR)" \ || { echo "Error: set SRC=/path/to/tree (or SCHEMA=, OPENAPI=, HAR=)" >&2; exit 1; } @$(RUN) $(PKG).book \ $(if $(SRC),--root "$(SRC)" --lang $(READER)) \ $(if $(SCHEMA),--schema "$(SCHEMA)") \ $(if $(OPENAPI),--openapi "$(OPENAPI)") \ $(if $(HAR),--har "$(HAR)") \ -o "$(if $(BOOK),$(BOOK),$(OUT)/book)" \ --style $(STYLE) $(THEME_ARG) $(SLUG_ARG) $(OVER_ARG) ir: ## Extract SRC into OUT/ir.json @test -n "$(SRC)" || { echo "Error: set SRC=/path/to/tree" >&2; exit 1; } @mkdir -p $(OUT) @$(RUN) $(PKG).extractors.python --root "$(SRC)" -o $(OUT)/ir.json @$(RUN) $(PKG).ir $(OUT)/ir.json db: ## Extract a graphgen-compatible SCHEMA into OUT/ir.json @test -n "$(SCHEMA)" || { echo "Error: set SCHEMA=/path/to/schema.json" >&2; exit 1; } @mkdir -p $(OUT) @$(RUN) $(PKG).extractors --schema "$(SCHEMA)" -o $(OUT)/ir.json @$(RUN) $(PKG).ir $(OUT)/ir.json view: ## OUT/ir.json -> OUT/view.json, the default view for its source type @$(RUN) $(PKG).ops $(OUT)/ir.json --overview -o $(OUT)/view.json graph: view ## OUT/view.json -> whatever its structure asks for @$(RUN) $(PKG).emitters auto $(OUT)/view.json -o $(OUT) \ --style $(STYLE) $(THEME_ARG) site: view ## OUT/view.json -> a self-contained docs site in OUT/site @$(RUN) $(PKG).emitters site $(OUT)/view.json -o $(OUT)/site \ --style $(STYLE) $(THEME_ARG) @echo " open $(OUT)/site/index.html" docs: ## Regenerate the figures in docs/ — docgen documented by docgen @mkdir -p docs/img @$(RUN) $(PKG).extractors.python --root $(HERE) -o /tmp/$(PKG)-docs.json >/dev/null @$(RUN) $(PKG).ops /tmp/$(PKG)-docs.json --overview -o /tmp/$(PKG)-docs-view.json >/dev/null @$(RUN) $(PKG).emitters dot /tmp/$(PKG)-docs-view.json -o docs/img/architecture.svg -q @$(RUN) $(PKG).emitters minimap /tmp/$(PKG)-docs.json -o docs/img/minimap.svg --scale 0.5 --width 860 @$(RUN) $(PKG).emitters erd $(OUT)/ir.json -o docs/img/erd.svg 2>/dev/null \ || echo " (erd figure kept — needs a schema IR at $(OUT)/ir.json to refresh)" @PYTHONPATH=$(PARENT) $(PY) -c "from $(PKG).emitters.site import VIEWER, _slots, _fill; \ from $(PKG).style import Style; import pathlib; \ pathlib.Path('$(HERE)/docs/viewer.html').write_text( \ _fill(VIEWER.replace('__TITLE__', 'docgen docs'), _slots(Style.load('lucid'))))" @echo " open $(HERE)/docs/index.html" explore: ## OUT/ir.json -> OUT/explore/ — navigate on one side, explore on the other @$(RUN) $(PKG).emitters explore $(OUT)/ir.json -o $(OUT)/explore \ --style $(STYLE) $(THEME_ARG) --scale $(SCALE) @echo " open $(OUT)/explore/explore.html" minimap: ## OUT/ir.json -> OUT/minimap.svg — what is where, read from the colours @$(RUN) $(PKG).emitters minimap $(OUT)/ir.json -o $(OUT)/minimap.svg \ --style $(STYLE) $(THEME_ARG) --scale $(SCALE) code: ## Extract C#/TypeScript from SRC (needs tree-sitter) @test -n "$(SRC)" || { echo "Error: set SRC=/path/to/tree" >&2; exit 1; } @mkdir -p $(OUT) @$(RUN) $(PKG).extractors code --root "$(SRC)" -o $(OUT)/ir.json @$(RUN) $(PKG).ir $(OUT)/ir.json index: ## OUT/ir.json -> OUT/index.md and OUT/sidebar.json @$(RUN) $(PKG).emitters index $(OUT)/ir.json -o $(OUT)/index.md @$(RUN) $(PKG).emitters index $(OUT)/ir.json -o $(OUT)/sidebar.json self: ## docgen's book of the widest tree it can see, then check it @$(eval SELF_SRC := $(shell PYTHONPATH=$(PARENT) $(PY) -c "from $(PKG) import reference; \ r = reference.root(); print(r if r else '$(HERE)')")) @echo " self-hosting on $(SELF_SRC)" @$(MAKE) --no-print-directory book SRC=$(SELF_SRC) SLUG=self \ BOOK=$(OUT)/book/self OUT=$(OUT) @echo @$(MAKE) --no-print-directory check BOOK=$(OUT)/book/self doctor: ## Report whether this machine can run it @printf 'python : '; $(PY) --version 2>&1 || echo MISSING @printf 'dot : '; (dot -V 2>&1) || echo 'MISSING — sudo apt install graphviz (only to render)' @printf 'tree-sit : '; $(PY) -c 'import tree_sitter, tree_sitter_c_sharp, tree_sitter_typescript; print("ok — C# and TypeScript available")' 2>/dev/null || echo 'absent — Python only. pip install tree_sitter tree_sitter_c_sharp tree_sitter_typescript' @printf 'networkx : '; $(PY) -c 'import networkx; print(networkx.__version__ + " — for lab/ experiments")' 2>/dev/null || echo 'absent — only used in lab/' @printf 'package : %s (from %s)\n' '$(PKG)' '$(PARENT)' @printf 'yaml : '; $(PY) -c 'import yaml; print("ok — needed only to read OpenAPI")' 2>/dev/null || echo 'absent — only used by the OpenAPI reader' @printf 'reference: '; PYTHONPATH=$(PARENT) $(PY) -c "from $(PKG) import reference; print(reference.describe())" @printf 'styles : '; $(RUN) $(PKG).style 2>/dev/null \ || $(RUN) $(PKG) 2>/dev/null \ || PYTHONPATH=$(PARENT) $(PY) -c "from $(PKG).style import Style; print(', '.join(Style.available()))" @PYTHONPATH=$(PARENT) $(PY) -c "import $(PKG).ir, $(PKG).emitters.dot, $(PKG).ops" >/dev/null 2>&1 \ && echo 'import : ok' || echo 'import : FAILED — is the folder intact?' clean: ## Delete OUT. Nothing else is ever written to @rm -rf "$(OUT)" && echo "Removed $(OUT)"