Docs: the context-pack map (2026-10-08)

Body facts, model page and rationale for the context-pack cuts in
eureka-substrate. The cut specs are the typed record in the mind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-10-08 21:18:49 +02:00
commit 776cc54ccd

363
docs/context-pack-map.md Normal file
View file

@ -0,0 +1,363 @@
# Context pack: map (eureka-substrate, 2026-10-08)
Imagination pass `imagination-context-pack`, session `self-2026-10-08`, for
ruling `eureka-substrate:ruling:context-pack-resolving-anchors`. Evidence:
`F:\Projects\eureka-toolcall-patterns-2026-10-08.md` (Eyes facts) and
`F:\Projects\eureka-context-pack-prior-art-2026-10-08.md` (prior art).
The cut_specs and the question are the typed record; this page holds body
facts, the model page and rationale only.
Pinned heads: Eureka main `0bb414d92728db61d2d587d749c94970ed2e5582` (also the
head of `hands/toolcall-relief`, whose changes are uncommitted in
`C:\eureka-toolcalls`); CultLib `05e67014`; Huginn `6b496eba`; Epiphany
`4006b14b`; Aetheria `e75083fe`.
## Body facts
Scratch: `C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\8ee3cab1-a20c-498d-a46b-e503a25ac030\scratchpad\cp\`
(`anchor-map.sh` prototype, `mindview.py`, `mindq.py`, `drift.py`, `all.json`).
This scratchpad is volatile; Hands re-derives from the specs, not from it.
B1. Diff-hunk line mapping resolves a (base, path, start, end) anchor onto any
later tree with git alone. Prototype `anchor-map.sh` parses `git diff -U0
<base> <target> -- <path>` hunk headers: hunks wholly before the range shift it
(MOVED), a hunk overlapping it marks it TOUCHED and widens the range by its net
lines, no hunk is SAME; `git diff -M --name-status` gives rename (RENAMED) and
delete (GONE). Target may be a commit or the working tree (`git diff <base>`
with no second commit). Dependencies: git and awk; no ctags, rg or tree-sitter.
B2. Real spec, real drift: `cultmath-tapes:cut_spec:cut-affine-shaders.r2`
(base `a7966142`) mapped onto `05150acd` (the merge the affine-shaders Hands
started from). Result: `CultMath.hlsl:509-510` MOVED to 510-511 (the same two
`#include` lines, checked by `git show`); `GlslMirrorTests.cs:317-319` TOUCHED,
mapped to 336-340, which is the end of `Families` with snoise-tie's two new
families inside it (checked); five anchors SAME. The spec's own prose carried
the workaround: "(GlslMirrorTests.cs:317-319 at a7966142; the lines move with
the merge)". `cut-huginn-persona-state.r2` carries another: "Hands re-takes the
anchor at the merged 13a/13b tip".
B3. Anchor accuracy at base: the four affine-shaders anchors read at
`a7966142` show exactly the code each `change` names
(`cultmath_iv_snoise_ball(float3, float)` at Interval.hlsl:83-89, the
`InlineData` list at HlslMirrorTests.cs:64-68, the presence asserts at
HlslSourceCompatibilityTests.cs:34-35, the Interval.hlsl lines in
build-unity-package.ps1:86-87). Imagination's line numbers are right at base;
what goes wrong is drift.
B4. Drift across the whole mind (`drift.py` over `all.json`: 203 cut_specs,
143 cut_reports, 1,260 file_changes). For every spec with a report and a local
repo, each file_change was mapped from `spec.base` to the first report's
`range.base`, the tree Hands started on. 557 anchors: SAME 444 (79.7%),
MOVED 64 (11.5%), TOUCHED 17 (3.1%), path absent at base 32 (5.7%). All 32
absent paths exist at the report head: they anchor files the cut creates
(none were typos). No base was unreachable. By repo: gamecult-site 55 of 125
drifted (44%), CultLib 17 of 245, Aetheria 5 of 127, Idunn 3 of 24, Bifrost 1
of 19, Epiphany, gamecult-ops and Huginn none. So about one anchor in seven was
stale when Hands first read it, concentrated in campaigns whose cuts land in
parallel.
B5. Worktree, rename, delete and failure probes (scratch clone of Eureka at
`0bb414d`): two lines inserted at 100 and line 185 edited, uncommitted:
`briefs.md:50-60` SAME, `180-190` TOUCHED to 182-192, `300-310` MOVED to
302-312. A staged `git mv` reads SAME+RENAMED; a staged `git rm` reads GONE.
Unreachable base prints `UNRESOLVED base <sha> not in this clone: git fetch
origin <sha>`; path absent at base prints `UNRESOLVED <path> absent at base`.
Limit: an unstaged delete plus an untracked new file is not detected as a
rename (git's rename detection needs both sides in the index or a commit).
Windows Git Bash prints an LF/CRLF warning per diff on stderr; the tool must
discard stderr of these calls. Line numbers are unaffected by CRLF.
B6. Cost: rename detection over a whole tree takes about 0.5-1 s per call on
Aetheria (`git diff -M --name-status`), so the drift script, which ran it per
anchor, took over 10 minutes for 557 anchors. The tool runs it once per
invocation and one `-U0` diff per distinct path.
B7. Symbol by name alone is not an identity. `git grep -W` with git's csharp
driver (supplied without touching the repo through `git -c
core.attributesFile=<file>`) prints the whole enclosing function, but the
probe name `Dispose` matched several members in one file
(`CultMeshContentSessions.cs`), and a bare name has no namespace. A symbol is
usable as a check that the resolved window contains it, and as the key for a
caller list, not as the anchor itself. Git's built-in funcname drivers cover
csharp, rust, python, kotlin and cpp (usable for HLSL/GLSL), and none covers
TypeScript.
B8. Tool availability. Starfire: Exuberant Ctags 5.8 (Strawberry Perl; no
TypeScript, Kotlin or HLSL), rg (Codex's copy), no tree-sitter, Python 3.13
via `py -3` (no `tree_sitter` module), git 2.53, gawk, perl. Yggdrasil host:
git 2.47, python3, awk, perl; no ctags, no rg. So git plus the Python standard
library is the only toolset common to both.
B9. Caller lists: `git grep -n -w -F <symbol>` takes about 0.4-0.6 s on CultLib
and Aetheria. `cultmath_iv_snoise_ball` has 20 hits in 8 files, including docs
and fixtures. `Dispose` has 386 hits, so a caller list needs a cap, and above
the cap it needs per-file counts rather than lines (SWE-agent: more than 50
results returns none and asks for a narrower query).
B10. Unity YAML: across the three most-modified Aetheria prefabs, fileIDs
(`--- !u!<class> &<id>`) are unique within each file and survive edits
(Djinni.prefab at 122e9292: 440 of 446 ids shared across a 91-hunk commit;
Inventory 116 of 116; Longinus 107 of 107). Diff-hunk mapping works on YAML
as on code. `&<fileID>` is a good symbol for a YAML anchor.
B11. The spec can be read without the model copying it. `mindview.py` spawns
`~/.eureka/bin/eureka-state.exe` (the binding in `~/.claude.json`
`mcpServers.eureka-state`: command plus `EUREKA_INSTANCE` and
`HUGINN_ENDPOINT`), speaks MCP JSON-RPC over stdio (initialize, initialized,
tools/call view) and prints the document, in 1.3 s. eureka-state has no CLI
mode of its own (`crates/eureka-state/src/main.rs:298`, stdio MCP only).
B12. Schema facts for a typed home. `CodeLocation { path: Short, line: u32,
end_line: Option<u32> }` (Epiphany `epiphany-pipeline/src/lib.rs:463`) is
shared by cut_spec file_changes, finding and follow_up locations, and mutation
records. The epoch doc (`lib.rs:879-886`) says evolution is additive at
`epiphany.pipeline.epoch.v2`: "a new named field with a serde default". No
`#[serde(default)]` exists in the leaf yet, so the `value_types!` macro has
never carried one. Huginn pins the leaf by git rev
(`crates/huginn-mind/Cargo.toml:22`, `ef956865`), so a field reaches the mind
only after a leaf change, a Huginn re-pin and a daemon plus eureka-state
redeploy.
B13. The Hands brief already asks for a hand-made pack: "Orient in one script.
One Bash call prints git status, the base and the spec's anchor ranges (`sed
-n`)" (briefs.md:276-278 at 0bb414d). That reads the frozen line numbers, so it
reads the wrong lines for the one anchor in seven that has drifted (B4).
B14. Prose `path:line` references in spec text (first, adds, keeps_moves,
change): 224 across 99 of 203 specs. Imagination already writes read anchors
inline, untyped (for example "as iv_frustum_ball's FrustumBall check
(GlslMirrorTests.cs:191) does").
## Model page
| Kind | Identity | Lifecycle | Authority |
|---|---|---|---|
| Edit anchor | `(cut_spec id, file_changes index)`; content `(spec.repo, spec.base, path, line, end_line)`. Namespace: the spec's repo at its base commit, which is injective: one path and range at one commit. A short `base` is resolved with `git rev-parse --verify <base>^{commit}`, and ambiguity is a loud failure. | Created with a spec revision and immutable. Changed only by revision N+1 with a superseding resolution. Its position at a later tree is derived at read time, never stored. A path absent at base is a file the cut creates (B4); it belongs in `adds`, and the tool reports it as NEW. | Imagination writes it (admits the spec). Hands and the tool never rewrite it. A wrong anchor is a deviation in Hands' report and a revision by Imagination. |
| Read anchor (code to see, not to change) | **Empty cell.** It has no typed home. Today it is prose `path:line` (B14) with no namespace (basenames), no base and no check. | None: it is not a document. | None. This is question `context-pack-read-anchors`. |
| Symbol (a check and a caller key) | **Empty cell.** A name is not injective (B7). Its only sound role is a needle that the resolved window must contain, plus a caller-list key. | Would be the anchor's. | Same question. |
| Resolved anchor and window (the expanded pack) | `(spec id, worktree HEAD, working-tree state)`. Derived and never persisted. | Printed into Hands' context. Stale once Hands edits inside it, and re-expanded for edited anchors only with `--since <start sha>`. No cache, no pack file. | `tools/context-pack.py` derives it. Nothing stores it. A stored pack would be a cache pretending to be truth. |
| The reader tool | `tools/context-pack.py` in GameCult/Eureka. | Lands by Forgejo PR after a Soul pass. Pruned when no brief names it (tools/INDEX.md rule). | Eureka repo. Owns "what Hands sees of a spec's anchors at its tree". It does not own anchors and does not write the mind. |
| Measurement baseline | The Eyes report above and its scripts (`cls.py`, `an2/an3/an4.py`) under this session's scratchpad `eyes-toolcalls\`. | **Volatile.** The scratchpad is session-scoped, so the scripts can vanish before the re-measure. | Self (follow_up `context-pack-measurement`). |
## Rationale
**Anchor grammar: base-pinned line range, resolved by diff-hunk mapping.** The
candidates were: symbol paths (`File::Type.Member`), git funcname (`git grep
-W`, `git log -L :fn:file`), ctags, tree-sitter queries, LSP document symbols,
and a text needle plus line hint. Every one of them except the line range needs
a per-language parser, and most need a dependency that is missing on Yggdrasil
or on both hosts (B8). Unity YAML has no symbol grammar in any of them, and a
bare symbol name is not injective (B7). The spec already pins a full
coordinate, `spec.base` plus `path:line-end_line`, and git can carry that
coordinate onto any later tree, the working tree included (B1, B5). This is
the way Gerrit ports review comments across patch sets and GitHub marks a
comment outdated: the anchor is stored where it was made, and diffs carry it
forward. It is language-agnostic, needs only git, and fails loudly on every
path that cannot be carried (base unreachable, path absent, range past EOF,
file deleted). One anchor in seven needed carrying in practice (B4), and the
`sed -n` orient step reads those wrong today (B13).
**Why the edit anchors are the v1 pack.** file_changes already hold the
planner's localization at base, accurate when mapped (B3), which is
Agentless's and ORACLE-SWE's edit-location handoff. The tool turns them into
SWE-agent-shaped output: one status table first (lost-in-the-middle: the
summary at the top), then windows of at most 100 lines (the SWE-agent ablation
optimum; full-file views lost 5.3 points), then caller lists capped at 50
lines with per-file counts beyond that (the iterative per-hit search lost 6
points). An output cap keeps the pack from becoming the 20%-more-steps
AGENTS.md overview (Gloaguen et al.). Re-expansion with `--since` answers the
re-read finding (54%, 1,198 repeats with no edit in between): the edited
windows come back fresh and nothing else is reprinted, as in SWE-agent's
viewer auto-refresh and CORVUS's current-contents registry.
**Why read anchors and symbols are a question rather than a cut.** Three homes
exist: a typed field (an additive `CodeLocation.symbol` and a `reads` list on
cut_spec, three passes across Epiphany, Huginn plus a deploy, and Eureka, B12);
a `Pack:` line convention in `first` (free, but structure carried in prose);
or a sidecar pack file. The sidecar was rejected outright: it is a second store
with its own lifecycle that goes stale the day a spec is revised. Which of the
first two, and whether now or after measuring v1, decides schema work in the
mind's own leaf. The question records options, evidence and a recommendation.
Nothing in cuts context-pack-tool and context-pack-briefs depends on the
answer.
**Base for the Eureka cuts.** Both cuts edit `tools/test-tools.sh` and
`references/briefs.md`, which `hands/toolcall-relief` is changing (uncommitted
in `C:\eureka-toolcalls`; its branch head equals main `0bb414d`). The specs pin
`base 0bb414d` and anchor at it. Hands starts from main after that PR merges,
and the first thing context-pack-briefs does is resolve its own briefs.md
anchors through the tool that context-pack-tool lands (the +39 lines shift
them). If the PR has not merged, Hands stops.
**Subtraction.** Removed: the Hands orient step's `sed -n` of frozen ranges
(briefs.md:276-278) and the "reading a file twice is a smell" advice that the
tool replaces. Imagination's prose drift notes ("the lines move with the
merge", "re-take the anchor") are no longer needed and the map brief says so.
Not added: a parser per language, a cache, a pack file, or a mind field.
## Read anchors and symbols in the schema (2026-10-08, imagination-read-anchors)
Imagination pass `imagination-read-anchors`, session `self-2026-10-08`, for
operator ruling `eureka-substrate:ruling:context-pack-read-anchors`
(typed-fields-now). Cuts: `leaf-read-anchors` (Epiphany), `huginn-read-anchors`
(Huginn), `ops-read-anchors-upgrade` (gamecult-ops), `context-pack-reads`
(Eureka). Pinned heads: Epiphany origin/main `ef9568655e75bb6cb362a038e2739a5ac5e777ee`
(the rev Huginn pins; the local `F:\Projects\Epiphany` main is behind it at
`2bf39eef`), Huginn main `6b496eba8c8cfde7b93bb8bc46b1961abca52fd4`,
gamecult-ops main `7e233877dfb21f75d5bbe994641781f718098eac`, Eureka main
`0bb414d92728db61d2d587d749c94970ed2e5582`. Probes ran in detached worktrees
under this session's scratchpad `ra\`, removed afterwards.
### Body facts
B15. Stored payload encoding. `PipelineDocument::prepare` calls CultLib
`prepare_entry_named` (cultcache-rs at `8fc74c7`, `lib.rs:2244-2259`), which is
`rmp_serde::to_vec_named`: every value struct is a MessagePack map keyed by
field name. `decode` is `rmp_serde::from_slice` (leaf `lib.rs:745-751`). No
`deny_unknown_fields` exists in the leaf. Consequences: (a) an old document
lacking a new key decodes when the field is `Option` or carries
`#[serde(default)]`; (b) an old daemon reading a new document ignores the
unknown key, so after a rollback it serves such documents without the field
while the stored bytes keep it.
B16. Re-encoding is load-bearing. Admission prepares every write afresh
(`admission.rs:143-157`) and the receipt digest is over those payloads
(`receipt.rs:123-126`); A9 compares it with stored receipts to answer
`AlreadyAdmitted` (`admission.rs:164-166`). So if a document without the new
fields encoded differently after the change (for example `symbol: null` or
`reads: []` written out), replaying any pre-upgrade batch would stop answering
`AlreadyAdmitted`, and target invariant `stored-documents-valid` ("No change
re-keys, re-encodes or invalidates a document already in the mind") would
break. Hence `skip_serializing_if` on both fields, not only `serde(default)`.
B17. The macro. `value_types!` (`lib.rs:415-436`) emits a struct whose fields
carry only an optional `#[schemars(extend("maxItems"))]`; it has no way to put
a serde attribute on a field. `Bounded for Option<T>` exists (`lib.rs:79-83`);
`Vec` is validated only through the `[max]` list form (`list`, `lib.rs:110`).
`=> extra` is a struct-level validation hook (`campaign_repos_are_distinct`,
`lib.rs:452-468`). Refusal variants are pinned at five by the schema test
(`lib.rs:2717`), so new bounds must reuse `FieldBound` and `InvalidFormat`.
B18. Who constructs the changed structs. Epiphany: `CodeLocation {` 3 sites and
`PipelineCutSpec {` 1 test sample, all in `lib.rs`; no other Epiphany crate
builds them. Huginn: `CodeLocation {` in `huginn-mind/src/fixtures.rs:88`,
`huginn-daemon/src/daemon.rs:360`, `eureka-state/tests/client.rs:59`;
`PipelineCutSpec {` in `fixtures.rs:192`, `daemon.rs:361`, `client.rs:44`
(`admission.rs:432` uses `..spec.clone()` and needs no edit). Published schemas
that embed `CodeLocation`: Epiphany `schemas/cultnet/epiphany.pipeline.{cut_spec,cut_report,finding,follow_up}.v2.schema.json`;
Huginn `schemas/cultnet/huginn.mind_{request,response}.v1.schema.json`, pinned
byte for byte by `wire.rs:276-280`. The leaf's own check is
`pipeline_published_schemas_match_derivation` (`lib.rs:2723`), which writes the
derived copies to a temp dir on failure.
B19. Size calibration. `daemon.rs:349-356`'s `cut_spec` claims "every list the
leaf bounds filled to its maximum"; `client.rs:220-236` calibrates `fits` (180
file changes) under `MAX_RESPONSE_BYTES` and `wide` (256) over it. Filling the
new fields in the daemon builder keeps its claim true; filling them in the
client builder would move the calibration.
B20. Admission has no tree. Huginn holds no repository checkout; admission
checks bounds, formats, keys and cross-document rules only. "The symbol is in
the window at base" cannot be an admission rule. Its owner is the reader tool,
run by Imagination at base after admitting (cut context-pack-briefs) and by
Hands at its tree.
B21. Index text. `huginn-mind/src/index.rs:42-70` indexes a cut_spec's title,
first, adds and keeps_moves only, not file_changes. Leaving `reads` out keeps
the index text unchanged, so no reindex follows the upgrade.
B22. Deploy owner. Huginn is not an Idunn target: `runbooks/huginn-yggdrasil.md`
(gamecult-ops) calls it "an interim deployment: a hand-installed systemd unit",
retired by Idunn only in Huginn's 14-I, and says "No brake gates deployment of
this unit". Target r2 names the same runbook as the deploy's canonical
implementation. The live release is `releases/107552dc4b27b8856b043616ae74c2ebe7908b66`
(read-only `readlink` on Yggdrasil, unit active); Huginn main since then changes
docs only (`git diff --stat 107552dc..6b496eba -- crates Cargo.toml Cargo.lock`
is empty). The client is `C:\Users\Meta\.eureka\bin\eureka-state.exe`, whose
MCP `admit` input schema is derived at run time from the leaf types through
schemars (`eureka-state/src/main.rs:73`), so a rebuilt client publishes the new
fields with no hand edit; an agent session sees them only after it starts
anew. The runbook's rollback paragraph (`:109-113`) is specific to the 64-byte
key widening.
B23. The harvest. Target r2's canonical implementations put the leaf at Huginn
`crates/eureka-pipeline`, and Huginn `docs/eureka-self-cut.md:435` (harvest H1)
copies it there "from Epiphany head `4006b14b`", which is 5 commits behind
`ef956865`. H1 has no cut_spec yet (eureka-body has only questions and
rulings). If H1 copied from `4006b14b` after this change merges, it would drop
both leaf-key-bound and the new fields.
### Rationale
**Where the fields land.** The leaf still lives in Epiphany and Huginn pins it
by git rev, so the field goes into Epiphany's leaf, then Huginn re-pins, as the
ruling's option text names. Waiting for H1 would make this one Huginn cut
instead of two, but H1 is unscheduled. The operator accepted the three-pass
cost in her words. Follow-up `leaf-harvest-copies-head` records the constraint
on H1. If H1 merges before `leaf-read-anchors` starts, Self stops the cut and
Imagination re-maps it onto `crates/eureka-pipeline` (one Huginn cut, no
re-pin).
**Shape.** `CodeLocation.symbol: Option<Symbol>` everywhere a location is (file
changes, findings, follow-ups, mutations). It means the same thing in every
one: a name the location's lines must contain. `PipelineCutSpec.reads:
Vec<ReadAnchor>[16]` with `ReadAnchor { location: CodeLocation, why: Short }`.
It is a distinct type, not `FileChange`, because a read changes nothing and a
field named `change` would lie. Reads are in the spec's repo at its base, like
file_changes. A read in another repo stays prose in `first`, because a read
cannot pin another repo's commit. `Symbol` is a new bounded text: 1-128 bytes,
no whitespace or control character. It is new, so no stored document can fail
it. A read's window is bounded by an `=> extra` hook on `ReadAnchor`: `line >=
1`, `end_line >= line` (`InvalidFormat`), and span at most 100 lines
(`FieldBound`, limit 100). 100 is the window cap the tool already uses. No new
bound touches `CodeLocation`'s existing fields, because stored findings would
then stop validating.
**The macro owns "additive".** `value_types!` gains a per-field marker written
after the type and any list maximum, for example `symbol: Option<Symbol> =
absent` and `reads: Vec<ReadAnchor>[16] = absent`. It emits
`#[serde(default, skip_serializing_if = "Absent::absent")]`. Trait `Absent` is
implemented for `Option<T>` and `Vec<T>` only, and the validate expansion
names `<$ty as Absent>::$marker`, so a marker word other than `absent`, or the
marker on any other type, is a compile error. The epoch doc and the schemas
README then say this marker is how a field is added at v2. This replaces the
hope that every future author remembers both attributes (B16).
**No admission rule in Huginn.** Bounds are the leaf's, and Huginn applies them
through `validate` on every write. The symbol-in-window check needs a tree
(B20) and belongs to `context-pack.py`, which exits nonzero on
`SYMBOL_MISSING`.
**Deploy.** The owner is the gamecult-ops runbook's Upgrade section. It is not
Idunn: Idunn does not own this unit, and no brake applies (B22). The ops cut
generalises the rollback paragraph for additive fields and then executes the
Upgrade steps. One addition: the read-back test also asserts that every stored
document re-prepares to its stored payload bytes. That checks B16 on the live
mind, not only in a unit test.
**Eureka.** One new cut after both v1 cuts and the deploy. The v1 specs stay as
they are, because v1 must not wait on the deploy.
**Subtraction.** Removed: prose `path:line` read references in specs for the
spec's own repo (224 today, B14), replaced by checked reads; Imagination's
"re-take the anchor" notes; Hands' first-pass grep for callers of named
symbols. Added: one trait, one text type, one struct, one macro marker, two
fields, one tool section. Not added: an admission rule, a Huginn type, an
index change, or a second store.
B24. The admit door drops unknown keys silently. The batch that admitted these
specs had one file_change location with a stray `"symbol": null`. It was
admitted as Committed (receipt `mind-commit-c528cc24...`), and a view of
`cut-context-pack-reads.r1` shows that location without the key. This is B15(b)
at the JSON door: a session that started before the client swap will drop
`reads` and `symbol` and still report success. Follow-up
`gap-admit-drops-unknown-keys` (owner: Huginn eureka-state). Meanwhile the ops
cut makes agents restart, and context-pack-reads stops if its admit schema
lacks `reads`.
Admitted (receipts `mind-commit-c528cc24...`, `mind-commit-5d97a6ad...`):
`eureka-substrate:cut_spec:cut-leaf-read-anchors.r1`,
`cut-huginn-read-anchors.r1`, `cut-ops-read-anchors-upgrade.r1`,
`cut-context-pack-reads.r1`; follow-ups `leaf-harvest-copies-head` and
`gap-admit-drops-unknown-keys`. Order: leaf, then Huginn, then ops (deploy),
then Eureka. The Eureka cut also waits for both v1 cuts.