signpost

Give models signposting
for repos.

signpost compiles a repository into a map an agent reads before it starts work — so it begins oriented instead of re-deriving the same structure every session. One binary. No server, no embeddings, no index to keep warm.

signpost, run on its own repository · 2026-08-19
$ signpost graph show .
analysed 258 files: 90 nodes, 224 edges
  1 file(s) not read: test fixture directory (1)
  9 file(s) of no recognised kind: .html (3), .css (1), .gitattributes (1), .gitignore (1), .svg (1), cname (1), and 1 more
  15 statement(s) named 8 table(s) no migration declares: orders (5), archive (2), counters (2), things (2), customers (1), and 3 more
  19 statement(s) build a table name at run time; no edge drawn
  history: 160 commits, 55 co-change pair(s)

  90 nodes, 224 edges, 40 clusters

concepts
  Document               38
  External Dependency    17
  Module                 22
  Pipeline               13

hubs (top 10 by degree)
  /modules/root                              in 18   out 34
  /modules/signpost                          in 16   out 33
  /modules/assemble                          in 12   out 16
  /modules/manifest                          in 15   out 11
  /references/agents-md                      in 0    out 22
  /modules/discover                          in 14   out 7
  /modules/okf                               in 11   out 9
  /modules/site                              in 11   out 9
  /modules/graph                             in 12   out 6
  /modules/extract                           in 9    out 8

The four dim lines are the gaps, and each is a different admission: a directory recorded rather than read, files of no kind it recognises, tables the code names that no migration declares, and statements whose table is built at run time, where drawing an edge would mean guessing. Coverage is not opt-in: the absence of a measurement is never a clean bill of health. The run continues past the hubs. See this graph rendered.

install

macOS, Linux

curl -fsSL https://raw.githubusercontent.com/3rg0n/signpost/main/install.sh | sh

Windows

iex "& { $(irm https://raw.githubusercontent.com/3rg0n/signpost/main/install.ps1) }"

With a Go toolchain

go install github.com/3rg0n/signpost/cmd/signpost@latest

Both scripts verify the release archive against the SHA-256 digests published with that tag, and install nothing if the digest does not match. Pass --version v0.1.0 to pin a release.

why

An agent opening an unfamiliar repository rediscovers the same things every time: which module owns what, where the entrypoints are, which files move together, what the docs claim versus what the code does.

That work is paid for on every session, thrown away at the end of every session, and inconsistent between runs. signpost does the derivation once, at a known commit, and writes it down where people can correct it in place — so the corrections survive.

The output is Open Knowledge Format markdown committed to the repository. Agents, people, and static site generators read it directly, which means the bundle keeps working for everyone who does not have signpost installed. It maintains the bundle; it does not serve it.

confidence

A guess and a fact are drawn differently.

extracted
Read directly out of the repository. An import statement, a service in a compose file, a line in CODEOWNERS.
inferred
Proposed rather than read, and marked as such in every format signpost writes — dashed in the diagrams, verbatim in the data.

storage imports auth read out of an import statement and the same edge proposed by a model are different claims. A rendered graph that flattened the two would make the guess look like a measurement, which is the failure this tool exists to avoid — so no export format is permitted to drop the distinction, and an edge to a concept that does not exist is dropped rather than drawn to an invented one.

Everything amber on this page is something signpost has not verified.

properties

  • Human edits compound. Generated prose lives between managed markers. Everything outside them is yours and is never touched. A generator that clobbers corrections teaches people to ignore it.
  • Stale fails loudly. verify exits non-zero. A silently stale knowledge artifact is worse than none, because it is confidently wrong.
  • Deterministic. Same commit in, identical bytes out. CI commits the bundle, so nondeterminism would mean commit churn in your repository.
  • Works with no model at all. The deterministic pass produces a complete structural bundle on its own. A model only adds what prose alone can supply.
  • Secrets are references. A Kubernetes Secret contributes its name and its key names; an env_file is recorded by path and never opened. The bundle gets committed.
  • Three dependencies, all patchable. go.mod requires the OpenTelemetry API, its trace API, and its SDK, and nothing else. The policy is not zero dependencies but patchable ones — few enough that shipping a CVE fix the day it lands stays routine. The OTLP exporter is hand-written, because upstream's links the entire gRPC stack to speak HTTP.

reads

Go, TypeScript, JavaScript, Python, Rust, Java, Kotlin, C, C++, Objective-C, Ruby, PHP, C#, shell, PowerShell, Vue, Svelte and Astro — imports, public surface, and entrypoints. Each extractor is scored against hand-labeled fixtures rather than asserted, at F1 1.000 for both imports and symbols.

