Skip to content
Back to skills

Visualizing Codebases

ASecurity

Use when asked to map, visualize or diagram a codebase — which services, modules, APIs, events and databases it has and how they connect, how data moves between methods, or to refresh a code map after new commits. Covers code-map.json, incremental rescans, deployables vs shared libraries, call matching between services, side effects, pinned traces and examples. Keywords: code map, architecture map, service graph, lineage, viz-code.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 25, 2026
developmentjavascripttypescriptpythonrustgojavarubyphpkotlinc#

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 20 files and shows the line behind each finding

Scanned October 25, 2026

npx -y skills add si1verwind17/codebase-map --skill visualizing-codebases --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Visualizing Codebases?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Visualizing Codebases
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/si1verwind17-visualizing-codebases/badge)](https://www.skillsdirectory.com/skills/si1verwind17-visualizing-codebases)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: visualizing-codebases
description: >-
  Use when asked to map, visualize or diagram a codebase — which services,
  modules, APIs, events and databases it has and how they connect, how data
  moves between methods, or to refresh a code map after new commits.
  Covers code-map.json, incremental rescans, deployables vs shared
  libraries, call matching between services, side effects, pinned traces and
  examples. Keywords: code map, architecture map, service graph, lineage, viz-code.
---

# Visualizing codebases

Scan the **committed** code of one repository into `code-map.json`: a tree of
nodes on zoom floors (services → packages → classes and files → methods),
the relations between them (calls between services, events, streams, batch
jobs, DB reads and writes, code imports), pinned traces to start reading
from, and example values for its entry points and data types. A viewer
renders the file; this skill only produces it. Scripts do the deterministic
structure pass; you do the meaning pass, which needs judgment.

Baseline: Python ≥ 3.10.12, `tree-sitter` 0.26.0,
`tree-sitter-language-pack` 1.20.0; source languages Scala, Python, Java,
Kotlin, TypeScript, JavaScript (with the `<script>` blocks of `.vue` and
`.svelte` files), Go, C#, Rust, Ruby and PHP.
Framework knowledge is data: built-in packs plus the `OUT/stack.yaml` you
write for each repo (§ Stack).
Field-by-field format, ids and a complete example: `format.md`. The files in
`references/` hold detail a step needs only sometimes: read one when a step says so.
Asked to send feedback on the skill (`send-feedback`, or in plain words):
follow `feedback.md` instead of a run.

## Setup

`SKILL_DIR` is this skill's folder. Create the environment once per machine:

```bash
python3 -m venv ~/.cache/viz-code-venv
~/.cache/viz-code-venv/bin/pip install -r SKILL_DIR/scripts/requirements.txt
PY=~/.cache/viz-code-venv/bin/python
```

Node 18+ is optional: it lets the skill check the drawing.

## Run

`REPO` is the project folder: the git top level, or a subfolder of a larger
repository (a monorepo project, a demo) — pass its path to map only that subtree.
In a subfolder scan, changes outside the folder (compose or k8s files at the
repository root) never mark facts stale: after changing them, rescan in full.

Ask the user for the output directory; default `docs/viz-code` (`OUT` below,
inside the repo).

Before your first look at the repo: config, compose and env files are never
`cat`ed, `head`ed or globbed into a listing with source files. Read them only as
§ Never read secrets says (`config.py`, key names only for `.env`), from the
very first command.

1. `$PY SKILL_DIR/scripts/extract.py REPO --out-dir OUT` — writes
   `structure.json` and `summary.md`. `--out-dir` is relative to `REPO`,
   not to the current directory. Add `--exclude GLOB` for generated or
   vendored code the defaults miss: `extract.py` adds the glob to
   `stack.yaml`'s `exclude` list, which every later run applies, so a rescan
   needs no flag (delete a glob there to scan its files again). In every language the defaults drop
   vendored code: every file under a folder named `vendor` or `third_party`,
   under a root `node_modules`, and minified JavaScript bundles (most of the
   file on lines over 1000 characters). In every language but Python and
   Scala they also drop build output: every file under a folder named
   `dist`, `build`, `out`, `bin`, `obj`, `.gradle`, `.next` or `coverage`
   (except Rust's `src/bin`, Cargo's extra binaries), `*.min.js`, `*.d.ts`,
   EF Core's `Migrations/*.Designer.cs` and `*ModelSnapshot.cs`, Gradle
   scripts (`*.gradle.kts`), and each language's test
   files (`*.test.ts`, `*.spec.ts`, `*_test.go`, `*_spec.rb`, `spec/`,
   `*Test.java`, `*Tests.cs`, Rust `tests/`, …), and inside a Rust source
   file every item under `#[cfg(test)]` (a `mod tests`). No flag brings such a
   folder back: when `summary.md`'s excluded list holds real source from
   one (a package named `build`), name it in `problems`; renaming the
   folder is the user's call. A generated file (a generator's marker in its
   first 40 lines, such as `Code generated … DO NOT EDIT`, `@generated`,
   `<auto-generated>` or `Generated by the protocol buffer compiler`, or a
   protoc output name such as `*.pb.go` or `*_pb2.py`) is scanned for its
   public data types and the routes it registers (with their handlers)
   only: its other classes, methods and calls are left out, and
   `summary.md` lists it.
