hologram-map 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.
- hologram_map-0.1.0/LICENSE +21 -0
- hologram_map-0.1.0/PKG-INFO +298 -0
- hologram_map-0.1.0/README.md +258 -0
- hologram_map-0.1.0/hologram.py +3102 -0
- hologram_map-0.1.0/hologram_map.egg-info/PKG-INFO +298 -0
- hologram_map-0.1.0/hologram_map.egg-info/SOURCES.txt +17 -0
- hologram_map-0.1.0/hologram_map.egg-info/dependency_links.txt +1 -0
- hologram_map-0.1.0/hologram_map.egg-info/entry_points.txt +2 -0
- hologram_map-0.1.0/hologram_map.egg-info/requires.txt +15 -0
- hologram_map-0.1.0/hologram_map.egg-info/top_level.txt +1 -0
- hologram_map-0.1.0/pyproject.toml +59 -0
- hologram_map-0.1.0/setup.cfg +4 -0
- hologram_map-0.1.0/tests/test_bench.py +312 -0
- hologram_map-0.1.0/tests/test_cli.py +147 -0
- hologram_map-0.1.0/tests/test_extract_langs.py +103 -0
- hologram_map-0.1.0/tests/test_freshness_and_markers.py +312 -0
- hologram_map-0.1.0/tests/test_more_langs.py +388 -0
- hologram_map-0.1.0/tests/test_simple_mode.py +566 -0
- hologram_map-0.1.0/tests/test_treesitter.py +83 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Fabisz
|
|
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.
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hologram-map
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Compress a codebase into a compact, deterministic markdown map embedded in your coding agent's context files
|
|
5
|
+
Author: Sebastian Fabisz
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lazymaniac/hologram
|
|
8
|
+
Project-URL: Repository, https://github.com/lazymaniac/hologram
|
|
9
|
+
Project-URL: Changelog, https://github.com/lazymaniac/hologram/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/lazymaniac/hologram/issues
|
|
11
|
+
Keywords: llm,agent,context,codebase-map,tree-sitter,claude,digest
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: grammars
|
|
26
|
+
Requires-Dist: tree-sitter; extra == "grammars"
|
|
27
|
+
Requires-Dist: tree-sitter-bash; extra == "grammars"
|
|
28
|
+
Requires-Dist: tree-sitter-c; extra == "grammars"
|
|
29
|
+
Requires-Dist: tree-sitter-c-sharp; extra == "grammars"
|
|
30
|
+
Requires-Dist: tree-sitter-cpp; extra == "grammars"
|
|
31
|
+
Requires-Dist: tree-sitter-css; extra == "grammars"
|
|
32
|
+
Requires-Dist: tree-sitter-go; extra == "grammars"
|
|
33
|
+
Requires-Dist: tree-sitter-html; extra == "grammars"
|
|
34
|
+
Requires-Dist: tree-sitter-java; extra == "grammars"
|
|
35
|
+
Requires-Dist: tree-sitter-kotlin; extra == "grammars"
|
|
36
|
+
Requires-Dist: tree-sitter-lua; extra == "grammars"
|
|
37
|
+
Requires-Dist: tree-sitter-rust; extra == "grammars"
|
|
38
|
+
Requires-Dist: tree-sitter-typescript; extra == "grammars"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# hologram
|
|
42
|
+
|
|
43
|
+
hologram reads your codebase and writes one compact map of it — public callables, type
|
|
44
|
+
field names, relationships, project-internal calls, private identifiers, and the test
|
|
45
|
+
files/classes that cover the project — directly into the context files your coding
|
|
46
|
+
agents already read. The map is in context from turn zero, before any exploration
|
|
47
|
+
begins.
|
|
48
|
+
|
|
49
|
+
It's a single Python file. It installs its own parsers the first time it needs them,
|
|
50
|
+
and git hooks keep the map up to date after every commit. Generation is fully
|
|
51
|
+
deterministic — no LLM involved — so the same code always produces the same map,
|
|
52
|
+
and a map diff always means the code changed.
|
|
53
|
+
|
|
54
|
+
The name: like a hologram, every fragment of the output carries the shape of the
|
|
55
|
+
whole. Token cost stays low by choosing compact facts instead of truncating them.
|
|
56
|
+
|
|
57
|
+
## What it's for
|
|
58
|
+
|
|
59
|
+
- **Feature planning** — plan against the real surface of the code: what already
|
|
60
|
+
exists, which module the new thing belongs in, which family of types it should
|
|
61
|
+
extend. Plans written this way survive contact with the codebase.
|
|
62
|
+
- **Implementation** — the agent (or you) finds the existing helper before writing a
|
|
63
|
+
second one, follows the house conventions, and places code where it belongs.
|
|
64
|
+
- **Code review** — `hologram diff` shows a pull request's API drift on one screen,
|
|
65
|
+
including the near-duplicate helpers that sneak in quietly.
|
|
66
|
+
- **Refactoring** — `×0` flags functions and classes with no statically observed
|
|
67
|
+
project references, and the `· deps` lines show which modules are coupled, before
|
|
68
|
+
you start pulling threads.
|
|
69
|
+
- **Debugging** — call chains, private-name lists, and `⋮N` body-size marks point at
|
|
70
|
+
the right file before you open a single one.
|
|
71
|
+
- **Onboarding** — a new teammate, human or agent, reads one block and knows the
|
|
72
|
+
territory: the modules, the vocabulary, the patterns.
|
|
73
|
+
|
|
74
|
+
## What the output looks like
|
|
75
|
+
|
|
76
|
+
The map of a small Java fixture:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
# hologram · 186 LOC · state 817a0445a77f
|
|
80
|
+
· C/R/I{fields} E{values} T:target · f(args):Ret > project calls · -=private · ?=tests · ×0=no static use · ✓=tested · ⋮N=lines · !E=throws · p{a,b}=pa,pb
|
|
81
|
+
· deps .→ids | engine→ids
|
|
82
|
+
src
|
|
83
|
+
App(C) ×0
|
|
84
|
+
main(args) ×0 > PricingEngine,evaluate,OrderId.of,ItemId.of
|
|
85
|
+
delta
|
|
86
|
+
AddOp,RemoveOp(R{nodeId}) : DeltaOp
|
|
87
|
+
weight():int ×0
|
|
88
|
+
DeltaOp(I) sealed:AddOp|RemoveOp
|
|
89
|
+
weight():int ×0
|
|
90
|
+
engine
|
|
91
|
+
OrderStatus(E{NEW,PAID,SHIPPED})
|
|
92
|
+
isTerminal():boolean ×0
|
|
93
|
+
PricePort(I)
|
|
94
|
+
quoteFor(order):Quote ×0
|
|
95
|
+
supports(order):boolean ×0
|
|
96
|
+
PricingEngine(C{basePrices}) : PricePort
|
|
97
|
+
quoteFor(order):Quote ×0 > evaluate
|
|
98
|
+
supports(order):boolean ×0
|
|
99
|
+
evaluate(order,items):Quote !UnknownItem > UnknownItemException,Quote
|
|
100
|
+
Quote(R{order,totalCents})
|
|
101
|
+
UnknownItemException(C) : RuntimeException
|
|
102
|
+
ids
|
|
103
|
+
ItemId,OrderId,UserId(R{value})
|
|
104
|
+
of(raw):⟨X⟩ > ⟨X⟩
|
|
105
|
+
? tests
|
|
106
|
+
src/test
|
|
107
|
+
PricingEngineTest.java{PricingEngineTest,BulkDiscounts}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Reading it is easier than it looks, and the legend on line 2 teaches the notation to
|
|
111
|
+
any LLM:
|
|
112
|
+
|
|
113
|
+
- **The tree** mirrors your directory layout, shared path prefixes stated once.
|
|
114
|
+
- **Types** expose field names rather than redundant field types.
|
|
115
|
+
`PricingEngine(C{basePrices}) : PricePort` is a class with a `basePrices` field
|
|
116
|
+
implementing `PricePort`. Records/interfaces use the same braces, enums list
|
|
117
|
+
values, aliases retain their target, and sealed interfaces retain permitted types.
|
|
118
|
+
- **Functions** show parameter names and return types: `evaluate(order,items):Quote`.
|
|
119
|
+
Types appear beside names only when overloads would otherwise collide.
|
|
120
|
+
- **Call chains** follow the `>`: what a function calls, in order. Variables resolve
|
|
121
|
+
to their declared types (`PricingEngine.evaluate`, not `engine.evaluate`), standard
|
|
122
|
+
library calls are dropped, and chains are transitively reduced — if `a > b` and
|
|
123
|
+
`b > c`, then `a`'s line doesn't repeat `c`.
|
|
124
|
+
- **Same-shape types group.** `ItemId,OrderId,UserId(R{value})` is a family in one
|
|
125
|
+
entry; `⟨X⟩` stands for each member's own name in the methods they share.
|
|
126
|
+
- **Markers**: `✓` = resolved call from a test · `⋮120` = the body is 120 lines ·
|
|
127
|
+
`×0` = no statically observed project reference to a function/class/method ·
|
|
128
|
+
`!UnknownItem` = throws (`Exception` suffix implied) · no `:Ret` = returns void ·
|
|
129
|
+
`» index.ts: A,B` = barrel re-exports.
|
|
130
|
+
- **Private members** always appear as names. Repeated prefixes factor losslessly:
|
|
131
|
+
`_extract_{java,python,typescript}` means those three exact identifiers.
|
|
132
|
+
- **Tests** list every detected test file and its classes. Test functions are omitted
|
|
133
|
+
because their names cost tokens without improving placement guidance.
|
|
134
|
+
- **`· deps a→b`** = module `a` uses types from module `b`: the import architecture
|
|
135
|
+
without reading imports.
|
|
136
|
+
- **`state`** hashes the exact sources plus the generator, so source or extraction/
|
|
137
|
+
rendering changes make old maps stale.
|
|
138
|
+
|
|
139
|
+
## Languages
|
|
140
|
+
|
|
141
|
+
| Language | What you get |
|
|
142
|
+
|---|---|
|
|
143
|
+
| Java, C#, TypeScript/JS, TSX/JSX | types with named fields, name-based signatures, relations, resolved calls, privates, aliases, object APIs, re-exports |
|
|
144
|
+
| Python | same, via the standard library's `ast` — zero dependencies |
|
|
145
|
+
| Kotlin | classes, data classes, enums, interfaces, named fields, supers, calls |
|
|
146
|
+
| Go, Rust, C, C++ | types, traits, structs, signatures, calls, receiver bindings |
|
|
147
|
+
| Vue, Svelte | the component plus everything in its `<script>` block |
|
|
148
|
+
| Lua | functions and methods with call chains (params by name — it's untyped) |
|
|
149
|
+
| Bash/zsh (`.sh`, `.bash`, `.zsh`) | functions (both definition forms) with command-call chains; `_name` = private |
|
|
150
|
+
| HTML | element ids and custom-element tags, plus nested `<script>`/`<style>` blocks run through the JS/CSS extractors (when those grammars are installed) |
|
|
151
|
+
| CSS | class/id selectors, custom properties (`--x`), `@keyframes` names — names only |
|
|
152
|
+
| Helm | template `define` names, `values.yaml` keys, chart name |
|
|
153
|
+
|
|
154
|
+
## Getting started
|
|
155
|
+
|
|
156
|
+
Install from PyPI (the `grammars` extra pulls in every tree-sitter parser up front):
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pip install "hologram-map[grammars]"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
hologram init --root /path/to/repo
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Or skip installation entirely — it's a single file. Clone it anywhere and point it
|
|
167
|
+
at a repo:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
python3 ~/workspace/hologram/hologram.py init --root /path/to/repo
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
That installs git hooks and embeds the map in every agent context file the repo
|
|
174
|
+
already has. From then on the hooks refresh it after every commit, merge, and
|
|
175
|
+
checkout. You never touch them again.
|
|
176
|
+
|
|
177
|
+
The first time it meets a language it has no parser for, it offers to set one up: it
|
|
178
|
+
creates a `.venv` next to itself and pip-installs the right tree-sitter grammar. You
|
|
179
|
+
type `y` once. Every later run finds that venv on its own, so plain
|
|
180
|
+
`python3 hologram.py …` always works. Python-only repos skip all of this — the
|
|
181
|
+
standard library is enough.
|
|
182
|
+
|
|
183
|
+
Everything it can do:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
hologram.py build --root . # refresh the embedded map
|
|
187
|
+
hologram.py build --root . --lang java # limit to one or more languages
|
|
188
|
+
hologram.py build --root . --if-stale # rebuild only if the code changed
|
|
189
|
+
hologram.py check --root . # is every context file current? exit 0 yes / 1 no
|
|
190
|
+
hologram.py diff HEAD~3 --root . # how did the API change since then?
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
A successful build prints the map's token cost and where it went:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
hologram: 1193 tokens embedded in CLAUDE.md, AGENTS.md
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Which agents get the map
|
|
200
|
+
|
|
201
|
+
`init`/`build` detect the context files a repo already uses and attach the map to each
|
|
202
|
+
one — the same map, everywhere, so Claude Code and Codex and Cursor can't drift apart:
|
|
203
|
+
|
|
204
|
+
| Agent | File it reads |
|
|
205
|
+
|---|---|
|
|
206
|
+
| Claude Code | `CLAUDE.md` |
|
|
207
|
+
| Codex, opencode, Amp, Jules, Zed | `AGENTS.md` |
|
|
208
|
+
| Gemini CLI | `GEMINI.md` |
|
|
209
|
+
| Qwen Code | `QWEN.md` |
|
|
210
|
+
| GitHub Copilot | `.github/copilot-instructions.md`, `.github/instructions/` |
|
|
211
|
+
| Cline | `.clinerules` (file or directory) |
|
|
212
|
+
| Cursor | `.cursorrules`, `.cursor/rules/` |
|
|
213
|
+
| Windsurf | `.windsurfrules`, `.windsurf/rules/` |
|
|
214
|
+
| Roo Code | `.roorules`, `.roo/rules/` |
|
|
215
|
+
|
|
216
|
+
Existing files are attached to, never invented: hologram only writes a context file
|
|
217
|
+
that already exists. Rule *directories* get one managed file of hologram's own
|
|
218
|
+
(`.cursor/rules/hologram.mdc`, `.clinerules/hologram.md`, …), created with whatever
|
|
219
|
+
front matter that agent needs to load it. A repo with none of these gets a `CLAUDE.md`.
|
|
220
|
+
|
|
221
|
+
Inside each file the map lives between two HTML-comment markers, and the block opens
|
|
222
|
+
with a short note telling the agent what it is looking at. Everything you wrote around
|
|
223
|
+
the block is preserved on every rebuild — the map is a block in your instructions
|
|
224
|
+
file, not a replacement for it.
|
|
225
|
+
|
|
226
|
+
## Staying fresh
|
|
227
|
+
|
|
228
|
+
A stale map is worse than none — an agent trusting a description of deleted code
|
|
229
|
+
is confidently wrong. Three commands make freshness a non-issue:
|
|
230
|
+
|
|
231
|
+
- `check` recomputes the `state` hash in milliseconds, without parsing anything, and
|
|
232
|
+
compares it against the stamp in every context file. Any target lagging means exit
|
|
233
|
+
1. Wire it into CI or an agent harness.
|
|
234
|
+
- `build --if-stale` uses the same probe, so "rebuild just in case" costs nothing
|
|
235
|
+
when nothing changed.
|
|
236
|
+
- `diff <rev>` points the same machinery backwards: it rebuilds the map as it
|
|
237
|
+
looked at an older revision and prints the difference — a pull request's API drift
|
|
238
|
+
on one screen.
|
|
239
|
+
|
|
240
|
+
## Does it actually help? An honest take
|
|
241
|
+
|
|
242
|
+
hologram exists because of one specific failure: an agent lands in a repo with no
|
|
243
|
+
map, greps its way to a partial picture, and writes code that already exists.
|
|
244
|
+
|
|
245
|
+
**The good.** An agent normally burns thousands of tokens re-discovering project
|
|
246
|
+
structure every single session, and most of what it reads gets discarded. The map
|
|
247
|
+
replaces that exploration. Duplication gets a real counterweight: "does this already
|
|
248
|
+
exist?" becomes something the agent can see rather than something it only catches by
|
|
249
|
+
grepping the exact right word. And because the map shows your conventions — all your
|
|
250
|
+
ID types are one-field records, your services take dependencies through constructors —
|
|
251
|
+
a model tends to extend the patterns it sees rather than invent parallel ones. Factored
|
|
252
|
+
private names, concise call lines, and the test index tell it which file to open first
|
|
253
|
+
without a raw symbol dump.
|
|
254
|
+
|
|
255
|
+
**The caveats.** None of this is enforced. The map competes for the model's
|
|
256
|
+
attention like everything else in context, and an agent can ignore it and reimplement
|
|
257
|
+
a helper anyway — it shifts the odds, it is not a guardrail. Function bodies stay
|
|
258
|
+
invisible: a 500-line algorithm and a one-liner expose the same signature, so the
|
|
259
|
+
map tells an agent what exists, never how well it's built. `✓` means a test
|
|
260
|
+
mentions the function, not that the function is correct. If your naming is misleading,
|
|
261
|
+
the map compresses and transmits the misleading names with perfect fidelity. Depth
|
|
262
|
+
varies by language — the table above is honest about which ones get the full
|
|
263
|
+
treatment.
|
|
264
|
+
|
|
265
|
+
**What's been measured.** The map in context, against the same agent without it, on a
|
|
266
|
+
private 133k-LOC codebase the model had never seen (10 headless sonnet sessions vs
|
|
267
|
+
matched baselines, transcripts reviewed by hand): **outcomes stayed equal while effort
|
|
268
|
+
dropped ~36% in turns and ~55% in searches, with navigation tasks 40% faster — one
|
|
269
|
+
answered in 4 turns with zero file reads, straight from the embedded map. Total tokens
|
|
270
|
+
came out level: the per-turn cost of the map was fully offset by fewer turns.** That is
|
|
271
|
+
the thesis doing what it was supposed to do — the map in context replaces exploration.
|
|
272
|
+
|
|
273
|
+
Caveats stay honest: n=1 per cell, one model, one corpus (results withheld — private);
|
|
274
|
+
duplication was zero in every condition, so the measured win is orientation speed, not
|
|
275
|
+
duplication prevention; and larger repos, weaker models, and chat-only contexts remain
|
|
276
|
+
unmeasured. On a famous OSS corpus the model has largely memorized, expect no benefit
|
|
277
|
+
at all — a control agent walks straight to the right API from training memory.
|
|
278
|
+
|
|
279
|
+
## How it works
|
|
280
|
+
|
|
281
|
+
One file, one pipeline: scan (only git-tracked files when inside a repo), extract,
|
|
282
|
+
render, embed. Each language has its own small extractor and they all produce the same
|
|
283
|
+
`Symbol` records, so everything downstream — receiver resolution, transitive
|
|
284
|
+
reduction, shape grouping, the final tree — is language-neutral and written once.
|
|
285
|
+
Formatting decisions were measured with a real tokenizer (o200k), not guessed.
|
|
286
|
+
|
|
287
|
+
## Tests
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
.venv/bin/python -m unittest discover -s tests
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Runs under plain `python3` too — tests for languages whose grammar isn't installed
|
|
294
|
+
just skip.
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# hologram
|
|
2
|
+
|
|
3
|
+
hologram reads your codebase and writes one compact map of it — public callables, type
|
|
4
|
+
field names, relationships, project-internal calls, private identifiers, and the test
|
|
5
|
+
files/classes that cover the project — directly into the context files your coding
|
|
6
|
+
agents already read. The map is in context from turn zero, before any exploration
|
|
7
|
+
begins.
|
|
8
|
+
|
|
9
|
+
It's a single Python file. It installs its own parsers the first time it needs them,
|
|
10
|
+
and git hooks keep the map up to date after every commit. Generation is fully
|
|
11
|
+
deterministic — no LLM involved — so the same code always produces the same map,
|
|
12
|
+
and a map diff always means the code changed.
|
|
13
|
+
|
|
14
|
+
The name: like a hologram, every fragment of the output carries the shape of the
|
|
15
|
+
whole. Token cost stays low by choosing compact facts instead of truncating them.
|
|
16
|
+
|
|
17
|
+
## What it's for
|
|
18
|
+
|
|
19
|
+
- **Feature planning** — plan against the real surface of the code: what already
|
|
20
|
+
exists, which module the new thing belongs in, which family of types it should
|
|
21
|
+
extend. Plans written this way survive contact with the codebase.
|
|
22
|
+
- **Implementation** — the agent (or you) finds the existing helper before writing a
|
|
23
|
+
second one, follows the house conventions, and places code where it belongs.
|
|
24
|
+
- **Code review** — `hologram diff` shows a pull request's API drift on one screen,
|
|
25
|
+
including the near-duplicate helpers that sneak in quietly.
|
|
26
|
+
- **Refactoring** — `×0` flags functions and classes with no statically observed
|
|
27
|
+
project references, and the `· deps` lines show which modules are coupled, before
|
|
28
|
+
you start pulling threads.
|
|
29
|
+
- **Debugging** — call chains, private-name lists, and `⋮N` body-size marks point at
|
|
30
|
+
the right file before you open a single one.
|
|
31
|
+
- **Onboarding** — a new teammate, human or agent, reads one block and knows the
|
|
32
|
+
territory: the modules, the vocabulary, the patterns.
|
|
33
|
+
|
|
34
|
+
## What the output looks like
|
|
35
|
+
|
|
36
|
+
The map of a small Java fixture:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
# hologram · 186 LOC · state 817a0445a77f
|
|
40
|
+
· C/R/I{fields} E{values} T:target · f(args):Ret > project calls · -=private · ?=tests · ×0=no static use · ✓=tested · ⋮N=lines · !E=throws · p{a,b}=pa,pb
|
|
41
|
+
· deps .→ids | engine→ids
|
|
42
|
+
src
|
|
43
|
+
App(C) ×0
|
|
44
|
+
main(args) ×0 > PricingEngine,evaluate,OrderId.of,ItemId.of
|
|
45
|
+
delta
|
|
46
|
+
AddOp,RemoveOp(R{nodeId}) : DeltaOp
|
|
47
|
+
weight():int ×0
|
|
48
|
+
DeltaOp(I) sealed:AddOp|RemoveOp
|
|
49
|
+
weight():int ×0
|
|
50
|
+
engine
|
|
51
|
+
OrderStatus(E{NEW,PAID,SHIPPED})
|
|
52
|
+
isTerminal():boolean ×0
|
|
53
|
+
PricePort(I)
|
|
54
|
+
quoteFor(order):Quote ×0
|
|
55
|
+
supports(order):boolean ×0
|
|
56
|
+
PricingEngine(C{basePrices}) : PricePort
|
|
57
|
+
quoteFor(order):Quote ×0 > evaluate
|
|
58
|
+
supports(order):boolean ×0
|
|
59
|
+
evaluate(order,items):Quote !UnknownItem > UnknownItemException,Quote
|
|
60
|
+
Quote(R{order,totalCents})
|
|
61
|
+
UnknownItemException(C) : RuntimeException
|
|
62
|
+
ids
|
|
63
|
+
ItemId,OrderId,UserId(R{value})
|
|
64
|
+
of(raw):⟨X⟩ > ⟨X⟩
|
|
65
|
+
? tests
|
|
66
|
+
src/test
|
|
67
|
+
PricingEngineTest.java{PricingEngineTest,BulkDiscounts}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Reading it is easier than it looks, and the legend on line 2 teaches the notation to
|
|
71
|
+
any LLM:
|
|
72
|
+
|
|
73
|
+
- **The tree** mirrors your directory layout, shared path prefixes stated once.
|
|
74
|
+
- **Types** expose field names rather than redundant field types.
|
|
75
|
+
`PricingEngine(C{basePrices}) : PricePort` is a class with a `basePrices` field
|
|
76
|
+
implementing `PricePort`. Records/interfaces use the same braces, enums list
|
|
77
|
+
values, aliases retain their target, and sealed interfaces retain permitted types.
|
|
78
|
+
- **Functions** show parameter names and return types: `evaluate(order,items):Quote`.
|
|
79
|
+
Types appear beside names only when overloads would otherwise collide.
|
|
80
|
+
- **Call chains** follow the `>`: what a function calls, in order. Variables resolve
|
|
81
|
+
to their declared types (`PricingEngine.evaluate`, not `engine.evaluate`), standard
|
|
82
|
+
library calls are dropped, and chains are transitively reduced — if `a > b` and
|
|
83
|
+
`b > c`, then `a`'s line doesn't repeat `c`.
|
|
84
|
+
- **Same-shape types group.** `ItemId,OrderId,UserId(R{value})` is a family in one
|
|
85
|
+
entry; `⟨X⟩` stands for each member's own name in the methods they share.
|
|
86
|
+
- **Markers**: `✓` = resolved call from a test · `⋮120` = the body is 120 lines ·
|
|
87
|
+
`×0` = no statically observed project reference to a function/class/method ·
|
|
88
|
+
`!UnknownItem` = throws (`Exception` suffix implied) · no `:Ret` = returns void ·
|
|
89
|
+
`» index.ts: A,B` = barrel re-exports.
|
|
90
|
+
- **Private members** always appear as names. Repeated prefixes factor losslessly:
|
|
91
|
+
`_extract_{java,python,typescript}` means those three exact identifiers.
|
|
92
|
+
- **Tests** list every detected test file and its classes. Test functions are omitted
|
|
93
|
+
because their names cost tokens without improving placement guidance.
|
|
94
|
+
- **`· deps a→b`** = module `a` uses types from module `b`: the import architecture
|
|
95
|
+
without reading imports.
|
|
96
|
+
- **`state`** hashes the exact sources plus the generator, so source or extraction/
|
|
97
|
+
rendering changes make old maps stale.
|
|
98
|
+
|
|
99
|
+
## Languages
|
|
100
|
+
|
|
101
|
+
| Language | What you get |
|
|
102
|
+
|---|---|
|
|
103
|
+
| Java, C#, TypeScript/JS, TSX/JSX | types with named fields, name-based signatures, relations, resolved calls, privates, aliases, object APIs, re-exports |
|
|
104
|
+
| Python | same, via the standard library's `ast` — zero dependencies |
|
|
105
|
+
| Kotlin | classes, data classes, enums, interfaces, named fields, supers, calls |
|
|
106
|
+
| Go, Rust, C, C++ | types, traits, structs, signatures, calls, receiver bindings |
|
|
107
|
+
| Vue, Svelte | the component plus everything in its `<script>` block |
|
|
108
|
+
| Lua | functions and methods with call chains (params by name — it's untyped) |
|
|
109
|
+
| Bash/zsh (`.sh`, `.bash`, `.zsh`) | functions (both definition forms) with command-call chains; `_name` = private |
|
|
110
|
+
| HTML | element ids and custom-element tags, plus nested `<script>`/`<style>` blocks run through the JS/CSS extractors (when those grammars are installed) |
|
|
111
|
+
| CSS | class/id selectors, custom properties (`--x`), `@keyframes` names — names only |
|
|
112
|
+
| Helm | template `define` names, `values.yaml` keys, chart name |
|
|
113
|
+
|
|
114
|
+
## Getting started
|
|
115
|
+
|
|
116
|
+
Install from PyPI (the `grammars` extra pulls in every tree-sitter parser up front):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install "hologram-map[grammars]"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
hologram init --root /path/to/repo
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or skip installation entirely — it's a single file. Clone it anywhere and point it
|
|
127
|
+
at a repo:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python3 ~/workspace/hologram/hologram.py init --root /path/to/repo
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
That installs git hooks and embeds the map in every agent context file the repo
|
|
134
|
+
already has. From then on the hooks refresh it after every commit, merge, and
|
|
135
|
+
checkout. You never touch them again.
|
|
136
|
+
|
|
137
|
+
The first time it meets a language it has no parser for, it offers to set one up: it
|
|
138
|
+
creates a `.venv` next to itself and pip-installs the right tree-sitter grammar. You
|
|
139
|
+
type `y` once. Every later run finds that venv on its own, so plain
|
|
140
|
+
`python3 hologram.py …` always works. Python-only repos skip all of this — the
|
|
141
|
+
standard library is enough.
|
|
142
|
+
|
|
143
|
+
Everything it can do:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
hologram.py build --root . # refresh the embedded map
|
|
147
|
+
hologram.py build --root . --lang java # limit to one or more languages
|
|
148
|
+
hologram.py build --root . --if-stale # rebuild only if the code changed
|
|
149
|
+
hologram.py check --root . # is every context file current? exit 0 yes / 1 no
|
|
150
|
+
hologram.py diff HEAD~3 --root . # how did the API change since then?
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
A successful build prints the map's token cost and where it went:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
hologram: 1193 tokens embedded in CLAUDE.md, AGENTS.md
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Which agents get the map
|
|
160
|
+
|
|
161
|
+
`init`/`build` detect the context files a repo already uses and attach the map to each
|
|
162
|
+
one — the same map, everywhere, so Claude Code and Codex and Cursor can't drift apart:
|
|
163
|
+
|
|
164
|
+
| Agent | File it reads |
|
|
165
|
+
|---|---|
|
|
166
|
+
| Claude Code | `CLAUDE.md` |
|
|
167
|
+
| Codex, opencode, Amp, Jules, Zed | `AGENTS.md` |
|
|
168
|
+
| Gemini CLI | `GEMINI.md` |
|
|
169
|
+
| Qwen Code | `QWEN.md` |
|
|
170
|
+
| GitHub Copilot | `.github/copilot-instructions.md`, `.github/instructions/` |
|
|
171
|
+
| Cline | `.clinerules` (file or directory) |
|
|
172
|
+
| Cursor | `.cursorrules`, `.cursor/rules/` |
|
|
173
|
+
| Windsurf | `.windsurfrules`, `.windsurf/rules/` |
|
|
174
|
+
| Roo Code | `.roorules`, `.roo/rules/` |
|
|
175
|
+
|
|
176
|
+
Existing files are attached to, never invented: hologram only writes a context file
|
|
177
|
+
that already exists. Rule *directories* get one managed file of hologram's own
|
|
178
|
+
(`.cursor/rules/hologram.mdc`, `.clinerules/hologram.md`, …), created with whatever
|
|
179
|
+
front matter that agent needs to load it. A repo with none of these gets a `CLAUDE.md`.
|
|
180
|
+
|
|
181
|
+
Inside each file the map lives between two HTML-comment markers, and the block opens
|
|
182
|
+
with a short note telling the agent what it is looking at. Everything you wrote around
|
|
183
|
+
the block is preserved on every rebuild — the map is a block in your instructions
|
|
184
|
+
file, not a replacement for it.
|
|
185
|
+
|
|
186
|
+
## Staying fresh
|
|
187
|
+
|
|
188
|
+
A stale map is worse than none — an agent trusting a description of deleted code
|
|
189
|
+
is confidently wrong. Three commands make freshness a non-issue:
|
|
190
|
+
|
|
191
|
+
- `check` recomputes the `state` hash in milliseconds, without parsing anything, and
|
|
192
|
+
compares it against the stamp in every context file. Any target lagging means exit
|
|
193
|
+
1. Wire it into CI or an agent harness.
|
|
194
|
+
- `build --if-stale` uses the same probe, so "rebuild just in case" costs nothing
|
|
195
|
+
when nothing changed.
|
|
196
|
+
- `diff <rev>` points the same machinery backwards: it rebuilds the map as it
|
|
197
|
+
looked at an older revision and prints the difference — a pull request's API drift
|
|
198
|
+
on one screen.
|
|
199
|
+
|
|
200
|
+
## Does it actually help? An honest take
|
|
201
|
+
|
|
202
|
+
hologram exists because of one specific failure: an agent lands in a repo with no
|
|
203
|
+
map, greps its way to a partial picture, and writes code that already exists.
|
|
204
|
+
|
|
205
|
+
**The good.** An agent normally burns thousands of tokens re-discovering project
|
|
206
|
+
structure every single session, and most of what it reads gets discarded. The map
|
|
207
|
+
replaces that exploration. Duplication gets a real counterweight: "does this already
|
|
208
|
+
exist?" becomes something the agent can see rather than something it only catches by
|
|
209
|
+
grepping the exact right word. And because the map shows your conventions — all your
|
|
210
|
+
ID types are one-field records, your services take dependencies through constructors —
|
|
211
|
+
a model tends to extend the patterns it sees rather than invent parallel ones. Factored
|
|
212
|
+
private names, concise call lines, and the test index tell it which file to open first
|
|
213
|
+
without a raw symbol dump.
|
|
214
|
+
|
|
215
|
+
**The caveats.** None of this is enforced. The map competes for the model's
|
|
216
|
+
attention like everything else in context, and an agent can ignore it and reimplement
|
|
217
|
+
a helper anyway — it shifts the odds, it is not a guardrail. Function bodies stay
|
|
218
|
+
invisible: a 500-line algorithm and a one-liner expose the same signature, so the
|
|
219
|
+
map tells an agent what exists, never how well it's built. `✓` means a test
|
|
220
|
+
mentions the function, not that the function is correct. If your naming is misleading,
|
|
221
|
+
the map compresses and transmits the misleading names with perfect fidelity. Depth
|
|
222
|
+
varies by language — the table above is honest about which ones get the full
|
|
223
|
+
treatment.
|
|
224
|
+
|
|
225
|
+
**What's been measured.** The map in context, against the same agent without it, on a
|
|
226
|
+
private 133k-LOC codebase the model had never seen (10 headless sonnet sessions vs
|
|
227
|
+
matched baselines, transcripts reviewed by hand): **outcomes stayed equal while effort
|
|
228
|
+
dropped ~36% in turns and ~55% in searches, with navigation tasks 40% faster — one
|
|
229
|
+
answered in 4 turns with zero file reads, straight from the embedded map. Total tokens
|
|
230
|
+
came out level: the per-turn cost of the map was fully offset by fewer turns.** That is
|
|
231
|
+
the thesis doing what it was supposed to do — the map in context replaces exploration.
|
|
232
|
+
|
|
233
|
+
Caveats stay honest: n=1 per cell, one model, one corpus (results withheld — private);
|
|
234
|
+
duplication was zero in every condition, so the measured win is orientation speed, not
|
|
235
|
+
duplication prevention; and larger repos, weaker models, and chat-only contexts remain
|
|
236
|
+
unmeasured. On a famous OSS corpus the model has largely memorized, expect no benefit
|
|
237
|
+
at all — a control agent walks straight to the right API from training memory.
|
|
238
|
+
|
|
239
|
+
## How it works
|
|
240
|
+
|
|
241
|
+
One file, one pipeline: scan (only git-tracked files when inside a repo), extract,
|
|
242
|
+
render, embed. Each language has its own small extractor and they all produce the same
|
|
243
|
+
`Symbol` records, so everything downstream — receiver resolution, transitive
|
|
244
|
+
reduction, shape grouping, the final tree — is language-neutral and written once.
|
|
245
|
+
Formatting decisions were measured with a real tokenizer (o200k), not guessed.
|
|
246
|
+
|
|
247
|
+
## Tests
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
.venv/bin/python -m unittest discover -s tests
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Runs under plain `python3` too — tests for languages whose grammar isn't installed
|
|
254
|
+
just skip.
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT — see [LICENSE](LICENSE).
|