Java and Kotlin share one resolution map because the compiler does, and it is built from the package declarations in the source. There is no pom.xml or build.gradle reader yet, so a JVM import naming no package in the repository is reported as a gap rather than turned into a Maven coordinate nobody declared.

C, C++ and Objective-C share one extractor, because they share a preprocessor and a header convention: a .h is any of the three and only its contents say which. An #include has no manifest behind it, so it resolves by walking outward from the including file's directory through the conventional include roots — and the delimiter is kept, because quoted and angled are different search rules.

Ruby, PHP and C# each resolve by a rule their own ecosystem states rather than by one shared convention. A Ruby require searches the load path; a PHP use resolves through the PSR-4 map in composer.json; a C# using names a namespace no manifest declares, so it is matched against the namespaces the repository's own files declare. Where the rule runs out, the import is reported as a gap and never turned into a package name nobody wrote — Corpus.DomainModel is not under Corpus.Domain, because a namespace nests on the dot.

Shell and PowerShell get two extractors rather than one — the C-family decision inverted, on the same test of whether one set of rules reads both. They agree on # for a comment and little else: a function nested inside another is global in shell and dies with the enclosing scope in PowerShell, so the same nesting means opposite things about the public surface. Shell has no registry behind it at all, so a source reaching no file is reported as a first-party import that found no page, never as a package somebody forgot to declare. PowerShell has both halves, and its runtime is two runtimes, because it runs on .NET: the engine modules are a closed list rather than a Microsoft.PowerShell.* prefix, so Microsoft.PowerShell.Crescendo stays the separately versioned gallery module it is.

Vue, Svelte and Astro are one reader, and it is not a new one. A single-file component is a document with program text inside it, so every region that is not a <script> block is blanked byte for byte and what remains goes to the TypeScript extractor. Blanking rather than slicing is the point: a slice renumbers every line after the first fence, and an import reported at the wrong line is worse than one not reported, because the position looks authoritative. Every script block is read, not the first — Vue's <script setup> beside a plain <script> is what its own migration path produces. A <style> block's @import is not an import: no stylesheet is a node here, so it appears in neither coverage gap.

Beyond source, signpost reads what a repository states about itself: go.mod, package.json, pyproject.toml, requirements.txt, Cargo.toml, Gemfile, composer.json and *.csproj for dependencies; CMakeLists.txt and MODULE.bazel/BUILD.bazel for what a project builds and which of its own libraries a target links, which for C is the only place that is stated; Containerfiles, compose files, GitHub Actions workflows, Kubernetes manifests and Helm charts for deployment; protobuf, OpenAPI and GraphQL SDL for contracts; SQL migrations, CODEOWNERS, ADRs and Makefiles for the rest.

status

v0.1.0 — the deterministic core, complete. No model required and no network, unless you opt into the semantic pass.

Solid rows have shipped. Dashed rows have not.
Component State
Graph model, metrics, Louvain clustering done
Discovery: gitignore, classification, bounded reads done
Extractors: Go, TS/JS, Python, Rust, Java, Kotlin, C, C++, Objective-C, Ruby, PHP, C#, shell, PowerShell, Vue, Svelte, Astro done
Manifest and infrastructure extraction done
Graph assembly and import resolution done
Export: Mermaid, DOT, GraphML, JSON done
signpost graph show, signpost graph export done
signpost graph diff — structural change between two commits done
Git signals: co-change, churn, ownership done
signpost build — OKF emit, edit preservation done
signpost verify done
signpost.yml — rebuild on push, gate pull requests done
signpost hooks — optional local post-commit reminder done
signpost update — replace the binary from a checksum-verified release done
signpost init github — scaffold that workflow into another repository done
.signpost.yml — per-repository defaults, no gate keys done
The graph viewer — in this repo, no JS dependencies done
signpost view — the same viewer on 127.0.0.1, for any repository done
signpost view -static — write that viewer to a directory, for a deploy done
signpost init pages — scaffold the Pages deploy into another repository done
Model backends: local IPC, or any OpenAI-compatible endpoint done
build -semantic — module role summaries, grounded and cited done
Semantic pass: doc-to-code linking, invariants, cluster labels v0.3
Viewer: search by name, path, or file · deep links to source done
Viewer: diff between commits declined — graph diff reports it as text

The deterministic core is usable end-to-end: build a bundle, commit it, and the workflow keeps it honest. build -semantic adds the summaries that say what a module is for, which no deterministic read can honestly produce — off unless you configure a backend and pass the flag, so the ordinary build stays offline and byte-stable. Every summary cites the files it rests on, and one citation that does not resolve drops the summary rather than the citation.