2. If `OUT/code-map.json` exists: `$PY SKILL_DIR/scripts/diff.py REPO --out-dir OUT`.
   - `none` → the code is unchanged; `diff.py` has refreshed the page and the
     audit, and written `meaning.json` from the map. Read `OUT/map-notes.md`:
     if a note the format can express is not yet on the map, apply it to
     `meaning.json` and continue from step 4. Do the same when the export
     had a `## Pins` section (`references/review.md`).
     Otherwise report the commit, the audit line and each note as applied
     or "not supported yet", then stop.
   - `rules` → the code is unchanged but the map predates newer meaning
     rules; `meaning.json` holds every fact, no page or audit refresh. Read
     `OUT/map-notes.md` and apply its notes as in `none`. For each
     `stale.json` `rules.recheck` entry (`Rules <N>: <entry>`), re-check the
     facts it names, or look in the code for the new kind of fact it adds,
     using the section it points to (a section moved out of this file is in
     `references/`: `stack.md` for § Stack's patterns, `interactions.md` for the
     queue sections, `review.md` for review comments and map notes). Leave other facts alone.
     Continue from step 4.
   - `incremental` → `meaning.json` holds every fact; the ones the change
     touched carry `"stale": true`: a line they cite or the method around it
     changed, an end node is gone or moved, or their route changed
     (`stale.json` lists them, and its `why` says what changed for each; `causes` counts the facts each reason
     flags, so work through it a cause at a time, and a gone method's `now <id>?` names its likely successor).
     Evidence on unchanged lines already points at their new place. Re-resolve
     each flagged fact against the new code: keep it
     (delete the flag), change it (edit, delete the flag), or drop it. A
     flagged example is never just kept: its class's fields or its handler's
     signature changed, so rewrite its value from the current ones
     (`stale.json` names the fields a type's value lacks, `value_lacks`, and
     the keys that are no longer fields, `value_extra`), or delete it when its
     class or handler is gone (§ Examples). Look at added source files for new facts. Leave
     unflagged facts alone.
     `assemble.py` refuses the map while any flag remains.
   - `full` → do the whole meaning pass; `stale.json` `reason` says why (for
     example a scanner version change, `stack.yaml`, a new language, a
     deployable or build file added or removed, a compose service's build or
     start command or a Dockerfile's `ENTRYPOINT`/`CMD` changed, a module added
     or removed; a dependency bump is not a reason). If `meaning.previous.json`
     exists, start from it: copy it to `meaning.json`, then resolve every
     `stale` flag (`why` as above). A scanner upgrade adds finer nodes (`val`
     methods, `$impl` classes, nested DAG tasks, proto keys) — re-point facts
     to them where they are more exact, look up ids with `find.py`, and do a
     full pass for anything new in `summary.md`. Never carry a fact you have
     not checked against the new structure.
   - For `incremental` and `full`, also apply `rules.recheck` when present
     (as in `rules`). `recheck: null` (`rules.floor` in `stale.json` names the oldest
     `CHANGES.md` section) means the map is older than that entry: the full
     pass covers it.
2b. Stack: before going on as step 2 says (in every mode but `none`), and
   on a first run right after step 1: if the Stack section at the top of
   `summary.md` does not read `nothing unrecognised`, do § Stack, then
   re-run step 1. Why after diff: the diff compares the map with the
   `stack.yaml` it was made with, and this run's assemble stamps your edits.
3. If `OUT/map-notes.md` exists, read it first and apply its free-text notes
   (`references/review.md`). Then the meaning pass (rules below) → write
   `OUT/meaning.json`.
4. `$PY SKILL_DIR/scripts/assemble.py REPO --out-dir OUT`. On exit 1 fix
   `meaning.json` from the printed errors and rerun until it exits 0. It also
   lays out every floor; items keep their positions from the previous map so
   the reviewer's mental map survives. Pass `--relayout` only when the user
   asks for a fresh arrangement. On exit 0 it also writes `OUT/index.html`:
   the prebuilt viewer with this map built in, when the map is 8 MB or less
   minified; a larger map is left out and the reader opens `code-map.json`
   from the page (the printed path says so: repeat it in the report). Exit 3 means the map is valid
   but the page was not written (the message says why): report it, do not
   touch `meaning.json`.
5. `assemble.py` prints the audit line last and writes `OUT/audit.md`. Fix
   each `ext.missing`, `external.unused`, `deployable.unscanned`, `queue.missing`, `request.unmatched`, `data.two-way-call`, `module.empty`, `example.missing`,
   `route.file-handler`, `route.setup-handler` and `pin.few` content warning
   in `meaning.json` and rerun `assemble.py`. An `ext.missing` key that names no
   outside endpoint (a test helper's, a tool's own setting such as `DOCKER_HOST`)
   gets a `problems` entry naming the key and why (an entry is
   `{"text": "...", "nodes": ["<node id>", ...]}`, `nodes` optional); that clears it. So does a
   `queue.missing` site that is no hand-off (`references/interactions.md`): name its `file:line`. A route whose handler is
   a file or the function that registers it (`route.file-handler`, `route.setup-handler`) gets its real handler
   from a `stack.yaml` handler group (an inline handler is a node of its own, in a function or at the file's top);
   when the code has none (a routes file read by the framework), a `problems` entry naming the handler id or a route's `file:line` says so and clears it. Report `name.container` and
   `pin.start-missing` warnings and audit errors (`audit.error`); do not fix
   them. Map limits and findings are reported, not fixed. The drawing checks
   stop at a budget on large maps (the line says how many floors were not
   checked); `--full-audit` checks every floor, slower: run it when the user asks.
6. Open `OUT/index.html` in a browser. (`view.py REPO --out-dir OUT` rewrites
   only the page, for when the viewer changed but the map did not.)
7. Report: commit scanned, the counts line it printed (paste that line as it is, never a retelling; when the user asks
   for one line of counts, it is that line), every entry in `problems`,
   the audit line, each map note as applied or "not supported yet", each
   `rules.recheck` entry applied, the number of pins and examples, each
   `## Pins` line applied, the `stack.yaml` entries added this run,
   each Stack line left unresolved with its reason,
   the path of `index.html`, a line pointing to the field guide at
   https://si1verwind17.github.io/codebase-map/, and remind the user to commit
   `OUT/code-map.json`, `OUT/stack.yaml`, `OUT/README.md`, `OUT/.gitignore`
   and `OUT/.gitattributes` (a `code-map.json` over 100 MB is more than GitHub
   takes in one file: give its size and leave the choice, such as Git LFS, to the user). `README.md` is the skill's, rewritten each run;
   `.gitignore` and `.gitattributes` are the skill's too but keep any line
   the user adds; `map-notes.md` is the user's; all are committed. The skill
   writes nothing outside `OUT`. Other files in `OUT` (`structure.json`, `summary.md`,
   `meaning.json`, `meaning.previous.json`, `stale.json`, `index.html`,
   `audit.md`) are working files, already gitignored.
   `CHANGES.md` is the skill's, not the user's: never edit it.
   When the run left something the skill could not handle — an audit
   error, a map limit, a content warning you could not clear, a Stack line
   left unresolved, a note "not supported yet", a script that failed — end
   the report with one paragraph: the skill's maintainers would like to
   hear about it to improve the skill; the user can send it with
   `send-feedback`; nothing goes without their review and yes. Do not start
   it yourself.

Read `summary.md`, not `structure.json` (it can be megabytes). Look up ids with
`$PY SKILL_DIR/scripts/find.py OUT/structure.json TEXT [--kind method]`.
Read source files **at the scanned commit**
(`git -C REPO show <commit>:./<path>`; commit and scanned folder in the
`summary.md` header), and config and compose files at it through
`$PY SKILL_DIR/scripts/config.py --repo REPO --commit <commit> <path> ...`
(§ Never read secrets), not from the working directory, which may hold someone
else's uncommitted edits. Keep the `./`: without it git resolves the path from
the repository's top level, and in a subfolder scan reads the wrong file
without an error.
`summary.md` also lists excluded files (tests, docs, vendored code), what it
did not scan (§ Not scanned near the top: source in languages without a
reader, by language and folder, the deployables made mostly of it, and the images of a stock database, broker,
search or monitoring server, which are the datastore or broker node they run, never a service), and
FastAPI `include_router` calls: the scanner joins their prefixes onto the
included router's routes (`composed onto N routes`); compose only those
marked `not composed` yourself.

## Committed code only

The scan reads the commit at `HEAD` through `git archive`, never the working
directory, and stamps that one commit hash; `assemble.py` refuses if `HEAD`
moved since `extract.py`. If the user expects uncommitted work in the map,
tell them to commit it first. `git archive` also leaves out files marked
`export-ignore`, submodule contents, and Git LFS file contents (it yields the
pointer): name any of these in `problems` when the repo uses them. Why: one hash identifies
exactly what the map shows, and another agent editing files during the scan
cannot leak half-written code into it.

## Never read secrets

Never print a value that may be a credential. Use config **key names**
(`ORDER_SERVICE_URL`, `DB_HOST`) as identifiers, never their values. Why: the
map is committed and shared, and a value read once ends up in evidence, ids or
problems. Which files you may read, and how:

- **Templates, read in full:** `.env.example`, `.env.sample`, `.env.template`
  and any `*.example.*`, `*.sample.*` or `*.template.*` config file.
- **Never opened:** `secrets*` files, key stores, certificates and private keys.
- **Key names only:** real env files (`.env`, `.env.*`, `*.env`), committed or
  not. List the keys with their line numbers, never the values:
  `grep -noE '^[[:space:]]*(export[[:space:]]+)?[A-Za-z_][A-Za-z0-9_.-]*[=:]' FILE`.
- **Every other config file** (YAML, `.properties`, TOML, INI, JSON, HOCON `.conf`,
  hjson, compose files, HTTP request files `.http`/`.rest`): read it only through
  `$PY SKILL_DIR/scripts/config.py FILE [FILE ...]`, which prints each line with
  every value that may be a credential hidden (a key naming a password, secret,
  token, API key or private key; a token-shaped value; a PEM block; the user and
  password inside a URL or a connection string). Hosts, paths, ports, crons,
  service names and a DSN's host and database stay readable; add
  `--repo REPO --commit <commit>` to read the files at the scanned commit.

Any other read of these files (`cat`, `head`, a `grep` or `awk` that prints
lines, a file read) prints the values: not allowed. A command that reads many files at once (a loop or glob over
`git ls-files`, `cat */*`, `grep -r`) reaches them too: leave config and env files
out of its list first, and search code with `git grep` over source paths.

Source code can hold a credential too (a hard-coded key, a seed script's password, a test fixture's token): when
one shows in what you read, never repeat its value anywhere (output, evidence, ids, problems); cite its `file:line`.

A relation whose target is only in a value you may not read stays `inferred`:
cite the key's line as evidence and name the key in `why`.

## Deployables, not folders

A `service` is something that runs. Find them in whatever the repo uses to
run things: compose files, Dockerfiles, Kubernetes or kustomize manifests,
Helm charts, orchestrator or app-host code that registers projects, a
`Procfile`, a worker command in a Makefile, a README or a script; cite the
one you found. A worker with no deploy file (a job or queue worker started by
a command) is still a `service`: its evidence is its entry point and the line
that starts it, and a `problems` entry says no deploy file runs it (the
service is inferred). Shared code with no deployment of its own is a `library`. Read
these first, then claim every module in
`summary.md` from a service or library (`modules: [...]`). A module is an sbt
project, a Python package or script folder, or the nearest folder holding a
build file of the language's ecosystem (`pom.xml`, `settings.gradle`,
`package.json` and its workspaces, `go.mod`, `Cargo.toml`, `*.csproj`,
`Gemfile`, `composer.json`), else the top folder; its `summary.md` heading
names the language and build. When one folder holds two languages, the
second one's module is `<path>#<lang>`: claim it under that name
(`"modules": ["dashboard#typescript"]`), as the heading writes it without `mod:`. Shared code that
services import is a `library` even when a Dockerfile copies it into their
image or a compose volume mounts it. Only application code that two deployments each run as their own (a
workflow module registered into two orchestrator projects) is claimed by both
services: its code sits under the service whose first-listed module it is,
and the other shows a twin of it, reported in `problems`. So are the roles one
package or jar runs as (a web server, its job worker and scheduler; a master
and its workers): one service per role, each citing its start line. A relation
one role makes to another starts at the service of the role making it, with
`via` naming the method in the shared code, and ends at the method that
handles it when the receiving role is the one the code sits under, else at
the receiving role's service. Why: a relation from a method under the first
service would credit that role with what another one does. When several roles
list the module first, its code sits under the one with the lowest id (stable
across rescans): give the role that serves the traffic the id that sorts first
(`svc:app` before `svc:worker`). An id need not repeat the `name`
(`svc:trainer` named `trainer-http`, beside `svc:trainer-grpc`).
When one module builds several deployables from code of their own (one
go.mod with `cmd/<svc>` and `internal/<svc>`, each built by its own compose
service or Dockerfile), each service lists the module and claims its own
packages: `"packages": [".::cmd/orders/", ".::internal/orders/"]` (package
ids from `structure.json`; a package claims those below it). Those sit under
it and its relations end at their methods; the packages no service claims
(shared `pkg/`) stay with the module's owner and show as twins in the others.

A browser app (an SPA, a WASM frontend) is a `service` even when the backend
builds it into its image or serves its files: it runs elsewhere, in the
browser. It cites the line that builds or serves it (the build step, the
static route), claims its modules, and its requests start from it.

When a library's code reads or writes a table for a service, that is one
relation per (service, table, direction): one `via` (the library method that
does most of it) and the call sites of the others in its `evidence`.
Every service cites the line that deploys it in `evidence` (validation
fails without it). Unclaimed modules are defaulted to libraries and reported. Why: drawing a
shared library as a service invents traffic that never crosses a network.

A deployable in a language the scanner does not read (`summary.md` § Not
scanned: a proxy or server config, an app in another language, a collector
built from its config) is still a `service`, never an `ext:` placeholder: it
has no modules, and its relations come from reading its code and config,
`inferred`, citing the lines. The audit's `deployable.unscanned` lists each
one no service cites (its Dockerfile, a line of its compose service, or its
compose service's name) and no `problems` entry names (the folder or its
Dockerfile).

Hosts written as service-registry or gateway route names resolve through the
registry or gateway config the repo has: a route the gateway's config declares
(`summary.md` § Gateway routes lists the ones the scanner reads; a target
written `${CART_HOST}` resolves through the env or compose file that sets it)
is a `request` from the gateway service to the target service, citing the
config line. A call to infrastructure with no route of its own (a config
server, a service registry, a tracing collector) is an `ext-call` to that
service when it is in the repo, or left out as platform plumbing: say which in
`problems`. A store or service chosen per deployment (kustomize overlays,
profiles, feature flags in config) gets `deployment_dependent: true` on each
relation to it, and every alternative is listed.

When code inside a library does anything at runtime (a call, an event, a
DB read or write) the actor is the service that imports and runs it: write
the relation `from` the service, with `via` naming the library method, once
per service that runs it. `assemble.py` rejects runtime relations that start
inside a library.

Workflow orchestrators (Flyte, Airflow, Dagster, Prefect) deploy by
registration, not by compose: each registered project or DAG group is a
service, with the registration line as its evidence. Jobs the docs say to
launch by hand on the orchestrator (`pyflyte run`, `airflow dags trigger`)
form one more service (e.g. `adhoc`), with that doc line as evidence; when
they sit in the same folder as scheduled jobs, both services claim that
module and the code sits under the one a deploy file runs (the scheduler),
the hand-run service showing a twin. A job that triggers another job
(`reference_task`, `TriggerDagRunOperator`, an external task sensor) is a
`batch` relation to the target task method, keyed `task <name>`: from the
calling service when the target job is in another service, from the calling
job's method when the target job is in the same service. The platform the
code runs on (the orchestrator itself, a Spark cluster, a Hive metastore,
Kubernetes) is not a node: name it in `problems` only when a reviewer needs to know. Why: the map
shows what the application does; platform boxes would sit on every floor and
say nothing.

## Matching interactions

Match an outbound call to an inbound route, topic or table by its **protocol
key**: HTTP method + path template, topic name, gRPC service/method, table
name. The structure pass already lists routes per module (the packs' frameworks,
`format.md` → Stack, `stack.yaml`, and the routes Next.js, SvelteKit and Nuxt derive from file paths; methods upper-cased, and without the
prefixes of mounts it could not join, which you compose onto them yourself (it joins a mount in its
own file, and one whose router another file exports through an import the mount's file names); a prefix only config
sets, such as a servlet context path or a URI version, goes into `stack.yaml` `base_paths`
once per deployable folder, citing the config line in the report; a segment written as a
constant is kept as `{Name}`, `/{Asset}/{id}` from `@Controller(RouteKey.Asset)`: look the
constant up and write its value in `request` and example keys; a path added to a name in the call,
`r.Get(options.BaseURL+"/x", h)`, is listed by its literal with (after `options.BaseURL`): find what the
name holds and compose it as a mount's prefix), URL literals, env keys, SQL tables, table pattern matches and client libraries. It also
lists outbound requests as `http clients` (method + path: match them to
routes like a URL): tapir client endpoints, calls through the code's own HTTP
helpers (a function passing its parameter on as a client call's address, so
`http.get(`albums/${id}`)` is `GET /albums/{id}`), client calls with a path, and
request objects holding a `method` and a `url`/`path`/`endpoint` (a generated client's
`request(cfg, {..})`, a query definition's `({method, url})`); § Likely requests pairs each such call, and each
gRPC client call, with the repo routes or rpc servers it likely reaches (by method and path, or proto key; a
parameter or a `*` segment of one side matches a parameter of the other; the `key` is the route's spelling), every
match listed: the scanner never writes the `request`, you confirm each pair against the code and the address the
caller is given and write it (the audit's `request.unmatched` names a paired call that starts none);
SQL kept outside the code (a mapper XML's statements, as SQL facts of the interface
method each names; the tables `.sql` files and ORM schemas and migrations create (Rails `create_table`, Laravel
`Schema::create`, Knex/Sequelize, EF Core, Liquibase, Django `CreateModel`), in § Tables defined in SQL, schema and
migration files), the routes proxy and gateway configs send on (nginx `location` + `proxy_pass`, Spring Cloud
Gateway, Envoy, YARP: prefix, target, rewrite, in § Gateway routes), framework-implemented interfaces (a repository or a
declarative HTTP client: every method bodyless, in a file importing a database or
HTTP client library) with their bases, annotation and methods: each method is
where a `db-read`/`db-write` (the table from the entity type the base names) or a
`request` (the path from the method's annotation) starts, and a call to a method
only the library base supplies (`repo.save`, `findAll`) has no node, so that
relation starts at the calling method, the gRPC services and rpcs of every `.proto` file as keys
`grpc <package>.<Service>/<Rpc>` (each once, with every `.proto` that declares it)
with the client and server ends tied to them, named as protoc's gRPC plugin names
generated code in Python, Java, Kotlin, Scala, Go, C# and JavaScript/TypeScript
(table in `scripts/vizcode/proto.py`; both ends found and keys equal →
`confirmed`; copy a key exactly as `summary.md` writes it), and each Airflow DAG's id,
schedule and tasks, each Flyte workflow's launch plan schedule and every
`reference_task`.

- `confirmed` only when both ends are in code or config and the keys match.
  Anything you reasoned is `inferred`, with a `why` a reviewer can check.
- A base URL that comes from config resolves through config files, compose
  files and env templates, read as **Never read secrets** allows; a compose
  `${KEY}` set only in a real `.env`, or a value in a hidden `environment:`
  block, resolves no further than its key name.
  A `confirmed` relation cites that config line in
  `evidence` next to both code ends. When the target differs per deployment,
  set `deployment_dependent: true` instead of choosing one.
- A target outside the repo becomes a placeholder node: `ext:http:<config key>`,
  `3p:<vendor>`, `db:<engine>:<config key>`, `topic:<broker>:<name>`. These
  keys let a later merge join maps of several repos. Name an `ext:`
  placeholder by its config key; when no config key exists, by the deployment
  that runs it (the compose service, the k8s deployment, the folder of an
  unscanned app): `ext:http:dashboard-frontend` for the app in
  `deployment/dashboard`. Never a bare role word: `ext:http:dashboard` for a
  frontend is ambiguous, so say which deployment. Notifications count:
  a client posting to a URL from config (`DISCORD_WEBHOOK_URL`, a Slack or
  Telegram token, SMTP settings) is an `ext-call` to `3p:<vendor>`, from each
  service that runs it (with `via` when the client lives in a library).
- An inbound webhook (a provider calling the service) is a `request` from the
  `external` (e.g. `3p:stripe`) to the service route that receives it,
  citing the route and the provider config if any.
- A message to an actor or a sharded entity (Akka, Orleans, shardcake) is a
  `request` from the sending method to the handler the entity hands that
  message type to, key `<framework> <entity type>`, citing the send, the
  registration and the dispatch. It may run on another replica of the same
  service; name matching never joins the two, so without it a trace stops at
  the send.
- Kinds: `request`, `event` (always through a `topic` node), `stream`
  (websocket, SSE, gRPC streaming; `bidirectional` when both ways), `batch`
  (scheduled or one-shot bulk jobs), `db-read`, `db-write`, `ext-call`.
  Never add a runtime relation for an import, and never add `calls`: the
  scanner writes `uses-code` for imports and resolves method-to-method
  `calls` by name itself. A call through an interface or trait ends at its
  method in `relations`; a trace goes on from there into each class that
  `implements` it (a Go type with the interface's methods too), so a
  relation starting at an implementation is reached.
- A generic job whose tables come from inputs (one replication task, many
  tables) is one relation with a wildcard key (`table sales.*`); list in
  `tables` the concrete tables its callers pass in (literal arguments, or
  variables set right before the call) or that configs name. This holds when
  every table is known: a caller passing three literal tables still gets one
  read and one write, each with a wildcard key and those three in `tables`,
  never a relation per table.
- `data` says which way data moves: `forward` (from → to), `back` (to → from)
  or `both`. Kinds default (db-read `back`, the rest `forward`); a `stream`
  has no default and must be set: a subscription the service opens is `back`.
  A call's reply is not data moving back: a `request` or `ext-call` stays
  `forward`, from the caller, whatever it returns (`both` draws a two-way belt,
  as if each end called the other); a flow both ways is a `bidirectional`
  stream. The layout orders the production line by it.

Read these optional facts from code and config. Each is set only on evidence
you can cite; leave it out when unsure. Why: the viewer draws the shape from
them, and a guessed shape is worse than a plain one. When you set a node
fact (`delivery`, `dead_letter_of`, `replicas`, a `store` override), put the
line that shows it in that node's `evidence`; `assemble.py` refuses it
without. Node facts go stale like relations when an evidence line changes.

| Fact | Signal in code or config |
|---|---|
| topic `delivery` `queue` | SQS, a RabbitMQ queue, Celery: one consumer takes each message |
| topic `delivery` `fan-out` | Kafka topic, Pub/Sub topic, SNS: every group or subscription gets a copy. A Pub/Sub pull subscription with one consumer is still `fan-out`; its relation carries `subscription: "pull"` |
| `subscription` `push` | Pub/Sub subscription with a `push_endpoint`; SNS to HTTP |
| `subscription` `pull` | a Pub/Sub pull subscription (`subscriber.pull`). Kafka, SQS and RabbitMQ consumers: leave `subscription` out; absent means the broker's normal consume |
| `group` | Kafka `group.id`; the subscription name. Set `subscription` and `group` only on the event from the topic (the consume side) |
| dead letter | `deadLetterPolicy`, a redrive policy, or a `*.dlq` topic produced on failure: the DLQ is a topic with `dead_letter_of` the source topic; the failure publish is an `event` to the topic with `on_failure: true`, set only on that event to the topic |
| service `replicas` | compose `deploy.replicas`, k8s `replicas:` |
| `schedule` | Airflow `schedule=`, a cron file, a Flyte `LaunchPlan` with a `CronSchedule`. Entries are a 5/6-field cron (presets like Airflow `@daily` and `@hourly` are written as cron: `0 0 * * *`, `0 * * * *`), `manual` for hand-launched jobs, or `on <event>` for event-triggered jobs |
| `job` | the DAG or task name |
| datastore `store` | set only when the engine default is wrong (a Redis used as the system of record) |

## Stack

What the scanner knows about libraries and route styles is data: the
built-in packs (`scripts/vizcode/packs/<lang>.yaml`) and `OUT/stack.yaml`,
which you create and write for this repo without asking; it is committed
and reused on later runs. The Stack section at the top of `summary.md` lists what neither
covers, total first: unrecognised imports (two `file:line` examples each; names
that look like a server, datastore, queue or client library first, marked so:
sort those before the rest),
server libraries without routes (an `http-in` or `http` import with no route
in its importers' modules), route-looking lines with no route (in a file importing a
server library, a verb, `route` or resource call or annotation with a path that no
pattern read: `Route::apiResource('songs', ..)`), entity-looking classes with no table
(in a file importing a database library, a class annotated as an entity or extending an
ORM model base that no table pattern named), and `stack.yaml` route or table
patterns with 0 matches.
It never lists the standard library (the packs' `ignore` lists, Go paths
without a host, Python's own list), common libraries that do no IO (JS/TS UI, utility and middleware libraries such
as react, lodash, cors; Rust serde, chrono, thiserror; Python pydantic, flask_login; PHP Carbon, Monolog: the
packs' `ignore` lists) or the repo's own code: relative imports,
module folders and package names, Python sibling modules and `<stem>_pb2`
stubs, Scala imports that start uppercase or name something declared in the
file, workspace and `go.mod`/Cargo package names (Cargo `[lib] name` first), tsconfig `paths` aliases (`"*"` too),
a JS/TS module root's own folders imported bare (`src/x`) when `package.json` names no such dependency,
Rust `crate::`/`self::`/`super::` and `mod` names. Nor what the packs' code
rules read (tapir, FastAPI, Airflow, Flyte, ZIO). The packs cover the common
frameworks, ORMs and clients of every language (`format.md` → Stack lists
them), so write `stack.yaml` entries only for what the Stack section lists
and for routes or tables you see the pattern counts miss (step 3). A pack
also ignores the wiring of the frameworks it covers (Spring, Micronaut,
Quarkus, NestJS, ASP.NET, Laravel, Symfony): an IO part of one of them that
the pack does not list never shows here, so when the code uses one (a
messaging or client module), add it to `imports`.

- Entity-looking classes with no table usually take their table from the ORM's
  naming default (JPA the entity name, snake_case under Spring Boot's default
  naming; Eloquent, ActiveRecord and gorm the snake-case plural; Flask-SQLAlchemy the
  snake-case class name, singular; Django `<app label>_<model name lower-cased>`; Spring
  Data Mongo the class name, first letter lower-cased): write those tables into `meaning.json` by that rule, citing the
  class line, or add a table pattern when the name is written in another style.
- Route-looking lines with no route are a route style no pattern reads: add a
  `stack.yaml` route pattern for that style, rescan, and check the lines are gone;
  lines that are no routes (an outbound call with a path) go into `problems`.
- Routes a framework registers have no line of their own (Spring Actuator turned on in config,
  ASP.NET `MapIdentityApi<T>()`, health-check and OpenAPI endpoints): when a request or a probe
  (a k8s `livenessProbe`) reaches one, end it at the method or config line that turns them on,
  `inferred`, keyed by the route it calls, and name that line in `problems`.

To sort the lines, write or fix patterns, and for the `stack.yaml` format (groups, route,
mount, annotation, table and queue entries, prefixes, effects): read `references/stack.md`.

## Side effects

For each client library, SQL table and `tables (patterns)` entry in
`summary.md`, find the method that performs the read or write and attach the
relation there, to a `table` node under its datastore. A pattern match says
which table, not which way: decide `db-read` or `db-write` from the code, as
for a client library. An entry ending `, read` or `, write` is an ORM call on
the table's entity in that method (`orm-call`): the relation's direction, from
that method. When an interface has several implementations, follow
the wiring (ZLayer, dependency injection, constructors) to the one production
runs. Why: a test double or an unused implementation is not the system.

## Queues

A queue hands work from one method to another without a call between them, so a trace stops at the
put and looks complete. When `summary.md` lists `in-process queues`, `jobs defined` or `jobs enqueued` (the
packs' job systems: Celery, RQ, Dramatiq, Sidekiq and ActiveJob, Laravel queues, Bull, Hangfire, Spring
`@Scheduled`/`@Async`; a `stack.yaml` `jobs` entry adds one), or the repo enqueues jobs another way,
pushes to a list or stream in a store, or uses a table as a queue: read `references/interactions.md`
(the `in-process-queue` node and its `hand-off`s; a store's or job system's queue as a `topic`). A broker's topics and
queues come from `messages published` and `messages consumed` in `summary.md`, paired by name in § Likely requests:
a `topic` node per topic or queue, an `event` to it from each sender and from it to each receiver; a `$name` is a
value the scan cannot read (a setting, an argument): find it in config through `config.py`.

## Package roles

The scanner gives each package a role from its facts: `entry` (routes),
`storage` (database clients or SQL), `outbound` (HTTP, gRPC or broker
clients), `model` (data classes only) or `logic`. The role decides where the
package sits on its service's floor. When one is wrong, override it in
`meaning.json` under `roles` (`{"<package id>": {"role": ..., "why": ...}}`).

## Domain context first

If the repo has `docs/domain-context.md`, read it before the meaning pass:
its module map, core path and per-module facts outrank inference. Cite it in
`evidence` and set `domain_context` in `meaning.json`. Why: it holds facts
the code cannot show, such as a table only an unused path writes.

## Pinned traces

The viewer traces data through the map from any node, upstream and
downstream. Pins are the traces it lists to start from: write `pins` in
`meaning.json` on a first scan.

- Pin 3–6 traces (fewer only when the map has fewer than 3 entry points):
  the core path from the domain context if there is one, then the main
  entry points of services the earlier pins' traces do not reach, until 6.
- A pin is `{id, title, start, direction, by}`. `start` is a node id of
  any kind: prefer an entry point's handler method (a pin says where data
  starts), or a table, topic, external or datastore; a service, library,
  package, file or class traces from the entry points inside it, else from
  its boundary methods (those a relation crosses its edge at), else from
  the container itself.
  Lines and floor groups cannot be pinned (their ids are not map nodes).
  `direction` is `downstream`, `upstream` or `both`; `by` is `agent`.
- The title says in plain words what happens along it ("driver reserves a
  bay, billing charges it"), never how much it matters: no key, main,
  critical, core or important in it. Why: the map cannot know which path
  matters most; the user pins and unpins what they want.
- `id` is the title as a slug (`driver-reserves-a-bay-billing-charges-it`);
  a repeated slug gets `-2`, `-3`.
- Never remove or rename a `by: user` pin, and never pin an id listed in
  `unpinned` (the user removed it).
- `diff.py` carries pins over unchanged. When a pin's `start` no longer
  exists, re-point it only when the code shows a rename (same file, the body
  moved under a new name); otherwise leave it: the audit lists it
  (`pin.start-missing`) for the user to decide.

## Examples

Example values show what the data looks like, the Swagger way: one per data
type and one per entry point. Make them up at scan time, realistic; the
viewer labels them `example values, not computed`. Never present them as
what the code would output. Values need not agree across a chain.

- **Entry points:** the handler method of a route, and the method a
  `request`, `event`, `stream` or `batch` relation ends at. Give each an
  `examples.entries` item (on a map of more than 100, each one within two
  hops downstream of a pin's start; `examples.py` lists the rest as `optional`): `node` (the handler), `key` (the key as on the
  relation, or `METHOD path` for a route, as `scripts/examples.py` prints
  it: `POST /v1/reservations`, a topic or stream name, `task nightly_settlement`
  for a batch), `request` (the payload that comes in; `{}` for a job that takes
  none) and, for a route, `response` (what it sends back; none for events,
  streams and jobs). A method that serves several routes gets one item per
  key. One item per route, not per spelling: a route that `files` lists
  without its mount prefix and a relation writes with it is one item, keyed
  as on the relation; a mounted route no relation names is one item, keyed
  bare as `examples.py` prints it.
- **Data types:** a class whose `fields` the scanner lists, referred to by a
  signature (`signature.params[].refs`, `signature.returns_refs`). Give an
  `examples.types` entry (`"<class id>": {"value": {...}}`) to every type in
  the refs of an entry point's handler and of the methods within two hops
  downstream of a pin's start. Not every type in the codebase. `value` is an
  object keyed by the class's field names; leave out a field you cannot fill
  sensibly, never add one it does not declare.
- Write type examples only for data types: payloads, records, entities. A
  class that holds services or dependencies (a repository, a client, a
  handler with injected collaborators) gets none, even when
  `scripts/examples.py` lists it (it marks one whose fields are all typed as
  collaborators `wiring, skip it`). Why: its fields are wiring, not data; an
  example of it shows nothing a reader needs.
- Leave `fingerprint` out: `assemble.py` stamps it. On a rescan `diff.py`
  flags an example `stale` when its class's fields or its handler's
  signature changed, or the class or handler is gone: rewrite the value (or
  delete the example when its class or handler is gone) and delete the flag.
- Write examples after the rest of the meaning pass: once `assemble.py`
  exits 0, `$PY SKILL_DIR/scripts/examples.py REPO --out-dir OUT` lists every
  entry point and data type that should have one, totals first, each `ok`,
  `missing` or `stale` (`ok 1/3 keys` when a method with several keys has
  fewer examples; `key gone` when an example's key is no longer one of its
  method's: change only that example's `key`, never the same path in other
  text). Write the missing ones (for types, data types only)
  and assemble again. The audit lists entry points still without one, and
  those with a key still without one (`example.missing`). A root route
  (`GET /`) under a mount stays a key apart from the relation's (`GET /v1`):
  give it an example under each key.

## Problems

Record everything you could not resolve in `problems`, with the nodes it
concerns. A visible gap is useful; a confident guess is not. A route's path
in problem text carries its mount prefix (`/notes/{id}`, never `/{id}`); bare
example keys under a mount are expected and get no problem.

## Large repos

When `summary.md` lists more modules than one pass can read carefully, run
the meaning pass per service (one sub-agent each, all following this
skill), then one pass that matches interactions between services. Without
sub-agents, make the same passes one after another, each service's part of
`meaning.json` written before the next. Either way:

- Write a script for what repeats across hundreds of items (route prefixes,
  matching requests to routes, example values from field names), check a
  sample of its output against the code, and say in the report what it made.
- While fixing `meaning.json`, run `assemble.py` with `VIZ_AUDIT=0`: no
  drawing checks, which take minutes on a large map. Run it once without for
  the audit line you report; on a large map that line counts the floors the
  budget left unchecked: report the count, never "the audit is clean".
- What you leave out (a subsystem, a protocol the scanner does not read, a
  share of the routes) goes into `problems` with the nodes it concerns and how
  much it covers.

## Review comments and map notes

`OUT/export-comment.md` (a reviewer's comments and pin changes from the viewer: read it before anything
else, act on it, delete it) and `OUT/map-notes.md` (the user's standing notes, read every run, never
edited): when either exists, read `references/review.md` first.

## Audit

The audit (`audit.py`, run by `assemble.py` and on `none`) never blocks the
map. Map limits are drawing-check failures the viewer cannot draw well yet:
report them, never fix them, never put them on the map. Findings are what the
map shows about the code; the only one is a cycle between services (events
through a topic do not count). Content warnings `ext.missing`, `external.unused`, `deployable.unscanned`, `queue.missing`, `request.unmatched`,
`data.two-way-call`, `module.empty`, `example.missing`, `route.file-handler`, `route.setup-handler` and `pin.few`
you fix in `meaning.json` (the route ones in `stack.yaml`, or a `problems` entry);
`name.container` is a labelling gap in the scanner or viewer,
`pin.start-missing` a pin whose start is gone (the user decides), and an
audit error (`audit.error`) is a check that broke: report them. Without
Node 18+ the drawing checks are skipped and the line says why. Check ids:
`format.md` → Audit.

## Common mistakes

- Treating a shared library, a scripts folder or a test helper as a service.
- Marking a URL built from config as `confirmed` without resolving the config.
- Calling a pin the key, main, critical, core or important path.
- Writing an example field the class does not declare, or a `fingerprint`.
- Re-resolving facts `diff.py` kept, which wastes the incremental scan and
  churns the committed map.

Files in this skill

  • CHANGES.md8.6 KB
  • SKILL.md45.4 KB
  • feedback.md5.3 KB
  • format.md56 KB
  • references/interactions.md4 KB
  • references/review.md2.8 KB
  • references/stack.md17.4 KB
  • scripts/assemble.py4 KB
  • scripts/audit.py14.5 KB
  • scripts/config.py2.1 KB
  • scripts/diff.py4.9 KB
  • scripts/examples.py4.7 KB
  • scripts/extract.py1.7 KB
  • scripts/feedback.py18.7 KB
  • scripts/find.py1.6 KB
  • scripts/requirements.txt101 B
  • scripts/schema/code-map.schema.json8.6 KB
  • scripts/validate.py757 B
  • scripts/view.py3.1 KB
  • scripts/vizcode/__init__.py347 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.