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 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.
verifyexits 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_fileis recorded by path and never opened. The bundle gets committed. -
Three dependencies, all patchable.
go.modrequires 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.
| 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.