Skip to content

feat(vocabulary): aggregate into vocabulary.json (M19a) - #28

Open
jgrodziski wants to merge 5 commits into
feat/m19a-vocabulary-splitfrom
feat/m19a-vocabulary-aggregate
Open

jgrodziski wants to merge 5 commits into
feat/m19a-vocabulary-splitfrom
feat/m19a-vocabulary-aggregate

Conversation

@jgrodziski

Copy link
Copy Markdown
Contributor

Third step of the word cloud / glossary (#6), milestone M19a in PLAN.md §20. Stacked on #27 (itself on #26): review those first; this PR's diff is the five commits after #27.

It counts the words of a whole model: buildVocabulary(graph) → the codegraph.vocabulary/1 artifact, with committed snapshots for every fixture. Nothing in the CLI calls it yet; PR 4 adds the codegraph vocabulary command and the drift report.

A qualified name's words weigh 1

Measured before this PR, with full names counted at their carrier's weight: Ruby's acme weighed 468 and Elixir's 392, against about 50 for the next domain word; TypeScript's packages weighed 168. A container's words are spoken again by every name it qualifies, so they drowned the words that name things.

  • Core, Lexicon.qualified: the kinds a language names by a path, each with the separator that ends the qualifier. . for Java packages, C#/Clojure namespaces, Python modules, Elixir modules and protocols; :: for Ruby classes, modules and constants; \ for PHP namespaces; / for every file kind. Go's package and Rust's module are one identifier, so neither is listed. validateProfile gains lexicon-not-a-separator (empty, or holding a letter, mark or digit) and reports an unknown kind as lexicon-unknown-kind. codegraph profiles, METAMODEL §5.3, docs/cli.md and both website pages follow.
  • The splitter returns the same tokens as before (a property pins it) plus qualifier: how many of them precede the last separator. AcmeOrder::Order::Line → acme, order, order, line with qualifier 3. C#'s interface prefix now reads the own name, so Acme.IOrder drops its I.
  • The weights gain a qualifier key, 1 by default.

This field was offered as a commit on #26; it lives here instead so #27 needs no rebase or force-push.

After the change, the corpus's top domain words: Ruby order 147, acme 96; Elixir order 99, acme 49; TypeScript order 79, acme 30, packages 21; Java order 40.

Aggregation

buildVocabulary(graph, {view?, weights?, technical?, name?}), pure over the analyzer's graph:

  • Kept is the view's verdict on a non-stub entity. A stub never speaks, whatever the view: String is a word another codebase chose.
  • Scopes come from traits: every kept TModule and TType. Each entity counts in the corpus, its containingModule and its containingType (the fold's nearest self-or-ancestor). An entity no module contains counts at the corpus alone and is listed in diagnostics.unplaced.
  • Language is the entity's model's, read from the union, never from its id. A word two languages class apart (type is a Go keyword, a Java word) is two terms.
  • Each scope has [term, weight, occurrences, carriers] rows by weight then term. An entity saying a word twice is one carrier. It also keeps its entities by role, what it skipped (unnamed, conventional, numeric), distinct and distinctDomain.
  • Each term carries its spread: how many module and type scopes speak it, and the one module when exactly one does (exclusive).

The artifact

codegraph.vocabulary/1 records its view, weights, classes, and the technical list's size and sha256, since a count means nothing without them. Terms are sorted by code unit then class; scopes are the corpus, then modules, then types, each in id order; rows address terms and parents by index. vocabularyToJsonString is compact by default; {pretty: true} writes one term and one scope per line.

Tests

  • A hand-counted toy corpus (two packages, three types, a nested one, members, an unnamed constructor, a conventional toString, two stubs), plus option, unplaced, no-lexicon (cobol), Ruby word-said-twice and multi-language cases.
  • Properties over generated corpora: closure (every index in range, sums equal rows, spread recomputed, every kept entity in one module or unplaced) and the same bytes whatever order the entities arrive in.
  • Six mutations of the aggregator, one at a time: each caught.
  • Fixture snapshots: fixtures/{java,csharp,typescript,elixir,ruby,unicode}/expected/vocabulary.json, pretty, rebuilt byte for byte, closed, and identical over a fresh graph. CODEGRAPH_UPDATE_SNAPSHOTS=1 rewrites them.
  • Hand counts: each fixture README counts one word name by name, and a test pins it, so a snapshot rewritten in one command still has to agree with a count a person made:
Fixture Word Weight Occurrences Carriers
java order 40 12 12
csharp order 42 14 14
csharp priceable 9 2 2
typescript legacy 11 4 4
typescript packages 21 21 21
elixir pricing 28 9 9
ruby pricing 8 5 5

The C# fixture had no README; it gains a short one that points to docs/csharp-extractor.md and holds the count.

Worth a look in review: a few classifications follow from the profiles and read oddly in the snapshots. Java's abstract (from AbstractOrder) and Ruby's class (from ClassMethods) tag as keywords, because those profiles reserve them.

Also

  • @codegraph/vocabulary now depends on @codegraph/analyzer, as PLAN §20 planned.
  • PLAN §20.1 and §20.4 describe what was built. drift.ts moves to the command's item, for PR 4.

Verification

  • pnpm -r build and pnpm -r typecheck green; eslint clean on every changed .ts/.mjs/.js; pnpm run gen:schemas leaves schemas/ unchanged; the website profiles page regenerates with no diff.
  • vocabulary 243 tests, core 544, cli 908; every other package passes except the Node 26 node:sqlite warning test noted on feat(core): language lexicons on profiles (M19a) #26.

Part of #6.

🤖 Generated with Claude Code

Jérémie Grodziski and others added 5 commits October 25, 2026 22:08
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>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
codegraph 2b1bdf3 Commit Preview URL

Branch Preview URL
Oct 26 2026, 07:04 AM

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