Skip to content

feat: the vocabulary, M19a–M19c (#27–#40) - #46

Merged
jgrodziski merged 89 commits into
mainfrom
feat/m19c-estimate-measured
Nov 5, 2026
Merged

jgrodziski merged 89 commits into
mainfrom
feat/m19c-estimate-measured

Conversation

@jgrodziski

Copy link
Copy Markdown
Contributor

The bottom of the M19 stack (issue #6, PLAN §20), brought to main as one PR so the stack can land in order: this, then #42, #43, #44 and #45 as GitHub retargets each one.

It carries fourteen PRs that were each reviewed and merged inside the stack, one onto the previous branch, on top of #26 (the profile lexicons, already on main):

PR
#27 feat(vocabulary): split, classify, weight (M19a)
#28 feat(vocabulary): aggregate into vocabulary.json (M19a)
#29 feat(cli): codegraph vocabulary and drift (M19a)
#30 fix(vocabulary): coverage measured, list corrected (M19a)
#31 feat(vocabulary): the word cloud as SVG (M19b)
#32 feat(navigator-ui): the Vocabulary tab (M19b)
#33 feat(cli): vocabulary: in the city (M19b)
#35 feat(insights): explain defines the names' words (M19c)
#36 feat(insights): the store indexes the words (M19c)
#37 feat(insights): the word readers (M19c)
#38 feat(cli): vocabulary --drift-insights (M19c)
#39 feat(serve): the glossary in the page (M19c)
#40 fix(insights): the estimate's measured word cost (M19c)

The diff against main is the stack's whole content up to #40: the @codegraph/vocabulary package, codegraph vocabulary, the Vocabulary tab and the vocabulary:<word> city metric, the words explain asks a model to define, the version-2 insights store and its readers, the glossary in the page. A local git merge-tree of main and the stack tip reports no conflict; #40's CI was green on every job.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PKCVh2RjqQgcaeCBoVEntz

Jérémie Grodziski and others added 30 commits October 23, 2026 17:13
New package @codegraph/vocabulary (PLAN.md §20.1). splitIdentifier(name,
kind, lexicon) breaks on every character that is not a letter, a mark or
a digit, then on camel-case boundaries read from Unicode categories:
lower→Upper, digit→Upper, and an Upper run before Upper+lower. A digit
run stays with its word (utf8, md5, v2); a piece of digits only is
dropped and counted. The lexicon adds the conventional names skipped
whole, the file kinds whose extension is no word, and C#'s I prefix.

Hand tables per language, read through each shipped lexicon, the
unicode fixture, and fast-check properties: words are lowercase slices
of the name, a separator splits a name into its parts' words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
createClassifier(lexicon, technical) tags each word, in this order:
keyword (the lexicon reserves it), short (one letter or digit, marks
excluded), technical (the shared list ∪ the profile's), else domain. A
keyword the splitter never yields whole (`defined?`, `__FILE__`)
classifies nothing: the `file` of `upload_file` is not `__FILE__`.

DEFAULT_TECHNICAL is the plumbing every language shares. Words two or
more lexicons listed (args, opts, ctx, err, callback, json, str, mutex,
mixin, async, repo) move into it and out of the profiles, as METAMODEL
§5.3 already says a profile's list is what lies beyond the shared one;
a test keeps the two disjoint. The website profiles page is
regenerated from the CLI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roleOf(entity, slots) reads traits, never the kind: TModule → module,
TType → type, TInvocable → operation, TStructural → parameter or local
when an entity lists it in parameters[] / localVariables[], else
attribute. A named entity with none of these (a TS namespace, a PHP
code file, a Python decorator) is `other`: principle 1 says its words
still count. DEFAULT_WEIGHTS: module and type 8, attribute 4,
operation 3, parameter, local and other 1.

On the fixtures, every listed parameter and local resolves; the C#
indexer's parameter is listed nowhere (a property has no
TWithParameters in that profile), weighs as an attribute, and is
pinned by id until the model lists it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
§20.1 states the digit and caseless-letter rules of the split, the
properties as tested, the keyword spellings that classify nothing, the
shared technical list (125 words, disjoint from every profile's), the
`other` role and the C# indexer parameter the model lists nowhere.
§20.4 ticks the three modules and keeps aggregate, artifact and drift
open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A profile's lexicon gains `qualified`: the kinds a language names by a
qualified path, each with the separator that ends the qualifier. Java's
package by `.`, C#'s namespace, Elixir's module and protocol, Clojure's
namespace and Python's module by `.`, Ruby's class, module and constant
by `::`, PHP's namespace by `\`, and every file kind by `/`. Go's
package and Rust's module are named by one identifier, so neither is
listed.

The vocabulary will weigh the words before the last separator as
incidental ones: on the fixtures, full names made `acme` weigh 392 in
Elixir and 468 in Ruby against about 50 for the next domain word.

validateProfile reports a qualified kind the profile does not define
(`lexicon-unknown-kind`) and a separator that is empty or holds a
letter, a mark or a digit (`lexicon-not-a-separator`). `profiles`
prints the row and carries the key in --json; METAMODEL §5.3, the CLI
reference and both website pages follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`splitIdentifier` now reports, beside the tokens, how many of them spell
the name's qualifier: what precedes the last separator of a kind the
lexicon declares qualified (`AcmeOrder::Order::Line` → 3 of acme,
order, order, line; `packages/order/src/basket.ts` → 3). The tokens
themselves never change, a property pins it; only the boundary is new.
The interface prefix now reads the own name after the qualifier.

The weights gain a `qualifier` key, 1 by default: a container's words,
spoken again by every name it qualifies, weigh as incidental ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`buildVocabulary(graph, options)` counts the words of every kept
entity's name into the scopes the fold puts it in: its type
(containingType), its module (containingModule) and the corpus. A stub
never speaks, whatever the view: its name was chosen elsewhere. An
entity's language is its model's, read from the union, never its id.

A word weighs its carrier's role, or `qualifier` (1) before the last
separator. Each scope keeps `[term, weight, occurrences, carriers]`
rows by weight then term, its entities by role, what it skipped
(unnamed, conventional, numeric) and its distinct and distinctDomain
counts. Each term carries its spread: the module and type scopes that
speak it, and the one module when only one does. A word two languages
class apart is two terms. Kept entities no module contains count at
the corpus alone and are listed.

The artifact, `codegraph.vocabulary/1`, records its view, weights,
classes and the technical list's size and sha256. It is compact by
default; `pretty` writes one term and one scope per line.

Tested on a hand-counted corpus and by properties over generated ones:
closure, conservation (every kept entity in one module or unplaced),
and the same bytes whatever the order entities arrive in.

The package now depends on @codegraph/analyzer, as PLAN.md planned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each fixture's model now has its words committed beside it:
`fixtures/<name>/expected/vocabulary.json` for java, csharp,
typescript, elixir, ruby and unicode. They are pretty-printed, one term
and one scope per line, so a change to the split or the weights reads
in review as the words it moved. The vocabulary's tests rebuild them
byte for byte, check their closure, and rebuild them over a fresh
graph; CODEGRAPH_UPDATE_SNAPSHOTS=1 rewrites them.

Each README counts one word by hand, name by name: Java's and C#'s
`order` (40 and 42, through the package and namespace qualifiers),
C#'s `priceable` (no word `i`), TypeScript's `legacy` and `packages`
(path qualifiers), Elixir's and Ruby's `pricing` (dots, `::` and
slashes). A test pins those counts, so a snapshot rewritten in one
command still has to agree with them. The C# fixture gains a README
for its count, pointing at docs/csharp-extractor.md for the rest.
The unicode README notes that terms sort by code unit too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PLAN §20.1 now has the lexicon's `qualified` key and the measurement
that called for it. It also covers the splitter's `qualifier` count and
its properties, the `qualifier` weight row, and the aggregation as
built: stubs never speak, nearest-fold scopes, per-scope counts, one
term per (word, class), unplaced and withoutLexicon. The artifact
section adds the technical list's sha256 and the pretty form.

In §20.4 the qualifier, aggregate.ts, artifact.ts and the snapshots
(the five fixtures plus unicode, hand counts pinned by a test) are
checked. drift.ts moves to the command's item, the next PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`parseBusinessTerms(text)` reads a plain glossary, one term per line,
each split with no lexicon so `Order Line` meets `order` and `line`.
Blank lines and `#` comments are skipped; a line naming no word (`2024`)
is reported by its number; a term whose words repeat an earlier one's
is that term.

`vocabularyDrift(artifact, terms, {level})` says, per business term,
which scopes of the level speak every one of its words (whatever class
the code gives them) and which words no scope of the level says. The
other half is the code-only words: `domain` terms some scope of the
level speaks that no business term contains, summed over those scopes,
by weight then term. Nothing is stemmed: `lines` is not `line`.

Tested on the hand-counted corpus at the three levels, and by
properties over the java and ruby fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`operationScope(graph, artifact, id, options)` counts one operation's
words as the artifact counts a scope: the operation and every kept
entity written inside it, lambdas and their parameters included, short
of a type or module, which are scopes of their own. Its rows index the
artifact's terms and its parent is the nearest stored scope, so a
reader shows it beside the stored ones. PLAN.md §20.5 keeps these out
of the artifact; `codegraph vocabulary --scope ID` will ask for one.

The counting rule moves into one function both paths call, so the
fixture snapshots are unchanged. An artifact built under other options
is refused rather than mis-indexed. A C# delegate, invocable but a
type, stays a type scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`parseTechnicalWords(text)` reads the list `--technical FILE` will
replace the shared one with: one word per line, lowercased as the
splitter lowers a token, blank lines and `#` comments skipped. A line
that is not one word (`HTTPServer`, `value object`, `2024`) could never
match a token, so it is refused by its line number instead of kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`analyze` printed its view as `name (filters)`, or `all (nothing
filtered …)`, through a private helper. It moves into view.ts beside
`resolveView`, so the next report that states its view prints it the
same way. analyze's output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`codegraph vocabulary [model.jsonl...]` reports the words the model's
names are made of, as `@codegraph/vocabulary` counts them (PLAN.md
§20.1). Every report states its view, weights and technical list.

- No flag: the corpus's 40 heaviest domain words. Each row gives
  weight, occurrences, carriers, and how many modules and types say it.
- `--level module|type`: one block per scope, ranked by distinct terms
  (the cognitive-load report), 10 words each by default.
- `--scope ID`: one stored module or type, or an operation counted on
  demand. An unknown id, a stub, or a member that is no operation is a
  usage error that says what the id is.
- `--class domain|technical|keyword|short|all` and `--top N` pick the
  rows.
- `--weights KEY=N,…` overrides weights over the defaults. An unknown
  key, a repeated key or a negative weight is a usage error.
- `--technical FILE` replaces the shared list whole; a line that is not
  one word is refused by its number.
- `--drift FILE` compares with a glossary at `--level`. It lists the
  business terms no scope says, with their missing words, the ones it
  says, and the domain words no term contains. It exits 3 when a term
  goes unsaid, as `analyze --report cycles` does for a cycle.
- `--out FILE` writes vocabulary.json beside the report.
- `--json`, `--internal-only`, `--declared-only` and `--no-cache` work
  as on the other commands. The cache path counts the same words.

Tests cover each flag, text/JSON parity, determinism, stream purity and
the exit codes end to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The command gets its section and synopsis in docs/cli.md and a website
page: the flags, what a scope is, how drift matches a glossary, the
exit codes, and two examples taken from the java fixture's real
output. The artifact gets a website page: every field of
`codegraph.vocabulary/1` and the weights table. The CLI index, the
artifacts index and the exit-codes table list both, and `vocabulary`
joins the commands that build and reuse a sibling model.db.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
§20.1's command paragraph now describes what was built:
- the header's view, weights and technical list;
- the 10-word blocks under --level;
- an operation's scope on demand, and the usage errors that say what an
  id is;
- the --weights and --technical rules, and --out beside the report;
- the glossary's comments, merges and wordless lines;
- drift's two halves and its exit 3;
- the flags a report would not read, refused;
- the JSON kind.

§20.4 checks the drift and command item. Left in M19a: the coverage
table on the real corpora.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on BroadleafCommerce, Mastodon, Plausible and codegraph itself
(PLAN.md §20.1), each word read off the names that carry it: 13 words
named the program's machinery, not the business, in a real corpus's top
ten, and join the shared technical list (125 → 138).

- name type value key map property field: the data vocabulary of every
  language (getName, propertyName, cacheKey, parsedValue, itemMap).
- extension extensions: Broadleaf's …ExtensionManager pattern. Moved
  from the C# profile, since a word two lists hold is the shared list's.
- read: the R of CRUD, missing from a list that claimed the CRUD verbs.
- serializer worker index: Mastodon's app/serializers/, app/workers/
  and search indexes; index is machinery in all four corpora.

Broadleaf's top ten now reads order item customer offer product admin
group fulfillment category payment. In the fixtures only value, name and
index change class; no weight moves. Snapshots, the C# profile page and
the CLI page's examples follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The §20.1 table filled on the five fixtures, BroadleafCommerce,
codegraph, Mastodon and Plausible (commits, machine and method stated),
what their unnamed entities are, the Broadleaf top ten reviewed and the
list correction argued word by word. The last M19a item is checked and
the milestone row marked done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
layoutCloud (PLAN.md §20.2): pure geometry, no randomness. Words by
weight, ties by text then class, so any order of the same words lays
out the same; the font size on the square root of the weight (area
reads as magnitude); each padded box at the first point of a 2:1
Archimedean spiral where it overlaps nothing placed. The measurer is
injected: a monospace default that counts a mark as no cell and a wide
character as two, or a page's measureText. Type imports only, so the
navigator's tab can run the same function.

Properties: no overlap, every box inside the canvas, order-independent,
weight order with sizes never inverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vocabularyCloudSvg draws one scope's words: size by weight, colour by
class and nothing else (a stylesheet keyed by class, light and dark;
no inline fill), a key when more than one class is drawn, and a legend
stating the scope, counts, weights, view and technical list. The font
is monospace, so the bytes are the same on every run:
fixtures/java/expected/vocabulary.svg is a snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--svg FILE draws the cloud of the one scope reported, the corpus or the
--scope one (an operation counted on demand too), from the words
--class and --top pick, and confirms on stderr. A cloud is one scope's,
so --svg with --level module|type or --drift is a usage error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/cli.md and the website's vocabulary page describe --svg: what
size, colour and the legend say, and when the flag is refused.
docs/vocabulary-broadleaf.svg is BroadleafCommerce's corpus cloud,
drawn by the command with --top 80.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
§20.1's command paragraph states when --svg is refused; §20.2 records
the layout's settings, the measurer's cells and the SVG's palette as
built, and the Broadleaf demo. The first M19b item is checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The artifact folds what a name says into every scope the entity belongs
to; the carriers lookup will keep it per entity. Both now read through
reading.ts (the split, the class, the role's weight, the qualifier), so a
carrier's share is exactly what the row counted. Snapshots unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vocabularyCarriers(graph, artifact, options) reads every kept entity once
and answers which names of a stored scope carry a term, each with what it
added to the row. A row is the sum of its carriers: a property over every
term of every scope of the six fixtures. A type or module carries its id,
a member the owner a page reveals it in; an artifact counted under other
weights, list or view is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codegraph/vocabulary/cloud is cloud.ts alone, built as its own entry: a
page can run the layout as a value without the analyzer, core, zod and the
SQLite loader behind the package's index. A test keeps cloud.ts's imports
type-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codegraph serve builds vocabulary.json from the same graph, view and
name as the navigator and the city, and serves it at /vocabulary.json.
/carriers.json?term=…[&scope=…][&class=…][&limit=…] answers which names
of a stored scope carry a word, from the model the server holds: 404 for
an unknown scope or term, 400 for a request it cannot ask or a text the
scope speaks as two terms. The index is built at the first answerable
request, and the graph is let go then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daemon's job writes vocabulary.json beside navigator.json and
city.json and serves it under the token. A project directory without one,
built before it existed, is rebuilt rather than reused. The daemon holds
no model between jobs, so /carriers.json is a 404 there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sixth tab: the words of the selected scope's names, the corpus until
a module or a type is selected (a member shows its type), as a cloud and
a table. The cloud is @codegraph/vocabulary/cloud's layout measured with
the page's monospace face; size is weight, colour the class alone, and a
legend states what was counted. Class chips, a top-N slider, the scope
chain to step up. A word asks carriers.json for its names, and each
reveals its node in Navigate; with no such route the tab says so.

The artifact kind, the carriers kind and the class list are literals
pinned to the package's; the package is imported for types only, its
./cloud entry as the one value. The Vite dev server serves
VOCABULARY_JSON as it does NAVIGATOR_JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md's package map, docs/cli.md's serve section and the website's
serve, vocabulary, vocabulary.json and environment pages: the sixth tab,
the third artifact, the carriers answer and its statuses, the daemon
without a carriers route, VOCABULARY_JSON for the dev server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jérémie Grodziski and others added 27 commits October 27, 2026 16:01
From feat/m19a-vocabulary-command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From feat/m19a-vocabulary-coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From feat/m19b-vocabulary-cloud.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From feat/m19b-vocabulary-tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From feat/m19b-vocabulary-metric.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migration 1, the first rung of the ladder to run on a real file,
creates insight_word: one row per entry of a block's `words`, the
entry as JSON text less its `meaning`, with word, concern, sense and
confidence generated over it, indexed on word and id. A record's rows
are written with it in the unit's transaction (the replaced record's
deleted first), an import rebuilds the table, and nothing is exported
from it. The migration projects the words a version-1 block already
holds: a build asking prompt 2 on a version-1 store wrote some.

A reader still refuses an older store; its message now says any
`explain` on it migrates it, --dry-run included, which calls no model.

Tests: a property that the table is every record's words in block
order for any side-car, rebuilt by an import; a put replacing words,
and a refused unit writing none; and, from fixtures/store-v1.sql (the
.dump of a store the version-1 build wrote, with its export), the
migration keeping records, failures, ledger and stamp, exporting the
same bytes, the reader's refusal, and a failed migration left at 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`codegraph insights` on a store the version-1 build wrote exits 2,
names the migration that calls no model, and leaves the file's bytes
alone; `explain --dry-run` on it migrates it to version 2, with its
six records, asks for no client and writes no side-car.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/insights.md describes insight_word and the migration, and that
a reader refuses an older store with the way to migrate it; docs/
cli.md says so for `codegraph insights`. The `--insights` help (and
the website's serve page, which restates it) and the route's comment
say the panel shows nothing for a store no explain has migrated yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
§20.3 records the store as built: JSON entries with generated
columns, no `meaning`, the backfill §20.3 said was not needed, the
reader rule kept, the real-file fixture, and the scale measured on a
synthetic side-car at Broadleaf's counts. §20.4 checks the item, the
real Broadleaf side-car owed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer 3 of PLAN §20.3, as pure functions over the store port.

The port gains four projections: wordHits(word) through the
insight_word index, glossaryEntries(word?) over the module blocks,
concernRows() and withoutWords(); stats() counts the records a model
wrote without words (prompt 1). glossary.ts rolls them up:
wordReport, polysemyReport, concernsReport, wordsAbsence (prompt 1's
or a template's) and moduleOf. A record is placed in its module by
its natural key, rendered by core and grouped on naturalKeyIndex;
the id is never parsed.

Lookups are exact on any string: a word with a lone surrogate is not
what the binding sends for the index's TEXT, so it is found by a
scan, and JS equality decides either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codegraph insights --word WORD, --glossary MODULE_ID, --polysemy and
--concerns: tab-separated like the command's other lists (the module
first, so the grouping survives sort), each with a --json kind. A
record explained under prompt 1 is answered as such: --id says so,
--glossary on such a module says why it has none, and the summary and
every word answer count what they cannot see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/cli.md: the four flags, their JSON kinds and the prompt-1
answers. docs/insights.md: the readers over the port. PLAN §20.3:
as built; §20.4 notes the readers and the prompt-1 answer as in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wordHits() with no word returns every entry, in side-car order then
block order, as glossaryEntries() does: what vocabulary --drift-insights
compares. The adapter already had the statement, for the lone-surrogate
scan. The projection property now checks it for any side-car.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
concernDrift sets each word a model defined beside the class the list
gives it in that entity's language: the technical words judged domain
somewhere, the domain words judged technical, with the judgements
against, surest first. businessJudged sets a glossary's terms against
both. The judgements come in this package's own shape; it imports
nothing from @codegraph/insights.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
Reads the insights store beside the first model (--insights FILE names
another) and prints the two lists, five judgements per word (--json:
all); with --drift, the business terms against both. A judgement never
sets the exit code; --drift keeps its exit 3. The store opener moves to
insights-paths.ts, shared with the insights command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
cli.md, insights.md (a third reader, beside the model), and the
website's vocabulary and exit-code pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
PLAN 20.3: the decisions and the Broadleaf-scale measurement; 20.4: the
reader is in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
answerGlossary(store, { term, scope, limit }, placement): the entries
of the entities under a module or a type, and the modules'
reconciliations, for serve's /glossary.json. Which entity is under a
scope is the model's, so the placement is injected: this package still
never opens a model. GLOSSARY_ANSWER_KIND is the page's pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
The third lookup of codegraph serve: what the explained entities under
a scope said a word means. It reads the store /insight.json reads (one
reader, opened on first request, now shared) and places entities by
the fold the carriers use, built at the first request a store can
answer. No store is a quiet 404, as for /insight.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
The insight and carriers loaders each carried the same "ask once per
URL, stop asking a server with no such route" logic; lookup.ts holds
it once, before a third lookup needs it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
The Vocabulary tab lists, under a clicked word's carriers, what the
explained entities of its scope said the word means: the modules'
reconciliations, then each entity's entry, in the explanation panel's
inference colour and badge. An entry reveals its entity in Navigate (a
member, its type). No store, no route: nothing is shown.

The Navigate panel reads a block's words and glossary by name, as
"order (domain) — sense. Meaning. In Order · 0.90": generically the
identifiers stood where a definition reads, and the types that
disagree on a word were dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
serve's third lookup, /glossary.json, in docs/cli.md and the website's
serve page (and the daemon's lack of it); the Vocabulary tab's
glossary on the vocabulary page; answerGlossary and its placement in
docs/insights.md. The --insights help now names both panels it feeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
The test runs `explain` through the real seam, which reads the process
environment. With OPENROUTER_API_KEY exported in the developer's shell
the run found a key, stopped at the no-terminal confirmation instead of
the missing-key error, and the test failed. It now clears the four
provider variables for its own duration (vi.stubEnv), as childEnv
already strips NO_COLOR from a spawned CLI.

No call could have gone out: without a terminal and without --yes the
run stops before the first call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
The per-word and per-entry figures were PLAN §20.3's guesses (30 and
40) and the block figures came from prompt 1. A live prompt-2 run of
gpt-5.6-luna over Spring PetClinic (106 calls, 96 356 completion
tokens, reasoning included) measured 31% more output than --estimate
said.

Each call's prompt was rebuilt with the planner (contextPackFor +
renderPrompts over the run's records; the rebuilt estimates sum to the
run's, token for token) and its completion tokens fitted on what it
asked: 466 / 534 / 527 per operation / type / module block, 121 per
word, 123 per glossary entry (r² 0.81 per call, against 0.13). Rounded:
470 / 530 / 530, 120 and 125. Fitted on half the calls, the figures
predict the other half's total within -7% and +15%; the old ones missed
by -29% and -33%. The glossary figure rests on five module calls.

The estimate line now says the figures were measured on a live run, for
which model, with reasoning counted. The tests that multiplied 450/650/
900 by hand use the exported constants, so they keep checking the
formula, not restating the measurement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
docs/insights.md (IN-6) and the explanation page state the refitted
figures and where they were measured. The how-to, the tutorial and the
discover page reprint the Java fixture's --estimate as it now reads:
52 900 output tokens (was 41 670), $0.0432 at $0.10/$0.60 (was $0.0365).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
§20.3's Cost paragraph keeps its estimate and gains the measurement that
replaces it: the live PetClinic run (Broadleaf's place as the test
corpus, by the user's decision), the fit, its out-of-sample check, the
caveat on the glossary figure, and the input gap left as it is (7.9%
from explanations a cold estimate cannot quote, 6.3% from a tokenizer at
3.76 characters per token). §20.4's estimate item: the measurement is
in; the live run on the Java fixture is still owed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKCVgyop39vhNa45r7zM8w
@jgrodziski
jgrodziski merged commit 6a47e09 into main Nov 5, 2026
60 checks passed
@jgrodziski
jgrodziski deleted the feat/m19c-estimate-measured branch November 5, 2026 10:46
@jgrodziski
jgrodziski restored the feat/m19c-estimate-measured branch November 5, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant