From d7106bc5ae32f82e8b2b243e9836dfdafda7ca9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Grodziski?= Date: Sun, 25 Oct 2026 22:08:38 +0100 Subject: [PATCH 1/6] feat(core): qualified kinds in the lexicon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- METAMODEL.md | 8 +++-- docs/cli.md | 2 +- packages/cli/src/commands/profiles.ts | 5 ++++ packages/cli/test/profiles.test.ts | 16 ++++++++-- packages/core/src/profile.ts | 29 ++++++++++++++++++- packages/core/src/profiles/clojure.ts | 1 + packages/core/src/profiles/csharp.ts | 1 + packages/core/src/profiles/elixir.ts | 1 + packages/core/src/profiles/java.ts | 1 + packages/core/src/profiles/javascript.ts | 2 +- packages/core/src/profiles/php.ts | 1 + packages/core/src/profiles/python.ts | 1 + packages/core/src/profiles/ruby.ts | 1 + packages/core/src/profiles/typescript.ts | 2 +- packages/core/test/profile.test.ts | 23 +++++++++++++++ packages/core/test/profiles.test.ts | 27 +++++++++++++++++ .../docs/reference/metamodel/profiles.md | 5 ++-- website/content/docs/reference/profiles.md | 9 ++++++ website/scripts/profiles-page.mjs | 5 +++- 19 files changed, 128 insertions(+), 12 deletions(-) diff --git a/METAMODEL.md b/METAMODEL.md index 6e9952c..97096fa 100644 --- a/METAMODEL.md +++ b/METAMODEL.md @@ -384,7 +384,7 @@ implemented* (robustness test of the schema). | `kinds` | `Record` | the licit trait compositions per entity kind | | `edges` | EdgeKind[] | edge kinds this language can emit | | `notes` | string[] | documented static-analysis blind spots (reflection, `Class.forName`, dynamic `require`, macros pre-expansion, …) | -| `lexicon` | `{keywords, conventional, technical?, fileKinds?, interfacePrefix?}` | the language's words, for the vocabulary (§5.3): reserved words, names the language dictates, plumbing words, kinds whose name is a file path, C#'s interface marker | +| `lexicon` | `{keywords, conventional, technical?, fileKinds?, qualified?, interfacePrefix?}` | the language's words, for the vocabulary (§5.3): reserved words, names the language dictates, plumbing words, kinds whose name is a file path or a qualified path, C#'s interface marker | **Validation** of an entity against its profile: 1. `kind` exists in the profile; @@ -437,14 +437,16 @@ lexicon, optional on a profile and present on every shipped one: | `conventional` | whole identifiers the language or its runtime dictates — overrides, protocol members, operators (`toString`, `__init__`, `<=>`). Their words say nothing about the domain, so such a name contributes none | | `technical` | plumbing words of the language beyond the vocabulary's shared list, spelled as the splitter yields them (lowercase letters and digits) | | `fileKinds` | kinds whose name is a file path (TypeScript's `module`, Ruby's `file`): the extension is not a word | +| `qualified` | kinds the language names by a qualified path, each with the separator that ends the qualifier: a Java `package` by `.`, a Ruby `class` by `::`, every file kind by `/`. The words before the last separator name a container (`AcmeOrder::` of `AcmeOrder::Order`), so they weigh as incidental words — still counted, never dropped | | `interfacePrefix` | `true` in C# alone: `IOrderService` names an interface `OrderService` | `validateProfile` rejects an entry that could never do what its list says: listed twice (`lexicon-duplicate`); a keyword with no letter or a name holding whitespace, a technical entry the splitter cannot yield (`lexicon-not-a-word`); a conventional name or technical word a keyword already claims -(`lexicon-keyword-overlap`); a file kind the profile does not define -(`lexicon-unknown-kind`). The lexicon is not on the wire: `schemas/` is +(`lexicon-keyword-overlap`); a file or qualified kind the profile does not +define (`lexicon-unknown-kind`); a separator that is empty or holds a letter, a +mark or a digit (`lexicon-not-a-separator`). The lexicon is not on the wire: `schemas/` is unchanged by it. --- diff --git a/docs/cli.md b/docs/cli.md index ff288c7..e1d4fdc 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -316,7 +316,7 @@ facts. Prints the language profiles core ships — the per-kind required and optional trait sets an extractor must respect, the edge kinds, the lexicon the vocabulary reads (reserved words, conventional names, technical words, file -kinds), and the notes. `--lang L` prints one profile in full; `--json` prints +kinds, qualified kinds and their separators), and the notes. `--lang L` prints one profile in full; `--json` prints the data as `{kind: "codegraph.profiles/1", profiles[]}`. ## The Java extractor diff --git a/packages/cli/src/commands/profiles.ts b/packages/cli/src/commands/profiles.ts index acc4621..87b1314 100644 --- a/packages/cli/src/commands/profiles.ts +++ b/packages/cli/src/commands/profiles.ts @@ -199,6 +199,10 @@ function lexiconLines(lexicon: Lexicon | undefined): readonly string[] { ]; if (lexicon.technical !== undefined) lines.push(` technical ${list([...lexicon.technical])}`); if (lexicon.fileKinds !== undefined) lines.push(` file kinds ${list([...lexicon.fileKinds])}`); + if (lexicon.qualified !== undefined) { + const kinds = Object.entries(lexicon.qualified).map(([kind, separator]) => `${kind} ${separator}`); + lines.push(` qualified ${list(kinds)}`); + } if (lexicon.interfacePrefix === true) lines.push(" interface prefix yes"); return lines; } @@ -245,6 +249,7 @@ function canonicalLexicon(lexicon: Lexicon): Record { }; if (lexicon.technical !== undefined) out["technical"] = lexicon.technical; if (lexicon.fileKinds !== undefined) out["fileKinds"] = lexicon.fileKinds; + if (lexicon.qualified !== undefined) out["qualified"] = lexicon.qualified; if (lexicon.interfacePrefix !== undefined) out["interfacePrefix"] = lexicon.interfacePrefix; return out; } diff --git a/packages/cli/test/profiles.test.ts b/packages/cli/test/profiles.test.ts index ea9d081..14603a0 100644 --- a/packages/cli/test/profiles.test.ts +++ b/packages/cli/test/profiles.test.ts @@ -116,6 +116,9 @@ describe("one language's full spec", () => { ` conventional ${lexicon.conventional.length === 0 ? "(none)" : lexicon.conventional.join(", ")}`, ...(lexicon.technical === undefined ? [] : [` technical ${lexicon.technical.join(", ")}`]), ...(lexicon.fileKinds === undefined ? [] : [` file kinds ${lexicon.fileKinds.join(", ")}`]), + ...(lexicon.qualified === undefined + ? [] + : [` qualified ${Object.entries(lexicon.qualified).map(([kind, sep]) => `${kind} ${sep}`).join(", ")}`]), ...(lexicon.interfacePrefix === true ? [" interface prefix yes"] : []), ]; expect(start, "no lexicon section").toBeGreaterThan(0); @@ -142,6 +145,14 @@ describe("one language's full spec", () => { expect(invoke(["profiles", "--lang", "java"]).io.stdout()).not.toContain("file kinds"); }); + it("prints each qualified kind with its separator, only where a profile declares them", () => { + expect(invoke(["profiles", "--lang", "rb"]).io.stdout()).toContain( + "\n qualified file /, class ::, module ::, constant ::\n", + ); + expect(invoke(["profiles", "--lang", "php"]).io.stdout()).toContain("\n qualified codeFile /, namespace \\\n"); + expect(invoke(["profiles", "--lang", "go"]).io.stdout()).not.toContain("qualified"); + }); + it("prints the declaration spaces exactly where a profile declares them", () => { const ts = invoke(["profiles", "--lang", "ts"]).io.stdout(); expect(ts).toContain("space"); @@ -192,8 +203,9 @@ describe("--json prints the profile data as-is", () => { const [json] = payload(["profiles", "--lang", lang]) as readonly Profile[]; return Object.keys(json?.lexicon ?? {}); }; - expect(keysOf("csharp")).toEqual(["keywords", "conventional", "technical", "interfacePrefix"]); - expect(keysOf("ts")).toEqual(["keywords", "conventional", "technical", "fileKinds"]); + expect(keysOf("csharp")).toEqual(["keywords", "conventional", "technical", "qualified", "interfacePrefix"]); + expect(keysOf("ts")).toEqual(["keywords", "conventional", "technical", "fileKinds", "qualified"]); + expect(keysOf("go")).toEqual(["keywords", "conventional", "technical"]); }); it("says the same thing as the text form (decision 8)", () => { diff --git a/packages/core/src/profile.ts b/packages/core/src/profile.ts index 5630654..d230c94 100644 --- a/packages/core/src/profile.ts +++ b/packages/core/src/profile.ts @@ -59,6 +59,13 @@ export interface Lexicon { readonly technical?: readonly string[] | undefined; /** Kinds whose name is a file path (`src/order.ts`): the extension is not a word. */ readonly fileKinds?: readonly string[] | undefined; + /** + * Kinds the language names by a qualified path, each with the separator that + * ends a qualifier: a Ruby class `AcmeOrder::Order::Line`, a file + * `lib/order.rb`. What precedes the last separator names a container, so its + * words weigh as incidental ones; they are still words. + */ + readonly qualified?: Readonly> | undefined; /** C#'s convention: `IOrderService` names an interface `OrderService`; the `I` is a marker, not a word. */ readonly interfacePrefix?: true | undefined; } @@ -69,6 +76,7 @@ export const LexiconSchema = z.object({ conventional: z.array(z.string()), technical: z.array(z.string()).optional(), fileKinds: z.array(z.string()).optional(), + qualified: z.record(z.string(), z.string()).optional(), interfacePrefix: z.literal(true).optional(), }); @@ -107,6 +115,7 @@ export const VALIDATION_CODES = [ "lexicon-not-a-word", "lexicon-keyword-overlap", "lexicon-unknown-kind", + "lexicon-not-a-separator", ] as const; export type ValidationCode = (typeof VALIDATION_CODES)[number]; @@ -467,11 +476,13 @@ export function validateProfile(profile: Profile): ValidationIssue[] { const SPLITTER_TOKEN = /^[\p{L}\p{M}\p{N}]+$/u; const LETTER = /\p{L}/u; const WHITESPACE = /\s/u; +const WORD_CHARACTER = /[\p{L}\p{M}\p{N}]/u; /** * An entry that could never do what its list says: listed twice, a keyword no * word can equal, a technical word the splitter cannot yield, a name the - * keyword class would claim first, a file kind the profile never declares. + * keyword class would claim first, a kind the profile never declares, a + * separator that would cut a word. */ function lexiconIssues(lexicon: Lexicon, kinds: Profile["kinds"]): ValidationIssue[] { const issues: ValidationIssue[] = []; @@ -551,6 +562,22 @@ function lexiconIssues(lexicon: Lexicon, kinds: Profile["kinds"]): ValidationIss }); } } + for (const [kind, separator] of Object.entries(lexicon.qualified ?? {})) { + if (!Object.hasOwn(kinds, kind)) { + issues.push({ + code: "lexicon-unknown-kind", + path: "lexicon.qualified", + message: `qualified kind "${kind}" is not a kind the profile defines`, + }); + } + if (separator === "" || WORD_CHARACTER.test(separator)) { + issues.push({ + code: "lexicon-not-a-separator", + path: "lexicon.qualified", + message: `${JSON.stringify(separator)} cannot end a qualifier: a separator is non-empty and holds no letter, mark or digit`, + }); + } + } return issues; } diff --git a/packages/core/src/profiles/clojure.ts b/packages/core/src/profiles/clojure.ts index 8e054c9..17dcbae 100644 --- a/packages/core/src/profiles/clojure.ts +++ b/packages/core/src/profiles/clojure.ts @@ -141,5 +141,6 @@ export const clojureProfile: Profile = { technical: [ "coll", "kw", "ns", "seq", "sym", "xf", "xform", ], + qualified: { namespace: "." }, }, }; diff --git a/packages/core/src/profiles/csharp.ts b/packages/core/src/profiles/csharp.ts index 5ab8482..3a15484 100644 --- a/packages/core/src/profiles/csharp.ts +++ b/packages/core/src/profiles/csharp.ts @@ -248,6 +248,7 @@ export const csharpProfile: Profile = { technical: [ "attribute", "enumerable", "enumerator", "extension", "extensions", "linq", ], + qualified: { namespace: "." }, interfacePrefix: true, }, }; diff --git a/packages/core/src/profiles/elixir.ts b/packages/core/src/profiles/elixir.ts index 0e884ae..a3873b8 100644 --- a/packages/core/src/profiles/elixir.ts +++ b/packages/core/src/profiles/elixir.ts @@ -159,5 +159,6 @@ export const elixirProfile: Profile = { "phoenix", "pid", "plug", "schema", "server", "socket", "sup", "supervisor", "web", ], fileKinds: ["file"], + qualified: { file: "/", module: ".", protocol: "." }, }, }; diff --git a/packages/core/src/profiles/java.ts b/packages/core/src/profiles/java.ts index 403f516..c7fb826 100644 --- a/packages/core/src/profiles/java.ts +++ b/packages/core/src/profiles/java.ts @@ -206,5 +206,6 @@ export const javaProfile: Profile = { technical: [ "annotation", "bean", "beans", "ejb", "jaxb", "jdbc", "jndi", "jpa", "pojo", "servlet", ], + qualified: { package: "." }, }, }; diff --git a/packages/core/src/profiles/javascript.ts b/packages/core/src/profiles/javascript.ts index 015227e..1b28e47 100644 --- a/packages/core/src/profiles/javascript.ts +++ b/packages/core/src/profiles/javascript.ts @@ -113,5 +113,5 @@ export const javascriptProfile: Profile = { ], // The module is the file: its name is a path whose extension is not a word. - lexicon: { ...ECMASCRIPT_LEXICON, fileKinds: ["module"] }, + lexicon: { ...ECMASCRIPT_LEXICON, fileKinds: ["module"], qualified: { module: "/" } }, }; diff --git a/packages/core/src/profiles/php.ts b/packages/core/src/profiles/php.ts index 7dbf31f..7a975bd 100644 --- a/packages/core/src/profiles/php.ts +++ b/packages/core/src/profiles/php.ts @@ -151,5 +151,6 @@ export const phpProfile: Profile = { "arr", "closure", "facade", "middleware", "php", ], fileKinds: ["codeFile"], + qualified: { codeFile: "/", namespace: "\\" }, }, }; diff --git a/packages/core/src/profiles/python.ts b/packages/core/src/profiles/python.ts index a749f65..dad09e5 100644 --- a/packages/core/src/profiles/python.ts +++ b/packages/core/src/profiles/python.ts @@ -133,5 +133,6 @@ export const pythonProfile: Profile = { technical: [ "dict", "kwargs", "metaclass", "py", "tuple", ], + qualified: { module: "." }, }, }; diff --git a/packages/core/src/profiles/ruby.ts b/packages/core/src/profiles/ruby.ts index 48f6239..c9aee01 100644 --- a/packages/core/src/profiles/ruby.ts +++ b/packages/core/src/profiles/ruby.ts @@ -136,5 +136,6 @@ export const rubyProfile: Profile = { "proc", "rake", "rakefile", ], fileKinds: ["file"], + qualified: { file: "/", class: "::", module: "::", constant: "::" }, }, }; diff --git a/packages/core/src/profiles/typescript.ts b/packages/core/src/profiles/typescript.ts index 9d3aad9..85c80ad 100644 --- a/packages/core/src/profiles/typescript.ts +++ b/packages/core/src/profiles/typescript.ts @@ -249,5 +249,5 @@ export const typescriptProfile: Profile = { ], // The module is the file: its name is a path whose extension is not a word. - lexicon: { ...ECMASCRIPT_LEXICON, fileKinds: ["module"] }, + lexicon: { ...ECMASCRIPT_LEXICON, fileKinds: ["module"], qualified: { module: "/" } }, }; diff --git a/packages/core/test/profile.test.ts b/packages/core/test/profile.test.ts index cf35586..b4eb8b3 100644 --- a/packages/core/test/profile.test.ts +++ b/packages/core/test/profile.test.ts @@ -422,6 +422,28 @@ describe("validateProfile — the lexicon", () => { expect(codes(issues)).toEqual(["lexicon-unknown-kind"]); expect(issues[0]?.path).toBe("lexicon.fileKinds"); }); + + it("accepts a qualified kind whose separator holds no word character", () => { + for (const separator of [".", "::", "/", "\\"]) { + expect(validateProfile(withLexicon({ ...base, qualified: { class: separator } })), separator).toEqual([]); + } + }); + + it("rejects a qualified kind the profile does not declare", () => { + const issues = validateProfile(withLexicon({ ...base, qualified: { module: "." } })); + expect(codes(issues)).toEqual(["lexicon-unknown-kind"]); + expect(issues[0]?.path).toBe("lexicon.qualified"); + }); + + // A separator is where a qualifier ends: one holding a letter or a digit + // would cut a word in two, and an empty one would end the qualifier anywhere. + it("rejects a separator that is empty or holds a letter, a mark or a digit", () => { + for (const separator of ["", "a", "::x", "1", "\u0301"]) { + const issues = validateProfile(withLexicon({ ...base, qualified: { class: separator } })); + expect(codes(issues), JSON.stringify(separator)).toEqual(["lexicon-not-a-separator"]); + expect(issues[0]?.path).toBe("lexicon.qualified"); + } + }); }); describe("ProfileSchema mirrors Profile", () => { @@ -436,6 +458,7 @@ describe("ProfileSchema mirrors Profile", () => { technical: ["impl"], interfacePrefix: true, fileKinds: ["class"], + qualified: { class: "::" }, }, }; expect(ProfileSchema.parse(full)).toEqual(full); diff --git a/packages/core/test/profiles.test.ts b/packages/core/test/profiles.test.ts index af8cf33..88f7015 100644 --- a/packages/core/test/profiles.test.ts +++ b/packages/core/test/profiles.test.ts @@ -842,4 +842,31 @@ describe("the lexicon of every shipped profile", () => { ts: ["module"], }); }); + + // A kind is qualified when the LANGUAGE names it by a path: a Java package is + // `com.acme.order`, a Ruby class `AcmeOrder::Order::Line`, an Elixir module + // `AcmeOrder.Pricing`. Go's package and Rust's module are named by one + // identifier (`package order`, `mod line`), their paths being something else. + it("declares exactly the kinds a language names by a qualified path", () => { + const qualified = Object.fromEntries( + profiles.flatMap(([lang, profile]) => (profile.lexicon?.qualified === undefined ? [] : [[lang, profile.lexicon.qualified]])), + ); + expect(qualified).toEqual({ + clj: { namespace: "." }, + csharp: { namespace: "." }, + ex: { file: "/", module: ".", protocol: "." }, + java: { package: "." }, + js: { module: "/" }, + php: { codeFile: "/", namespace: "\\" }, + python: { module: "." }, + rb: { file: "/", class: "::", module: "::", constant: "::" }, + ts: { module: "/" }, + }); + }); + + it("qualifies every file kind by its directories", () => { + for (const [lang, profile] of profiles) { + for (const kind of profile.lexicon?.fileKinds ?? []) expect(profile.lexicon?.qualified?.[kind], `${lang}:${kind}`).toBe("/"); + } + }); }); diff --git a/website/content/docs/reference/metamodel/profiles.md b/website/content/docs/reference/metamodel/profiles.md index bc73be4..b44dd55 100644 --- a/website/content/docs/reference/metamodel/profiles.md +++ b/website/content/docs/reference/metamodel/profiles.md @@ -11,7 +11,7 @@ A profile is **data, not code**: the contract stating what a given language's ex | `kinds` | `Record` | the licit trait compositions per entity kind | | `edges` | EdgeKind[] | edge kinds this language can emit | | `notes` | string[] | documented static-analysis blind spots (reflection, `Class.forName`, dynamic `require`, macros pre-expansion, …) | -| `lexicon` | `{keywords, conventional, technical?, fileKinds?, interfacePrefix?}` | the language's words, for the vocabulary — see [The lexicon](#the-lexicon) | +| `lexicon` | `{keywords, conventional, technical?, fileKinds?, qualified?, interfacePrefix?}` | the language's words, for the vocabulary — see [The lexicon](#the-lexicon) | The eleven shipped profiles are rendered in full on [Language profiles](/docs/reference/profiles/). @@ -45,9 +45,10 @@ The vocabulary splits every name into words — `OrderManagerRepository` is `ord | `conventional` | whole identifiers the language or its runtime dictates — overrides, protocol members, operators (`toString`, `__init__`, `<=>`). Their words say nothing about the domain, so such a name contributes none | | `technical` | plumbing words of the language beyond the vocabulary's shared list, spelled as the splitter yields them | | `fileKinds` | kinds whose name is a file path (TypeScript's `module`, Ruby's `file`): the extension is not a word | +| `qualified` | kinds the language names by a qualified path, each with the separator that ends the qualifier: a Java `package` by `.`, a Ruby `class` by `::`, every file kind by `/`. The words before the last separator name a container (`AcmeOrder::` of `AcmeOrder::Order`), so they weigh as incidental words — still counted, never dropped | | `interfacePrefix` | `true` in C# alone: `IOrderService` names an interface `OrderService` | -Profile validation rejects an entry that could never do what its list says: one listed twice, a keyword with no letter, a technical entry the splitter cannot yield, a name a keyword already claims, a file kind the profile does not define. +Profile validation rejects an entry that could never do what its list says: one listed twice, a keyword with no letter, a technical entry the splitter cannot yield, a name a keyword already claims, a file or qualified kind the profile does not define, a separator that is empty or would cut a word. ## Framework profiles diff --git a/website/content/docs/reference/profiles.md b/website/content/docs/reference/profiles.md index f54494c..7e19ebc 100644 --- a/website/content/docs/reference/profiles.md +++ b/website/content/docs/reference/profiles.md @@ -57,6 +57,7 @@ A profile that is specified but has no shipped extractor is complete, not defici - Reserved words: `def` · `if` · `do` · `let` · `quote` · `var` · `fn` · `loop` · `recur` · `throw` · `try` · `catch` · `finally` · `monitor-enter` · `monitor-exit` · `new` · `set!` · `nil` · `true` · `false` - Conventional names: `-main` · `-init` · `-post-init` · `-toString` · `-equals` · `-hashCode` - Technical words: `coll` · `kw` · `ns` · `seq` · `sym` · `xf` · `xform` +- Qualified kinds: `namespace` by `.` **Notes** @@ -101,6 +102,7 @@ A profile that is specified but has no shipped extractor is complete, not defici - Reserved words: `abstract` · `as` · `base` · `bool` · `break` · `byte` · `case` · `catch` · `char` · `checked` · `class` · `const` · `continue` · `decimal` · `default` · `delegate` · `do` · `double` · `else` · `enum` · `event` · `explicit` · `extern` · `false` · `finally` · `fixed` · `float` · `for` · `foreach` · `goto` · `if` · `implicit` · `in` · `int` · `interface` · `internal` · `is` · `lock` · `long` · `namespace` · `new` · `null` · `object` · `operator` · `out` · `override` · `params` · `private` · `protected` · `public` · `readonly` · `ref` · `return` · `sbyte` · `sealed` · `short` · `sizeof` · `stackalloc` · `static` · `string` · `struct` · `switch` · `this` · `throw` · `true` · `try` · `typeof` · `uint` · `ulong` · `unchecked` · `unsafe` · `ushort` · `using` · `virtual` · `void` · `volatile` · `while` - Conventional names: `ToString` · `Equals` · `GetHashCode` · `Finalize` · `Main` · `CompareTo` · `Dispose` · `DisposeAsync` · `GetEnumerator` · `GetAsyncEnumerator` · `MoveNext` · `MoveNextAsync` · `Clone` · `Deconstruct` · `this[]` · `op_Addition` · `op_Subtraction` · `op_Multiply` · `op_Division` · `op_Modulus` · `op_Equality` · `op_Inequality` · `op_LessThan` · `op_GreaterThan` · `op_LessThanOrEqual` · `op_GreaterThanOrEqual` · `op_Implicit` · `op_Explicit` · `op_UnaryNegation` · `op_UnaryPlus` · `op_LogicalNot` · `op_OnesComplement` · `op_Increment` · `op_Decrement` · `op_True` · `op_False` · `op_BitwiseAnd` · `op_BitwiseOr` · `op_ExclusiveOr` · `op_LeftShift` · `op_RightShift` · `op_UnsignedRightShift` - Technical words: `attribute` · `enumerable` · `enumerator` · `extension` · `extensions` · `linq` +- Qualified kinds: `namespace` by `.` - Interface prefix: a leading `I` before Upper + lower is a marker, not a word **Notes** @@ -149,6 +151,7 @@ A profile that is specified but has no shipped extractor is complete, not defici - Conventional names: `__using__` · `__before_compile__` · `__after_compile__` · `__after_verify__` · `__on_definition__` · `__struct__` · `__info__` · `__deriving__` · `__impl__` · `__protocol__` · `start_link` · `child_spec` · `init` · `handle_call` · `handle_cast` · `handle_info` · `handle_continue` · `terminate` · `code_change` · `format_status` · `to_string` · `inspect` · `@moduledoc` · `@doc` · `@typedoc` · `@spec` · `@type` · `@typep` · `@opaque` · `@callback` · `@macrocallback` · `@optional_callbacks` · `@impl` · `@behaviour` · `@derive` · `@enforce_keys` · `@external_resource` · `@compile` · `@deprecated` · `@dialyzer` · `@on_load` · `@vsn` · `@before_compile` · `@after_compile` · `@after_verify` · `@on_definition` · `@nifs` · `@fallback_to_any` · `@file` - Technical words: `attrs` · `behaviour` · `changeset` · `conn` · `ecto` · `ets` · `gen` · `live` · `mix` · `otp` · `phoenix` · `pid` · `plug` · `schema` · `server` · `socket` · `sup` · `supervisor` · `web` - File kinds: `file` +- Qualified kinds: `file` by `/` · `module` by `.` · `protocol` by `.` **Notes** @@ -233,6 +236,7 @@ A profile that is specified but has no shipped extractor is complete, not defici - Reserved words: `abstract` · `assert` · `boolean` · `break` · `byte` · `case` · `catch` · `char` · `class` · `const` · `continue` · `default` · `do` · `double` · `else` · `enum` · `extends` · `final` · `finally` · `float` · `for` · `goto` · `if` · `implements` · `import` · `instanceof` · `int` · `interface` · `long` · `native` · `new` · `package` · `private` · `protected` · `public` · `return` · `short` · `static` · `strictfp` · `super` · `switch` · `synchronized` · `this` · `throw` · `throws` · `transient` · `try` · `void` · `volatile` · `while` · `true` · `false` · `null` - Conventional names: `toString` · `equals` · `hashCode` · `clone` · `finalize` · `main` · `compareTo` · `iterator` · `hasNext` · `valueOf` · `serialVersionUID` · `readObject` · `writeObject` · `readObjectNoData` · `readResolve` · `writeReplace` - Technical words: `annotation` · `bean` · `beans` · `ejb` · `jaxb` · `jdbc` · `jndi` · `jpa` · `pojo` · `servlet` +- Qualified kinds: `package` by `.` **Notes** @@ -275,6 +279,7 @@ A profile that is specified but has no shipped extractor is complete, not defici - Conventional names: `constructor` · `toString` · `toLocaleString` · `valueOf` · `toJSON` · `[Symbol.iterator]` · `[Symbol.asyncIterator]` · `[Symbol.hasInstance]` · `[Symbol.toPrimitive]` · `[Symbol.toStringTag]` · `[Symbol.dispose]` · `[Symbol.asyncDispose]` - Technical words: `cb` · `fn` · `promise` · `prototype` - File kinds: `module` +- Qualified kinds: `module` by `/` **Notes** @@ -317,6 +322,7 @@ A profile that is specified but has no shipped extractor is complete, not defici - Conventional names: `__construct` · `__destruct` · `__call` · `__callStatic` · `__get` · `__set` · `__isset` · `__unset` · `__sleep` · `__wakeup` · `__serialize` · `__unserialize` · `__toString` · `__invoke` · `__set_state` · `__clone` · `__debugInfo` - Technical words: `arr` · `closure` · `facade` · `middleware` · `php` - File kinds: `codeFile` +- Qualified kinds: `codeFile` by `/` · `namespace` by `\` **Notes** @@ -352,6 +358,7 @@ A profile that is specified but has no shipped extractor is complete, not defici - Reserved words: `False` · `None` · `True` · `and` · `as` · `assert` · `async` · `await` · `break` · `class` · `continue` · `def` · `del` · `elif` · `else` · `except` · `finally` · `for` · `from` · `global` · `if` · `import` · `in` · `is` · `lambda` · `nonlocal` · `not` · `or` · `pass` · `raise` · `return` · `try` · `while` · `with` · `yield` - Conventional names: `self` · `cls` · `__init__` · `__new__` · `__del__` · `__repr__` · `__str__` · `__bytes__` · `__format__` · `__hash__` · `__bool__` · `__eq__` · `__ne__` · `__lt__` · `__le__` · `__gt__` · `__ge__` · `__len__` · `__iter__` · `__next__` · `__aiter__` · `__anext__` · `__reversed__` · `__contains__` · `__getitem__` · `__setitem__` · `__delitem__` · `__missing__` · `__getattr__` · `__getattribute__` · `__setattr__` · `__delattr__` · `__dir__` · `__get__` · `__set__` · `__delete__` · `__set_name__` · `__call__` · `__enter__` · `__exit__` · `__aenter__` · `__aexit__` · `__await__` · `__init_subclass__` · `__class_getitem__` · `__post_init__` · `__add__` · `__sub__` · `__mul__` · `__matmul__` · `__truediv__` · `__floordiv__` · `__mod__` · `__pow__` · `__neg__` · `__pos__` · `__abs__` · `__invert__` · `__and__` · `__or__` · `__xor__` · `__lshift__` · `__rshift__` · `__int__` · `__float__` · `__index__` · `__round__` · `__copy__` · `__deepcopy__` · `__reduce__` · `__getstate__` · `__setstate__` · `__slots__` · `__all__` · `__name__` · `__main__` · `__doc__` · `__dict__` - Technical words: `dict` · `kwargs` · `metaclass` · `py` · `tuple` +- Qualified kinds: `module` by `.` **Notes** @@ -386,6 +393,7 @@ A profile that is specified but has no shipped extractor is complete, not defici - Conventional names: `initialize` · `initialize_copy` · `to_s` · `to_str` · `to_a` · `to_ary` · `to_h` · `to_hash` · `to_proc` · `inspect` · `hash` · `eql?` · `==` · `===` · `=~` · `!~` · `!=` · `<=>` · `<` · `>` · `<=` · `>=` · `+` · `-` · `*` · `/` · `%` · `**` · `<<` · `>>` · `&` · `|` · `^` · `~` · `!` · `+@` · `-@` · `[]` · `[]=` · `each` · `coerce` · `method_missing` · `respond_to_missing?` · `respond_to?` · `included` · `extended` · `inherited` · `prepended` · `method_added` · `const_missing` - Technical words: `accessor` · `attr` · `concern` · `concerns` · `ext` · `gem` · `gemspec` · `klass` · `lambda` · `proc` · `rake` · `rakefile` - File kinds: `file` +- Qualified kinds: `file` by `/` · `class` by `::` · `module` by `::` · `constant` by `::` **Notes** @@ -487,6 +495,7 @@ Declaration spaces: `abstractClass`, `arrowFunction`, `class`, `enum`, `function - Conventional names: `constructor` · `toString` · `toLocaleString` · `valueOf` · `toJSON` · `[Symbol.iterator]` · `[Symbol.asyncIterator]` · `[Symbol.hasInstance]` · `[Symbol.toPrimitive]` · `[Symbol.toStringTag]` · `[Symbol.dispose]` · `[Symbol.asyncDispose]` - Technical words: `cb` · `fn` · `promise` · `prototype` - File kinds: `module` +- Qualified kinds: `module` by `/` **Notes** diff --git a/website/scripts/profiles-page.mjs b/website/scripts/profiles-page.mjs index 5b4047c..7db2ab5 100644 --- a/website/scripts/profiles-page.mjs +++ b/website/scripts/profiles-page.mjs @@ -31,7 +31,7 @@ function parse(text) { // ` keywords a, b` … up to the blank line; absent rows stay absent. const lexicon = {}; for (let i = lines.indexOf("lexicon:") + 1; i > 0 && i < lines.length && lines[i] !== ""; i++) { - const row = /^ {2}(keywords|conventional|technical|file kinds|interface prefix)\s+(.*)$/.exec(lines[i]); + const row = /^ {2}(keywords|conventional|technical|file kinds|qualified|interface prefix)\s+(.*)$/.exec(lines[i]); if (row) lexicon[row[1]] = row[2] === "(none)" ? [] : row[2].split(", "); } const notes = []; @@ -91,6 +91,9 @@ for (const lang of langs) { out.push("- Conventional names: " + words(lx["conventional"])); if (lx["technical"]) out.push("- Technical words: " + words(lx["technical"])); if (lx["file kinds"]) out.push("- File kinds: " + words(lx["file kinds"])); + // ` qualified class ::, file /`: a kind, a space, its separator. + const by = (entry) => entry.replace(/^(\S+) (.+)$/, "`$1` by `$2`"); + if (lx["qualified"]) out.push("- Qualified kinds: " + lx["qualified"].map(by).join(" · ")); if (lx["interface prefix"]) out.push("- Interface prefix: a leading `I` before Upper + lower is a marker, not a word"); out.push("", "**Notes**", ""); for (const note of p.notes) out.push("- " + note); From b99d1046f65b00bd117d60d23dce61f70287d512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Grodziski?= Date: Sun, 25 Oct 2026 22:11:49 +0100 Subject: [PATCH 2/6] feat(vocabulary): split the qualifier off a name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- packages/vocabulary/src/split.ts | 51 ++++++--- packages/vocabulary/src/weight.ts | 6 +- packages/vocabulary/test/split.test.ts | 133 +++++++++++++++++++----- packages/vocabulary/test/weight.test.ts | 11 +- 4 files changed, 159 insertions(+), 42 deletions(-) diff --git a/packages/vocabulary/src/split.ts b/packages/vocabulary/src/split.ts index 351e26d..90db7d4 100644 --- a/packages/vocabulary/src/split.ts +++ b/packages/vocabulary/src/split.ts @@ -2,21 +2,28 @@ import type { Lexicon } from "@codegraph/core"; /** * The split (PLAN.md §20.1): one name → the words it is made of. The lexicon - * says which names are conventional, which kinds are named by a file path and - * whether an interface marker leads the name; every word boundary is universal, - * so the algorithm knows no language. + * says which names are conventional, which kinds are named by a file path or a + * qualified path and whether an interface marker leads the name; every word + * boundary is universal, so the algorithm knows no language. */ export interface SplitName { /** The name's words, lowercased, in the order the name spells them. */ readonly tokens: readonly string[]; + /** How many of `tokens`, from the first, spell the qualifier (`AcmeOrder::` of `AcmeOrder::Order`). */ + readonly qualifier: number; /** Pieces made of digits only, dropped (`2024` of `promo#2024`). */ readonly numeric: number; /** The whole name is one the language dictates (`toString`): it has no word. */ readonly conventional: boolean; } -const CONVENTIONAL: SplitName = Object.freeze({ tokens: Object.freeze([]), numeric: 0, conventional: true }); +const CONVENTIONAL: SplitName = Object.freeze({ + tokens: Object.freeze([]), + qualifier: 0, + numeric: 0, + conventional: true, +}); /** Maximal runs of letters, marks and digits: every other character breaks a word. */ const PIECE = /[\p{L}\p{M}\p{N}]+/gu; @@ -32,18 +39,38 @@ export function splitIdentifier(name: string, kind: string, lexicon: Lexicon | u let text = name; if (lexicon?.fileKinds?.includes(kind) === true) text = dropExtension(text); - if (lexicon?.interfacePrefix === true) text = text.replace(INTERFACE_PREFIX, ""); + let qualifier = ""; + let own = text; + const separator = separatorOf(lexicon, kind); + const cut = separator === undefined ? -1 : text.lastIndexOf(separator); + if (separator !== undefined && cut >= 0) { + qualifier = text.slice(0, cut); + own = text.slice(cut + separator.length); + } + // Own-name rules read what follows the qualifier: `Acme.IOrder` names `Order`. + if (lexicon?.interfacePrefix === true) own = own.replace(INTERFACE_PREFIX, ""); const tokens: string[] = []; let numeric = 0; - for (const [piece] of text.matchAll(PIECE)) { - for (const word of camelWords(piece)) { - if (DIGITS_ONLY.test(word)) numeric += 1; - // `toLowerCase` is locale-independent: a Turkish machine writes the same bytes. - else tokens.push(word.toLowerCase()); + const push = (part: string): void => { + for (const [piece] of part.matchAll(PIECE)) { + for (const word of camelWords(piece)) { + if (DIGITS_ONLY.test(word)) numeric += 1; + // `toLowerCase` is locale-independent: a Turkish machine writes the same bytes. + else tokens.push(word.toLowerCase()); + } } - } - return { tokens, numeric, conventional: false }; + }; + push(qualifier); + const qualifierTokens = tokens.length; + push(own); + return { tokens, qualifier: qualifierTokens, numeric, conventional: false }; +} + +/** An own key only: Java has a `constructor` kind, and every object inherits one. */ +function separatorOf(lexicon: Lexicon | undefined, kind: string): string | undefined { + const qualified = lexicon?.qualified; + return qualified !== undefined && Object.hasOwn(qualified, kind) ? qualified[kind] : undefined; } /** The extension of the last path segment; a leading dot (`.eslintrc`) is not one. */ diff --git a/packages/vocabulary/src/weight.ts b/packages/vocabulary/src/weight.ts index 95b9f83..5af3b3c 100644 --- a/packages/vocabulary/src/weight.ts +++ b/packages/vocabulary/src/weight.ts @@ -10,7 +10,10 @@ import type { Entity, EntityId } from "@codegraph/core"; /** From the outermost container in; `other` is a named entity no role trait describes. */ export const ROLES = ["module", "type", "operation", "attribute", "parameter", "local", "other"] as const; export type Role = (typeof ROLES)[number]; -export type Weights = Readonly>; +/** Every role, then the words of a name's qualifier, whatever its role (`AcmeOrder::` of `AcmeOrder::Order`). */ +export const WEIGHT_KEYS = [...ROLES, "qualifier"] as const; +export type WeightKey = (typeof WEIGHT_KEYS)[number]; +export type Weights = Readonly>; export const DEFAULT_WEIGHTS: Weights = Object.freeze({ module: 8, // a concept the corpus names once and refers to everywhere @@ -20,6 +23,7 @@ export const DEFAULT_WEIGHTS: Weights = Object.freeze({ parameter: 1, // incidental names local: 1, other: 1, // a TS namespace, a PHP code file: no trait says what it is + qualifier: 1, // a container's words, spoken again by every name it qualifies }); /** The structural entities some entity lists as its parameters or its locals. */ diff --git a/packages/vocabulary/test/split.test.ts b/packages/vocabulary/test/split.test.ts index 1b5c907..12280fb 100644 --- a/packages/vocabulary/test/split.test.ts +++ b/packages/vocabulary/test/split.test.ts @@ -18,18 +18,27 @@ function lexiconOf(lang: string): Lexicon { return lexicon; } -/** [kind, name, tokens, numeric pieces dropped]; `null` tokens = a conventional name. */ -type Row = readonly [kind: string, name: string, tokens: readonly string[] | null, numeric?: number]; +/** + * [kind, name, tokens, counts]; `null` tokens = a conventional name. `numeric` + * counts the digit-only pieces dropped, `qualifier` the leading tokens that + * spell the name's qualifier. + */ +type Row = readonly [ + kind: string, + name: string, + tokens: readonly string[] | null, + counts?: { readonly numeric?: number; readonly qualifier?: number }, +]; -function check(lang: string | undefined, rows: readonly Row[]): void { - const lexicon = lang === undefined ? undefined : lexiconOf(lang); - for (const [kind, name, tokens, numeric = 0] of rows) { +function check(lang: string | Lexicon | undefined, rows: readonly Row[]): void { + const lexicon = typeof lang === "string" ? lexiconOf(lang) : lang; + for (const [kind, name, tokens, { numeric = 0, qualifier = 0 } = {}] of rows) { it(`${kind} ${name}`, () => { const split = splitIdentifier(name, kind, lexicon); if (tokens === null) { - expect(split).toEqual({ tokens: [], numeric: 0, conventional: true }); + expect(split).toEqual({ tokens: [], qualifier: 0, numeric: 0, conventional: true }); } else { - expect(split).toEqual({ tokens, numeric, conventional: false }); + expect(split).toEqual({ tokens, qualifier, numeric, conventional: false }); } }); } @@ -52,9 +61,9 @@ describe("camel case and acronyms, with no lexicon", () => { ["class", "V2Api", ["v2", "api"]], ["class", "HTTP2Server", ["http2", "server"]], ["module", "i18n", ["i18n"]], - ["class", "2FA", ["fa"], 1], + ["class", "2FA", ["fa"], { numeric: 1 }], ["class", "Promo2024", ["promo2024"]], - ["field", "line_2", ["line"], 1], + ["field", "line_2", ["line"], { numeric: 1 }], // Every character that is not a letter, a mark or a digit breaks a word. ["method", "->", []], ["method", "a--b", ["a", "b"]], @@ -78,7 +87,7 @@ describe("Unicode letters, not ASCII", () => { describe("Java", () => { check("java", [ - ["package", "com.acme.order", ["com", "acme", "order"]], + ["package", "com.acme.order", ["com", "acme", "order"], { qualifier: 2 }], ["class", "EmptyBasketException", ["empty", "basket", "exception"]], ["method", "notifyShipped", ["notify", "shipped"]], ["method", "toString", null], @@ -97,8 +106,8 @@ describe("C#", () => { ["method", "IsFree", ["is", "free"]], ["class", "IO", ["io"]], ["class", "IOError", ["io", "error"]], - ["class", "Repository`1", ["repository"], 1], - ["namespace", "Acme.Order.Adapter", ["acme", "order", "adapter"]], + ["class", "Repository`1", ["repository"], { numeric: 1 }], + ["namespace", "Acme.Order.Adapter", ["acme", "order", "adapter"], { qualifier: 2 }], ["method", "op_Addition", null], ["property", "this[]", null], ["method", "ToString", null], @@ -107,10 +116,10 @@ describe("C#", () => { describe("TypeScript", () => { check("ts", [ - ["module", "legacy/acme-report.ts", ["legacy", "acme", "report"]], - ["module", "packages/order/src/promo#2024.ts", ["packages", "order", "src", "promo"], 1], - ["module", "packages/order/src/ambient.d.ts", ["packages", "order", "src", "ambient", "d"]], - ["module", "packages/order/src/ui/order-table.tsx", ["packages", "order", "src", "ui", "order", "table"]], + ["module", "legacy/acme-report.ts", ["legacy", "acme", "report"], { qualifier: 1 }], + ["module", "packages/order/src/promo#2024.ts", ["packages", "order", "src", "promo"], { numeric: 1, qualifier: 3 }], + ["module", "packages/order/src/ambient.d.ts", ["packages", "order", "src", "ambient", "d"], { qualifier: 3 }], + ["module", "packages/order/src/ui/order-table.tsx", ["packages", "order", "src", "ui", "order", "table"], { qualifier: 4 }], ["module", "legacy-lib", ["legacy", "lib"]], // The extension is dropped for a FILE kind only. ["class", "legacy/acme.ts", ["legacy", "acme", "ts"]], @@ -125,7 +134,7 @@ describe("TypeScript", () => { describe("JavaScript", () => { check("js", [ - ["module", "src/app.mjs", ["src", "app"]], + ["module", "src/app.mjs", ["src", "app"], { qualifier: 1 }], ["module", ".eslintrc", ["eslintrc"]], ["method", "toJSON", null], ]); @@ -133,13 +142,13 @@ describe("JavaScript", () => { describe("Ruby", () => { check("rb", [ - ["class", "AcmeOrder::Order::Line", ["acme", "order", "order", "line"]], - ["constant", "AcmeOrder::Order::MAX_LINES", ["acme", "order", "order", "max", "lines"]], + ["class", "AcmeOrder::Order::Line", ["acme", "order", "order", "line"], { qualifier: 3 }], + ["constant", "AcmeOrder::Order::MAX_LINES", ["acme", "order", "order", "max", "lines"], { qualifier: 3 }], ["method", "valid?", ["valid"]], ["method", "save!", ["save"]], ["method", "name=", ["name"]], ["field", "@total", ["total"]], - ["file", "lib/acme_order/promo#2024.rb", ["lib", "acme", "order", "promo"], 1], + ["file", "lib/acme_order/promo#2024.rb", ["lib", "acme", "order", "promo"], { numeric: 1, qualifier: 3 }], ["file", "Gemfile", ["gemfile"]], ["file", "acme_order.gemspec", ["acme", "order"]], ["method", "<=>", null], @@ -152,9 +161,9 @@ describe("Ruby", () => { describe("Elixir", () => { check("ex", [ // A module's dots are not an extension: `module` is not a file kind here. - ["module", "AcmeOrder.Notifier.Sms.Gateway", ["acme", "order", "notifier", "sms", "gateway"]], - ["file", "lib/acme_order/notifier.ex", ["lib", "acme", "order", "notifier"]], - ["file", "test/acme_order/order_test.exs", ["test", "acme", "order", "order", "test"]], + ["module", "AcmeOrder.Notifier.Sms.Gateway", ["acme", "order", "notifier", "sms", "gateway"], { qualifier: 4 }], + ["file", "lib/acme_order/notifier.ex", ["lib", "acme", "order", "notifier"], { qualifier: 3 }], + ["file", "test/acme_order/order_test.exs", ["test", "acme", "order", "order", "test"], { qualifier: 3 }], ["function", "available?", ["available"]], ["function", "fail!", ["fail"]], ["attribute", "@max_lines", ["max", "lines"]], @@ -195,8 +204,8 @@ describe("Rust", () => { describe("PHP", () => { check("php", [ - ["codeFile", "src/Order/OrderRepository.php", ["src", "order", "order", "repository"]], - ["namespace", "App\\Order", ["app", "order"]], + ["codeFile", "src/Order/OrderRepository.php", ["src", "order", "order", "repository"], { qualifier: 2 }], + ["namespace", "App\\Order", ["app", "order"], { qualifier: 1 }], ["property", "$orderTotal", ["order", "total"]], ["method", "__construct", null], ["method", "__toString", null], @@ -209,11 +218,48 @@ describe("Clojure", () => { ["function", "order-total", ["order", "total"]], ["function", "valid?", ["valid"]], ["var", "*out*", ["out"]], - ["namespace", "acme.order.core", ["acme", "order", "core"]], + ["namespace", "acme.order.core", ["acme", "order", "core"], { qualifier: 2 }], ["function", "-main", null], ]); }); +// The words before a qualified kind's LAST separator name its container: they +// are still words, and the split says how many lead the list. +describe("qualified names", () => { + describe("in the shipped lexicons", () => { + check("rb", [ + ["module", "AcmeOrder", ["acme", "order"]], + // `::` is the separator: one colon is a break between words, nothing more. + ["class", "Order:Line", ["order", "line"]], + ["class", "AcmeOrder::", ["acme", "order"], { qualifier: 2 }], + ["file", "lib/2024/order.rb", ["lib", "order"], { numeric: 1, qualifier: 1 }], + ["method", "AcmeOrder::Order.create", ["acme", "order", "order", "create"]], + ]); + check("ex", [ + ["protocol", "AcmeOrder.Priceable", ["acme", "order", "priceable"], { qualifier: 2 }], + ["module", "AcmeOrder.Priceable.AcmeOrder.Money", ["acme", "order", "priceable", "acme", "order", "money"], { qualifier: 5 }], + ["module", "legacy.mod", ["legacy", "mod"], { qualifier: 1 }], + ["attribute", "@max_lines", ["max", "lines"]], + ]); + }); + + describe("by own kind only", () => { + // `constructor` is a Java kind and a key every object inherits: read as an + // inherited key, `Object` itself would be the separator, spelled as its source. + check({ keywords: [], conventional: [], qualified: { package: "." } }, [ + ["constructor", "function Object() { [native code] }", ["function", "object", "native", "code"]], + ]); + }); + + describe("with an interface prefix", () => { + // The marker leads the entity's OWN name, after its qualifier. + check({ keywords: [], conventional: [], qualified: { interface: "." }, interfacePrefix: true }, [ + ["interface", "Acme.IOrder", ["acme", "order"], { qualifier: 1 }], + ["interface", "IOrder", ["order"]], + ]); + }); +}); + describe("the unicode fixture through the splitter", () => { // fixtures/unicode exists to break code-unit assumptions: a fullwidth letter // and a supplementary-plane letter (a surrogate pair in UTF-16). @@ -308,6 +354,7 @@ describe("properties", () => { const right = splitIdentifier(b, "class", undefined); expect(splitIdentifier(a + sep + b, "class", undefined)).toEqual({ tokens: [...left.tokens, ...right.tokens], + qualifier: 0, numeric: left.numeric + right.numeric, conventional: false, }); @@ -315,11 +362,43 @@ describe("properties", () => { ); }); + // Qualification decides which words weigh as a container's, never which + // words there are. C# is left out: its interface prefix reads the own name. + it("moves no word: the same split with the qualified kinds removed", () => { + const plain = fc.constantFrom(lexiconOf("java"), lexiconOf("ts"), lexiconOf("rb"), lexiconOf("ex"), lexiconOf("php")); + const anyKind = fc.constantFrom("class", "module", "file", "package", "constant", "protocol", "method"); + fc.assert( + fc.property(identifier, anyKind, plain, (name, kind, lexicon) => { + const { qualifier, ...split } = splitIdentifier(name, kind, lexicon); + const { qualifier: none, ...unqualified } = splitIdentifier(name, kind, { ...lexicon, qualified: undefined }); + expect(split).toEqual(unqualified); + expect(none).toBe(0); + expect(qualifier).toBeLessThanOrEqual(split.tokens.length); + if (lexicon.qualified?.[kind] === undefined) expect(qualifier).toBe(0); + }), + ); + }); + + it("counts as the qualifier the words before the last separator", () => { + const cases = fc.constantFrom(["rb", "class", "::"], ["ex", "module", "."], ["java", "package", "."]) ; + fc.assert( + fc.property(cases, identifier, identifier, ([lang, kind, sep], a, b) => { + fc.pre(!b.includes(sep)); + const split = splitIdentifier(a + sep + b, kind, lexiconOf(lang)); + fc.pre(!split.conventional); + const left = splitIdentifier(a, kind, undefined); + const right = splitIdentifier(b, kind, undefined); + expect(split.tokens).toEqual([...left.tokens, ...right.tokens]); + expect(split.qualifier).toBe(left.tokens.length); + }), + ); + }); + it("gives a conventional name no word at all", () => { const java = lexiconOf("java"); fc.assert( fc.property(fc.constantFrom(...java.conventional), kinds, (name, kind) => { - expect(splitIdentifier(name, kind, java)).toEqual({ tokens: [], numeric: 0, conventional: true }); + expect(splitIdentifier(name, kind, java)).toEqual({ tokens: [], qualifier: 0, numeric: 0, conventional: true }); }), ); }); diff --git a/packages/vocabulary/test/weight.test.ts b/packages/vocabulary/test/weight.test.ts index b1de65f..aac8cc0 100644 --- a/packages/vocabulary/test/weight.test.ts +++ b/packages/vocabulary/test/weight.test.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from "node:url"; import { PROFILES, TRAIT_NAMES, readModelFileSync, type Entity, type TraitName } from "@codegraph/core"; import fc from "fast-check"; import { describe, expect, it } from "vitest"; -import { DEFAULT_WEIGHTS, ROLES, roleOf, structuralSlots, type Role } from "../src/weight.js"; +import { DEFAULT_WEIGHTS, ROLES, WEIGHT_KEYS, roleOf, structuralSlots, type Role } from "../src/weight.js"; /** * The role of the entity carrying a name decides the weight of its words @@ -30,8 +30,15 @@ describe("the roles and their default weights", () => { parameter: 1, local: 1, other: 1, + qualifier: 1, }); - expect(Object.keys(DEFAULT_WEIGHTS).sort()).toEqual([...ROLES].sort()); + }); + + // A qualifier (`AcmeOrder::` of `AcmeOrder::Order`) is not a role: it is the + // part of a name that names a container, whatever the name's role. + it("weigh every role, and a qualifier's words, once each", () => { + expect(WEIGHT_KEYS).toEqual([...ROLES, "qualifier"]); + expect(Object.keys(DEFAULT_WEIGHTS).sort()).toEqual([...WEIGHT_KEYS].sort()); }); }); From cde7ed303c6d2560b8668f61396487957db7a708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Grodziski?= Date: Sun, 25 Oct 2026 22:19:48 +0100 Subject: [PATCH 3/6] feat(vocabulary): aggregate into vocabulary.json `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 --- packages/vocabulary/package.json | 1 + packages/vocabulary/src/aggregate.ts | 289 ++++++++++++++++++++ packages/vocabulary/src/artifact.ts | 116 ++++++++ packages/vocabulary/src/index.ts | 2 + packages/vocabulary/test/aggregate.test.ts | 294 +++++++++++++++++++++ packages/vocabulary/test/artifact.test.ts | 30 +++ packages/vocabulary/test/closure.ts | 105 ++++++++ packages/vocabulary/test/toy.ts | 103 ++++++++ pnpm-lock.yaml | 3 + 9 files changed, 943 insertions(+) create mode 100644 packages/vocabulary/src/aggregate.ts create mode 100644 packages/vocabulary/src/artifact.ts create mode 100644 packages/vocabulary/test/aggregate.test.ts create mode 100644 packages/vocabulary/test/artifact.test.ts create mode 100644 packages/vocabulary/test/closure.ts create mode 100644 packages/vocabulary/test/toy.ts diff --git a/packages/vocabulary/package.json b/packages/vocabulary/package.json index d451e86..90136cc 100644 --- a/packages/vocabulary/package.json +++ b/packages/vocabulary/package.json @@ -19,6 +19,7 @@ "test": "vitest run" }, "dependencies": { + "@codegraph/analyzer": "workspace:*", "@codegraph/core": "workspace:*" }, "devDependencies": { diff --git a/packages/vocabulary/src/aggregate.ts b/packages/vocabulary/src/aggregate.ts new file mode 100644 index 0000000..6040b23 --- /dev/null +++ b/packages/vocabulary/src/aggregate.ts @@ -0,0 +1,289 @@ +import { createHash } from "node:crypto"; +import { getProfile, type Entity, type EntityId } from "@codegraph/core"; +import { + compareIds, + entityName, + folderFor, + hasTrait, + identityView, + type CodeGraph, + type View, +} from "@codegraph/analyzer"; +import { + VOCABULARY_ARTEFACT_KIND, + VOCABULARY_GENERATOR, + type ScopeLevel, + type TermRow, + type VocabularyArtifact, + type VocabularyScope, + type VocabularyTerm, +} from "./artifact.js"; +import { DEFAULT_TECHNICAL, TERM_CLASSES, createClassifier, type Classify, type TermClass } from "./classify.js"; +import { splitIdentifier } from "./split.js"; +import { DEFAULT_WEIGHTS, ROLES, WEIGHT_KEYS, roleOf, structuralSlots, type Role, type Weights } from "./weight.js"; + +export interface VocabularyOptions { + readonly view?: View; + readonly weights?: Weights; + /** Replaces the shared technical list whole; each profile's own words apply either way. */ + readonly technical?: readonly string[]; + /** Display name for the corpus; defaults to the model roots' basenames. */ + readonly name?: string; +} + +/** One scope while it fills; a table row is `[weight, occurrences, carriers]` by interned term. */ +interface Tally { + readonly level: ScopeLevel; + readonly entity?: Entity; + entities: number; + readonly roles: Record; + readonly skipped: { unnamed: number; conventional: number; numeric: number }; + readonly table: Map; +} + +/** + * Count the words of every kept entity's name into the scopes the FOLD puts it + * in (PLAN.md §20 principle 5): its type (`containingType`), its module + * (`containingModule`) and the corpus. Pure: the graph is read, never written. + * + * Kept is the view's verdict on a non-stub entity. A stub's name is a word + * another corpus chose (`String`), so stubs never speak, whatever the view. + * An entity's language is its model's — read from the union, never from its id. + */ +export function buildVocabulary(graph: CodeGraph, options: VocabularyOptions = {}): VocabularyArtifact { + const view = options.view ?? identityView; + const weights = options.weights ?? DEFAULT_WEIGHTS; + const technical = options.technical ?? DEFAULT_TECHNICAL; + const folder = folderFor(graph); + const slots = structuralSlots(graph.entities.values()); + const langOf = languagesOf(graph); + const corpus = corpusOf(graph, options.name); + + const kept: Entity[] = []; + for (const id of graph.ids()) { + const entity = graph.entity(id); + if (entity !== undefined && !graph.isStub(id) && view.entity(entity, graph)) kept.push(entity); + } + + // --- the scopes: kept modules and types, by trait -------------------- + const root = tally("corpus"); + const modules = new Map(); + const types = new Map(); + for (const entity of kept) { + if (hasTrait(entity, "TModule")) modules.set(entity.id, tally("module", entity)); + else if (hasTrait(entity, "TType")) types.set(entity.id, tally("type", entity)); + } + + // --- the words: one entity at a time, into every scope it folds into --- + const terms = new Interner(); + const classifiers = new Map(); + const classifierOf = (lang: string): Classify => { + let classify = classifiers.get(lang); + if (classify === undefined) { + classify = createClassifier(getProfile(lang)?.lexicon, technical); + classifiers.set(lang, classify); + } + return classify; + }; + const unplaced: EntityId[] = []; + for (const entity of kept) { + const module = modules.get(folder.containingModule(entity.id) ?? ""); + const type = types.get(folder.containingType(entity.id) ?? ""); + if (module === undefined) unplaced.push(entity.id); + const into = [root, ...(module === undefined ? [] : [module]), ...(type === undefined ? [] : [type])]; + for (const scope of into) scope.entities += 1; + + const name = entityName(entity); + if (name === undefined) { + for (const scope of into) scope.skipped.unnamed += 1; + continue; + } + const lang = langOf.get(entity.id) ?? ""; + const split = splitIdentifier(name, entity.kind, getProfile(lang)?.lexicon); + if (split.conventional) { + for (const scope of into) scope.skipped.conventional += 1; + continue; + } + const role = roleOf(entity, slots); + const classify = classifierOf(lang); + + // This entity's own table first: a word it says twice has one carrier. + const own = new Map(); + split.tokens.forEach((token, i) => { + const term = terms.intern(token, classify(token)); + const row = own.get(term) ?? [0, 0]; + row[0] += i < split.qualifier ? weights.qualifier : weights[role]; + row[1] += 1; + own.set(term, row); + }); + for (const scope of into) { + scope.skipped.numeric += split.numeric; + scope.roles[role] += 1; + for (const [term, [weight, occurrences]] of own) { + const row = scope.table.get(term) ?? [0, 0, 0]; + row[0] += weight; + row[1] += occurrences; + row[2] += 1; + scope.table.set(term, row); + } + } + } + + // --- the artifact: terms sorted, scopes indexed, rows remapped -------- + const order = terms.sorted(); + const index = new Map(order.map((term, i) => [term.id, i] as const)); + const tallies = [root, ...sortedById(modules), ...sortedById(types)]; + const scopeIndex = new Map(); + tallies.forEach((scope, i) => { + if (scope.entity !== undefined) scopeIndex.set(scope.entity.id, i); + }); + + const spread = order.map(() => ({ modules: 0, types: 0, exclusive: -1 })); + const scopes: VocabularyScope[] = tallies.map((scope, i) => { + const rows: TermRow[] = [...scope.table] + .map(([term, [weight, occurrences, carriers]]): TermRow => [index.get(term) ?? -1, weight, occurrences, carriers]) + .sort((a, b) => b[1] - a[1] || a[0] - b[0]); + for (const [term] of rows) { + const counts = spread[term]; + if (counts === undefined || scope.level === "corpus") continue; + if (scope.level === "type") counts.types += 1; + else { + counts.modules += 1; + counts.exclusive = i; + } + } + return scopeOf(scope, rows, order, parentScope(graph, scope.entity, scopeIndex), corpus.name); + }); + + const words = [...new Set(technical)].sort(compareIds); + return { + kind: VOCABULARY_ARTEFACT_KIND, + generatedBy: VOCABULARY_GENERATOR, + view: view.descriptor, + corpus, + langs: graph.union.langs, + weights: Object.fromEntries(WEIGHT_KEYS.map((key) => [key, weights[key]])) as Weights, + classes: TERM_CLASSES, + technical: { words: words.length, sha256: createHash("sha256").update(words.join("\n"), "utf8").digest("hex") }, + terms: order.map(({ text, class: termClass }, i): VocabularyTerm => { + const { modules: speaking, types: typed, exclusive } = spread[i] ?? { modules: 0, types: 0, exclusive: -1 }; + return { text, class: termClass, modules: speaking, types: typed, ...(speaking === 1 ? { exclusive } : {}) }; + }), + scopes, + diagnostics: { + unplaced, + withoutLexicon: graph.union.langs.filter((lang) => getProfile(lang)?.lexicon === undefined), + }, + }; +} + +function tally(level: ScopeLevel, entity?: Entity): Tally { + return { + level, + ...(entity === undefined ? {} : { entity }), + entities: 0, + roles: Object.fromEntries(ROLES.map((role) => [role, 0])) as Record, + skipped: { unnamed: 0, conventional: 0, numeric: 0 }, + table: new Map(), + }; +} + +function sortedById(scopes: ReadonlyMap): Tally[] { + return [...scopes].sort(([a], [b]) => compareIds(a, b)).map(([, scope]) => scope); +} + +function scopeOf( + scope: Tally, + rows: readonly TermRow[], + terms: readonly { readonly class: TermClass }[], + parent: number | undefined, + corpusName: string, +): VocabularyScope { + const { entity } = scope; + return { + level: scope.level, + ...(entity === undefined ? {} : { id: entity.id }), + name: entity === undefined ? corpusName : (entityName(entity) ?? entity.id), + ...(entity === undefined ? {} : { kind: entity.kind }), + ...(parent === undefined ? {} : { parent }), + entities: scope.entities, + roles: scope.roles, + skipped: scope.skipped, + distinct: rows.length, + distinctDomain: rows.filter(([term]) => terms[term]?.class === "domain").length, + weight: rows.reduce((sum, row) => sum + row[1], 0), + occurrences: rows.reduce((sum, row) => sum + row[2], 0), + terms: rows, + }; +} + +/** The nearest scope up the containment chain the model writes; the corpus when there is none. */ +function parentScope( + graph: CodeGraph, + entity: Entity | undefined, + scopeIndex: ReadonlyMap, +): number | undefined { + if (entity === undefined) return undefined; + const seen = new Set([entity.id]); + for (let cursor = graph.parentOf(entity.id); cursor !== undefined && !seen.has(cursor); cursor = graph.parentOf(cursor)) { + const found = scopeIndex.get(cursor); + if (found !== undefined) return found; + seen.add(cursor); + } + return 0; +} + +/** Each id's language, from the model that declares it first — the graph's own rule. */ +function languagesOf(graph: CodeGraph): Map { + const langs = new Map(); + for (const model of graph.union.models) { + for (const entity of model.entities) if (!langs.has(entity.id)) langs.set(entity.id, model.lang); + } + return langs; +} + +/** Words interned once per (text, class): a class is decided per language. */ +class Interner { + private readonly ids = new Map>(); + private readonly entries: { readonly id: number; readonly text: string; readonly class: TermClass }[] = []; + + intern(text: string, termClass: TermClass): number { + let byClass = this.ids.get(text); + if (byClass === undefined) { + byClass = new Map(); + this.ids.set(text, byClass); + } + const existing = byClass.get(termClass); + if (existing !== undefined) return existing; + const id = this.entries.length; + byClass.set(termClass, id); + this.entries.push({ id, text, class: termClass }); + return id; + } + + /** By text in code-unit order, then by the classes' order: the same bytes on every machine. */ + sorted(): readonly { readonly id: number; readonly text: string; readonly class: TermClass }[] { + return [...this.entries].sort( + (a, b) => compareIds(a.text, b.text) || TERM_CLASSES.indexOf(a.class) - TERM_CLASSES.indexOf(b.class), + ); + } +} + +/** + * The corpus line of the artifact — the city's and the navigator's rule: + * deduped, sorted root basenames (or the caller's override), roots verbatim. + */ +function corpusOf(graph: CodeGraph, name: string | undefined): { name: string; roots: readonly string[] } { + const roots = [...new Set(graph.union.models.map((model) => model.root))] + .filter((root) => root.length > 0) + .sort(); + const basenames = [ + ...new Set( + roots + .map((root) => root.replace(/\/+$/, "")) + .map((root) => root.slice(root.lastIndexOf("/") + 1)) + .filter((base) => base.length > 0), + ), + ].sort(); + return { name: name ?? (basenames.length > 0 ? basenames.join(" + ") : "codegraph"), roots }; +} diff --git a/packages/vocabulary/src/artifact.ts b/packages/vocabulary/src/artifact.ts new file mode 100644 index 0000000..7f41e63 --- /dev/null +++ b/packages/vocabulary/src/artifact.ts @@ -0,0 +1,116 @@ +import type { ViewDescriptor } from "@codegraph/analyzer"; +import type { TermClass } from "./classify.js"; +import type { Role, Weights } from "./weight.js"; + +/** + * THE VOCABULARY ARTIFACT (PLAN.md §20.1): the words of a corpus's names, + * counted per scope — the corpus, each module, each type — as the fold places + * every entity. A third model beside the city's and the navigator's, derived + * from the first and never written back into it. + * + * Indexes, not ids, address its rows: a term is its position in `terms`, a + * scope's parent its position in `scopes`. Scopes carry the entity's rendered + * id, opaque, so another artifact's building or tree node finds its words. + */ + +export const VOCABULARY_ARTEFACT_KIND = "codegraph.vocabulary/1"; +export const VOCABULARY_GENERATOR = "@codegraph/vocabulary"; + +export const SCOPE_LEVELS = ["corpus", "module", "type"] as const; +export type ScopeLevel = (typeof SCOPE_LEVELS)[number]; + +/** `[term, weight, occurrences, carriers]`: `term` indexes `terms`; carriers are distinct entities. */ +export type TermRow = readonly [term: number, weight: number, occurrences: number, carriers: number]; + +export interface VocabularyTerm { + readonly text: string; + /** Decided per language: one word two languages class apart is two terms. */ + readonly class: TermClass; + /** How many module scopes, and how many type scopes, speak it. */ + readonly modules: number; + readonly types: number; + /** The one module scope that speaks it, when exactly one does. */ + readonly exclusive?: number; +} + +/** What a scope's entities did not contribute. */ +export interface Skipped { + /** Entities with no name: a constructor, a lambda. */ + readonly unnamed: number; + /** Names the language dictates (`toString`): no word at all. */ + readonly conventional: number; + /** Pieces of digits only (`2024` of `promo#2024`), dropped. */ + readonly numeric: number; +} + +export interface VocabularyScope { + readonly level: ScopeLevel; + /** The entity's rendered id; absent on the corpus. Compared, never parsed. */ + readonly id?: string; + readonly name: string; + /** The entity kind, verbatim; absent on the corpus. */ + readonly kind?: string; + /** The nearest scope the model declares around it; absent on the corpus. */ + readonly parent?: number; + /** Kept entities the fold puts here: each one unnamed, conventional, or counted in `roles`. */ + readonly entities: number; + /** The entities that spoke words, by the role of each. */ + readonly roles: Readonly>; + readonly skipped: Skipped; + /** Terms in the table, and the `domain` ones among them: the cognitive-load proxy. */ + readonly distinct: number; + readonly distinctDomain: number; + /** Sums over the table. */ + readonly weight: number; + readonly occurrences: number; + /** By weight descending, then term. */ + readonly terms: readonly TermRow[]; +} + +export interface VocabularyDiagnostics { + /** Kept entities no module contains, sorted: counted at the corpus alone. */ + readonly unplaced: readonly string[]; + /** Languages core has no lexicon for, sorted: split by the universal rules alone. */ + readonly withoutLexicon: readonly string[]; +} + +export interface VocabularyArtifact { + /** Always `codegraph.vocabulary/1` — derived, never an interchange model.jsonl. */ + readonly kind: string; + readonly generatedBy: string; + /** A count without its view is not a fact; nor without its weights or its technical list. */ + readonly view: ViewDescriptor; + readonly corpus: { readonly name: string; readonly roots: readonly string[] }; + readonly langs: readonly string[]; + readonly weights: Weights; + readonly classes: readonly TermClass[]; + /** The shared technical list: its size, and a sha256 over its sorted words, one per line. */ + readonly technical: { readonly words: number; readonly sha256: string }; + /** Sorted by text, then class. */ + readonly terms: readonly VocabularyTerm[]; + /** The corpus first, then modules, then types, each level in id order. */ + readonly scopes: readonly VocabularyScope[]; + readonly diagnostics: VocabularyDiagnostics; +} + +/** + * The artifact as JSON, deterministically: every array the builder writes is + * sorted, so insertion order is the byte order. Compact by default (thousands + * of scopes on a real corpus); `pretty` puts one term and one scope per line, + * the form of the committed snapshots, so a diff reads word by word. + */ +export function vocabularyToJsonString( + artifact: VocabularyArtifact, + options?: { readonly pretty?: boolean }, +): string { + if (options?.pretty !== true) return JSON.stringify(artifact); + const entries = Object.entries(artifact).map(([key, value]) => { + const text = Array.isArray(value) && (key === "terms" || key === "scopes") ? rows(value) : JSON.stringify(value); + return ` ${JSON.stringify(key)}: ${text}`; + }); + return `{\n${entries.join(",\n")}\n}\n`; +} + +function rows(values: readonly unknown[]): string { + return values.length === 0 ? "[]" : `[\n${values.map((value) => ` ${JSON.stringify(value)}`).join(",\n")}\n ]`; +} diff --git a/packages/vocabulary/src/index.ts b/packages/vocabulary/src/index.ts index c7b58de..68675fb 100644 --- a/packages/vocabulary/src/index.ts +++ b/packages/vocabulary/src/index.ts @@ -9,3 +9,5 @@ export * from "./split.js"; export * from "./classify.js"; export * from "./weight.js"; +export * from "./artifact.js"; +export * from "./aggregate.js"; diff --git a/packages/vocabulary/test/aggregate.test.ts b/packages/vocabulary/test/aggregate.test.ts new file mode 100644 index 0000000..1d05129 --- /dev/null +++ b/packages/vocabulary/test/aggregate.test.ts @@ -0,0 +1,294 @@ +import { createHash } from "node:crypto"; +import type { Entity } from "@codegraph/core"; +import { composeViews, internalOnly, makeView } from "@codegraph/analyzer"; +import fc from "fast-check"; +import { describe, expect, it } from "vitest"; +import { buildVocabulary } from "../src/aggregate.js"; +import { VOCABULARY_ARTEFACT_KIND, VOCABULARY_GENERATOR, vocabularyToJsonString, type VocabularyArtifact } from "../src/artifact.js"; +import { DEFAULT_TECHNICAL, TERM_CLASSES } from "../src/classify.js"; +import { DEFAULT_WEIGHTS, ROLES } from "../src/weight.js"; +import { expectClosed } from "./closure.js"; +import { cls, graphOf, method, orderCorpus, pkg, structural } from "./toy.js"; + +/** + * Aggregation (PLAN.md §20.1): every kept, named entity's words, weighted by + * its role, summed into the scopes the FOLD puts it in — its type, its module, + * the corpus. The corpus below is counted by hand, word by word. + */ + +const java = (entities: readonly Entity[]) => graphOf([{ lang: "java", entities }]); + +/** A scope's table as `term → [weight, occurrences, carriers]`, in row order. */ +function table(artifact: VocabularyArtifact, index: number): readonly (readonly [string, number, number, number])[] { + return (artifact.scopes[index]?.terms ?? []).map(([term, w, n, c]) => [artifact.terms[term]?.text ?? "?", w, n, c]); +} + +const NO_ROLES = Object.fromEntries(ROLES.map((role) => [role, 0])); + +describe("the hand-counted corpus", () => { + const graph = java(orderCorpus()); + const artifact = buildVocabulary(graph); + + it("states what it is, and under which scheme it counted", () => { + expect(artifact.kind).toBe(VOCABULARY_ARTEFACT_KIND); + expect(artifact.generatedBy).toBe(VOCABULARY_GENERATOR); + expect(artifact.view).toEqual({ name: "all", filters: [] }); + expect(artifact.corpus).toEqual({ name: "java", roots: ["toy/java"] }); + expect(artifact.langs).toEqual(["java"]); + expect(artifact.weights).toEqual(DEFAULT_WEIGHTS); + expect(artifact.classes).toEqual(TERM_CLASSES); + const digest = createHash("sha256").update([...DEFAULT_TECHNICAL].sort().join("\n"), "utf8").digest("hex"); + expect(artifact.technical).toEqual({ words: DEFAULT_TECHNICAL.length, sha256: digest }); + }); + + it("interns every word once, sorted, with its class and its spread", () => { + expect(artifact.terms).toEqual([ + { text: "acme", class: "domain", modules: 2, types: 0 }, + { text: "billing", class: "domain", modules: 1, types: 0, exclusive: 2 }, + { text: "com", class: "technical", modules: 2, types: 0 }, + { text: "get", class: "technical", modules: 1, types: 1, exclusive: 1 }, + { text: "invoice", class: "domain", modules: 1, types: 1, exclusive: 2 }, + { text: "line", class: "domain", modules: 1, types: 2, exclusive: 1 }, + { text: "lines", class: "domain", modules: 1, types: 1, exclusive: 1 }, + { text: "order", class: "domain", modules: 2, types: 1 }, + { text: "qty", class: "domain", modules: 1, types: 1, exclusive: 1 }, + { text: "total", class: "domain", modules: 1, types: 1, exclusive: 1 }, + ]); + }); + + it("orders the scopes root, modules, types, each with its parent scope", () => { + expect(artifact.scopes.map(({ level, id, name, kind, parent }) => ({ level, id, name, kind, parent }))).toEqual([ + { level: "corpus", id: undefined, name: "java", kind: undefined, parent: undefined }, + { level: "module", id: "java:com.acme.order", name: "com.acme.order", kind: "package", parent: 0 }, + { level: "module", id: "java:com.acme.order.billing", name: "com.acme.order.billing", kind: "package", parent: 1 }, + { level: "type", id: "java:com.acme.order.billing/Invoice", name: "Invoice", kind: "class", parent: 2 }, + { level: "type", id: "java:com.acme.order/Order", name: "Order", kind: "class", parent: 1 }, + // A nested type is its own scope, under its outer type. + { level: "type", id: "java:com.acme.order/Order$Line", name: "Line", kind: "class", parent: 4 }, + ]); + }); + + it("weighs each word by the role of its carrier, a qualifier at 1", () => { + // order: the package (8), `order.` qualifying billing (1), the class (8), orderLine (1). + expect(table(artifact, 0)).toEqual([ + ["order", 18, 4, 4], + ["line", 9, 2, 2], + ["billing", 8, 1, 1], + ["invoice", 8, 1, 1], + ["lines", 4, 1, 1], + ["qty", 4, 1, 1], + ["total", 4, 2, 2], + ["get", 3, 1, 1], + ["acme", 2, 2, 2], + ["com", 2, 2, 2], + ]); + expect(artifact.scopes[0]).toMatchObject({ + entities: 12, + roles: { module: 2, type: 3, operation: 1, attribute: 2, parameter: 1, local: 1, other: 0 }, + skipped: { unnamed: 1, conventional: 1, numeric: 0 }, + distinct: 10, + distinctDomain: 8, + weight: 62, + occurrences: 17, + }); + }); + + it("folds a member into its nearest type and module, never the outer type", () => { + expect(table(artifact, 1)).toEqual([ + ["order", 17, 3, 3], + ["line", 9, 2, 2], + ["lines", 4, 1, 1], + ["qty", 4, 1, 1], + ["total", 4, 2, 2], + ["get", 3, 1, 1], + ["acme", 1, 1, 1], + ["com", 1, 1, 1], + ]); + expect(table(artifact, 2)).toEqual([ + ["billing", 8, 1, 1], + ["invoice", 8, 1, 1], + ["acme", 1, 1, 1], + ["com", 1, 1, 1], + ["order", 1, 1, 1], + ]); + expect(table(artifact, 3)).toEqual([["invoice", 8, 1, 1]]); + expect(table(artifact, 4)).toEqual([ + ["order", 9, 2, 2], + ["lines", 4, 1, 1], + ["total", 4, 2, 2], + ["get", 3, 1, 1], + ["line", 1, 1, 1], + ]); + expect(table(artifact, 5)).toEqual([ + ["line", 8, 1, 1], + ["qty", 4, 1, 1], + ]); + expect(artifact.scopes.map((scope) => scope.entities)).toEqual([12, 10, 2, 1, 7, 2]); + expect(artifact.scopes[4]).toMatchObject({ + roles: { ...NO_ROLES, type: 1, operation: 1, attribute: 1, parameter: 1, local: 1 }, + skipped: { unnamed: 1, conventional: 1, numeric: 0 }, + }); + }); + + it("never counts a stub, nor makes one a scope", () => { + expect(artifact.terms.map((term) => term.text)).not.toContain("string"); + expect(artifact.scopes.map((scope) => scope.id)).not.toContain("java:java.lang"); + expect(artifact.diagnostics).toEqual({ unplaced: [], withoutLexicon: [] }); + }); + + it("is closed", () => { + expectClosed(artifact, graph); + }); +}); + +describe("options", () => { + const graph = java(orderCorpus()); + + it("counts under the view, and says which", () => { + const noLocals = makeView("noLocals", (entity) => entity.kind !== "localVariable", () => true); + const view = composeViews(internalOnly, noLocals); + const artifact = buildVocabulary(graph, { view }); + expect(artifact.view).toEqual(view.descriptor); + expect(table(artifact, 0)).toContainEqual(["total", 3, 1, 1]); + expect(artifact.scopes[0]?.entities).toBe(11); + }); + + it("weighs with the weights given, and records them; a word weighing 0 still occurs", () => { + const weights = { ...DEFAULT_WEIGHTS, qualifier: 0, type: 5 }; + const artifact = buildVocabulary(graph, { weights }); + expect(artifact.weights).toEqual(weights); + expect(table(artifact, 0)).toContainEqual(["acme", 0, 2, 2]); + expect(table(artifact, 0)).toContainEqual(["invoice", 5, 1, 1]); + }); + + it("classifies with the technical list given, and records its digest, whatever its order", () => { + const artifact = buildVocabulary(graph, { technical: ["order", "total"] }); + const reversed = buildVocabulary(graph, { technical: ["total", "order"] }); + expect(artifact.terms.find((term) => term.text === "order")?.class).toBe("technical"); + expect(artifact.terms.find((term) => term.text === "get")?.class).toBe("domain"); + expect(artifact.technical.words).toBe(2); + expect(artifact.technical.sha256).toBe(createHash("sha256").update("order\ntotal", "utf8").digest("hex")); + expect(reversed.technical).toEqual(artifact.technical); + }); + + it("names the corpus as asked", () => { + expect(buildVocabulary(graph, { name: "acme" }).scopes[0]?.name).toBe("acme"); + }); +}); + +describe("what no scope holds", () => { + it("counts an entity no module contains at the root alone, and names it", () => { + const graph = java([pkg("java:p", "shop"), cls("java:p/Order", "Order", "java:p"), cls("java:Loose", "Loose")]); + const artifact = buildVocabulary(graph); + expect(artifact.diagnostics.unplaced).toEqual(["java:Loose"]); + expect(table(artifact, 0)).toEqual([ + ["loose", 8, 1, 1], + ["order", 8, 1, 1], + ["shop", 8, 1, 1], + ]); + // A type is a scope whether or not a module holds it. + expect(artifact.scopes.map((scope) => scope.id)).toEqual([undefined, "java:p", "java:Loose", "java:p/Order"]); + expect(artifact.scopes[2]?.parent).toBe(0); + expectClosed(artifact, graph); + }); + + it("splits a language with no lexicon by the universal rules, and says so", () => { + const graph = graphOf([{ lang: "cobol", entities: [pkg("cobol:m", "PAYROLL-RUN"), cls("cobol:m/A", "toString", "cobol:m")] }]); + const artifact = buildVocabulary(graph); + expect(artifact.diagnostics.withoutLexicon).toEqual(["cobol"]); + expect(artifact.terms.map((term) => term.text)).toEqual(["payroll", "run", "string", "to"]); + expect(artifact.terms.find((term) => term.text === "run")?.class).toBe("technical"); + }); +}); + +describe("a word said twice", () => { + // Ruby names a class by its constant path and a file by its path: the + // qualifier's words weigh 1, the own name's 8, and one entity is one carrier. + const graph = graphOf([ + { + lang: "rb", + entities: [ + { ...pkg("rb:lib/acme_order/order.rb", "lib/acme_order/order.rb"), kind: "file" } as Entity, + { ...cls("rb:lib/acme_order/order.rb/AcmeOrder::Order", "AcmeOrder::Order", "rb:lib/acme_order/order.rb"), kind: "class" } as Entity, + ], + }, + ]); + const artifact = buildVocabulary(graph); + + it("counts every occurrence and one carrier per entity", () => { + expect(table(artifact, 0)).toEqual([ + ["order", 18, 4, 2], + ["acme", 2, 2, 2], + ["lib", 1, 1, 1], + ]); + expectClosed(artifact, graph); + }); +}); + +describe("several languages", () => { + // `type` is reserved by Go and an identifier to Java: one word, two classes, + // so two terms — a class is decided per language, never averaged. + const graph = graphOf([ + { lang: "java", entities: [pkg("java:p", "shop"), cls("java:p/Type", "Type", "java:p")] }, + { lang: "go", entities: [pkg("go:q", "type")] }, + ]); + const artifact = buildVocabulary(graph); + + it("interns a word once per class it takes", () => { + expect(artifact.langs).toEqual(["go", "java"]); + expect(artifact.terms.filter((term) => term.text === "type").map((term) => term.class)).toEqual(["domain", "keyword"]); + expect(artifact.corpus.roots).toEqual(["toy/go", "toy/java"]); + expectClosed(artifact, graph); + }); +}); + +describe("properties", () => { + // Small corpora of packages, classes, members and parameters, named from + // words the classes split differently, with the odd unnamed or conventional one. + const WORDS = ["order", "Order", "line", "LINE", "getTotal", "toString", "acme_order", "HTTPServer", "id", "x", "new", "2024", "promo#2024"]; + const name = fc.array(fc.constantFrom(...WORDS), { minLength: 1, maxLength: 3 }).map((ws) => ws.join("")); + const member = fc.record({ name: fc.option(name, { nil: undefined }), params: fc.array(name, { maxLength: 2 }) }); + const type = fc.record({ name, members: fc.array(member, { maxLength: 3 }), nested: fc.boolean() }); + const module = fc.record({ name, types: fc.array(type, { maxLength: 3 }) }); + const corpus = fc.array(module, { minLength: 1, maxLength: 3 }).map((modules) => { + const entities: Entity[] = []; + modules.forEach((m, mi) => { + const mid = `java:m${mi}`; + entities.push(pkg(mid, `com.acme.${m.name}`)); + m.types.forEach((t, ti) => { + const parent = t.nested && ti > 0 ? `${mid}/T${ti - 1}` : mid; + const tid = `${mid}/T${ti}`; + entities.push(cls(tid, t.name, parent)); + t.members.forEach((mem, k) => { + const id = `${tid}.m${k}`; + const params = mem.params.map((_, p) => `${id}#p${p}`); + entities.push(method(id, mem.name, tid, { parameters: params })); + mem.params.forEach((p, i) => entities.push(structural(params[i] ?? "", "parameter", p, id))); + }); + }); + }); + return entities; + }); + + it("is closed, and every kept entity lands in one module", () => { + fc.assert( + fc.property(corpus, (entities) => { + const graph = java(entities); + expectClosed(buildVocabulary(graph), graph); + }), + ); + }); + + it("writes the same bytes whatever the order the entities arrive in", () => { + fc.assert( + fc.property( + corpus.chain((entities) => fc.tuple(fc.constant(entities), fc.shuffledSubarray(entities, { minLength: entities.length }))), + ([entities, shuffled]) => { + const a = vocabularyToJsonString(buildVocabulary(java(entities))); + const b = vocabularyToJsonString(buildVocabulary(java(shuffled))); + expect(b).toBe(a); + }, + ), + ); + }); +}); diff --git a/packages/vocabulary/test/artifact.test.ts b/packages/vocabulary/test/artifact.test.ts new file mode 100644 index 0000000..5343949 --- /dev/null +++ b/packages/vocabulary/test/artifact.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { buildVocabulary } from "../src/aggregate.js"; +import { vocabularyToJsonString } from "../src/artifact.js"; +import { graphOf, orderCorpus } from "./toy.js"; + +/** + * The artifact as text. Compact by default, like the navigator's: a real + * corpus has thousands of scopes. Pretty is the committed snapshots' form — + * one term and one scope per line, so a review reads a diff of words. + */ +describe("vocabularyToJsonString", () => { + const artifact = buildVocabulary(graphOf([{ lang: "java", entities: orderCorpus() }])); + + it("writes one line by default, and the artifact parses back whole", () => { + const compact = vocabularyToJsonString(artifact); + expect(compact).not.toContain("\n"); + expect(JSON.parse(compact)).toEqual(JSON.parse(JSON.stringify(artifact))); + }); + + it("writes one term and one scope per line when pretty, saying the same thing", () => { + const pretty = vocabularyToJsonString(artifact, { pretty: true }); + expect(JSON.parse(pretty)).toEqual(JSON.parse(vocabularyToJsonString(artifact))); + expect(pretty.endsWith("}\n")).toBe(true); + const lines = new Set(pretty.split("\n").map((line) => line.trim().replace(/,$/, ""))); + for (const term of artifact.terms) expect(lines.has(JSON.stringify(term)), term.text).toBe(true); + for (const scope of artifact.scopes) expect(lines.has(JSON.stringify(scope)), scope.name).toBe(true); + expect(pretty.split("\n").length).toBeLessThan(artifact.terms.length + artifact.scopes.length + 30); + expect(pretty.startsWith(`{\n "kind": "codegraph.vocabulary/1",\n`)).toBe(true); + }); +}); diff --git a/packages/vocabulary/test/closure.ts b/packages/vocabulary/test/closure.ts new file mode 100644 index 0000000..e9fa529 --- /dev/null +++ b/packages/vocabulary/test/closure.ts @@ -0,0 +1,105 @@ +import { hasTrait, type CodeGraph } from "@codegraph/analyzer"; +import { expect } from "vitest"; +import { TERM_CLASSES } from "../src/classify.js"; +import type { VocabularyArtifact } from "../src/artifact.js"; + +/** + * The artifact's closure (PLAN.md §20.1): every scope id is an entity the + * model declares, kept and of the scope's level; every index is in range; and + * every count is what the tables add up to. A dangling index is unwritable. + */ +export function expectClosed(artifact: VocabularyArtifact, graph: CodeGraph): void { + const { terms, scopes } = artifact; + const classOrder = (c: string): number => TERM_CLASSES.indexOf(c as (typeof TERM_CLASSES)[number]); + + // Terms: interned once per (text, class), sorted by text, then class. + for (let i = 1; i < terms.length; i += 1) { + const [a, b] = [terms[i - 1], terms[i]]; + if (a === undefined || b === undefined) throw new Error("unreachable"); + expect(a.text < b.text || (a.text === b.text && classOrder(a.class) < classOrder(b.class)), `${a.text} before ${b.text}`).toBe(true); + } + + const [root, ...rest] = scopes; + expect(root?.level).toBe("corpus"); + expect(root?.id).toBeUndefined(); + expect(root?.parent).toBeUndefined(); + const modules = rest.filter((scope) => scope.level === "module"); + const types = rest.filter((scope) => scope.level === "type"); + // Root, then modules in id order, then types in id order. + expect(rest).toEqual([...modules, ...types]); + for (const level of [modules, types]) { + const ids = level.map((scope) => scope.id ?? ""); + expect(ids).toEqual([...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))); + expect(new Set(ids).size).toBe(ids.length); + } + + const modulesSpeaking = new Map(); + const typesSpeaking = new Map(); + const speakers = new Map(); + scopes.forEach((scope, index) => { + if (scope.level !== "corpus") { + const entity = graph.entity(scope.id ?? ""); + expect(entity, `${scope.id} is declared`).toBeDefined(); + if (entity === undefined) return; + expect(graph.isStub(entity.id), `${scope.id} is no stub`).toBe(false); + expect(hasTrait(entity, scope.level === "module" ? "TModule" : "TType"), `${scope.id} is a ${scope.level}`).toBe(true); + expect(scope.kind).toBe(entity.kind); + expect(scope.parent, `${scope.id} has a parent scope`).toBeDefined(); + expect(scope.parent).not.toBe(index); + expect(scope.parent).toBeGreaterThanOrEqual(0); + expect(scope.parent).toBeLessThan(scopes.length); + } + + const roles = Object.values(scope.roles).reduce((sum, n) => sum + n, 0); + expect(scope.entities, `${scope.name}: every entity is unnamed, conventional or carries a role`).toBe( + roles + scope.skipped.unnamed + scope.skipped.conventional, + ); + + let weight = 0; + let occurrences = 0; + const seen = new Set(); + scope.terms.forEach(([term, w, n, carriers], row) => { + expect(term).toBeGreaterThanOrEqual(0); + expect(term).toBeLessThan(terms.length); + expect(seen.has(term), `${scope.name}: ${terms[term]?.text} once`).toBe(false); + seen.add(term); + expect(n).toBeGreaterThan(0); + expect(carriers).toBeGreaterThan(0); + expect(carriers).toBeLessThanOrEqual(n); + expect(carriers).toBeLessThanOrEqual(scope.entities); + expect(w).toBeGreaterThanOrEqual(0); + const previous = scope.terms[row - 1]; + if (previous !== undefined) { + expect(previous[1] > w || (previous[1] === w && previous[0] < term), `${scope.name}: rows by weight, then term`).toBe(true); + } + weight += w; + occurrences += n; + if (scope.level === "module") modulesSpeaking.set(term, (modulesSpeaking.get(term) ?? 0) + 1); + if (scope.level === "type") typesSpeaking.set(term, (typesSpeaking.get(term) ?? 0) + 1); + if (scope.level === "module") speakers.set(term, [...(speakers.get(term) ?? []), index]); + }); + expect(scope.weight).toBe(weight); + expect(scope.occurrences).toBe(occurrences); + expect(scope.distinct).toBe(scope.terms.length); + expect(scope.distinctDomain).toBe(scope.terms.filter(([term]) => terms[term]?.class === "domain").length); + }); + + // The root speaks every term; the spread is what the scope tables say. + expect(new Set(root?.terms.map(([term]) => term))).toEqual(new Set(terms.map((_, index) => index))); + terms.forEach((term, index) => { + expect(term.modules, term.text).toBe(modulesSpeaking.get(index) ?? 0); + expect(term.types, term.text).toBe(typesSpeaking.get(index) ?? 0); + const only = speakers.get(index); + expect(term.exclusive, term.text).toBe(only?.length === 1 ? only[0] : undefined); + }); + + // Every kept entity folds into one module scope, or is unplaced and said so. + const sum = (key: "entities" | "weight" | "occurrences"): number => + modules.reduce((total, scope) => total + scope[key], 0); + expect(root?.entities).toBe(sum("entities") + artifact.diagnostics.unplaced.length); + if (artifact.diagnostics.unplaced.length === 0) { + expect(root?.weight).toBe(sum("weight")); + expect(root?.occurrences).toBe(sum("occurrences")); + } + for (const id of artifact.diagnostics.unplaced) expect(graph.entity(id), id).toBeDefined(); +} diff --git a/packages/vocabulary/test/toy.ts b/packages/vocabulary/test/toy.ts new file mode 100644 index 0000000..26e2220 --- /dev/null +++ b/packages/vocabulary/test/toy.ts @@ -0,0 +1,103 @@ +import type { Edge, Entity, Model } from "@codegraph/core"; +import { buildGraph, loadModels, type CodeGraph } from "@codegraph/analyzer"; + +/** Toy corpora for the aggregation: entities written as an extractor would. */ + +export function graphOf(models: readonly { lang: string; entities: readonly Entity[]; edges?: readonly Edge[] }[]): CodeGraph { + const payloads: Model[] = models.map(({ lang, entities, edges = [] }) => ({ + schemaVersion: "1.0.0", + lang, + extractor: { name: "test", version: "0.0.0" }, + root: `toy/${lang}`, + entities: [...entities], + edges: [...edges], + })); + return buildGraph(loadModels(payloads, { sources: payloads.map((model) => `toy-${model.lang}`) }).union); +} + +const child = (parent: string | undefined): Record => + parent === undefined ? {} : { parent }; +const childOf = (parent: string | undefined): readonly string[] => (parent === undefined ? [] : ["TChildOf"]); + +export function pkg(id: string, name: string, parent?: string, isStub = false): Entity { + return { + id, + kind: "package", + traits: ["TNamed", "TWithChildren", "TModule", ...childOf(parent)], + name, + isStub, + definedIn: isStub ? [] : ["T.java"], + ...child(parent), + } as Entity; +} + +export function cls(id: string, name: string, parent?: string, isStub = false): Entity { + return { + id, + kind: "class", + traits: ["TNamed", "TWithChildren", "TType", ...childOf(parent)], + name, + isStub, + ...child(parent), + } as Entity; +} + +export function method( + id: string, + name: string | undefined, + parent: string, + slots: { parameters?: readonly string[]; localVariables?: readonly string[] } = {}, +): Entity { + return { + id, + kind: name === undefined ? "constructor" : "method", + traits: [ + ...(name === undefined ? [] : ["TNamed"]), + "TInvocable", + "TWithParameters", + "TWithLocalVariables", + "TChildOf", + ], + ...(name === undefined ? {} : { name }), + signature: `${name ?? ""}()`, + parameters: [...(slots.parameters ?? [])], + localVariables: [...(slots.localVariables ?? [])], + parent, + } as Entity; +} + +export function structural(id: string, kind: string, name: string, parent: string): Entity { + return { id, kind, traits: ["TNamed", "TStructural", "TChildOf"], name, parent } as Entity; +} + +/** + * The hand-counted corpus of aggregate.test.ts: two packages, three types (one + * nested), a method with a parameter and a local, a conventional and an + * unnamed member, and two stubs whose names must never count. + */ +export function orderCorpus(): readonly Entity[] { + const order = "java:com.acme.order"; + const billing = "java:com.acme.order.billing"; + const Order = `${order}/Order`; + const getTotal = `${Order}.getTotal(Line)`; + const Line = `${Order}$Line`; + return [ + pkg(order, "com.acme.order"), + pkg(billing, "com.acme.order.billing", order), + cls(Order, "Order", order), + method(getTotal, "getTotal", Order, { + parameters: [`${getTotal}#param:orderLine`], + localVariables: [`${getTotal}#local:total`], + }), + structural(`${getTotal}#param:orderLine`, "parameter", "orderLine", getTotal), + structural(`${getTotal}#local:total`, "localVariable", "total", getTotal), + method(`${Order}.toString()`, "toString", Order), + method(`${Order}.()`, undefined, Order), + structural(`${Order}#lines`, "attribute", "lines", Order), + cls(Line, "Line", Order), + structural(`${Line}#qty`, "attribute", "qty", Line), + cls(`${billing}/Invoice`, "Invoice", billing), + pkg("java:java.lang", "java.lang", undefined, true), + cls("java:java.lang/String", "String", "java:java.lang", true), + ]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb7c213..0bd25a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,6 +373,9 @@ importers: packages/vocabulary: dependencies: + '@codegraph/analyzer': + specifier: workspace:* + version: link:../analyzer '@codegraph/core': specifier: workspace:* version: link:../core From 9df387bc248f4423c36153589ad68c57ade1a9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Grodziski?= Date: Sun, 25 Oct 2026 22:23:52 +0100 Subject: [PATCH 4/6] test(fixtures): vocabulary.json snapshots Each fixture's model now has its words committed beside it: `fixtures//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 --- fixtures/csharp/README.md | 27 +++ fixtures/csharp/expected/vocabulary.json | 159 ++++++++++++++++ fixtures/elixir/README.md | 25 +++ fixtures/elixir/expected/vocabulary.json | 164 ++++++++++++++++ fixtures/java/README.md | 22 +++ fixtures/java/expected/vocabulary.json | 123 ++++++++++++ fixtures/ruby/README.md | 21 +++ fixtures/ruby/expected/vocabulary.json | 186 +++++++++++++++++++ fixtures/typescript/README.md | 20 ++ fixtures/typescript/expected/vocabulary.json | 170 +++++++++++++++++ fixtures/unicode/README.md | 5 + fixtures/unicode/expected/vocabulary.json | 26 +++ packages/vocabulary/test/fixtures.test.ts | 71 +++++++ 13 files changed, 1019 insertions(+) create mode 100644 fixtures/csharp/README.md create mode 100644 fixtures/csharp/expected/vocabulary.json create mode 100644 fixtures/elixir/expected/vocabulary.json create mode 100644 fixtures/java/expected/vocabulary.json create mode 100644 fixtures/ruby/expected/vocabulary.json create mode 100644 fixtures/typescript/expected/vocabulary.json create mode 100644 fixtures/unicode/expected/vocabulary.json create mode 100644 packages/vocabulary/test/fixtures.test.ts diff --git a/fixtures/csharp/README.md b/fixtures/csharp/README.md new file mode 100644 index 0000000..ac46823 --- /dev/null +++ b/fixtures/csharp/README.md @@ -0,0 +1,27 @@ +# `fixtures/csharp` — the reference corpus for the C# extractor + +What this corpus pins, file by file, and how to reproduce its snapshot is in +[`docs/csharp-extractor.md`](../../docs/csharp-extractor.md). + +## The vocabulary: `order` and `priceable`, counted by hand + +`expected/vocabulary.json` is the words of this model's names +(`@codegraph/vocabulary`, PLAN.md §20), rebuilt byte for byte by +`pnpm --filter @codegraph/vocabulary test` (`CODEGRAPH_UPDATE_SNAPSHOTS=1` +rewrites it). A word weighs the role of the entity whose name carries it — +module and type 8, attribute 4, operation 3, parameter and local 1 — except a +word of the name's qualifier, before the last separator of a qualified kind, +which weighs 1. One word, counted by hand from the model and pinned by the +vocabulary's tests: + +| name | kind | role | weight | +|---|---|---|---| +| `Acme.Order` | namespace | module | 8 | +| `AbstractOrder`, `Order`, `OrderService` | class | type | 3 × 8 | +| `order` × 6 | parameter | parameter | 6 × 1 | +| `order` | localVariable | local | 1 | +| `Acme.Order.Adapter`, `Acme.Order.Extensions`, `Acme.Order.Legacy` | namespace | qualifier | 3 × 1 | + +**`order`: weight 42, 14 occurrences, 14 carriers.** The interface prefix is a +marker, not a word: `IPriceable` (type, 8) and the parameter `priceable` (1) +make **`priceable`: weight 9, 2 occurrences**, and no word `i`. diff --git a/fixtures/csharp/expected/vocabulary.json b/fixtures/csharp/expected/vocabulary.json new file mode 100644 index 0000000..78a6cb4 --- /dev/null +++ b/fixtures/csharp/expected/vocabulary.json @@ -0,0 +1,159 @@ +{ + "kind": "codegraph.vocabulary/1", + "generatedBy": "@codegraph/vocabulary", + "view": {"name":"all","filters":[]}, + "corpus": {"name":"src","roots":["fixtures/csharp/src"]}, + "langs": ["csharp"], + "weights": {"module":8,"type":8,"operation":3,"attribute":4,"parameter":1,"local":1,"other":1,"qualifier":1}, + "classes": ["domain","technical","keyword","short"], + "technical": {"words":125,"sha256":"c16bd05be59a0c3281696359e4b4f7b3efe7860512b4ab37519f8c3a063c95f7"}, + "terms": [ + {"text":"abstract","class":"keyword","modules":1,"types":1,"exclusive":1}, + {"text":"acme","class":"domain","modules":4,"types":0}, + {"text":"adapter","class":"technical","modules":1,"types":1,"exclusive":2}, + {"text":"add","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"allocator","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"amount","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"amounts","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"anything","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"apply","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"archive","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"attribute","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"audited","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"basket","class":"domain","modules":1,"types":3,"exclusive":1}, + {"text":"batch","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"best","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"bill","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"both","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"builder","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"cents","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"chain","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"channel","class":"domain","modules":1,"types":3,"exclusive":1}, + {"text":"check","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"converter","class":"domain","modules":1,"types":1,"exclusive":2}, + {"text":"count","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"counts","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"cursor","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"describe","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"discount","class":"domain","modules":1,"types":4,"exclusive":1}, + {"text":"discountable","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"document","class":"domain","modules":1,"types":1,"exclusive":2}, + {"text":"doubled","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"e","class":"short","modules":1,"types":1,"exclusive":1}, + {"text":"empty","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"ensure","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"exception","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"extensions","class":"technical","modules":1,"types":1,"exclusive":3}, + {"text":"factor","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"find","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"first","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"free","class":"domain","modules":2,"types":3}, + {"text":"get","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"guard","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"halved","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"handler","class":"technical","modules":1,"types":2,"exclusive":1}, + {"text":"head","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"index","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"invoice","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"is","class":"keyword","modules":1,"types":1,"exclusive":3}, + {"text":"item","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"items","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"join","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"largest","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"ledger","class":"domain","modules":2,"types":2}, + {"text":"left","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"legacy","class":"domain","modules":1,"types":0,"exclusive":4}, + {"text":"length","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"line","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"lines","class":"domain","modules":1,"types":3,"exclusive":1}, + {"text":"list","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"log","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"max","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"message","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"money","class":"domain","modules":2,"types":3}, + {"text":"naïve","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"next","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"notifications","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"notify","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"of","class":"technical","modules":1,"types":2,"exclusive":1}, + {"text":"on","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"order","class":"domain","modules":4,"types":5}, + {"text":"orders","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"part","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"parts","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"percent","class":"domain","modules":1,"types":3,"exclusive":1}, + {"text":"phone","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"plain","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"post","class":"domain","modules":1,"types":1,"exclusive":2}, + {"text":"predicate","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"price","class":"domain","modules":1,"types":4,"exclusive":1}, + {"text":"priceable","class":"domain","modules":2,"types":2}, + {"text":"priced","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"prices","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"quantity","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"reference","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"reporting","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"repository","class":"technical","modules":1,"types":2,"exclusive":1}, + {"text":"right","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"rounded","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"rows","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"résumé","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"save","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"second","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"serialize","class":"domain","modules":1,"types":1,"exclusive":2}, + {"text":"service","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"shipment","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"shipped","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"size","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"stock","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"store","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"subscribe","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"tag","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"tail","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"times","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"to","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"today","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"total","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"trail","class":"domain","modules":2,"types":3}, + {"text":"typed","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"value","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"values","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"verbose","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"web","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"zero","class":"domain","modules":2,"types":3} + ], + "scopes": [ + {"level":"corpus","name":"src","entities":210,"roles":{"module":4,"type":26,"operation":45,"attribute":38,"parameter":54,"local":9,"other":0},"skipped":{"unnamed":32,"conventional":2,"numeric":0},"distinct":113,"distinctDomain":95,"weight":707,"occurrences":202,"terms":[[69,42,14,14],[12,21,4,4],[20,18,5,5],[62,18,4,4],[27,17,4,4],[78,17,6,6],[2,16,2,2],[35,16,2,2],[85,16,2,2],[106,16,3,3],[39,15,4,4],[52,13,3,3],[105,12,3,3],[25,11,2,2],[57,11,3,3],[83,11,5,5],[95,11,3,3],[56,10,3,3],[112,10,3,3],[43,9,2,2],[44,9,3,3],[79,9,2,2],[0,8,1,1],[10,8,1,1],[11,8,1,1],[13,8,1,1],[22,8,1,1],[23,8,2,2],[28,8,1,1],[32,8,1,1],[34,8,1,1],[41,8,1,1],[45,8,2,2],[49,8,2,2],[54,8,1,1],[58,8,1,1],[65,8,1,1],[84,8,1,1],[89,8,1,1],[93,8,1,1],[94,8,1,1],[97,8,1,1],[47,7,2,2],[60,7,2,2],[5,6,3,3],[9,6,2,2],[26,6,2,2],[67,6,2,2],[18,5,2,2],[24,5,2,2],[81,5,2,2],[100,5,2,2],[101,5,2,2],[1,4,4,4],[38,4,2,2],[63,4,1,1],[68,4,1,1],[74,4,1,1],[75,4,1,1],[96,4,2,2],[98,4,1,1],[107,4,1,1],[110,4,1,1],[111,4,1,1],[3,3,1,1],[4,3,1,1],[8,3,1,1],[15,3,1,1],[16,3,1,1],[19,3,1,1],[21,3,1,1],[30,3,1,1],[33,3,1,1],[37,3,1,1],[40,3,1,1],[42,3,1,1],[50,3,1,1],[51,3,1,1],[55,3,1,1],[59,3,1,1],[64,3,1,1],[66,3,1,1],[73,3,3,3],[76,3,1,1],[80,3,1,1],[87,3,1,1],[90,3,1,1],[92,3,1,1],[99,3,1,1],[102,3,1,1],[103,3,1,1],[104,3,1,1],[29,2,2,2],[48,2,2,2],[61,2,2,2],[70,2,2,2],[82,2,2,2],[108,2,2,2],[109,2,2,2],[6,1,1,1],[7,1,1,1],[14,1,1,1],[17,1,1,1],[31,1,1,1],[36,1,1,1],[46,1,1,1],[53,1,1,1],[71,1,1,1],[72,1,1,1],[77,1,1,1],[86,1,1,1],[88,1,1,1],[91,1,1,1]]}, + {"level":"module","id":"csharp:Acme.Order","name":"Acme.Order","kind":"namespace","parent":0,"entities":183,"roles":{"module":1,"type":22,"operation":39,"attribute":33,"parameter":48,"local":9,"other":0},"skipped":{"unnamed":29,"conventional":2,"numeric":0},"distinct":99,"distinctDomain":84,"weight":570,"occurrences":167,"terms":[[69,39,11,11],[12,21,4,4],[20,18,5,5],[27,17,4,4],[78,17,6,6],[85,16,2,2],[39,12,3,3],[105,12,3,3],[25,11,2,2],[57,11,3,3],[83,11,5,5],[95,11,3,3],[56,10,3,3],[43,9,2,2],[62,9,2,2],[0,8,1,1],[10,8,1,1],[11,8,1,1],[13,8,1,1],[23,8,2,2],[28,8,1,1],[32,8,1,1],[34,8,1,1],[41,8,1,1],[45,8,2,2],[49,8,2,2],[65,8,1,1],[79,8,1,1],[84,8,1,1],[89,8,1,1],[93,8,1,1],[94,8,1,1],[97,8,1,1],[60,7,2,2],[5,6,3,3],[9,6,2,2],[26,6,2,2],[67,6,2,2],[112,6,2,2],[18,5,2,2],[24,5,2,2],[52,5,2,2],[81,5,2,2],[100,5,2,2],[38,4,2,2],[63,4,1,1],[68,4,1,1],[74,4,1,1],[75,4,1,1],[96,4,2,2],[98,4,1,1],[106,4,1,1],[107,4,1,1],[110,4,1,1],[111,4,1,1],[3,3,1,1],[4,3,1,1],[8,3,1,1],[15,3,1,1],[16,3,1,1],[19,3,1,1],[21,3,1,1],[33,3,1,1],[37,3,1,1],[40,3,1,1],[50,3,1,1],[51,3,1,1],[59,3,1,1],[64,3,1,1],[66,3,1,1],[73,3,3,3],[80,3,1,1],[87,3,1,1],[90,3,1,1],[99,3,1,1],[102,3,1,1],[103,3,1,1],[104,3,1,1],[48,2,2,2],[61,2,2,2],[70,2,2,2],[82,2,2,2],[108,2,2,2],[109,2,2,2],[1,1,1,1],[6,1,1,1],[7,1,1,1],[14,1,1,1],[17,1,1,1],[31,1,1,1],[36,1,1,1],[46,1,1,1],[53,1,1,1],[71,1,1,1],[72,1,1,1],[77,1,1,1],[86,1,1,1],[88,1,1,1],[91,1,1,1]]}, + {"level":"module","id":"csharp:Acme.Order.Adapter","name":"Acme.Order.Adapter","kind":"namespace","parent":0,"entities":10,"roles":{"module":1,"type":2,"operation":2,"attribute":1,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":8,"weight":54,"occurrences":12,"terms":[[2,16,2,2],[106,12,2,2],[22,8,1,1],[52,8,1,1],[76,3,1,1],[92,3,1,1],[29,2,2,2],[1,1,1,1],[69,1,1,1]]}, + {"level":"module","id":"csharp:Acme.Order.Extensions","name":"Acme.Order.Extensions","kind":"namespace","parent":0,"entities":8,"roles":{"module":1,"type":1,"operation":3,"attribute":1,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":10,"distinctDomain":8,"weight":48,"occurrences":13,"terms":[[35,16,2,2],[62,9,2,2],[47,7,2,2],[112,4,1,1],[30,3,1,1],[39,3,1,1],[42,3,1,1],[1,1,1,1],[69,1,1,1],[79,1,1,1]]}, + {"level":"module","id":"csharp:Acme.Order.Legacy","name":"Acme.Order.Legacy","kind":"namespace","parent":1,"entities":9,"roles":{"module":1,"type":1,"operation":1,"attribute":3,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":7,"weight":35,"occurrences":10,"terms":[[44,9,3,3],[54,8,1,1],[58,8,1,1],[101,5,2,2],[55,3,1,1],[1,1,1,1],[69,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order.Adapter/LedgerAdapter","name":"LedgerAdapter","kind":"class","parent":2,"entities":7,"roles":{"module":0,"type":1,"operation":2,"attribute":1,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":28,"occurrences":7,"terms":[[2,8,1,1],[52,8,1,1],[106,4,1,1],[76,3,1,1],[92,3,1,1],[29,2,2,2]]}, + {"level":"type","id":"csharp:Acme.Order.Adapter/TrailConverter","name":"TrailConverter","kind":"class","parent":2,"entities":2,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":16,"occurrences":2,"terms":[[22,8,1,1],[106,8,1,1]]}, + {"level":"type","id":"csharp:Acme.Order.Extensions/MoneyExtensions","name":"MoneyExtensions","kind":"class","parent":3,"entities":7,"roles":{"module":0,"type":1,"operation":3,"attribute":1,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":6,"weight":38,"occurrences":10,"terms":[[62,9,2,2],[35,8,1,1],[47,7,2,2],[112,4,1,1],[30,3,1,1],[39,3,1,1],[42,3,1,1],[79,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order.Legacy/List","name":"List","kind":"class","parent":4,"entities":8,"roles":{"module":0,"type":1,"operation":1,"attribute":3,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":25,"occurrences":7,"terms":[[44,9,3,3],[58,8,1,1],[101,5,2,2],[55,3,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/AbstractOrder","name":"AbstractOrder","kind":"class","parent":1,"entities":9,"roles":{"module":0,"type":1,"operation":2,"attribute":3,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":6,"weight":36,"occurrences":9,"terms":[[83,9,3,3],[0,8,1,1],[69,8,1,1],[105,4,1,1],[27,3,1,1],[78,3,1,1],[73,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/AuditedAttribute","name":"AuditedAttribute","kind":"class","parent":1,"entities":5,"roles":{"module":0,"type":1,"operation":0,"attribute":2,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":25,"occurrences":5,"terms":[[10,8,1,1],[11,8,1,1],[100,5,2,2],[110,4,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Basket","name":"Basket","kind":"class","parent":1,"entities":13,"roles":{"module":0,"type":1,"operation":2,"attribute":5,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":1,"numeric":0},"distinct":9,"distinctDomain":7,"weight":39,"occurrences":11,"terms":[[12,8,1,1],[105,8,2,2],[23,4,1,1],[45,4,1,1],[57,4,1,1],[3,3,1,1],[25,3,1,1],[40,3,1,1],[56,2,2,2]]}, + {"level":"type","id":"csharp:Acme.Order/Basket.Cursor","name":"Cursor","kind":"class","parent":11,"entities":6,"roles":{"module":0,"type":1,"operation":1,"attribute":2,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":20,"occurrences":5,"terms":[[25,8,1,1],[12,5,2,2],[45,4,1,1],[64,3,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Basket.Line","name":"Line","kind":"class","parent":11,"entities":4,"roles":{"module":0,"type":1,"operation":0,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":13,"occurrences":3,"terms":[[56,8,1,1],[5,5,2,2]]}, + {"level":"type","id":"csharp:Acme.Order/Basket.Line.Discount","name":"Discount","kind":"class","parent":13,"entities":4,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":15,"occurrences":4,"terms":[[27,8,1,1],[8,3,1,1],[103,3,1,1],[5,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Batch","name":"Batch","kind":"class","parent":1,"entities":13,"roles":{"module":0,"type":1,"operation":4,"attribute":2,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":8,"weight":32,"occurrences":11,"terms":[[13,8,1,1],[24,5,2,2],[81,5,2,2],[96,4,2,2],[4,3,1,1],[26,3,1,1],[80,3,1,1],[7,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Channel","name":"Channel","kind":"enum","parent":1,"entities":5,"roles":{"module":0,"type":1,"operation":0,"attribute":3,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":20,"occurrences":4,"terms":[[20,8,1,1],[74,4,1,1],[98,4,1,1],[111,4,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/EmptyBasketException","name":"EmptyBasketException","kind":"class","parent":1,"entities":3,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":25,"occurrences":4,"terms":[[12,8,1,1],[32,8,1,1],[34,8,1,1],[61,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/IDiscountable","name":"IDiscountable","kind":"interface","parent":1,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":3,"weight":12,"occurrences":3,"terms":[[28,8,1,1],[27,3,1,1],[73,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/IPriceable","name":"IPriceable","kind":"interface","parent":1,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":11,"occurrences":2,"terms":[[79,8,1,1],[78,3,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Money","name":"Money","kind":"record","parent":1,"entities":12,"roles":{"module":0,"type":1,"operation":3,"attribute":1,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":1,"numeric":0},"distinct":8,"distinctDomain":8,"weight":25,"occurrences":9,"terms":[[62,8,1,1],[18,5,2,2],[78,3,1,1],[102,3,1,1],[112,3,1,1],[36,1,1,1],[53,1,1,1],[86,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Notifications","name":"Notifications","kind":"class","parent":1,"entities":25,"roles":{"module":0,"type":1,"operation":7,"attribute":2,"parameter":8,"local":1,"other":0},"skipped":{"unnamed":6,"conventional":0,"numeric":0},"distinct":16,"distinctDomain":13,"weight":56,"occurrences":22,"terms":[[95,11,3,3],[65,8,1,1],[39,4,2,2],[68,4,1,1],[69,4,4,4],[16,3,1,1],[19,3,1,1],[59,3,1,1],[66,3,1,1],[67,3,1,1],[99,3,1,1],[112,3,1,1],[38,1,1,1],[43,1,1,1],[61,1,1,1],[91,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Notifications.Free","name":"Free","kind":"class","parent":21,"entities":5,"roles":{"module":0,"type":1,"operation":1,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":16,"occurrences":4,"terms":[[39,8,1,1],[78,8,3,3]]}, + {"level":"type","id":"csharp:Acme.Order/Order","name":"Order","kind":"class","parent":1,"entities":12,"roles":{"module":0,"type":1,"operation":1,"attribute":4,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":8,"weight":35,"occurrences":11,"terms":[[20,9,3,3],[69,8,1,1],[57,4,1,1],[60,4,1,1],[106,4,1,1],[27,3,1,1],[83,2,2,2],[73,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/OrderService","name":"OrderService","kind":"class","parent":1,"entities":14,"roles":{"module":0,"type":1,"operation":3,"attribute":3,"parameter":4,"local":2,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":8,"weight":43,"occurrences":14,"terms":[[69,10,3,3],[93,8,1,1],[9,6,2,2],[52,5,2,2],[75,4,1,1],[107,4,1,1],[15,3,1,1],[70,2,2,2],[46,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Reporting","name":"Reporting","kind":"class","parent":1,"entities":23,"roles":{"module":0,"type":1,"operation":8,"attribute":0,"parameter":8,"local":4,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":20,"distinctDomain":18,"weight":47,"occurrences":22,"terms":[[84,8,1,1],[26,3,1,1],[38,3,1,1],[50,3,1,1],[51,3,1,1],[57,3,1,1],[60,3,1,1],[67,3,1,1],[87,3,1,1],[104,3,1,1],[108,2,2,2],[109,2,2,2],[6,1,1,1],[14,1,1,1],[17,1,1,1],[20,1,1,1],[62,1,1,1],[71,1,1,1],[72,1,1,1],[88,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Repository","name":"Repository","kind":"class","parent":1,"entities":4,"roles":{"module":0,"type":1,"operation":0,"attribute":2,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":2,"weight":16,"occurrences":3,"terms":[[85,8,1,1],[23,4,1,1],[49,4,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Repository`1","name":"Repository","kind":"class","parent":1,"entities":8,"roles":{"module":0,"type":1,"operation":2,"attribute":1,"parameter":2,"local":1,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":3,"weight":21,"occurrences":7,"terms":[[85,8,1,1],[49,4,1,1],[37,3,1,1],[90,3,1,1],[48,2,2,2],[77,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/Résumé","name":"Résumé","kind":"class","parent":1,"entities":3,"roles":{"module":0,"type":1,"operation":0,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":12,"occurrences":2,"terms":[[89,8,1,1],[63,4,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/ShipmentHandler","name":"ShipmentHandler","kind":"delegate","parent":1,"entities":2,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":2,"weight":17,"occurrences":3,"terms":[[43,8,1,1],[94,8,1,1],[69,1,1,1]]}, + {"level":"type","id":"csharp:Acme.Order/StockGuard","name":"StockGuard","kind":"class","parent":1,"entities":7,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":2,"local":1,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":25,"occurrences":7,"terms":[[41,8,1,1],[97,8,1,1],[21,3,1,1],[33,3,1,1],[82,2,2,2],[31,1,1,1]]} + ], + "diagnostics": {"unplaced":[],"withoutLexicon":[]} +} diff --git a/fixtures/elixir/README.md b/fixtures/elixir/README.md index acf99c2..b6472c8 100644 --- a/fixtures/elixir/README.md +++ b/fixtures/elixir/README.md @@ -57,3 +57,28 @@ must skip it, count it, and still write its file record. | `lib/acme_order/two_modules.ex` | Two `defmodule`s in one file, both children of it; `@channels` is an attribute whose value is an array of unevaluated atoms; `&Atom.to_string/1` a reference to `/Atom`; `alias AcmeOrder.Channel` inside the second names this file — a self-edge, dropped. | | | `lib/acme_order/promo#2024.ex` | A `#` in a file name → `promo%232024.ex` in the key, the written path in `name`; `%Money{money \| amount: …}` from another module: a READ of `Money.amount` in the head and a WRITE in the update. | `#` is a reserved separator of the rendered id; cross-module field accesses are what the navigator lists. | | `test/acme_order/order_test.exs` | `use ExUnit.Case` → `/ExUnit.Case`; the `test` blocks are module-level code, so their calls (`Order.create/2`, `Order.total/1`, `Money.zero/0`) are invocations FROM the test module; `%Line{…}` is a reference to `Line` whose fields are unbound (Ecto-injected). | Tests are corpus; a `test` macro's body is compile-time code of the module. | + +## The vocabulary: `pricing`, counted by hand + +`expected/vocabulary.json` is the words of this model's names +(`@codegraph/vocabulary`, PLAN.md §20), rebuilt byte for byte by +`pnpm --filter @codegraph/vocabulary test` (`CODEGRAPH_UPDATE_SNAPSHOTS=1` +rewrites it). A word weighs the role of the entity whose name carries it — +module and type 8, attribute 4, operation 3, parameter and local 1 — except a +word of the name's qualifier, before the last separator of a qualified kind, +which weighs 1. One word, counted by hand from the model and pinned by the +vocabulary's tests: + +| name | kind | role | weight | +|---|---|---|---| +| `lib/acme_order/pricing.ex` | file | module | 8 | +| `AcmeOrder.Pricing` | module | type | 8 | +| `pricing` (`AcmeOrder.Order`'s struct field) | field | attribute | 4 | +| `pricing_for` | function | operation | 3 | +| `pricing` | parameter | parameter | 1 | +| `lib/acme_order/pricing/premium.ex`, `…/standard.ex` | file | qualifier | 2 × 1 | +| `AcmeOrder.Pricing.Premium`, `AcmeOrder.Pricing.Standard` | module | qualifier | 2 × 1 | + +**`pricing`: weight 28, 9 occurrences, 9 carriers.** A module's dots and a +file's slashes both qualify: `AcmeOrder.Pricing.Premium` speaks `premium` at 8 +and `acme`, `order`, `pricing` at 1. diff --git a/fixtures/elixir/expected/vocabulary.json b/fixtures/elixir/expected/vocabulary.json new file mode 100644 index 0000000..1ac5a64 --- /dev/null +++ b/fixtures/elixir/expected/vocabulary.json @@ -0,0 +1,164 @@ +{ + "kind": "codegraph.vocabulary/1", + "generatedBy": "@codegraph/vocabulary", + "view": {"name":"all","filters":[]}, + "corpus": {"name":"src","roots":["fixtures/elixir/src"]}, + "langs": ["ex"], + "weights": {"module":8,"type":8,"operation":3,"attribute":4,"parameter":1,"local":1,"other":1,"qualifier":1}, + "classes": ["domain","technical","keyword","short"], + "technical": {"words":125,"sha256":"c16bd05be59a0c3281696359e4b4f7b3efe7860512b4ab37519f8c3a063c95f7"}, + "terms": [ + {"text":"a","class":"short","modules":1,"types":1,"exclusive":8}, + {"text":"acme","class":"domain","modules":21,"types":27}, + {"text":"add","class":"technical","modules":2,"types":2}, + {"text":"all","class":"domain","modules":1,"types":1,"exclusive":20}, + {"text":"amount","class":"domain","modules":1,"types":1,"exclusive":8}, + {"text":"any","class":"domain","modules":1,"types":1,"exclusive":11}, + {"text":"application","class":"domain","modules":2,"types":2}, + {"text":"apply","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"args","class":"technical","modules":1,"types":1,"exclusive":3}, + {"text":"assert","class":"domain","modules":1,"types":1,"exclusive":7}, + {"text":"atom","class":"domain","modules":1,"types":0,"exclusive":5}, + {"text":"attrs","class":"technical","modules":1,"types":1,"exclusive":18}, + {"text":"available","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"b","class":"short","modules":1,"types":1,"exclusive":8}, + {"text":"broken","class":"domain","modules":1,"types":0,"exclusive":6}, + {"text":"call","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"changeset","class":"technical","modules":1,"types":1,"exclusive":18}, + {"text":"channel","class":"domain","modules":4,"types":5}, + {"text":"channels","class":"domain","modules":1,"types":1,"exclusive":20}, + {"text":"chars","class":"domain","modules":1,"types":1,"exclusive":8}, + {"text":"config","class":"technical","modules":1,"types":0,"exclusive":1}, + {"text":"create","class":"technical","modules":1,"types":1,"exclusive":10}, + {"text":"currency","class":"domain","modules":2,"types":5}, + {"text":"deliver","class":"domain","modules":1,"types":2,"exclusive":9}, + {"text":"deps","class":"domain","modules":1,"types":1,"exclusive":21}, + {"text":"describe","class":"domain","modules":2,"types":2}, + {"text":"discount","class":"domain","modules":2,"types":2}, + {"text":"dynamic","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"email","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"errors","class":"technical","modules":1,"types":2,"exclusive":4}, + {"text":"fail","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"for","class":"technical","modules":2,"types":2}, + {"text":"fun","class":"domain","modules":2,"types":2}, + {"text":"gateway","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"hello","class":"domain","modules":1,"types":1,"exclusive":5}, + {"text":"helper","class":"technical","modules":1,"types":0,"exclusive":23}, + {"text":"is","class":"technical","modules":1,"types":1,"exclusive":7}, + {"text":"item","class":"domain","modules":2,"types":2}, + {"text":"legacy","class":"domain","modules":2,"types":1}, + {"text":"length","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"lib","class":"technical","modules":19,"types":0}, + {"text":"line","class":"domain","modules":4,"types":4}, + {"text":"lines","class":"domain","modules":2,"types":2}, + {"text":"list","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"macros","class":"domain","modules":1,"types":1,"exclusive":7}, + {"text":"many","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"max","class":"domain","modules":2,"types":2}, + {"text":"message","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"mix","class":"technical","modules":1,"types":1,"exclusive":21}, + {"text":"mod","class":"domain","modules":2,"types":2}, + {"text":"module","class":"domain","modules":1,"types":0,"exclusive":5}, + {"text":"modules","class":"domain","modules":1,"types":0,"exclusive":20}, + {"text":"money","class":"domain","modules":4,"types":5}, + {"text":"name","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"names","class":"domain","modules":1,"types":1,"exclusive":20}, + {"text":"new","class":"technical","modules":1,"types":1,"exclusive":8}, + {"text":"notifier","class":"domain","modules":1,"types":3,"exclusive":9}, + {"text":"notify","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"of","class":"technical","modules":1,"types":1,"exclusive":17}, + {"text":"opts","class":"technical","modules":4,"types":4}, + {"text":"order","class":"domain","modules":21,"types":27}, + {"text":"peek","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"pid","class":"technical","modules":2,"types":2}, + {"text":"premium","class":"domain","modules":1,"types":1,"exclusive":12}, + {"text":"price","class":"domain","modules":6,"types":9}, + {"text":"priceable","class":"domain","modules":1,"types":4,"exclusive":11}, + {"text":"pricing","class":"domain","modules":5,"types":5}, + {"text":"project","class":"domain","modules":1,"types":1,"exclusive":21}, + {"text":"promo","class":"domain","modules":1,"types":0,"exclusive":15}, + {"text":"promo2024","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"qty","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"reason","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"reference","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"release","class":"domain","modules":2,"types":2}, + {"text":"repo","class":"technical","modules":1,"types":1,"exclusive":16}, + {"text":"reporting","class":"domain","modules":1,"types":1,"exclusive":17}, + {"text":"reserve","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"rethrow","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"schema","class":"technical","modules":1,"types":0,"exclusive":18}, + {"text":"send","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"sku","class":"domain","modules":3,"types":3}, + {"text":"sms","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"snapshot","class":"domain","modules":1,"types":1,"exclusive":17}, + {"text":"standard","class":"domain","modules":1,"types":1,"exclusive":13}, + {"text":"start","class":"domain","modules":1,"types":1,"exclusive":2}, + {"text":"state","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"stock","class":"domain","modules":2,"types":2}, + {"text":"string","class":"domain","modules":1,"types":1,"exclusive":8}, + {"text":"table","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"test","class":"technical","modules":2,"types":1}, + {"text":"text","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"too","class":"domain","modules":1,"types":1,"exclusive":4}, + {"text":"total","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"two","class":"domain","modules":1,"types":0,"exclusive":20}, + {"text":"value","class":"domain","modules":1,"types":1,"exclusive":7}, + {"text":"with","class":"technical","modules":1,"types":1,"exclusive":3}, + {"text":"zero","class":"domain","modules":1,"types":1,"exclusive":8} + ], + "scopes": [ + {"level":"corpus","name":"src","entities":181,"roles":{"module":23,"type":28,"operation":50,"attribute":11,"parameter":49,"local":0,"other":0},"skipped":{"unnamed":14,"conventional":6,"numeric":1},"distinct":97,"distinctDomain":75,"weight":887,"occurrences":318,"terms":[[60,99,62,55],[1,49,49,47],[52,35,7,7],[66,28,9,9],[64,27,9,9],[89,26,5,3],[41,23,7,7],[17,20,6,6],[6,19,3,3],[40,19,19,19],[65,19,5,5],[86,19,3,3],[56,18,4,4],[22,17,6,6],[29,17,3,3],[27,16,2,2],[42,16,3,3],[44,16,2,2],[48,16,2,2],[63,16,2,2],[74,16,2,2],[75,16,2,2],[83,16,2,2],[54,11,2,2],[67,11,2,2],[20,9,2,1],[49,9,2,2],[5,8,1,1],[10,8,1,1],[14,8,1,1],[28,8,1,1],[33,8,1,1],[35,8,1,1],[45,8,1,1],[46,8,2,2],[50,8,1,1],[51,8,1,1],[68,8,1,1],[69,8,1,1],[80,8,6,6],[91,8,1,1],[93,8,1,1],[47,7,2,2],[2,6,2,2],[23,6,2,2],[25,6,2,2],[26,6,2,2],[31,6,2,2],[73,6,2,2],[4,5,2,2],[72,5,2,2],[18,4,1,1],[59,4,4,4],[88,4,1,1],[3,3,1,1],[7,3,1,1],[9,3,1,1],[12,3,1,1],[15,3,1,1],[16,3,1,1],[21,3,1,1],[24,3,1,1],[30,3,1,1],[34,3,1,1],[36,3,1,1],[37,3,3,3],[38,3,3,3],[39,3,1,1],[55,3,1,1],[57,3,1,1],[58,3,1,1],[61,3,1,1],[76,3,1,1],[77,3,1,1],[79,3,1,1],[82,3,1,1],[84,3,1,1],[92,3,1,1],[95,3,1,1],[96,3,1,1],[32,2,2,2],[62,2,2,2],[85,2,2,2],[94,2,2,2],[0,1,1,1],[8,1,1,1],[11,1,1,1],[13,1,1,1],[19,1,1,1],[43,1,1,1],[53,1,1,1],[70,1,1,1],[71,1,1,1],[78,1,1,1],[81,1,1,1],[87,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ex:config%2Fconfig.exs","name":"config/config.exs","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":1,"distinctDomain":0,"weight":9,"occurrences":2,"terms":[[20,9,2,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fapplication.ex","name":"lib/acme_order/application.ex","kind":"file","parent":0,"entities":5,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":4,"weight":24,"occurrences":8,"terms":[[6,16,2,2],[84,3,1,1],[1,2,2,2],[60,2,2,2],[40,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fdynamic.ex","name":"lib/acme_order/dynamic.ex","kind":"file","parent":0,"entities":14,"roles":{"module":1,"type":1,"operation":4,"attribute":0,"parameter":8,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":18,"distinctDomain":13,"weight":50,"occurrences":22,"terms":[[27,16,2,2],[66,4,2,2],[15,3,1,1],[31,3,1,1],[64,3,1,1],[73,3,1,1],[79,3,1,1],[95,3,1,1],[1,2,2,2],[60,2,2,2],[8,1,1,1],[32,1,1,1],[40,1,1,1],[41,1,1,1],[49,1,1,1],[53,1,1,1],[62,1,1,1],[80,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Ferrors.ex","name":"lib/acme_order/errors.ex","kind":"file","parent":0,"entities":11,"roles":{"module":1,"type":2,"operation":3,"attribute":2,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":13,"distinctDomain":11,"weight":67,"occurrences":20,"terms":[[29,17,3,3],[42,8,1,1],[45,8,1,1],[91,8,1,1],[47,7,2,2],[46,4,1,1],[1,3,3,3],[30,3,1,1],[60,3,3,3],[77,3,1,1],[32,1,1,1],[40,1,1,1],[71,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Flegacy%2Fatom_module.ex","name":"lib/acme_order/legacy/atom_module.ex","kind":"file","parent":0,"entities":3,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":7,"weight":32,"occurrences":9,"terms":[[10,8,1,1],[49,8,1,1],[50,8,1,1],[34,3,1,1],[38,2,2,2],[1,1,1,1],[40,1,1,1],[60,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Flegacy%2Fbroken.ex","name":"lib/acme_order/legacy/broken.ex","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":4,"weight":12,"occurrences":5,"terms":[[14,8,1,1],[1,1,1,1],[38,1,1,1],[40,1,1,1],[60,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fmacros.ex","name":"lib/acme_order/macros.ex","kind":"file","parent":0,"entities":8,"roles":{"module":1,"type":1,"operation":2,"attribute":0,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":9,"distinctDomain":6,"weight":36,"occurrences":14,"terms":[[44,16,2,2],[17,6,2,2],[9,3,1,1],[36,3,1,1],[1,2,2,2],[60,2,2,2],[94,2,2,2],[40,1,1,1],[59,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fmoney.ex","name":"lib/acme_order/money.ex","kind":"file","parent":0,"entities":14,"roles":{"module":1,"type":2,"operation":3,"attribute":2,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":1,"numeric":0},"distinct":13,"distinctDomain":8,"weight":54,"occurrences":21,"terms":[[52,24,3,3],[4,5,2,2],[22,5,2,2],[1,3,3,3],[2,3,1,1],[55,3,1,1],[60,3,3,3],[96,3,1,1],[0,1,1,1],[13,1,1,1],[19,1,1,1],[40,1,1,1],[87,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fnotifier.ex","name":"lib/acme_order/notifier.ex","kind":"file","parent":0,"entities":12,"roles":{"module":1,"type":3,"operation":4,"attribute":0,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":12,"distinctDomain":11,"weight":60,"occurrences":24,"terms":[[56,18,4,4],[28,8,1,1],[33,8,1,1],[23,6,2,2],[60,6,6,6],[1,4,4,4],[39,3,1,1],[57,3,1,1],[40,1,1,1],[43,1,1,1],[81,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Forder.ex","name":"lib/acme_order/order.ex","kind":"file","parent":0,"entities":17,"roles":{"module":1,"type":1,"operation":4,"attribute":5,"parameter":6,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":14,"distinctDomain":10,"weight":66,"occurrences":24,"terms":[[60,21,7,5],[42,8,2,2],[72,5,2,2],[17,4,1,1],[41,4,2,2],[46,4,1,1],[66,4,1,1],[2,3,1,1],[21,3,1,1],[25,3,1,1],[92,3,1,1],[1,2,2,2],[40,1,1,1],[59,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fpriceable.ex","name":"lib/acme_order/priceable.ex","kind":"file","parent":0,"entities":21,"roles":{"module":1,"type":4,"operation":8,"attribute":0,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":4,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":8,"weight":86,"occurrences":35,"terms":[[65,19,5,5],[60,16,9,6],[22,12,4,4],[64,12,4,4],[52,9,2,2],[5,8,1,1],[1,7,7,5],[37,2,2,2],[40,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fpricing%2Fpremium.ex","name":"lib/acme_order/pricing/premium.ex","kind":"file","parent":0,"entities":5,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":7,"weight":28,"occurrences":12,"terms":[[63,16,2,2],[64,3,1,1],[1,2,2,2],[60,2,2,2],[66,2,2,2],[17,1,1,1],[40,1,1,1],[41,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fpricing%2Fstandard.ex","name":"lib/acme_order/pricing/standard.ex","kind":"file","parent":0,"entities":8,"roles":{"module":1,"type":1,"operation":2,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":7,"weight":31,"occurrences":13,"terms":[[83,16,2,2],[26,3,1,1],[64,3,1,1],[1,2,2,2],[60,2,2,2],[66,2,2,2],[40,1,1,1],[52,1,1,1],[59,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fpricing.ex","name":"lib/acme_order/pricing.ex","kind":"file","parent":0,"entities":4,"roles":{"module":1,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":27,"occurrences":9,"terms":[[66,16,2,2],[26,3,1,1],[64,3,1,1],[1,2,2,2],[60,2,2,2],[40,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fpromo%232024.ex","name":"lib/acme_order/promo#2024.ex","kind":"file","parent":0,"entities":4,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":1},"distinct":7,"distinctDomain":6,"weight":25,"occurrences":9,"terms":[[68,8,1,1],[69,8,1,1],[7,3,1,1],[1,2,2,2],[60,2,2,2],[40,1,1,1],[52,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Frepo.ex","name":"lib/acme_order/repo.ex","kind":"file","parent":0,"entities":2,"roles":{"module":1,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":2,"weight":21,"occurrences":7,"terms":[[74,16,2,2],[1,2,2,2],[60,2,2,2],[40,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Freporting.ex","name":"lib/acme_order/reporting.ex","kind":"file","parent":0,"entities":7,"roles":{"module":1,"type":1,"operation":3,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":10,"distinctDomain":8,"weight":41,"occurrences":15,"terms":[[75,16,2,2],[60,6,4,4],[25,3,1,1],[58,3,1,1],[64,3,1,1],[82,3,1,1],[86,3,1,1],[1,2,2,2],[37,1,1,1],[40,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fschema%2Fline.ex","name":"lib/acme_order/schema/line.ex","kind":"file","parent":0,"entities":7,"roles":{"module":1,"type":1,"operation":2,"attribute":0,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":4,"weight":34,"occurrences":14,"terms":[[41,17,3,3],[80,4,2,2],[16,3,1,1],[31,3,1,1],[1,2,2,2],[60,2,2,2],[11,1,1,1],[40,1,1,1],[78,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Fstock.ex","name":"lib/acme_order/stock.ex","kind":"file","parent":0,"entities":23,"roles":{"module":1,"type":1,"operation":4,"attribute":1,"parameter":8,"local":0,"other":0},"skipped":{"unnamed":4,"conventional":4,"numeric":0},"distinct":14,"distinctDomain":11,"weight":45,"occurrences":20,"terms":[[86,16,2,2],[88,4,1,1],[12,3,1,1],[61,3,1,1],[73,3,1,1],[76,3,1,1],[80,3,3,3],[1,2,2,2],[60,2,2,2],[85,2,2,2],[40,1,1,1],[59,1,1,1],[62,1,1,1],[70,1,1,1]]}, + {"level":"module","id":"ex:lib%2Facme_order%2Ftwo_modules.ex","name":"lib/acme_order/two_modules.ex","kind":"file","parent":0,"entities":6,"roles":{"module":1,"type":2,"operation":2,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":8,"weight":50,"occurrences":15,"terms":[[54,11,2,2],[17,9,2,2],[51,8,1,1],[93,8,1,1],[18,4,1,1],[1,3,3,3],[3,3,1,1],[60,3,3,3],[40,1,1,1]]}, + {"level":"module","id":"ex:mix.exs","name":"mix.exs","kind":"file","parent":0,"entities":5,"roles":{"module":1,"type":1,"operation":3,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":35,"occurrences":8,"terms":[[48,16,2,2],[67,11,2,2],[6,3,1,1],[24,3,1,1],[1,1,1,1],[60,1,1,1]]}, + {"level":"module","id":"ex:test%2Facme_order%2Forder_test.exs","name":"test/acme_order/order_test.exs","kind":"file","parent":0,"entities":2,"roles":{"module":1,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":2,"weight":37,"occurrences":9,"terms":[[60,18,4,2],[89,17,3,2],[1,2,2,2]]}, + {"level":"module","id":"ex:test%2Ftest_helper.exs","name":"test/test_helper.exs","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":0,"weight":17,"occurrences":3,"terms":[[89,9,2,1],[35,8,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fapplication.ex/AcmeOrder%2EApplication","name":"AcmeOrder.Application","kind":"module","parent":2,"entities":4,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":13,"occurrences":4,"terms":[[6,8,1,1],[84,3,1,1],[1,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fdynamic.ex/AcmeOrder%2EDynamic","name":"AcmeOrder.Dynamic","kind":"module","parent":3,"entities":13,"roles":{"module":0,"type":1,"operation":4,"attribute":0,"parameter":8,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":17,"distinctDomain":13,"weight":39,"occurrences":18,"terms":[[27,8,1,1],[66,4,2,2],[15,3,1,1],[31,3,1,1],[64,3,1,1],[73,3,1,1],[79,3,1,1],[95,3,1,1],[1,1,1,1],[8,1,1,1],[32,1,1,1],[41,1,1,1],[49,1,1,1],[53,1,1,1],[60,1,1,1],[62,1,1,1],[80,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Ferrors.ex/AcmeOrder%2EErrors","name":"AcmeOrder.Errors","kind":"module","parent":4,"entities":5,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":6,"weight":18,"occurrences":7,"terms":[[29,8,1,1],[30,3,1,1],[77,3,1,1],[1,1,1,1],[32,1,1,1],[60,1,1,1],[71,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Ferrors.ex/AcmeOrder%2EErrors%2ETooManyLines","name":"AcmeOrder.Errors.TooManyLines","kind":"module","parent":4,"entities":5,"roles":{"module":0,"type":1,"operation":1,"attribute":2,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":7,"weight":38,"occurrences":9,"terms":[[42,8,1,1],[45,8,1,1],[91,8,1,1],[47,7,2,2],[46,4,1,1],[1,1,1,1],[29,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Flegacy%2Fatom_module.ex/legacy%2Emod","name":"legacy.mod","kind":"module","parent":5,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":3,"weight":12,"occurrences":3,"terms":[[49,8,1,1],[34,3,1,1],[38,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fmacros.ex/AcmeOrder%2EMacros","name":"AcmeOrder.Macros","kind":"module","parent":7,"entities":7,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":8,"distinctDomain":6,"weight":25,"occurrences":10,"terms":[[44,8,1,1],[17,6,2,2],[9,3,1,1],[36,3,1,1],[94,2,2,2],[1,1,1,1],[59,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fmoney.ex/AcmeOrder%2EMoney","name":"AcmeOrder.Money","kind":"module","parent":8,"entities":10,"roles":{"module":0,"type":1,"operation":3,"attribute":2,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":10,"distinctDomain":6,"weight":31,"occurrences":12,"terms":[[52,8,1,1],[4,5,2,2],[22,5,2,2],[2,3,1,1],[55,3,1,1],[96,3,1,1],[0,1,1,1],[1,1,1,1],[13,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fmoney.ex/String%2EChars%2EAcmeOrder%2EMoney","name":"String.Chars.AcmeOrder.Money","kind":"module","parent":8,"entities":3,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":1,"numeric":0},"distinct":5,"distinctDomain":5,"weight":12,"occurrences":5,"terms":[[52,8,1,1],[1,1,1,1],[19,1,1,1],[60,1,1,1],[87,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fnotifier.ex/AcmeOrder%2ENotifier","name":"AcmeOrder.Notifier","kind":"module","parent":9,"entities":5,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":6,"weight":18,"occurrences":7,"terms":[[56,8,1,1],[39,3,1,1],[57,3,1,1],[60,2,2,2],[1,1,1,1],[43,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fnotifier.ex/AcmeOrder%2ENotifier%2EEmail","name":"AcmeOrder.Notifier.Email","kind":"module","parent":9,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":15,"occurrences":6,"terms":[[28,8,1,1],[23,3,1,1],[60,2,2,2],[1,1,1,1],[56,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fnotifier.ex/AcmeOrder%2ENotifier%2ESms%2EGateway","name":"AcmeOrder.Notifier.Sms.Gateway","kind":"module","parent":9,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":7,"weight":16,"occurrences":7,"terms":[[33,8,1,1],[23,3,1,1],[1,1,1,1],[56,1,1,1],[60,1,1,1],[81,1,1,1],[90,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Forder.ex/AcmeOrder%2EOrder","name":"AcmeOrder.Order","kind":"module","parent":10,"entities":16,"roles":{"module":0,"type":1,"operation":4,"attribute":5,"parameter":6,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":13,"distinctDomain":10,"weight":55,"occurrences":20,"terms":[[60,12,5,4],[42,8,2,2],[72,5,2,2],[17,4,1,1],[41,4,2,2],[46,4,1,1],[66,4,1,1],[2,3,1,1],[21,3,1,1],[25,3,1,1],[92,3,1,1],[1,1,1,1],[59,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fpriceable.ex/AcmeOrder%2EPriceable","name":"AcmeOrder.Priceable","kind":"protocol","parent":11,"entities":5,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":6,"weight":18,"occurrences":7,"terms":[[65,8,1,1],[22,3,1,1],[64,3,1,1],[37,2,2,2],[1,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fpriceable.ex/AcmeOrder%2EPriceable%2EAcmeOrder%2EMoney","name":"AcmeOrder.Priceable.AcmeOrder.Money","kind":"module","parent":11,"entities":5,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":6,"weight":20,"occurrences":9,"terms":[[52,9,2,2],[22,3,1,1],[64,3,1,1],[1,2,2,1],[60,2,2,1],[65,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fpriceable.ex/AcmeOrder%2EPriceable%2EAcmeOrder%2EOrder","name":"AcmeOrder.Priceable.AcmeOrder.Order","kind":"module","parent":11,"entities":5,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":20,"occurrences":9,"terms":[[60,11,4,2],[22,3,1,1],[64,3,1,1],[1,2,2,1],[65,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fpriceable.ex/AcmeOrder%2EPriceable%2EAny","name":"AcmeOrder.Priceable.Any","kind":"module","parent":11,"entities":5,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":6,"weight":17,"occurrences":6,"terms":[[5,8,1,1],[22,3,1,1],[64,3,1,1],[1,1,1,1],[60,1,1,1],[65,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fpricing%2Fpremium.ex/AcmeOrder%2EPricing%2EPremium","name":"AcmeOrder.Pricing.Premium","kind":"module","parent":12,"entities":4,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":7,"weight":16,"occurrences":7,"terms":[[63,8,1,1],[64,3,1,1],[1,1,1,1],[17,1,1,1],[41,1,1,1],[60,1,1,1],[66,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fpricing%2Fstandard.ex/AcmeOrder%2EPricing%2EStandard","name":"AcmeOrder.Pricing.Standard","kind":"module","parent":13,"entities":7,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":7,"weight":19,"occurrences":8,"terms":[[83,8,1,1],[26,3,1,1],[64,3,1,1],[1,1,1,1],[52,1,1,1],[59,1,1,1],[60,1,1,1],[66,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fpricing.ex/AcmeOrder%2EPricing","name":"AcmeOrder.Pricing","kind":"module","parent":14,"entities":3,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":16,"occurrences":5,"terms":[[66,8,1,1],[26,3,1,1],[64,3,1,1],[1,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fpromo%232024.ex/AcmeOrder%2EPromo2024","name":"AcmeOrder.Promo2024","kind":"module","parent":15,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":14,"occurrences":5,"terms":[[69,8,1,1],[7,3,1,1],[1,1,1,1],[52,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Frepo.ex/AcmeOrder%2ERepo","name":"AcmeOrder.Repo","kind":"module","parent":16,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":2,"weight":10,"occurrences":3,"terms":[[74,8,1,1],[1,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Freporting.ex/AcmeOrder%2EReporting","name":"AcmeOrder.Reporting","kind":"module","parent":17,"entities":6,"roles":{"module":0,"type":1,"operation":3,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":8,"weight":30,"occurrences":11,"terms":[[75,8,1,1],[60,5,3,3],[25,3,1,1],[58,3,1,1],[64,3,1,1],[82,3,1,1],[86,3,1,1],[1,1,1,1],[37,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fschema%2Fline.ex/AcmeOrder%2ELine","name":"AcmeOrder.Line","kind":"module","parent":18,"entities":6,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":4,"weight":22,"occurrences":9,"terms":[[41,9,2,2],[80,4,2,2],[16,3,1,1],[31,3,1,1],[1,1,1,1],[11,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Fstock.ex/AcmeOrder%2EStock","name":"AcmeOrder.Stock","kind":"module","parent":19,"entities":22,"roles":{"module":0,"type":1,"operation":4,"attribute":1,"parameter":8,"local":0,"other":0},"skipped":{"unnamed":4,"conventional":4,"numeric":0},"distinct":13,"distinctDomain":11,"weight":34,"occurrences":16,"terms":[[86,8,1,1],[88,4,1,1],[12,3,1,1],[61,3,1,1],[73,3,1,1],[76,3,1,1],[80,3,3,3],[85,2,2,2],[1,1,1,1],[59,1,1,1],[60,1,1,1],[62,1,1,1],[70,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Ftwo_modules.ex/AcmeOrder%2EChannel","name":"AcmeOrder.Channel","kind":"module","parent":20,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":17,"occurrences":5,"terms":[[17,8,1,1],[18,4,1,1],[3,3,1,1],[1,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:lib%2Facme_order%2Ftwo_modules.ex/AcmeOrder%2EChannel%2ENames","name":"AcmeOrder.Channel.Names","kind":"module","parent":20,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":14,"occurrences":5,"terms":[[54,11,2,2],[1,1,1,1],[17,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:mix.exs/AcmeOrder%2EMixProject","name":"AcmeOrder.MixProject","kind":"module","parent":21,"entities":4,"roles":{"module":0,"type":1,"operation":3,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":27,"occurrences":7,"terms":[[67,11,2,2],[48,8,1,1],[6,3,1,1],[24,3,1,1],[1,1,1,1],[60,1,1,1]]}, + {"level":"type","id":"ex:test%2Facme_order%2Forder_test.exs/AcmeOrder%2EOrderTest","name":"AcmeOrder.OrderTest","kind":"module","parent":22,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":2,"weight":18,"occurrences":4,"terms":[[60,9,2,1],[89,8,1,1],[1,1,1,1]]} + ], + "diagnostics": {"unplaced":[],"withoutLexicon":[]} +} diff --git a/fixtures/java/README.md b/fixtures/java/README.md index e4da463..482879e 100644 --- a/fixtures/java/README.md +++ b/fixtures/java/README.md @@ -123,3 +123,25 @@ Spoon 11.5.0, `setNoClasspath(true)`, `setComplianceLevel(17)`, Re-measure with a throwaway Spoon program, not from memory, whenever this corpus changes. + +## The vocabulary: `order`, counted by hand + +`expected/vocabulary.json` is the words of this model's names +(`@codegraph/vocabulary`, PLAN.md §20), rebuilt byte for byte by +`pnpm --filter @codegraph/vocabulary test` (`CODEGRAPH_UPDATE_SNAPSHOTS=1` +rewrites it). A word weighs the role of the entity whose name carries it — +module and type 8, attribute 4, operation 3, parameter and local 1 — except a +word of the name's qualifier, before the last separator of a qualified kind, +which weighs 1. One word, counted by hand from the model and pinned by the +vocabulary's tests: + +| name | kind | role | weight | +|---|---|---|---| +| `com.acme.order` | package | module | 8 | +| `AbstractOrder`, `Order`, `OrderService` | class | type | 3 × 8 | +| `order` × 5 (`both`, `freeOf`, `notifyShipped`, `bill`, a lambda) | parameter | parameter | 5 × 1 | +| `order` (in `archive`) | localVariable | local | 1 | +| `com.acme.order.adapter`, `com.acme.order.legacy` | package | qualifier | 2 × 1 | + +**`order` at the corpus: weight 40, 12 occurrences, 12 carriers.** The two +`orders` parameters of `archive` are another word: nothing stems. diff --git a/fixtures/java/expected/vocabulary.json b/fixtures/java/expected/vocabulary.json new file mode 100644 index 0000000..69b04a8 --- /dev/null +++ b/fixtures/java/expected/vocabulary.json @@ -0,0 +1,123 @@ +{ + "kind": "codegraph.vocabulary/1", + "generatedBy": "@codegraph/vocabulary", + "view": {"name":"all","filters":[]}, + "corpus": {"name":"src","roots":["fixtures/java/src"]}, + "langs": ["java"], + "weights": {"module":8,"type":8,"operation":3,"attribute":4,"parameter":1,"local":1,"other":1,"qualifier":1}, + "classes": ["domain","technical","keyword","short"], + "technical": {"words":125,"sha256":"c16bd05be59a0c3281696359e4b4f7b3efe7860512b4ab37519f8c3a063c95f7"}, + "terms": [ + {"text":"abstract","class":"keyword","modules":1,"types":1,"exclusive":1}, + {"text":"acme","class":"domain","modules":3,"types":0}, + {"text":"adapter","class":"technical","modules":1,"types":1,"exclusive":2}, + {"text":"add","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"allocator","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"amount","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"apply","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"archive","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"audited","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"base","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"basket","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"batch","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"best","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"bill","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"both","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"cents","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"chain","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"channel","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"check","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"com","class":"technical","modules":3,"types":0}, + {"text":"counts","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"cursor","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"discount","class":"domain","modules":1,"types":3,"exclusive":1}, + {"text":"discountable","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"document","class":"domain","modules":1,"types":1,"exclusive":2}, + {"text":"empty","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"ensure","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"exception","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"factor","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"first","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"free","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"guard","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"head","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"index","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"invoice","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"join","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"ledger","class":"domain","modules":2,"types":2}, + {"text":"legacy","class":"domain","modules":1,"types":0,"exclusive":3}, + {"text":"length","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"line","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"lines","class":"domain","modules":1,"types":3,"exclusive":1}, + {"text":"list","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"log","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"max","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"message","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"money","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"next","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"notifications","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"notify","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"of","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"on","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"order","class":"domain","modules":3,"types":4}, + {"text":"orders","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"out","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"part","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"parts","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"percent","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"phone","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"post","class":"domain","modules":1,"types":1,"exclusive":2}, + {"text":"price","class":"domain","modules":1,"types":6,"exclusive":1}, + {"text":"priceable","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"priced","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"prices","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"quantity","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"reference","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"reporting","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"rows","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"running","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"scaled","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"second","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"service","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"shipped","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"size","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"stock","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"store","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"tail","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"times","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"to","class":"technical","modules":1,"types":1,"exclusive":1}, + {"text":"today","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"total","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"trail","class":"domain","modules":2,"types":2}, + {"text":"value","class":"domain","modules":1,"types":2,"exclusive":1}, + {"text":"values","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"web","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"zero","class":"domain","modules":1,"types":1,"exclusive":1} + ], + "scopes": [ + {"level":"corpus","name":"src","entities":152,"roles":{"module":3,"type":19,"operation":43,"attribute":21,"parameter":35,"local":6,"other":0},"skipped":{"unnamed":25,"conventional":0,"numeric":0},"distinct":85,"distinctDomain":75,"weight":503,"occurrences":146,"terms":[[51,40,12,12],[59,18,6,6],[2,16,2,2],[10,16,2,2],[17,16,4,4],[22,14,3,3],[36,13,3,3],[21,11,2,2],[40,11,3,3],[79,11,3,3],[64,10,5,5],[5,9,4,4],[39,9,2,2],[0,8,1,1],[8,8,1,1],[11,8,1,1],[15,8,3,3],[23,8,1,1],[25,8,1,1],[27,8,1,1],[31,8,1,1],[32,8,3,3],[37,8,1,1],[41,8,1,1],[45,8,1,1],[47,8,1,1],[60,8,1,1],[65,8,1,1],[70,8,1,1],[73,8,1,1],[80,8,2,2],[43,7,2,2],[67,7,2,2],[71,7,2,2],[7,6,2,2],[20,5,2,2],[62,5,2,2],[75,5,2,2],[29,4,2,2],[33,4,1,1],[50,4,1,1],[57,4,1,1],[74,4,1,1],[81,4,2,2],[83,4,1,1],[1,3,3,3],[3,3,1,1],[4,3,1,1],[6,3,1,1],[13,3,1,1],[14,3,1,1],[16,3,1,1],[18,3,1,1],[19,3,3,3],[26,3,1,1],[30,3,1,1],[35,3,1,1],[38,3,1,1],[42,3,1,1],[46,3,1,1],[48,3,1,1],[49,3,1,1],[58,3,1,1],[61,3,1,1],[68,3,1,1],[72,3,1,1],[76,3,1,1],[77,3,1,1],[78,3,1,1],[84,3,1,1],[44,2,2,2],[52,2,2,2],[56,2,2,2],[63,2,2,2],[82,2,2,2],[9,1,1,1],[12,1,1,1],[24,1,1,1],[28,1,1,1],[34,1,1,1],[53,1,1,1],[54,1,1,1],[55,1,1,1],[66,1,1,1],[69,1,1,1]]}, + {"level":"module","id":"java:com.acme.order","name":"com.acme.order","kind":"package","parent":0,"entities":137,"roles":{"module":1,"type":17,"operation":40,"attribute":18,"parameter":32,"local":6,"other":0},"skipped":{"unnamed":23,"conventional":0,"numeric":0},"distinct":77,"distinctDomain":68,"weight":433,"occurrences":126,"terms":[[51,38,10,10],[59,18,6,6],[10,16,2,2],[17,16,4,4],[22,14,3,3],[21,11,2,2],[40,11,3,3],[79,11,3,3],[64,10,5,5],[5,9,4,4],[39,9,2,2],[0,8,1,1],[8,8,1,1],[11,8,1,1],[15,8,3,3],[23,8,1,1],[25,8,1,1],[27,8,1,1],[31,8,1,1],[45,8,1,1],[47,8,1,1],[60,8,1,1],[65,8,1,1],[70,8,1,1],[73,8,1,1],[43,7,2,2],[67,7,2,2],[71,7,2,2],[7,6,2,2],[20,5,2,2],[36,5,2,2],[62,5,2,2],[29,4,2,2],[33,4,1,1],[50,4,1,1],[57,4,1,1],[74,4,1,1],[80,4,1,1],[81,4,2,2],[83,4,1,1],[3,3,1,1],[4,3,1,1],[6,3,1,1],[13,3,1,1],[14,3,1,1],[16,3,1,1],[18,3,1,1],[26,3,1,1],[30,3,1,1],[35,3,1,1],[42,3,1,1],[46,3,1,1],[48,3,1,1],[49,3,1,1],[61,3,1,1],[68,3,1,1],[72,3,1,1],[76,3,1,1],[77,3,1,1],[78,3,1,1],[84,3,1,1],[44,2,2,2],[52,2,2,2],[56,2,2,2],[63,2,2,2],[82,2,2,2],[1,1,1,1],[9,1,1,1],[12,1,1,1],[19,1,1,1],[28,1,1,1],[34,1,1,1],[53,1,1,1],[54,1,1,1],[55,1,1,1],[66,1,1,1],[69,1,1,1]]}, + {"level":"module","id":"java:com.acme.order.adapter","name":"com.acme.order.adapter","kind":"package","parent":1,"entities":6,"roles":{"module":1,"type":1,"operation":1,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":6,"weight":35,"occurrences":9,"terms":[[2,16,2,2],[36,8,1,1],[80,4,1,1],[58,3,1,1],[1,1,1,1],[19,1,1,1],[24,1,1,1],[51,1,1,1]]}, + {"level":"module","id":"java:com.acme.order.legacy","name":"com.acme.order.legacy","kind":"package","parent":1,"entities":9,"roles":{"module":1,"type":1,"operation":2,"attribute":2,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":7,"weight":35,"occurrences":11,"terms":[[32,8,3,3],[37,8,1,1],[41,8,1,1],[75,5,2,2],[38,3,1,1],[1,1,1,1],[19,1,1,1],[51,1,1,1]]}, + {"level":"type","id":"java:com.acme.order.adapter/LedgerAdapter","name":"LedgerAdapter","kind":"class","parent":2,"entities":5,"roles":{"module":0,"type":1,"operation":1,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":4,"weight":24,"occurrences":5,"terms":[[2,8,1,1],[36,8,1,1],[80,4,1,1],[58,3,1,1],[24,1,1,1]]}, + {"level":"type","id":"java:com.acme.order.legacy/List","name":"List","kind":"class","parent":3,"entities":8,"roles":{"module":0,"type":1,"operation":2,"attribute":2,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":24,"occurrences":7,"terms":[[32,8,3,3],[41,8,1,1],[75,5,2,2],[38,3,1,1]]}, + {"level":"type","id":"java:com.acme.order/AbstractOrder","name":"AbstractOrder","kind":"class","parent":1,"entities":7,"roles":{"module":0,"type":1,"operation":2,"attribute":2,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":4,"weight":31,"occurrences":7,"terms":[[0,8,1,1],[51,8,1,1],[64,8,3,3],[79,4,1,1],[59,3,1,1]]}, + {"level":"type","id":"java:com.acme.order/Audited","name":"Audited","kind":"annotation","parent":1,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":11,"occurrences":2,"terms":[[8,8,1,1],[81,3,1,1]]}, + {"level":"type","id":"java:com.acme.order/Basket","name":"Basket","kind":"class","parent":1,"entities":8,"roles":{"module":0,"type":1,"operation":3,"attribute":2,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":26,"occurrences":7,"terms":[[10,8,1,1],[79,7,2,2],[40,4,1,1],[3,3,1,1],[21,3,1,1],[39,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/Basket.Cursor","name":"Cursor","kind":"class","parent":8,"entities":4,"roles":{"module":0,"type":1,"operation":1,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":3,"weight":15,"occurrences":3,"terms":[[21,8,1,1],[33,4,1,1],[46,3,1,1]]}, + {"level":"type","id":"java:com.acme.order/Basket.Line","name":"Line","kind":"class","parent":8,"entities":5,"roles":{"module":0,"type":1,"operation":1,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":16,"occurrences":4,"terms":[[5,8,3,3],[39,8,1,1]]}, + {"level":"type","id":"java:com.acme.order/Basket.Line.Discount","name":"Discount","kind":"class","parent":10,"entities":4,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":15,"occurrences":4,"terms":[[22,8,1,1],[6,3,1,1],[77,3,1,1],[5,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/Batch","name":"Batch","kind":"class","parent":1,"entities":16,"roles":{"module":0,"type":1,"operation":6,"attribute":3,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":3,"conventional":0,"numeric":0},"distinct":10,"distinctDomain":9,"weight":41,"occurrences":13,"terms":[[11,8,1,1],[67,7,2,2],[20,5,2,2],[62,5,2,2],[4,3,1,1],[59,3,1,1],[61,3,1,1],[68,3,1,1],[72,3,1,1],[9,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/Channel","name":"Channel","kind":"enum","parent":1,"entities":6,"roles":{"module":0,"type":1,"operation":1,"attribute":3,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":23,"occurrences":5,"terms":[[17,8,1,1],[57,4,1,1],[74,4,1,1],[83,4,1,1],[59,3,1,1]]}, + {"level":"type","id":"java:com.acme.order/Discountable","name":"Discountable","kind":"interface","parent":1,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":3,"weight":12,"occurrences":3,"terms":[[23,8,1,1],[22,3,1,1],[56,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/EmptyBasketException","name":"EmptyBasketException","kind":"class","parent":1,"entities":3,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":25,"occurrences":4,"terms":[[10,8,1,1],[25,8,1,1],[27,8,1,1],[44,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/Money","name":"Money","kind":"record","parent":1,"entities":9,"roles":{"module":0,"type":1,"operation":4,"attribute":1,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":6,"weight":26,"occurrences":8,"terms":[[15,8,3,3],[45,8,1,1],[59,3,1,1],[76,3,1,1],[84,3,1,1],[28,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/Notifications","name":"Notifications","kind":"class","parent":1,"entities":22,"roles":{"module":0,"type":1,"operation":6,"attribute":1,"parameter":7,"local":0,"other":0},"skipped":{"unnamed":7,"conventional":0,"numeric":0},"distinct":14,"distinctDomain":12,"weight":47,"occurrences":18,"terms":[[47,8,1,1],[71,7,2,2],[50,4,1,1],[51,4,4,4],[14,3,1,1],[16,3,1,1],[30,3,1,1],[42,3,1,1],[48,3,1,1],[49,3,1,1],[59,3,1,1],[29,1,1,1],[44,1,1,1],[69,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/Order","name":"Order","kind":"class","parent":1,"entities":12,"roles":{"module":0,"type":1,"operation":2,"attribute":3,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":2,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":8,"weight":34,"occurrences":11,"terms":[[17,8,3,3],[51,8,1,1],[40,4,1,1],[43,4,1,1],[80,4,1,1],[22,3,1,1],[64,2,2,2],[56,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/OrderService","name":"OrderService","kind":"class","parent":1,"entities":12,"roles":{"module":0,"type":1,"operation":3,"attribute":1,"parameter":4,"local":2,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":6,"weight":35,"occurrences":12,"terms":[[51,10,3,3],[70,8,1,1],[7,6,2,2],[36,5,2,2],[13,3,1,1],[52,2,2,2],[34,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/Priceable","name":"Priceable","kind":"interface","parent":1,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":11,"occurrences":2,"terms":[[60,8,1,1],[59,3,1,1]]}, + {"level":"type","id":"java:com.acme.order/Reporting","name":"Reporting","kind":"class","parent":1,"entities":15,"roles":{"module":0,"type":1,"operation":5,"attribute":0,"parameter":4,"local":4,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":13,"distinctDomain":13,"weight":31,"occurrences":14,"terms":[[65,8,1,1],[29,3,1,1],[35,3,1,1],[40,3,1,1],[43,3,1,1],[78,3,1,1],[82,2,2,2],[12,1,1,1],[53,1,1,1],[54,1,1,1],[55,1,1,1],[66,1,1,1],[81,1,1,1]]}, + {"level":"type","id":"java:com.acme.order/StockGuard","name":"StockGuard","kind":"class","parent":1,"entities":6,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":24,"occurrences":6,"terms":[[31,8,1,1],[73,8,1,1],[18,3,1,1],[26,3,1,1],[63,2,2,2]]} + ], + "diagnostics": {"unplaced":[],"withoutLexicon":[]} +} diff --git a/fixtures/ruby/README.md b/fixtures/ruby/README.md index c6545b5..a4e702c 100644 --- a/fixtures/ruby/README.md +++ b/fixtures/ruby/README.md @@ -140,3 +140,24 @@ writes the one a program that loaded it would produce and merges it with | `Ledger#open?` is defined at `ledger.rb:6` | nothing, counted: a method no `def` writes has no entity, and gets none | The files and the entities are the same lines with and without it. + +## The vocabulary: `pricing`, counted by hand + +`expected/vocabulary.json` is the words of this model's names +(`@codegraph/vocabulary`, PLAN.md §20), rebuilt byte for byte by +`pnpm --filter @codegraph/vocabulary test` (`CODEGRAPH_UPDATE_SNAPSHOTS=1` +rewrites it). A word weighs the role of the entity whose name carries it — +module and type 8, attribute 4, operation 3, parameter and local 1 — except a +word of the name's qualifier, before the last separator of a qualified kind, +which weighs 1. One word, counted by hand from the model and pinned by the +vocabulary's tests: + +| name | kind | role | weight | +|---|---|---|---| +| `@pricing` | field | attribute | 4 | +| `lib/acme_order/pricing/premium.rb`, `…/standard.rb` | file | qualifier | 2 × 1 | +| `AcmeOrder::Pricing::Premium`, `AcmeOrder::Pricing::Standard` | class | qualifier | 2 × 1 | + +**`pricing`: weight 8, 5 occurrences, 5 carriers** — no entity is named +`Pricing` itself, so the word is only ever a qualifier or a field. A constant +path's `::` and a file's `/` qualify alike. diff --git a/fixtures/ruby/expected/vocabulary.json b/fixtures/ruby/expected/vocabulary.json new file mode 100644 index 0000000..80c1df8 --- /dev/null +++ b/fixtures/ruby/expected/vocabulary.json @@ -0,0 +1,186 @@ +{ + "kind": "codegraph.vocabulary/1", + "generatedBy": "@codegraph/vocabulary", + "view": {"name":"all","filters":[]}, + "corpus": {"name":"src","roots":["fixtures/ruby/src"]}, + "langs": ["rb"], + "weights": {"module":8,"type":8,"operation":3,"attribute":4,"parameter":1,"local":1,"other":1,"qualifier":1}, + "classes": ["domain","technical","keyword","short"], + "technical": {"words":125,"sha256":"c16bd05be59a0c3281696359e4b4f7b3efe7860512b4ab37519f8c3a063c95f7"}, + "terms": [ + {"text":"a","class":"short","modules":1,"types":0,"exclusive":6}, + {"text":"acme","class":"domain","modules":25,"types":31}, + {"text":"add","class":"technical","modules":1,"types":1,"exclusive":18}, + {"text":"all","class":"domain","modules":1,"types":1,"exclusive":12}, + {"text":"annotate","class":"domain","modules":1,"types":1,"exclusive":26}, + {"text":"archive","class":"domain","modules":1,"types":1,"exclusive":25}, + {"text":"audit","class":"domain","modules":2,"types":2}, + {"text":"auditable","class":"domain","modules":1,"types":2,"exclusive":7}, + {"text":"audited","class":"domain","modules":1,"types":1,"exclusive":7}, + {"text":"base","class":"technical","modules":2,"types":2}, + {"text":"bin","class":"domain","modules":1,"types":0,"exclusive":4}, + {"text":"blank","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"block","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"broken","class":"domain","modules":1,"types":0,"exclusive":15}, + {"text":"build","class":"technical","modules":3,"types":2}, + {"text":"call","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"cents","class":"domain","modules":1,"types":1,"exclusive":17}, + {"text":"class","class":"keyword","modules":1,"types":2,"exclusive":7}, + {"text":"close","class":"domain","modules":1,"types":1,"exclusive":14}, + {"text":"config","class":"technical","modules":1,"types":0,"exclusive":5}, + {"text":"console","class":"domain","modules":1,"types":0,"exclusive":4}, + {"text":"core","class":"technical","modules":1,"types":0,"exclusive":9}, + {"text":"currency","class":"domain","modules":2,"types":2}, + {"text":"current","class":"domain","modules":1,"types":1,"exclusive":14}, + {"text":"default","class":"technical","modules":1,"types":1,"exclusive":18}, + {"text":"divisor","class":"domain","modules":1,"types":1,"exclusive":17}, + {"text":"dynamic","class":"domain","modules":1,"types":5,"exclusive":10}, + {"text":"error","class":"technical","modules":1,"types":1,"exclusive":11}, + {"text":"errors","class":"technical","modules":1,"types":0,"exclusive":11}, + {"text":"export","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"ext","class":"technical","modules":1,"types":0,"exclusive":9}, + {"text":"for","class":"keyword","modules":2,"types":2}, + {"text":"format","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"found","class":"domain","modules":1,"types":1,"exclusive":11}, + {"text":"gemfile","class":"domain","modules":1,"types":0,"exclusive":1}, + {"text":"ghost","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"handlers","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"helper","class":"technical","modules":1,"types":0,"exclusive":31}, + {"text":"hook","class":"domain","modules":1,"types":1,"exclusive":24}, + {"text":"import","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"install","class":"domain","modules":1,"types":1,"exclusive":24}, + {"text":"invoice","class":"domain","modules":1,"types":1,"exclusive":12}, + {"text":"invoices","class":"domain","modules":1,"types":1,"exclusive":12}, + {"text":"label","class":"domain","modules":1,"types":1,"exclusive":13}, + {"text":"lat","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"ledger","class":"domain","modules":1,"types":1,"exclusive":14}, + {"text":"legacy","class":"domain","modules":2,"types":1}, + {"text":"lib","class":"technical","modules":24,"types":0}, + {"text":"line","class":"domain","modules":1,"types":2,"exclusive":18}, + {"text":"lines","class":"domain","modules":2,"types":2}, + {"text":"lng","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"load","class":"technical","modules":1,"types":1,"exclusive":10}, + {"text":"loader","class":"domain","modules":2,"types":1}, + {"text":"many","class":"domain","modules":1,"types":1,"exclusive":11}, + {"text":"max","class":"domain","modules":2,"types":2}, + {"text":"methods","class":"domain","modules":1,"types":2,"exclusive":7}, + {"text":"money","class":"domain","modules":1,"types":1,"exclusive":17}, + {"text":"name","class":"domain","modules":1,"types":1,"exclusive":7}, + {"text":"names","class":"domain","modules":1,"types":1,"exclusive":7}, + {"text":"not","class":"keyword","modules":1,"types":1,"exclusive":11}, + {"text":"note","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"odd","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"options","class":"technical","modules":1,"types":1,"exclusive":18}, + {"text":"order","class":"domain","modules":25,"types":31}, + {"text":"orders","class":"domain","modules":1,"types":0,"exclusive":29}, + {"text":"other","class":"domain","modules":1,"types":1,"exclusive":17}, + {"text":"patch","class":"domain","modules":1,"types":0,"exclusive":6}, + {"text":"path","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"ping","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"point","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"pong","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"premium","class":"domain","modules":1,"types":1,"exclusive":21}, + {"text":"price","class":"domain","modules":4,"types":4}, + {"text":"priceable","class":"domain","modules":1,"types":1,"exclusive":20}, + {"text":"priced","class":"domain","modules":1,"types":1,"exclusive":20}, + {"text":"pricing","class":"domain","modules":3,"types":3}, + {"text":"promo","class":"domain","modules":1,"types":0,"exclusive":23}, + {"text":"promo2024","class":"domain","modules":1,"types":1,"exclusive":23}, + {"text":"qty","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"rails","class":"domain","modules":1,"types":1,"exclusive":24}, + {"text":"railtie","class":"domain","modules":1,"types":0,"exclusive":24}, + {"text":"rakefile","class":"technical","modules":1,"types":0,"exclusive":2}, + {"text":"recent","class":"domain","modules":1,"types":1,"exclusive":25}, + {"text":"record","class":"domain","modules":1,"types":1,"exclusive":25}, + {"text":"reference","class":"domain","modules":2,"types":2}, + {"text":"reporting","class":"domain","modules":1,"types":1,"exclusive":26}, + {"text":"rest","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"root","class":"domain","modules":1,"types":1,"exclusive":28}, + {"text":"rounding","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"roundings","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"settle","class":"domain","modules":1,"types":1,"exclusive":12}, + {"text":"shout","class":"domain","modules":1,"types":1,"exclusive":13}, + {"text":"sku","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"spec","class":"technical","modules":2,"types":0}, + {"text":"stamp","class":"domain","modules":1,"types":1,"exclusive":7}, + {"text":"stamped","class":"domain","modules":1,"types":2,"exclusive":7}, + {"text":"standard","class":"domain","modules":1,"types":1,"exclusive":22}, + {"text":"string","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"summary","class":"domain","modules":1,"types":1,"exclusive":26}, + {"text":"tasks","class":"domain","modules":1,"types":0,"exclusive":29}, + {"text":"text","class":"domain","modules":1,"types":1,"exclusive":13}, + {"text":"too","class":"domain","modules":1,"types":1,"exclusive":11}, + {"text":"total","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"unnamed","class":"domain","modules":1,"types":1,"exclusive":26}, + {"text":"version","class":"domain","modules":1,"types":1,"exclusive":27}, + {"text":"wrapped","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"zero","class":"domain","modules":1,"types":1,"exclusive":17} + ], + "scopes": [ + {"level":"corpus","name":"src","entities":157,"roles":{"module":31,"type":32,"operation":35,"attribute":16,"parameter":31,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":11,"numeric":1},"distinct":107,"distinctDomain":88,"weight":1004,"occurrences":326,"terms":[[63,147,75,66],[1,96,61,61],[47,24,24,24],[26,21,7,7],[93,18,4,2],[7,17,3,3],[9,17,3,3],[49,17,4,4],[17,16,2,2],[41,16,2,2],[43,16,2,2],[45,16,2,2],[52,16,2,2],[55,16,2,2],[56,16,2,2],[69,16,2,2],[71,16,2,2],[73,16,2,2],[83,16,2,2],[85,16,2,2],[96,16,2,2],[97,16,2,2],[22,14,5,5],[72,12,4,4],[104,12,2,2],[46,10,3,3],[14,9,3,3],[48,9,2,2],[95,9,2,2],[0,8,1,1],[13,8,1,1],[19,8,1,1],[20,8,1,1],[27,8,1,1],[28,8,1,1],[33,8,1,1],[34,8,1,1],[35,8,1,1],[37,8,1,1],[38,8,1,1],[53,8,1,1],[59,8,1,1],[61,8,1,1],[64,8,1,1],[66,8,1,1],[75,8,5,5],[76,8,1,1],[77,8,1,1],[80,8,1,1],[81,8,1,1],[101,8,1,1],[105,8,1,1],[84,7,4,4],[6,6,2,2],[15,6,2,2],[31,6,2,2],[90,6,2,2],[54,5,2,2],[78,5,2,2],[92,5,2,2],[16,4,1,1],[24,4,1,1],[36,4,1,1],[44,4,1,1],[50,4,1,1],[60,4,1,1],[89,4,1,1],[2,3,1,1],[3,3,1,1],[4,3,1,1],[5,3,1,1],[8,3,1,1],[11,3,1,1],[18,3,1,1],[23,3,1,1],[29,3,1,1],[39,3,1,1],[40,3,1,1],[51,3,1,1],[68,3,1,1],[70,3,1,1],[74,3,1,1],[82,3,1,1],[87,3,1,1],[91,3,1,1],[94,3,1,1],[98,3,1,1],[102,3,1,1],[103,3,1,1],[106,3,1,1],[12,2,2,2],[25,2,2,2],[62,2,2,2],[65,2,2,2],[10,1,1,1],[21,1,1,1],[30,1,1,1],[32,1,1,1],[42,1,1,1],[57,1,1,1],[58,1,1,1],[67,1,1,1],[79,1,1,1],[86,1,1,1],[88,1,1,1],[99,1,1,1],[100,1,1,1]]}, + {"level":"module","id":"rb:Gemfile","name":"Gemfile","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":1,"distinctDomain":1,"weight":8,"occurrences":1,"terms":[[34,8,1,1]]}, + {"level":"module","id":"rb:Rakefile","name":"Rakefile","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":1,"distinctDomain":0,"weight":8,"occurrences":1,"terms":[[81,8,1,1]]}, + {"level":"module","id":"rb:acme_order.gemspec","name":"acme_order.gemspec","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":16,"occurrences":2,"terms":[[1,8,1,1],[63,8,1,1]]}, + {"level":"module","id":"rb:bin%2Fconsole","name":"bin/console","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":9,"occurrences":2,"terms":[[20,8,1,1],[10,1,1,1]]}, + {"level":"module","id":"rb:config.ru","name":"config.ru","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":1,"distinctDomain":0,"weight":8,"occurrences":1,"terms":[[19,8,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fa_order_patch.rb","name":"lib/acme_order/a_order_patch.rb","kind":"file","parent":0,"entities":3,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":4,"weight":40,"occurrences":10,"terms":[[63,18,4,2],[0,8,1,1],[66,8,1,1],[6,3,1,1],[1,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fauditable.rb","name":"lib/acme_order/auditable.rb","kind":"file","parent":0,"entities":12,"roles":{"module":1,"type":4,"operation":3,"attribute":0,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":13,"distinctDomain":10,"weight":81,"occurrences":26,"terms":[[7,17,3,3],[17,16,2,2],[55,16,2,2],[95,9,2,2],[1,5,5,5],[63,5,5,5],[6,3,1,1],[8,3,1,1],[94,3,1,1],[9,1,1,1],[47,1,1,1],[57,1,1,1],[58,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fbase.rb","name":"lib/acme_order/base.rb","kind":"file","parent":0,"entities":3,"roles":{"module":1,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":4,"distinctDomain":2,"weight":21,"occurrences":7,"terms":[[9,16,2,2],[1,2,2,2],[63,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fcore_ext%2Fstring.rb","name":"lib/acme_order/core_ext/string.rb","kind":"file","parent":0,"entities":3,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":4,"weight":24,"occurrences":8,"terms":[[97,16,2,2],[11,3,1,1],[1,1,1,1],[21,1,1,1],[30,1,1,1],[47,1,1,1],[63,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fdynamic.rb","name":"lib/acme_order/dynamic.rb","kind":"file","parent":0,"entities":10,"roles":{"module":1,"type":5,"operation":3,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":12,"distinctDomain":10,"weight":81,"occurrences":30,"terms":[[26,21,7,7],[35,8,1,1],[52,8,1,1],[61,8,1,1],[105,8,1,1],[1,7,7,7],[63,7,7,7],[36,4,1,1],[51,3,1,1],[68,3,1,1],[70,3,1,1],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Ferrors.rb","name":"lib/acme_order/errors.rb","kind":"file","parent":0,"entities":6,"roles":{"module":1,"type":3,"operation":0,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":11,"distinctDomain":7,"weight":66,"occurrences":17,"terms":[[27,8,1,1],[28,8,1,1],[33,8,1,1],[49,8,1,1],[53,8,1,1],[59,8,1,1],[101,8,1,1],[1,4,4,4],[63,4,4,4],[47,1,1,1],[54,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Finvoice.rb","name":"lib/acme_order/invoice.rb","kind":"file","parent":0,"entities":6,"roles":{"module":1,"type":1,"operation":3,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":6,"weight":34,"occurrences":12,"terms":[[41,16,2,2],[90,6,2,2],[3,3,1,1],[14,3,1,1],[1,2,2,2],[63,2,2,2],[42,1,1,1],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Flabel.rb","name":"lib/acme_order/label.rb","kind":"file","parent":0,"entities":5,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":6,"distinctDomain":5,"weight":25,"occurrences":9,"terms":[[43,16,2,2],[91,3,1,1],[1,2,2,2],[63,2,2,2],[47,1,1,1],[100,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fledger.rb","name":"lib/acme_order/ledger.rb","kind":"file","parent":0,"entities":4,"roles":{"module":1,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":27,"occurrences":9,"terms":[[45,16,2,2],[18,3,1,1],[23,3,1,1],[1,2,2,2],[63,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Flegacy%2Fbroken.rb","name":"lib/acme_order/legacy/broken.rb","kind":"file","parent":0,"entities":6,"roles":{"module":1,"type":1,"operation":2,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":8,"weight":30,"occurrences":12,"terms":[[46,9,2,2],[13,8,1,1],[29,3,1,1],[39,3,1,1],[1,2,2,2],[63,2,2,2],[32,1,1,1],[47,1,1,1],[67,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Flegacy%2Floader.rb","name":"lib/acme_order/legacy/loader.rb","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":4,"weight":12,"occurrences":5,"terms":[[52,8,1,1],[1,1,1,1],[46,1,1,1],[47,1,1,1],[63,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fmoney.rb","name":"lib/acme_order/money.rb","kind":"file","parent":0,"entities":14,"roles":{"module":1,"type":1,"operation":1,"attribute":2,"parameter":5,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":4,"numeric":0},"distinct":9,"distinctDomain":8,"weight":37,"occurrences":15,"terms":[[56,16,2,2],[22,5,2,2],[16,4,1,1],[106,3,1,1],[1,2,2,2],[25,2,2,2],[63,2,2,2],[65,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Forder.rb","name":"lib/acme_order/order.rb","kind":"file","parent":0,"entities":35,"roles":{"module":1,"type":2,"operation":6,"attribute":10,"parameter":13,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":2,"numeric":0},"distinct":23,"distinctDomain":18,"weight":120,"occurrences":51,"terms":[[63,26,12,6],[22,9,3,3],[48,9,2,2],[49,9,3,3],[1,6,6,6],[15,6,2,2],[84,6,3,3],[78,5,2,2],[92,5,2,2],[24,4,1,1],[54,4,1,1],[60,4,1,1],[75,4,1,1],[89,4,1,1],[2,3,1,1],[14,3,1,1],[72,3,1,1],[102,3,1,1],[12,2,2,2],[62,2,2,2],[47,1,1,1],[86,1,1,1],[88,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fpoint.rb","name":"lib/acme_order/point.rb","kind":"file","parent":0,"entities":5,"roles":{"module":1,"type":1,"operation":0,"attribute":2,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":6,"distinctDomain":5,"weight":29,"occurrences":9,"terms":[[69,16,2,2],[44,4,1,1],[50,4,1,1],[1,2,2,2],[63,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fpriceable.rb","name":"lib/acme_order/priceable.rb","kind":"file","parent":0,"entities":4,"roles":{"module":1,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":27,"occurrences":9,"terms":[[73,16,2,2],[72,3,1,1],[74,3,1,1],[1,2,2,2],[63,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fpricing%2Fpremium.rb","name":"lib/acme_order/pricing/premium.rb","kind":"file","parent":0,"entities":4,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":5,"weight":30,"occurrences":12,"terms":[[71,16,2,2],[31,3,1,1],[63,3,3,3],[72,3,1,1],[1,2,2,2],[75,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fpricing%2Fstandard.rb","name":"lib/acme_order/pricing/standard.rb","kind":"file","parent":0,"entities":4,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":5,"weight":30,"occurrences":12,"terms":[[96,16,2,2],[31,3,1,1],[63,3,3,3],[72,3,1,1],[1,2,2,2],[75,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fpromo%232024.rb","name":"lib/acme_order/promo#2024.rb","kind":"file","parent":0,"entities":2,"roles":{"module":1,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":1},"distinct":5,"distinctDomain":4,"weight":21,"occurrences":7,"terms":[[76,8,1,1],[77,8,1,1],[1,2,2,2],[63,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Frailtie.rb","name":"lib/acme_order/railtie.rb","kind":"file","parent":0,"entities":3,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":6,"weight":39,"occurrences":9,"terms":[[1,9,2,2],[63,9,2,2],[38,8,1,1],[80,8,1,1],[40,3,1,1],[47,1,1,1],[79,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Frecord.rb","name":"lib/acme_order/record.rb","kind":"file","parent":0,"entities":4,"roles":{"module":1,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":27,"occurrences":9,"terms":[[83,16,2,2],[5,3,1,1],[82,3,1,1],[1,2,2,2],[63,2,2,2],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Freporting.rb","name":"lib/acme_order/reporting.rb","kind":"file","parent":0,"entities":8,"roles":{"module":1,"type":1,"operation":3,"attribute":0,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":7,"weight":33,"occurrences":13,"terms":[[85,16,2,2],[63,4,4,4],[4,3,1,1],[98,3,1,1],[103,3,1,1],[1,2,2,2],[47,1,1,1],[84,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order%2Fversion.rb","name":"lib/acme_order/version.rb","kind":"file","parent":0,"entities":3,"roles":{"module":1,"type":1,"operation":0,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":33,"occurrences":9,"terms":[[104,12,2,2],[1,10,3,3],[63,10,3,3],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Facme_order.rb","name":"lib/acme_order.rb","kind":"file","parent":0,"entities":3,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":36,"occurrences":6,"terms":[[1,16,2,2],[63,16,2,2],[87,3,1,1],[47,1,1,1]]}, + {"level":"module","id":"rb:lib%2Ftasks%2Forders.rake","name":"lib/tasks/orders.rake","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":2,"weight":10,"occurrences":3,"terms":[[64,8,1,1],[47,1,1,1],[99,1,1,1]]}, + {"level":"module","id":"rb:spec%2Facme_order%2Forder_spec.rb","name":"spec/acme_order/order_spec.rb","kind":"file","parent":0,"entities":2,"roles":{"module":1,"type":0,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":2,"weight":25,"occurrences":7,"terms":[[63,12,3,2],[93,9,2,1],[14,3,1,1],[1,1,1,1]]}, + {"level":"module","id":"rb:spec%2Fspec_helper.rb","name":"spec/spec_helper.rb","kind":"file","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":0,"weight":17,"occurrences":3,"terms":[[93,9,2,1],[37,8,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fa_order_patch.rb/AcmeOrder::Order","name":"AcmeOrder::Order","kind":"class","parent":6,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":3,"weight":13,"occurrences":4,"terms":[[63,9,2,1],[6,3,1,1],[1,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fauditable.rb/AcmeOrder::Auditable","name":"AcmeOrder::Auditable","kind":"module","parent":7,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":13,"occurrences":4,"terms":[[7,8,1,1],[6,3,1,1],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fauditable.rb/AcmeOrder::Auditable::ClassMethods","name":"AcmeOrder::Auditable::ClassMethods","kind":"module","parent":33,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":6,"weight":23,"occurrences":7,"terms":[[17,8,1,1],[55,8,1,1],[8,3,1,1],[1,1,1,1],[7,1,1,1],[58,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fauditable.rb/AcmeOrder::Stamped","name":"AcmeOrder::Stamped","kind":"module","parent":7,"entities":3,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":4,"distinctDomain":3,"weight":11,"occurrences":4,"terms":[[95,8,1,1],[1,1,1,1],[9,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fauditable.rb/AcmeOrder::Stamped::ClassMethods","name":"AcmeOrder::Stamped::ClassMethods","kind":"module","parent":35,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":6,"weight":23,"occurrences":7,"terms":[[17,8,1,1],[55,8,1,1],[94,3,1,1],[1,1,1,1],[57,1,1,1],[63,1,1,1],[95,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fbase.rb/AcmeOrder::Base","name":"AcmeOrder::Base","kind":"class","parent":8,"entities":2,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":3,"distinctDomain":2,"weight":10,"occurrences":3,"terms":[[9,8,1,1],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fcore_ext%2Fstring.rb/String","name":"String","kind":"class","parent":9,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":11,"occurrences":2,"terms":[[97,8,1,1],[11,3,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fdynamic.rb/AcmeOrder::Dynamic","name":"AcmeOrder::Dynamic","kind":"module","parent":10,"entities":2,"roles":{"module":0,"type":1,"operation":0,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":17,"occurrences":7,"terms":[[26,9,2,2],[36,4,1,1],[1,2,2,2],[63,2,2,2]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fdynamic.rb/AcmeOrder::Dynamic::Ghost","name":"AcmeOrder::Dynamic::Ghost","kind":"class","parent":39,"entities":3,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":6,"weight":17,"occurrences":6,"terms":[[35,8,1,1],[68,3,1,1],[70,3,1,1],[1,1,1,1],[26,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fdynamic.rb/AcmeOrder::Dynamic::Loader","name":"AcmeOrder::Dynamic::Loader","kind":"class","parent":39,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":4,"weight":14,"occurrences":5,"terms":[[52,8,1,1],[51,3,1,1],[1,1,1,1],[26,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fdynamic.rb/AcmeOrder::Dynamic::Odd","name":"AcmeOrder::Dynamic::Odd","kind":"class","parent":39,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":11,"occurrences":4,"terms":[[61,8,1,1],[1,1,1,1],[26,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fdynamic.rb/AcmeOrder::Dynamic::Wrapped","name":"AcmeOrder::Dynamic::Wrapped","kind":"class","parent":39,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":11,"occurrences":4,"terms":[[105,8,1,1],[1,1,1,1],[26,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Ferrors.rb/AcmeOrder::Error","name":"AcmeOrder::Error","kind":"class","parent":11,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":2,"weight":10,"occurrences":3,"terms":[[27,8,1,1],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Ferrors.rb/AcmeOrder::NotFound","name":"AcmeOrder::NotFound","kind":"class","parent":11,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":18,"occurrences":4,"terms":[[33,8,1,1],[59,8,1,1],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Ferrors.rb/AcmeOrder::TooManyLines","name":"AcmeOrder::TooManyLines","kind":"class","parent":11,"entities":3,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":6,"distinctDomain":6,"weight":27,"occurrences":6,"terms":[[49,8,1,1],[53,8,1,1],[101,8,1,1],[1,1,1,1],[54,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Finvoice.rb/AcmeOrder::Invoice","name":"AcmeOrder::Invoice","kind":"class","parent":12,"entities":5,"roles":{"module":0,"type":1,"operation":3,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":6,"weight":23,"occurrences":8,"terms":[[41,8,1,1],[90,6,2,2],[3,3,1,1],[14,3,1,1],[1,1,1,1],[42,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Flabel.rb/AcmeOrder::Label","name":"AcmeOrder::Label","kind":"class","parent":13,"entities":4,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":5,"distinctDomain":5,"weight":14,"occurrences":5,"terms":[[43,8,1,1],[91,3,1,1],[1,1,1,1],[63,1,1,1],[100,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fledger.rb/AcmeOrder::Ledger","name":"AcmeOrder::Ledger","kind":"class","parent":14,"entities":3,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":16,"occurrences":5,"terms":[[45,8,1,1],[18,3,1,1],[23,3,1,1],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Flegacy%2Fbroken.rb/AcmeOrder::Legacy","name":"AcmeOrder::Legacy","kind":"class","parent":15,"entities":5,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":7,"weight":18,"occurrences":7,"terms":[[46,8,1,1],[29,3,1,1],[39,3,1,1],[1,1,1,1],[32,1,1,1],[63,1,1,1],[67,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fmoney.rb/AcmeOrder::Money","name":"AcmeOrder::Money","kind":"class","parent":17,"entities":13,"roles":{"module":0,"type":1,"operation":1,"attribute":2,"parameter":5,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":4,"numeric":0},"distinct":8,"distinctDomain":8,"weight":26,"occurrences":11,"terms":[[56,8,1,1],[22,5,2,2],[16,4,1,1],[106,3,1,1],[25,2,2,2],[65,2,2,2],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Forder.rb/AcmeOrder::Order","name":"AcmeOrder::Order","kind":"class","parent":18,"entities":28,"roles":{"module":0,"type":1,"operation":6,"attribute":8,"parameter":11,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":1,"numeric":0},"distinct":20,"distinctDomain":16,"weight":88,"occurrences":39,"terms":[[63,15,8,4],[22,9,3,3],[49,9,3,3],[15,6,2,2],[84,6,3,3],[1,4,4,4],[24,4,1,1],[54,4,1,1],[60,4,1,1],[75,4,1,1],[89,4,1,1],[2,3,1,1],[14,3,1,1],[72,3,1,1],[102,3,1,1],[12,2,2,2],[62,2,2,2],[48,1,1,1],[86,1,1,1],[88,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Forder.rb/AcmeOrder::Order::Line","name":"AcmeOrder::Order::Line","kind":"class","parent":52,"entities":6,"roles":{"module":0,"type":1,"operation":0,"attribute":2,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":5,"distinctDomain":5,"weight":21,"occurrences":8,"terms":[[48,8,1,1],[78,5,2,2],[92,5,2,2],[63,2,2,1],[1,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fpoint.rb/AcmeOrder::Point","name":"AcmeOrder::Point","kind":"class","parent":19,"entities":4,"roles":{"module":0,"type":1,"operation":0,"attribute":2,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":1,"numeric":0},"distinct":5,"distinctDomain":5,"weight":18,"occurrences":5,"terms":[[69,8,1,1],[44,4,1,1],[50,4,1,1],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fpriceable.rb/AcmeOrder::Priceable","name":"AcmeOrder::Priceable","kind":"module","parent":20,"entities":3,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":16,"occurrences":5,"terms":[[73,8,1,1],[72,3,1,1],[74,3,1,1],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fpricing%2Fpremium.rb/AcmeOrder::Pricing::Premium","name":"AcmeOrder::Pricing::Premium","kind":"class","parent":21,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":18,"occurrences":7,"terms":[[71,8,1,1],[31,3,1,1],[72,3,1,1],[63,2,2,2],[1,1,1,1],[75,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fpricing%2Fstandard.rb/AcmeOrder::Pricing::Standard","name":"AcmeOrder::Pricing::Standard","kind":"class","parent":22,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":18,"occurrences":7,"terms":[[96,8,1,1],[31,3,1,1],[72,3,1,1],[63,2,2,2],[1,1,1,1],[75,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fpromo%232024.rb/AcmeOrder::Promo2024","name":"AcmeOrder::Promo2024","kind":"class","parent":23,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":3,"weight":10,"occurrences":3,"terms":[[77,8,1,1],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Frailtie.rb/Rails::AcmeOrderHook","name":"Rails::AcmeOrderHook","kind":"class","parent":24,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":28,"occurrences":5,"terms":[[1,8,1,1],[38,8,1,1],[63,8,1,1],[40,3,1,1],[79,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Frecord.rb/AcmeOrder::Record","name":"AcmeOrder::Record","kind":"class","parent":25,"entities":3,"roles":{"module":0,"type":1,"operation":2,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":16,"occurrences":5,"terms":[[83,8,1,1],[5,3,1,1],[82,3,1,1],[1,1,1,1],[63,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Freporting.rb/AcmeOrder::Reporting","name":"AcmeOrder::Reporting","kind":"module","parent":26,"entities":7,"roles":{"module":0,"type":1,"operation":3,"attribute":0,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":7,"weight":22,"occurrences":9,"terms":[[85,8,1,1],[4,3,1,1],[63,3,3,3],[98,3,1,1],[103,3,1,1],[1,1,1,1],[84,1,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order%2Fversion.rb/AcmeOrder","name":"AcmeOrder","kind":"module","parent":27,"entities":2,"roles":{"module":0,"type":1,"operation":0,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":3,"weight":22,"occurrences":5,"terms":[[1,9,2,2],[63,9,2,2],[104,4,1,1]]}, + {"level":"type","id":"rb:lib%2Facme_order.rb/AcmeOrder","name":"AcmeOrder","kind":"module","parent":28,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":3,"weight":19,"occurrences":3,"terms":[[1,8,1,1],[63,8,1,1],[87,3,1,1]]} + ], + "diagnostics": {"unplaced":[],"withoutLexicon":[]} +} diff --git a/fixtures/typescript/README.md b/fixtures/typescript/README.md index 5646c1e..ae6c54d 100644 --- a/fixtures/typescript/README.md +++ b/fixtures/typescript/README.md @@ -75,3 +75,23 @@ edges : 127 (self-edges dropped: 1, indirect calls dropped: 2, computed imports Every dropped thing is counted: the model is a lower bound that says where it stopped, never a guess. + +## The vocabulary: `legacy`, counted by hand + +`expected/vocabulary.json` is the words of this model's names +(`@codegraph/vocabulary`, PLAN.md §20), rebuilt byte for byte by +`pnpm --filter @codegraph/vocabulary test` (`CODEGRAPH_UPDATE_SNAPSHOTS=1` +rewrites it). A word weighs the role of the entity whose name carries it — +module and type 8, attribute 4, operation 3, parameter and local 1 — except a +word of the name's qualifier, before the last separator of a qualified kind, +which weighs 1. One word, counted by hand from the model and pinned by the +vocabulary's tests: + +| name | kind | role | weight | +|---|---|---|---| +| `legacy-lib` | module | module | 8 | +| `legacy/acme-report.ts`, `legacy/acme.ts`, `packages/order/src/legacy/list.ts` | module | qualifier | 3 × 1 | + +**`legacy`: weight 11, 4 occurrences, 4 carriers.** A module is named by its +path: the directories qualify the file, so `packages` (21 modules) weighs 21, +not 168. diff --git a/fixtures/typescript/expected/vocabulary.json b/fixtures/typescript/expected/vocabulary.json new file mode 100644 index 0000000..6914b78 --- /dev/null +++ b/fixtures/typescript/expected/vocabulary.json @@ -0,0 +1,170 @@ +{ + "kind": "codegraph.vocabulary/1", + "generatedBy": "@codegraph/vocabulary", + "view": {"name":"all","filters":[]}, + "corpus": {"name":"src","roots":["fixtures/typescript/src"]}, + "langs": ["ts"], + "weights": {"module":8,"type":8,"operation":3,"attribute":4,"parameter":1,"local":1,"other":1,"qualifier":1}, + "classes": ["domain","technical","keyword","short"], + "technical": {"words":125,"sha256":"c16bd05be59a0c3281696359e4b4f7b3efe7860512b4ab37519f8c3a063c95f7"}, + "terms": [ + {"text":"a","class":"short","modules":1,"types":0,"exclusive":21}, + {"text":"abstract","class":"technical","modules":1,"types":1,"exclusive":4}, + {"text":"acme","class":"domain","modules":3,"types":1}, + {"text":"adapter","class":"technical","modules":1,"types":1,"exclusive":14}, + {"text":"add","class":"technical","modules":1,"types":1,"exclusive":7}, + {"text":"ambient","class":"domain","modules":1,"types":0,"exclusive":5}, + {"text":"amount","class":"domain","modules":3,"types":2}, + {"text":"apply","class":"domain","modules":1,"types":0,"exclusive":24}, + {"text":"as","class":"technical","modules":1,"types":1,"exclusive":16}, + {"text":"audited","class":"domain","modules":1,"types":0,"exclusive":9}, + {"text":"augment","class":"domain","modules":1,"types":0,"exclusive":6}, + {"text":"b","class":"short","modules":1,"types":0,"exclusive":21}, + {"text":"basket","class":"domain","modules":1,"types":1,"exclusive":7}, + {"text":"best","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"bill","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"both","class":"domain","modules":1,"types":0,"exclusive":17}, + {"text":"cents","class":"domain","modules":1,"types":2,"exclusive":16}, + {"text":"chain","class":"domain","modules":1,"types":0,"exclusive":17}, + {"text":"channel","class":"domain","modules":2,"types":2}, + {"text":"context","class":"technical","modules":1,"types":0,"exclusive":9}, + {"text":"count","class":"domain","modules":1,"types":1,"exclusive":7}, + {"text":"create","class":"technical","modules":1,"types":0,"exclusive":7}, + {"text":"currency","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"d","class":"short","modules":1,"types":0,"exclusive":5}, + {"text":"decorators","class":"domain","modules":1,"types":0,"exclusive":9}, + {"text":"default","class":"keyword","modules":1,"types":0,"exclusive":11}, + {"text":"describe","class":"domain","modules":2,"types":1}, + {"text":"discount","class":"domain","modules":3,"types":3}, + {"text":"discountable","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"dynamic","class":"domain","modules":1,"types":0,"exclusive":13}, + {"text":"ensure","class":"domain","modules":1,"types":0,"exclusive":21}, + {"text":"error","class":"technical","modules":1,"types":1,"exclusive":21}, + {"text":"extra","class":"domain","modules":1,"types":1,"exclusive":3}, + {"text":"first","class":"domain","modules":1,"types":0,"exclusive":17}, + {"text":"handler","class":"technical","modules":1,"types":0,"exclusive":11}, + {"text":"head","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"high","class":"domain","modules":1,"types":1,"exclusive":8}, + {"text":"id","class":"technical","modules":2,"types":2}, + {"text":"index","class":"domain","modules":2,"types":0}, + {"text":"input","class":"domain","modules":1,"types":0,"exclusive":21}, + {"text":"items","class":"domain","modules":1,"types":1,"exclusive":2}, + {"text":"lazy","class":"domain","modules":1,"types":0,"exclusive":13}, + {"text":"ledger","class":"domain","modules":1,"types":1,"exclusive":14}, + {"text":"legacy","class":"domain","modules":4,"types":0}, + {"text":"length","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"lib","class":"technical","modules":1,"types":0,"exclusive":3}, + {"text":"line","class":"domain","modules":2,"types":3}, + {"text":"lines","class":"domain","modules":2,"types":2}, + {"text":"list","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"load","class":"technical","modules":2,"types":0}, + {"text":"log","class":"domain","modules":1,"types":0,"exclusive":17}, + {"text":"low","class":"domain","modules":1,"types":1,"exclusive":8}, + {"text":"max","class":"domain","modules":3,"types":2}, + {"text":"message","class":"domain","modules":2,"types":1}, + {"text":"money","class":"domain","modules":1,"types":1,"exclusive":16}, + {"text":"name","class":"domain","modules":1,"types":0,"exclusive":13}, + {"text":"note","class":"domain","modules":1,"types":1,"exclusive":22}, + {"text":"notifications","class":"domain","modules":1,"types":0,"exclusive":17}, + {"text":"on","class":"technical","modules":1,"types":0,"exclusive":17}, + {"text":"open","class":"domain","modules":1,"types":1,"exclusive":6}, + {"text":"ops","class":"domain","modules":1,"types":0,"exclusive":7}, + {"text":"order","class":"domain","modules":22,"types":4}, + {"text":"orders","class":"domain","modules":1,"types":0,"exclusive":21}, + {"text":"packages","class":"domain","modules":21,"types":0}, + {"text":"parse","class":"domain","modules":1,"types":1,"exclusive":18}, + {"text":"pct","class":"domain","modules":3,"types":3}, + {"text":"phone","class":"domain","modules":1,"types":1,"exclusive":8}, + {"text":"post","class":"domain","modules":1,"types":1,"exclusive":14}, + {"text":"price","class":"domain","modules":4,"types":4}, + {"text":"priceable","class":"domain","modules":1,"types":1,"exclusive":19}, + {"text":"priced","class":"domain","modules":1,"types":1,"exclusive":24}, + {"text":"pricing","class":"domain","modules":1,"types":0,"exclusive":24}, + {"text":"priority","class":"domain","modules":1,"types":1,"exclusive":8}, + {"text":"promo","class":"domain","modules":1,"types":0,"exclusive":20}, + {"text":"props","class":"domain","modules":1,"types":0,"exclusive":23}, + {"text":"rate","class":"domain","modules":1,"types":0,"exclusive":24}, + {"text":"reason","class":"domain","modules":1,"types":1,"exclusive":10}, + {"text":"reference","class":"domain","modules":2,"types":2}, + {"text":"registry","class":"domain","modules":1,"types":1,"exclusive":2}, + {"text":"reject","class":"domain","modules":1,"types":0,"exclusive":21}, + {"text":"render","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"report","class":"domain","modules":2,"types":2}, + {"text":"reporting","class":"domain","modules":2,"types":0}, + {"text":"row","class":"domain","modules":1,"types":0,"exclusive":23}, + {"text":"rows","class":"domain","modules":1,"types":0,"exclusive":23}, + {"text":"résumé","class":"domain","modules":1,"types":1,"exclusive":22}, + {"text":"second","class":"domain","modules":1,"types":0,"exclusive":17}, + {"text":"secret","class":"domain","modules":1,"types":1,"exclusive":16}, + {"text":"shipped","class":"domain","modules":1,"types":0,"exclusive":17}, + {"text":"shout","class":"domain","modules":1,"types":0,"exclusive":17}, + {"text":"src","class":"technical","modules":21,"types":0}, + {"text":"store","class":"domain","modules":1,"types":1,"exclusive":8}, + {"text":"table","class":"domain","modules":1,"types":0,"exclusive":23}, + {"text":"tag","class":"domain","modules":1,"types":0,"exclusive":9}, + {"text":"tail","class":"domain","modules":1,"types":1,"exclusive":15}, + {"text":"target","class":"domain","modules":1,"types":0,"exclusive":9}, + {"text":"tax","class":"domain","modules":1,"types":0,"exclusive":24}, + {"text":"text","class":"domain","modules":2,"types":2}, + {"text":"thing","class":"domain","modules":2,"types":2}, + {"text":"total","class":"domain","modules":2,"types":2}, + {"text":"ui","class":"domain","modules":1,"types":0,"exclusive":23}, + {"text":"value","class":"domain","modules":2,"types":2}, + {"text":"values","class":"domain","modules":1,"types":1,"exclusive":9}, + {"text":"verbose","class":"domain","modules":1,"types":0,"exclusive":9}, + {"text":"web","class":"domain","modules":1,"types":1,"exclusive":8}, + {"text":"window","class":"domain","modules":1,"types":1,"exclusive":6}, + {"text":"wrapper","class":"technical","modules":1,"types":1,"exclusive":6}, + {"text":"zero","class":"domain","modules":1,"types":1,"exclusive":16} + ], + "scopes": [ + {"level":"corpus","name":"src","entities":179,"roles":{"module":24,"type":22,"operation":37,"attribute":28,"parameter":39,"local":6,"other":4},"skipped":{"unnamed":18,"conventional":1,"numeric":1},"distinct":108,"distinctDomain":90,"weight":812,"occurrences":244,"terms":[[61,79,35,32],[2,30,6,6],[81,24,3,3],[18,21,4,4],[63,21,21,21],[90,21,21,21],[46,18,4,4],[1,16,2,2],[3,16,2,2],[12,16,2,2],[28,16,2,2],[38,16,2,2],[42,16,2,2],[48,16,2,2],[54,16,2,2],[69,16,2,2],[85,16,2,2],[16,13,3,3],[6,12,5,5],[47,12,3,3],[68,12,4,4],[73,12,2,2],[43,11,4,4],[52,11,3,3],[92,11,2,2],[27,9,3,3],[31,9,2,2],[82,9,2,2],[98,9,2,2],[5,8,1,1],[10,8,1,1],[23,8,1,1],[24,8,1,1],[34,8,1,1],[41,8,1,1],[45,8,1,1],[57,8,1,1],[70,8,1,1],[72,8,1,1],[78,8,1,1],[99,8,2,2],[105,8,1,1],[106,8,1,1],[96,7,2,2],[26,6,2,2],[37,6,3,3],[49,6,2,2],[64,6,2,2],[77,6,3,3],[22,4,1,1],[35,4,1,1],[36,4,1,1],[40,4,1,1],[51,4,1,1],[56,4,1,1],[58,4,1,1],[60,4,1,1],[66,4,1,1],[75,4,1,1],[76,4,1,1],[83,4,2,2],[87,4,1,1],[88,4,1,1],[89,4,1,1],[91,4,1,1],[94,4,1,1],[97,4,2,2],[104,4,1,1],[4,3,1,1],[7,3,1,1],[8,3,1,1],[9,3,1,1],[14,3,1,1],[15,3,1,1],[17,3,1,1],[20,3,1,1],[21,3,1,1],[25,3,1,1],[30,3,1,1],[32,3,1,1],[44,3,1,1],[50,3,1,1],[53,3,3,3],[59,3,1,1],[65,3,3,3],[67,3,1,1],[79,3,1,1],[80,3,1,1],[107,3,1,1],[101,2,2,2],[0,1,1,1],[11,1,1,1],[13,1,1,1],[19,1,1,1],[29,1,1,1],[33,1,1,1],[39,1,1,1],[55,1,1,1],[62,1,1,1],[71,1,1,1],[74,1,1,1],[84,1,1,1],[86,1,1,1],[93,1,1,1],[95,1,1,1],[100,1,1,1],[102,1,1,1],[103,1,1,1]]}, + {"level":"module","id":"ts:legacy%2Facme-report.ts","name":"legacy/acme-report.ts","kind":"module","parent":0,"entities":6,"roles":{"module":1,"type":2,"operation":1,"attribute":0,"parameter":0,"local":0,"other":2},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":6,"weight":38,"occurrences":8,"terms":[[81,16,2,2],[2,9,2,2],[46,8,1,1],[80,3,1,1],[43,1,1,1],[61,1,1,1]]}, + {"level":"module","id":"ts:legacy%2Facme.ts","name":"legacy/acme.ts","kind":"module","parent":0,"entities":5,"roles":{"module":1,"type":1,"operation":0,"attribute":1,"parameter":0,"local":0,"other":2},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":23,"occurrences":6,"terms":[[2,9,2,2],[78,8,1,1],[40,4,1,1],[43,1,1,1],[61,1,1,1]]}, + {"level":"module","id":"ts:legacy-lib","name":"legacy-lib","kind":"module","parent":0,"entities":7,"roles":{"module":1,"type":1,"operation":3,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":4,"weight":38,"occurrences":8,"terms":[[43,8,1,1],[45,8,1,1],[98,8,1,1],[37,5,2,2],[26,3,1,1],[32,3,1,1],[49,3,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fabstract-order.ts","name":"packages/order/src/abstract-order.ts","kind":"module","parent":0,"entities":9,"roles":{"module":1,"type":1,"operation":2,"attribute":2,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":7,"weight":51,"occurrences":13,"terms":[[61,17,3,2],[1,16,2,2],[77,5,2,2],[99,4,1,1],[27,3,1,1],[68,3,1,1],[63,1,1,1],[65,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fambient.d.ts","name":"packages/order/src/ambient.d.ts","kind":"module","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":3,"weight":19,"occurrences":5,"terms":[[5,8,1,1],[23,8,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Faugment.ts","name":"packages/order/src/augment.ts","kind":"module","parent":0,"entities":7,"roles":{"module":1,"type":2,"operation":1,"attribute":1,"parameter":1,"local":1,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":10,"distinctDomain":7,"weight":44,"occurrences":11,"terms":[[2,12,2,2],[10,8,1,1],[105,8,1,1],[106,8,1,1],[59,3,1,1],[37,1,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1],[98,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fbasket.ts","name":"packages/order/src/basket.ts","kind":"module","parent":0,"entities":18,"roles":{"module":1,"type":2,"operation":3,"attribute":5,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":3,"conventional":1,"numeric":0},"distinct":13,"distinctDomain":10,"weight":63,"occurrences":18,"terms":[[12,16,2,2],[46,10,3,3],[47,8,2,2],[6,5,2,2],[52,4,1,1],[60,4,1,1],[99,4,1,1],[4,3,1,1],[20,3,1,1],[21,3,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fchannel.ts","name":"packages/order/src/channel.ts","kind":"module","parent":0,"entities":8,"roles":{"module":1,"type":2,"operation":0,"attribute":5,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":10,"distinctDomain":9,"weight":47,"occurrences":11,"terms":[[18,16,2,2],[72,8,1,1],[36,4,1,1],[51,4,1,1],[66,4,1,1],[91,4,1,1],[104,4,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fdecorators.ts","name":"packages/order/src/decorators.ts","kind":"module","parent":0,"entities":12,"roles":{"module":1,"type":1,"operation":2,"attribute":0,"parameter":5,"local":2,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":14,"distinctDomain":12,"weight":32,"occurrences":14,"terms":[[24,8,1,1],[81,8,1,1],[9,3,1,1],[52,3,1,1],[13,1,1,1],[19,1,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1],[93,1,1,1],[95,1,1,1],[101,1,1,1],[102,1,1,1],[103,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fdiscountable.ts","name":"packages/order/src/discountable.ts","kind":"module","parent":0,"entities":5,"roles":{"module":1,"type":1,"operation":1,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":6,"weight":27,"occurrences":8,"terms":[[28,16,2,2],[76,4,1,1],[27,3,1,1],[61,1,1,1],[63,1,1,1],[65,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fhandler.ts","name":"packages/order/src/handler.ts","kind":"module","parent":0,"entities":3,"roles":{"module":1,"type":0,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":2,"weight":15,"occurrences":6,"terms":[[34,8,1,1],[25,3,1,1],[61,2,2,2],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Findex.ts","name":"packages/order/src/index.ts","kind":"module","parent":0,"entities":1,"roles":{"module":1,"type":0,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":11,"occurrences":4,"terms":[[38,8,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Flazy.ts","name":"packages/order/src/lazy.ts","kind":"module","parent":0,"entities":5,"roles":{"module":1,"type":0,"operation":1,"attribute":0,"parameter":1,"local":2,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":6,"weight":17,"occurrences":8,"terms":[[41,8,1,1],[49,3,1,1],[29,1,1,1],[55,1,1,1],[61,1,1,1],[63,1,1,1],[82,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fledger-adapter.ts","name":"packages/order/src/ledger-adapter.ts","kind":"module","parent":0,"entities":4,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":4,"weight":39,"occurrences":9,"terms":[[3,16,2,2],[42,16,2,2],[67,3,1,1],[61,2,2,2],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Flegacy%2Flist.ts","name":"packages/order/src/legacy/list.ts","kind":"module","parent":0,"entities":5,"roles":{"module":1,"type":1,"operation":1,"attribute":2,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":7,"weight":31,"occurrences":9,"terms":[[48,16,2,2],[35,4,1,1],[94,4,1,1],[44,3,1,1],[43,1,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fmoney.ts","name":"packages/order/src/money.ts","kind":"module","parent":0,"entities":13,"roles":{"module":1,"type":2,"operation":5,"attribute":2,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":12,"distinctDomain":10,"weight":55,"occurrences":16,"terms":[[54,16,2,2],[16,13,3,3],[6,6,2,2],[87,4,1,1],[8,3,1,1],[68,3,1,1],[97,3,1,1],[107,3,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1],[101,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fnotifications.ts","name":"packages/order/src/notifications.ts","kind":"module","parent":0,"entities":18,"roles":{"module":1,"type":0,"operation":3,"attribute":2,"parameter":6,"local":0,"other":0},"skipped":{"unnamed":6,"conventional":0,"numeric":0},"distinct":13,"distinctDomain":11,"weight":38,"occurrences":16,"terms":[[57,8,1,1],[58,4,1,1],[88,4,1,1],[89,4,1,1],[15,3,1,1],[17,3,1,1],[50,3,1,1],[61,3,3,3],[53,2,2,2],[33,1,1,1],[63,1,1,1],[86,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Forder.ts","name":"packages/order/src/order.ts","kind":"module","parent":0,"entities":14,"roles":{"module":1,"type":1,"operation":4,"attribute":3,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":13,"distinctDomain":12,"weight":51,"occurrences":17,"terms":[[61,17,3,2],[64,6,2,2],[18,5,2,2],[22,4,1,1],[47,4,1,1],[52,4,1,1],[14,3,1,1],[27,3,1,1],[63,1,1,1],[65,1,1,1],[77,1,1,1],[90,1,1,1],[97,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fpriceable.ts","name":"packages/order/src/priceable.ts","kind":"module","parent":0,"entities":3,"roles":{"module":1,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":4,"weight":22,"occurrences":6,"terms":[[69,16,2,2],[68,3,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fpromo%232024.ts","name":"packages/order/src/promo#2024.ts","kind":"module","parent":0,"entities":2,"roles":{"module":1,"type":0,"operation":0,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":1},"distinct":4,"distinctDomain":3,"weight":15,"occurrences":5,"terms":[[73,12,2,2],[61,1,1,1],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Freporting.ts","name":"packages/order/src/reporting.ts","kind":"module","parent":0,"entities":17,"roles":{"module":1,"type":1,"operation":3,"attribute":0,"parameter":7,"local":1,"other":0},"skipped":{"unnamed":4,"conventional":0,"numeric":0},"distinct":13,"distinctDomain":9,"weight":44,"occurrences":17,"terms":[[61,11,4,4],[31,9,2,2],[82,8,1,1],[26,3,1,1],[30,3,1,1],[79,3,1,1],[0,1,1,1],[11,1,1,1],[39,1,1,1],[53,1,1,1],[62,1,1,1],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Frésumé.ts","name":"packages/order/src/résumé.ts","kind":"module","parent":0,"entities":3,"roles":{"module":1,"type":1,"operation":0,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":4,"weight":23,"occurrences":6,"terms":[[85,16,2,2],[56,4,1,1],[61,1,1,1],[63,1,1,1],[90,1,1,1]]}, + {"level":"module","id":"ts:packages%2Forder%2Fsrc%2Fui%2Forder-table.tsx","name":"packages/order/src/ui/order-table.tsx","kind":"module","parent":0,"entities":7,"roles":{"module":1,"type":0,"operation":2,"attribute":0,"parameter":3,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":8,"distinctDomain":7,"weight":32,"occurrences":12,"terms":[[61,12,3,2],[92,11,2,2],[83,4,2,2],[63,1,1,1],[74,1,1,1],[84,1,1,1],[90,1,1,1],[100,1,1,1]]}, + {"level":"module","id":"ts:packages%2Fpricing%2Fsrc%2Findex.ts","name":"packages/pricing/src/index.ts","kind":"module","parent":0,"entities":6,"roles":{"module":1,"type":1,"operation":2,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":10,"distinctDomain":9,"weight":37,"occurrences":11,"terms":[[38,8,1,1],[70,8,1,1],[96,7,2,2],[75,4,1,1],[7,3,1,1],[68,3,1,1],[6,1,1,1],[63,1,1,1],[71,1,1,1],[90,1,1,1]]}, + {"level":"type","id":"ts:legacy%2Facme-report.ts/Acme.Order.Line","name":"Line","kind":"typeAlias","parent":1,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":1,"distinctDomain":1,"weight":8,"occurrences":1,"terms":[[46,8,1,1]]}, + {"level":"type","id":"ts:legacy%2Facme-report.ts/Acme.Order.Report","name":"Report","kind":"class","parent":1,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":11,"occurrences":2,"terms":[[81,8,1,1],[80,3,1,1]]}, + {"level":"type","id":"ts:legacy%2Facme.ts/Acme.Order.Registry","name":"Registry","kind":"class","parent":2,"entities":2,"roles":{"module":0,"type":1,"operation":0,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":12,"occurrences":2,"terms":[[78,8,1,1],[40,4,1,1]]}, + {"level":"type","id":"ts:legacy-lib/Thing","name":"Thing","kind":"class","parent":3,"entities":4,"roles":{"module":0,"type":1,"operation":2,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":18,"occurrences":4,"terms":[[98,8,1,1],[37,4,1,1],[26,3,1,1],[32,3,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fabstract-order.ts/AbstractOrder","name":"AbstractOrder","kind":"abstractClass","parent":4,"entities":8,"roles":{"module":0,"type":1,"operation":2,"attribute":2,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":7,"distinctDomain":6,"weight":32,"occurrences":8,"terms":[[1,8,1,1],[61,8,1,1],[77,5,2,2],[99,4,1,1],[27,3,1,1],[68,3,1,1],[65,1,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Faugment.ts/AcmeWindow","name":"AcmeWindow","kind":"interface","parent":6,"entities":2,"roles":{"module":0,"type":1,"operation":0,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":20,"occurrences":3,"terms":[[2,12,2,2],[105,8,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Faugment.ts/Wrapper","name":"Wrapper","kind":"class","parent":6,"entities":4,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":1,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":2,"weight":13,"occurrences":4,"terms":[[106,8,1,1],[59,3,1,1],[37,1,1,1],[98,1,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fbasket.ts/Basket","name":"Basket","kind":"class","parent":7,"entities":8,"roles":{"module":0,"type":1,"operation":2,"attribute":2,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":5,"weight":24,"occurrences":7,"terms":[[12,8,1,1],[47,4,1,1],[99,4,1,1],[4,3,1,1],[20,3,1,1],[46,2,2,2]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fbasket.ts/Line","name":"Line","kind":"class","parent":7,"entities":4,"roles":{"module":0,"type":1,"operation":0,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":13,"occurrences":3,"terms":[[46,8,1,1],[6,5,2,2]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fchannel.ts/Channel","name":"Channel","kind":"enum","parent":8,"entities":4,"roles":{"module":0,"type":1,"operation":0,"attribute":3,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":20,"occurrences":4,"terms":[[18,8,1,1],[66,4,1,1],[91,4,1,1],[104,4,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fchannel.ts/Priority","name":"Priority","kind":"enum","parent":8,"entities":3,"roles":{"module":0,"type":1,"operation":0,"attribute":2,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":3,"weight":16,"occurrences":3,"terms":[[72,8,1,1],[36,4,1,1],[51,4,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fdecorators.ts/Report","name":"Report","kind":"class","parent":9,"entities":5,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":2,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":5,"distinctDomain":5,"weight":14,"occurrences":5,"terms":[[81,8,1,1],[52,3,1,1],[13,1,1,1],[101,1,1,1],[102,1,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fdiscountable.ts/Discountable","name":"Discountable","kind":"interface","parent":10,"entities":4,"roles":{"module":0,"type":1,"operation":1,"attribute":1,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":16,"occurrences":4,"terms":[[28,8,1,1],[76,4,1,1],[27,3,1,1],[65,1,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fledger-adapter.ts/LedgerAdapter","name":"LedgerAdapter","kind":"class","parent":14,"entities":3,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":3,"weight":20,"occurrences":4,"terms":[[3,8,1,1],[42,8,1,1],[67,3,1,1],[61,1,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Flegacy%2Flist.ts/List","name":"List","kind":"class","parent":15,"entities":4,"roles":{"module":0,"type":1,"operation":1,"attribute":2,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":4,"distinctDomain":4,"weight":19,"occurrences":4,"terms":[[48,8,1,1],[35,4,1,1],[94,4,1,1],[44,3,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fmoney.ts/Cents","name":"Cents","kind":"typeAlias","parent":16,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":1,"distinctDomain":1,"weight":8,"occurrences":1,"terms":[[16,8,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fmoney.ts/Money","name":"Money","kind":"class","parent":16,"entities":11,"roles":{"module":0,"type":1,"operation":5,"attribute":2,"parameter":2,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":9,"distinctDomain":8,"weight":36,"occurrences":11,"terms":[[54,8,1,1],[6,6,2,2],[16,5,2,2],[87,4,1,1],[8,3,1,1],[68,3,1,1],[97,3,1,1],[107,3,1,1],[101,1,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Forder.ts/Order","name":"Order","kind":"class","parent":18,"entities":13,"roles":{"module":0,"type":1,"operation":4,"attribute":3,"parameter":4,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":11,"distinctDomain":11,"weight":40,"occurrences":13,"terms":[[61,8,1,1],[64,6,2,2],[18,5,2,2],[22,4,1,1],[47,4,1,1],[52,4,1,1],[14,3,1,1],[27,3,1,1],[65,1,1,1],[77,1,1,1],[97,1,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Fpriceable.ts/Priceable","name":"Priceable","kind":"interface","parent":19,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":11,"occurrences":2,"terms":[[69,8,1,1],[68,3,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Freporting.ts/OrderError","name":"OrderError","kind":"class","parent":21,"entities":3,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":1,"local":0,"other":0},"skipped":{"unnamed":1,"conventional":0,"numeric":0},"distinct":3,"distinctDomain":2,"weight":17,"occurrences":3,"terms":[[31,8,1,1],[61,8,1,1],[53,1,1,1]]}, + {"level":"type","id":"ts:packages%2Forder%2Fsrc%2Frésumé.ts/Résumé","name":"Résumé","kind":"class","parent":22,"entities":2,"roles":{"module":0,"type":1,"operation":0,"attribute":1,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":12,"occurrences":2,"terms":[[85,8,1,1],[56,4,1,1]]}, + {"level":"type","id":"ts:packages%2Fpricing%2Fsrc%2Findex.ts/Priced","name":"Priced","kind":"interface","parent":24,"entities":2,"roles":{"module":0,"type":1,"operation":1,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":2,"weight":11,"occurrences":2,"terms":[[70,8,1,1],[68,3,1,1]]} + ], + "diagnostics": {"unplaced":[],"withoutLexicon":[]} +} diff --git a/fixtures/unicode/README.md b/fixtures/unicode/README.md index cc328fb..128a1ce 100644 --- a/fixtures/unicode/README.md +++ b/fixtures/unicode/README.md @@ -43,3 +43,8 @@ than the model's. to the storage engine.** A query that needs canonical order must sort in TypeScript with `compareIds`, or `ORDER BY` a column the importer wrote in canonical order — never rely on SQLite's own collation to reproduce it. + +`expected/vocabulary.json` pins the same order on WORDS: the terms are sorted by +code unit, so `𠀀` (the short word of `𠀀Supplementary`) comes before `a` (of +`AFullwidth`), and the type scopes run `Ascii`, `𠀀Supplementary`, +`AFullwidth` — the vocabulary sorts in TypeScript, like everything else. diff --git a/fixtures/unicode/expected/vocabulary.json b/fixtures/unicode/expected/vocabulary.json new file mode 100644 index 0000000..f519bf3 --- /dev/null +++ b/fixtures/unicode/expected/vocabulary.json @@ -0,0 +1,26 @@ +{ + "kind": "codegraph.vocabulary/1", + "generatedBy": "@codegraph/vocabulary", + "view": {"name":"all","filters":[]}, + "corpus": {"name":"src","roots":["fixtures/unicode/src"]}, + "langs": ["java"], + "weights": {"module":8,"type":8,"operation":3,"attribute":4,"parameter":1,"local":1,"other":1,"qualifier":1}, + "classes": ["domain","technical","keyword","short"], + "technical": {"words":125,"sha256":"c16bd05be59a0c3281696359e4b4f7b3efe7860512b4ab37519f8c3a063c95f7"}, + "terms": [ + {"text":"ascii","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"fullwidth","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"sorting","class":"domain","modules":1,"types":0,"exclusive":1}, + {"text":"supplementary","class":"domain","modules":1,"types":1,"exclusive":1}, + {"text":"𠀀","class":"short","modules":1,"types":1,"exclusive":1}, + {"text":"a","class":"short","modules":1,"types":1,"exclusive":1} + ], + "scopes": [ + {"level":"corpus","name":"src","entities":4,"roles":{"module":1,"type":3,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":4,"weight":48,"occurrences":6,"terms":[[0,8,1,1],[1,8,1,1],[2,8,1,1],[3,8,1,1],[4,8,1,1],[5,8,1,1]]}, + {"level":"module","id":"java:sorting","name":"sorting","kind":"package","parent":0,"entities":4,"roles":{"module":1,"type":3,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":6,"distinctDomain":4,"weight":48,"occurrences":6,"terms":[[0,8,1,1],[1,8,1,1],[2,8,1,1],[3,8,1,1],[4,8,1,1],[5,8,1,1]]}, + {"level":"type","id":"java:sorting/Ascii","name":"Ascii","kind":"class","parent":1,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":1,"distinctDomain":1,"weight":8,"occurrences":1,"terms":[[0,8,1,1]]}, + {"level":"type","id":"java:sorting/𠀀Supplementary","name":"𠀀Supplementary","kind":"class","parent":1,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":1,"weight":16,"occurrences":2,"terms":[[3,8,1,1],[4,8,1,1]]}, + {"level":"type","id":"java:sorting/AFullwidth","name":"AFullwidth","kind":"class","parent":1,"entities":1,"roles":{"module":0,"type":1,"operation":0,"attribute":0,"parameter":0,"local":0,"other":0},"skipped":{"unnamed":0,"conventional":0,"numeric":0},"distinct":2,"distinctDomain":1,"weight":16,"occurrences":2,"terms":[[1,8,1,1],[5,8,1,1]]} + ], + "diagnostics": {"unplaced":[],"withoutLexicon":[]} +} diff --git a/packages/vocabulary/test/fixtures.test.ts b/packages/vocabulary/test/fixtures.test.ts new file mode 100644 index 0000000..110419d --- /dev/null +++ b/packages/vocabulary/test/fixtures.test.ts @@ -0,0 +1,71 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { readModelFileSync } from "@codegraph/core"; +import { buildGraph, loadModels, type CodeGraph } from "@codegraph/analyzer"; +import { describe, expect, it } from "vitest"; +import { buildVocabulary } from "../src/aggregate.js"; +import { vocabularyToJsonString } from "../src/artifact.js"; +import { expectClosed } from "./closure.js"; + +/** + * Every fixture's model → `fixtures//expected/vocabulary.json`, byte for + * byte (PLAN.md §20 principle 6). The snapshot is the pretty form, one term and + * one scope per line, so a change to the split, the classes or the weights + * reads in review as the words it moved. `CODEGRAPH_UPDATE_SNAPSHOTS=1` + * rewrites them first — review the diff. + */ + +const FIXTURES = ["java", "csharp", "typescript", "elixir", "ruby", "unicode"] as const; + +const path = (fixture: string, file: string): string => + fileURLToPath(new URL(`../../../fixtures/${fixture}/expected/${file}`, import.meta.url)); + +function graphOf(fixture: string): CodeGraph { + const model = readModelFileSync(path(fixture, "model.jsonl")); + return buildGraph(loadModels(model, { sources: [`fixtures/${fixture}`] }).union); +} + +describe.each(FIXTURES)("fixtures/%s", (fixture) => { + const graph = graphOf(fixture); + const text = vocabularyToJsonString(buildVocabulary(graph), { pretty: true }); + const snapshot = path(fixture, "vocabulary.json"); + if (process.env["CODEGRAPH_UPDATE_SNAPSHOTS"] !== undefined) writeFileSync(snapshot, text); + + it("reproduces expected/vocabulary.json exactly", () => { + expect(existsSync(snapshot), `${snapshot} is committed`).toBe(true); + expect(text).toBe(readFileSync(snapshot, "utf8")); + }); + + it("is closed", () => { + expectClosed(buildVocabulary(graph), graph); + }); + + it("is the same on a second run over a fresh graph", () => { + expect(vocabularyToJsonString(buildVocabulary(graphOf(fixture)), { pretty: true })).toBe(text); + }); +}); + +// The words each fixture's README counts by hand, name by name. A snapshot +// can be rewritten in one command; these cannot, so a change to the split, the +// qualifiers or the weights has to be argued against a count a person made. +describe("the hand counts of the fixtures' READMEs", () => { + const rows: readonly (readonly [fixture: string, word: string, weight: number, occurrences: number, carriers: number])[] = [ + ["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], + ]; + + it.each(rows)("%s: %s weighs %i over %i occurrences, %i carriers", (fixture, word, weight, occurrences, carriers) => { + const artifact = buildVocabulary(graphOf(fixture)); + const term = artifact.terms.findIndex((entry) => entry.text === word && entry.class === "domain"); + expect(artifact.scopes[0]?.terms.find(([index]) => index === term)?.slice(1)).toEqual([weight, occurrences, carriers]); + }); + + it("csharp: the interface marker is no word", () => { + expect(buildVocabulary(graphOf("csharp")).terms.map((entry) => entry.text)).not.toContain("i"); + }); +}); From 2b1bdf3eb152b1b8a4177af6cd86993e1c424dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Grodziski?= Date: Sun, 25 Oct 2026 22:25:10 +0100 Subject: [PATCH 5/6] docs(plan): qualifiers, aggregation, artifact as built MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- PLAN.md | 124 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/PLAN.md b/PLAN.md index 720b7ab..fc4c45a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -4914,6 +4914,10 @@ lexicon?: { // splitter yields it: bean, jdbc (Java); concern (Ruby); live, web (Elixir) fileKinds?: string[]; // kinds whose name is a file path: TS/JS `module`, Ruby and Elixir // `file`, PHP `codeFile` — the extension is not a word + qualified?: Record; + // kinds the language names by a qualified path, with the separator + // that ends the qualifier: Java `package` ".", Ruby `class` "::", + // every file kind "/" — the qualifier's words weigh as incidental interfacePrefix?: true; // C#: `IOrderService` → order, service (I + Upper + lower) }; ``` @@ -4927,6 +4931,19 @@ character that is not a letter, a mark or a digit anyway — or `<=>`, a path, its `module` the dotted `AcmeOrder.Registry`, whose `.Registry` a profile-wide flag would drop as an extension), so it became `fileKinds`. +`qualified` was added when the aggregation was measured (M19a, below): a +qualified name speaks its container's words again, on every entity the +container holds. Full names made `acme` weigh 468 in the Ruby fixture and 392 +in the Elixir one, against about 50 for the next domain word, and `packages` +168 in the TypeScript one. A kind is listed when the LANGUAGE names it by a +path — Java's `package`, C#'s `namespace`, Clojure's `namespace` and Python's +`module` by `.`, Elixir's `module` and `protocol` by `.`, Ruby's `class`, +`module` and `constant` by `::`, PHP's `namespace` by `\`, every file kind by +`/`; Go's package (`package order`) and Rust's module (`mod line`) are named by +one identifier and are not listed. `validateProfile` gains +`lexicon-not-a-separator` (empty, or holding a letter, a mark or a digit), and +an undeclared qualified kind is `lexicon-unknown-kind`. + `ProfileSchema` mirrors it (a test parses every shipped profile through it unchanged); `codegraph profiles --lang X` prints it in full, between the kinds and the notes (the command's completeness rule); `schemas/` is unchanged — a @@ -4941,12 +4958,16 @@ the five file kinds above. TypeScript reserves exactly ECMAScript's words, so the two profiles read one `ECMASCRIPT_LEXICON`. **The split** (`split.ts`): one function, `splitIdentifier(name, kind, -lexicon)`, in six steps, each pinned by a table of hand cases per idiom: +lexicon)` → `{tokens, qualifier, numeric, conventional}`, in six steps, each +pinned by a table of hand cases per idiom: 1. for a kind in `fileKinds`, drop the extension of the last path segment - (`legacy/acme-report.ts` → `legacy/acme-report`); then the interface - prefix when the flag is set and the name starts `I` + Upper + lower - (`IOrderService`, not `IO`); + (`legacy/acme-report.ts` → `legacy/acme-report`); for a kind in + `qualified`, cut the name at the LAST occurrence of its separator — the + words before it are the qualifier, counted in `qualifier` + (`AcmeOrder::Order::Line` → 3 of `acme`, `order`, `order`, `line`); then + the interface prefix on the own name when the flag is set and it starts + `I` + Upper + lower (`IOrderService`, not `IO`; `Acme.IOrder` → `order`); 2. split on every character that is not a Unicode letter, mark or digit — universal, no data: `@total` → `total`, `valid?` → `valid`, `save!`, `name=`, `Order::Line`, `max_lines`, `acme-report`, `Repository\`1` → @@ -4974,7 +4995,10 @@ a non-empty run of letters, marks and digits, lowercase, never digits only, and a slice of the lowered name; the same input gives the same tokens; `split(a + sep + b) = split(a) ++ split(b)` for any character that is not a letter, a mark or a digit — without a lexicon, since the file kinds, the -interface prefix and a conventional name read the WHOLE name. A Σ is left +interface prefix and a conventional name read the WHOLE name. Qualification +moves no word: the tokens are the same with the qualified kinds removed, and +for a qualified kind `qualifier` is the number of words of `a` in +`a + separator + b`. A Σ is left out of the generator: its lower case depends on what follows it, so a token's lower case and its slice of the lowered name may differ (`ΟΔΟΣ` → `οδος`, pinned by hand). @@ -5021,32 +5045,50 @@ recorded in the artifact and overridable with `--weights role=n,…`: | `operation` | 3 | something it does; the prefix is usually technical (`get`, `find`, `build`) | | `parameter`, `local` | 1 | incidental: the "variable buried deep in a method" of the issue | | `other` | 1 | no trait says what it is | +| `qualifier` | 1 | not a role: the words before the last separator of a qualified name, whatever its role — a container's words, spoken again by every name it qualifies | An occurrence is one token in one name; a name carrying a word twice counts -twice. Both the weighted sum and the raw occurrence count are kept — one is -a judgement, the other a fact. - -**Scopes and aggregation** (`aggregate.ts`). Scopes are the kept -(view-filtered, non-stub) modules and types, plus the corpus root; an -operation's table is computed on demand (`--scope ID`), not stored (§20.5). -A scope's table is the union of the tables of every kept entity whose fold -container it is: `term → { weight, occurrences, carriers }` where `carriers` -is the number of distinct entities carrying the word. Per scope: `distinct` -and `distinctDomain` (the cognitive-load proxy), total `weight` and -`occurrences`, `skipped` by reason. Per term at the root: `spread.modules` -and `spread.types` (how many scopes speak it) and `exclusive` (the modules -whose table alone has it) — the overlap and the drift inputs. - -**The artifact** `vocabulary.json`, kind `codegraph.vocabulary/1`: a header -(generator, view descriptor, `weights`, `classes`, the technical list's -digest, `langs`), `terms[]` interned and sorted with their class, `scopes[]` -(root first, then modules then types in id order: `id`, `level`, `name`, -`kind`, `parent` scope index, `terms: [termIndex, weight, occurrences, -carriers][]` by weight descending then term) and `diagnostics` (skipped -counts, entities the fold could not place). Every scope id is an entity the -model declares and every index is in range — the closure property, tested. -Size is measured on BroadleafCommerce (3 312 types, 502 modules) before any -cap is considered; none is planned. +twice, and is one carrier. Both the weighted sum and the raw occurrence count +are kept — one is a judgement, the other a fact; a word weighing 0 still +occurs. + +**Scopes and aggregation** (`aggregate.ts`, `buildVocabulary(graph, +{view?, weights?, technical?, name?})`). Kept is the view's verdict on a +non-stub entity; a stub never speaks, whatever the view — its name was chosen +by another corpus (`String`). Scopes are the kept modules and types (by trait: +TModule first), plus the corpus root; an operation's table is computed on +demand (`--scope ID`), not stored (§20.5). A scope's table is the union of the +tables of every kept entity whose fold container it is — the NEAREST: a nested +type is its own scope, and its members are not its outer type's — +`term → { weight, occurrences, carriers }` where `carriers` is the number of +distinct entities carrying the word. Per scope: `entities` (each one unnamed, +conventional, or counted in `roles` by its role), `skipped` by reason, +`distinct` and `distinctDomain` (the cognitive-load proxy), total `weight` and +`occurrences`. Per term: `modules` and `types` (how many scopes speak it) and +`exclusive` (the one module scope whose table has it, when exactly one does) — +the overlap and the drift inputs. A class is decided per language (Go +reserves `type`, Java does not), so a word is interned once per class it +takes; an entity's language is its model's, read from the union, never from +its id. A kept entity no module contains counts at the root alone and is +listed (`unplaced`); a language without a lexicon is split by the universal +rules alone and listed (`withoutLexicon`). Every scope's parent is the +nearest scope up the containment chain the model writes, else the root. + +**The artifact** `vocabulary.json`, kind `codegraph.vocabulary/1` +(`artifact.ts`): a header (generator, view descriptor, `corpus`, `langs`, +`weights`, `classes`, `technical: {words, sha256}` — the shared list's size +and a sha256 over its sorted words, one per line), `terms[]` interned and +sorted by text in code-unit order then class, `scopes[]` (root first, then +modules then types in id order: `level`, `id`, `name`, `kind`, `parent` scope +index, the counts above, `terms: [termIndex, weight, occurrences, +carriers][]` by weight descending then term) and `diagnostics` (`unplaced`, +`withoutLexicon`). Every scope id is a kept entity of its level, every index +is in range, and the counts add up — the closure property, tested on the +fixtures and on generated corpora, with conservation (every kept entity in one +module scope or unplaced) and order-independence (the same bytes whatever the +order entities arrive in). Compact by default; `pretty` writes one term and one +scope per line, the snapshots' form. Size is measured on BroadleafCommerce +(3 312 types, 502 modules) before any cap is considered; none is planned. **The command.** `codegraph vocabulary [--level corpus|module|type] [--scope ID] [--top N] [--class domain|technical|keyword|short|all] @@ -5283,15 +5325,21 @@ never looks like a fact. `weight.ts`; the hand tables per idiom and the properties of §20.1; the unicode fixture through the splitter; the shared words out of the profiles. -- [ ] `aggregate.ts` on `containingType`/`containingModule`, `artifact.ts` - (`codegraph.vocabulary/1`), `drift.ts`. -- [ ] `codegraph vocabulary` (§20.1): the report, `--level`, `--scope`, - `--class`, `--weights`, `--technical`, `--drift`, `--out`, `--json`, - the view flags; usage errors for an unknown scope, role or class. -- [ ] `fixtures//expected/vocabulary.json` for the five fixtures, - byte-identical on every run; hand counts in each fixture's README - (`order` in the Java fixture: which names, which weights); the closure - and determinism properties over every fixture. +- [x] `qualified` in the lexicon (the kinds named by a qualified path and + their separators), its validation code, `profiles --lang` printing it; + the splitter's `qualifier` count and the `qualifier` weight. +- [x] `aggregate.ts` on `containingType`/`containingModule`, `artifact.ts` + (`codegraph.vocabulary/1`); the package now depends on the analyzer. +- [ ] `drift.ts` and `codegraph vocabulary` (§20.1): the report, `--level`, + `--scope`, `--class`, `--weights`, `--technical`, `--drift`, `--out`, + `--json`, the view flags; usage errors for an unknown scope, role or + class. +- [x] `fixtures//expected/vocabulary.json` for the five fixtures and + `fixtures/unicode`, byte-identical on every run + (`CODEGRAPH_UPDATE_SNAPSHOTS=1` rewrites them); hand counts in each + fixture's README (`order` in the Java fixture: which names, which + weights), pinned by a test; the closure and determinism properties over + every fixture. - [ ] The coverage table of §20.1 filled on the fixtures, Broadleaf, codegraph itself, Mastodon and Plausible; the Broadleaf top ten reviewed; the technical list corrected as data where it leaked. From 512210da2c70f6363a1512e482c23f38cf67de6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Grodziski?= Date: Tue, 27 Oct 2026 11:00:36 +0100 Subject: [PATCH 6/6] test(vocabulary): closure check asserts once The closure property timed out on CI (5.07 s on #32, 5.2 s on #28, 1.7 to 3.8 s on the runs between). The cost was the check, not the build: over 100 sampled corpora, building took 17 ms and expectClosed 279 ms, an `expect` per row of every scope. The same checks now run in plain code, collect every breach, and assert once that there is none; a failure lists them all. The property drops from about 320 ms to 34 ms locally. Doctoring a real artifact 34 ways, one breach each, both the old and the new check fail every time. Co-Authored-By: Claude Opus 5.5 --- packages/vocabulary/test/closure.ts | 92 ++++++++++++++++------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/packages/vocabulary/test/closure.ts b/packages/vocabulary/test/closure.ts index e9fa529..1aadebf 100644 --- a/packages/vocabulary/test/closure.ts +++ b/packages/vocabulary/test/closure.ts @@ -7,30 +7,41 @@ import type { VocabularyArtifact } from "../src/artifact.js"; * The artifact's closure (PLAN.md §20.1): every scope id is an entity the * model declares, kept and of the scope's level; every index is in range; and * every count is what the tables add up to. A dangling index is unwritable. + * + * Checked in plain code and asserted once, listing every breach: an `expect` + * per row made the check sixteen times slower than the build it checks, and + * timed a property out on CI. */ export function expectClosed(artifact: VocabularyArtifact, graph: CodeGraph): void { const { terms, scopes } = artifact; + const problems: string[] = []; + const check = (holds: boolean, problem: () => string): void => { + if (!holds) problems.push(problem()); + }; const classOrder = (c: string): number => TERM_CLASSES.indexOf(c as (typeof TERM_CLASSES)[number]); // Terms: interned once per (text, class), sorted by text, then class. for (let i = 1; i < terms.length; i += 1) { const [a, b] = [terms[i - 1], terms[i]]; if (a === undefined || b === undefined) throw new Error("unreachable"); - expect(a.text < b.text || (a.text === b.text && classOrder(a.class) < classOrder(b.class)), `${a.text} before ${b.text}`).toBe(true); + check(a.text < b.text || (a.text === b.text && classOrder(a.class) < classOrder(b.class)), () => `${a.text} (${a.class}) before ${b.text} (${b.class})`); } const [root, ...rest] = scopes; - expect(root?.level).toBe("corpus"); - expect(root?.id).toBeUndefined(); - expect(root?.parent).toBeUndefined(); + check(root?.level === "corpus", () => `the first scope is the corpus, not ${root?.level}`); + check(root?.id === undefined, () => `the root has no id, not ${root?.id}`); + check(root?.parent === undefined, () => `the root has no parent, not ${root?.parent}`); const modules = rest.filter((scope) => scope.level === "module"); const types = rest.filter((scope) => scope.level === "type"); // Root, then modules in id order, then types in id order. - expect(rest).toEqual([...modules, ...types]); + const leveled = [...modules, ...types]; + check(rest.length === leveled.length && rest.every((scope, i) => scope === leveled[i]), () => "modules come before types, and nothing else follows the root"); for (const level of [modules, types]) { const ids = level.map((scope) => scope.id ?? ""); - expect(ids).toEqual([...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))); - expect(new Set(ids).size).toBe(ids.length); + ids.forEach((id, i) => { + const previous = ids[i - 1]; + check(previous === undefined || previous < id, () => `${previous} before ${id}, once`); + }); } const modulesSpeaking = new Map(); @@ -39,38 +50,33 @@ export function expectClosed(artifact: VocabularyArtifact, graph: CodeGraph): vo scopes.forEach((scope, index) => { if (scope.level !== "corpus") { const entity = graph.entity(scope.id ?? ""); - expect(entity, `${scope.id} is declared`).toBeDefined(); + check(entity !== undefined, () => `${scope.id} is declared`); if (entity === undefined) return; - expect(graph.isStub(entity.id), `${scope.id} is no stub`).toBe(false); - expect(hasTrait(entity, scope.level === "module" ? "TModule" : "TType"), `${scope.id} is a ${scope.level}`).toBe(true); - expect(scope.kind).toBe(entity.kind); - expect(scope.parent, `${scope.id} has a parent scope`).toBeDefined(); - expect(scope.parent).not.toBe(index); - expect(scope.parent).toBeGreaterThanOrEqual(0); - expect(scope.parent).toBeLessThan(scopes.length); + check(!graph.isStub(entity.id), () => `${scope.id} is no stub`); + check(hasTrait(entity, scope.level === "module" ? "TModule" : "TType"), () => `${scope.id} is a ${scope.level}`); + check(scope.kind === entity.kind, () => `${scope.id}: kind ${scope.kind}, declared ${entity.kind}`); + const { parent } = scope; + check(parent !== undefined && parent !== index && parent >= 0 && parent < scopes.length, () => `${scope.id}: parent scope ${parent} of ${scopes.length}, itself ${index}`); } const roles = Object.values(scope.roles).reduce((sum, n) => sum + n, 0); - expect(scope.entities, `${scope.name}: every entity is unnamed, conventional or carries a role`).toBe( - roles + scope.skipped.unnamed + scope.skipped.conventional, - ); + const accounted = roles + scope.skipped.unnamed + scope.skipped.conventional; + check(scope.entities === accounted, () => `${scope.name}: ${scope.entities} entities, ${accounted} unnamed, conventional or carrying a role`); let weight = 0; let occurrences = 0; const seen = new Set(); scope.terms.forEach(([term, w, n, carriers], row) => { - expect(term).toBeGreaterThanOrEqual(0); - expect(term).toBeLessThan(terms.length); - expect(seen.has(term), `${scope.name}: ${terms[term]?.text} once`).toBe(false); + const at = `${scope.name} row ${row}`; + check(term >= 0 && term < terms.length, () => `${at}: term ${term} of ${terms.length}`); + check(!seen.has(term), () => `${at}: ${terms[term]?.text} once`); seen.add(term); - expect(n).toBeGreaterThan(0); - expect(carriers).toBeGreaterThan(0); - expect(carriers).toBeLessThanOrEqual(n); - expect(carriers).toBeLessThanOrEqual(scope.entities); - expect(w).toBeGreaterThanOrEqual(0); + check(n > 0, () => `${at}: ${n} occurrences`); + check(carriers > 0 && carriers <= n && carriers <= scope.entities, () => `${at}: ${carriers} carriers, ${n} occurrences, ${scope.entities} entities`); + check(w >= 0, () => `${at}: weight ${w}`); const previous = scope.terms[row - 1]; if (previous !== undefined) { - expect(previous[1] > w || (previous[1] === w && previous[0] < term), `${scope.name}: rows by weight, then term`).toBe(true); + check(previous[1] > w || (previous[1] === w && previous[0] < term), () => `${at}: rows by weight, then term`); } weight += w; occurrences += n; @@ -78,28 +84,34 @@ export function expectClosed(artifact: VocabularyArtifact, graph: CodeGraph): vo if (scope.level === "type") typesSpeaking.set(term, (typesSpeaking.get(term) ?? 0) + 1); if (scope.level === "module") speakers.set(term, [...(speakers.get(term) ?? []), index]); }); - expect(scope.weight).toBe(weight); - expect(scope.occurrences).toBe(occurrences); - expect(scope.distinct).toBe(scope.terms.length); - expect(scope.distinctDomain).toBe(scope.terms.filter(([term]) => terms[term]?.class === "domain").length); + check(Object.is(scope.weight, weight), () => `${scope.name}: weight ${scope.weight}, rows ${weight}`); + check(Object.is(scope.occurrences, occurrences), () => `${scope.name}: ${scope.occurrences} occurrences, rows ${occurrences}`); + check(scope.distinct === scope.terms.length, () => `${scope.name}: ${scope.distinct} distinct, ${scope.terms.length} rows`); + const domain = scope.terms.filter(([term]) => terms[term]?.class === "domain").length; + check(scope.distinctDomain === domain, () => `${scope.name}: ${scope.distinctDomain} distinct domain, ${domain} rows`); }); // The root speaks every term; the spread is what the scope tables say. - expect(new Set(root?.terms.map(([term]) => term))).toEqual(new Set(terms.map((_, index) => index))); + const spoken = new Set(root?.terms.map(([term]) => term)); + check(spoken.size === terms.length && terms.every((_, index) => spoken.has(index)), () => `the root speaks ${spoken.size} of ${terms.length} terms`); terms.forEach((term, index) => { - expect(term.modules, term.text).toBe(modulesSpeaking.get(index) ?? 0); - expect(term.types, term.text).toBe(typesSpeaking.get(index) ?? 0); const only = speakers.get(index); - expect(term.exclusive, term.text).toBe(only?.length === 1 ? only[0] : undefined); + const exclusive = only?.length === 1 ? only[0] : undefined; + check(term.modules === (modulesSpeaking.get(index) ?? 0), () => `${term.text}: ${term.modules} modules, ${modulesSpeaking.get(index) ?? 0} say it`); + check(term.types === (typesSpeaking.get(index) ?? 0), () => `${term.text}: ${term.types} types, ${typesSpeaking.get(index) ?? 0} say it`); + check(term.exclusive === exclusive, () => `${term.text}: exclusive to ${term.exclusive}, not ${exclusive}`); }); // Every kept entity folds into one module scope, or is unplaced and said so. const sum = (key: "entities" | "weight" | "occurrences"): number => modules.reduce((total, scope) => total + scope[key], 0); - expect(root?.entities).toBe(sum("entities") + artifact.diagnostics.unplaced.length); - if (artifact.diagnostics.unplaced.length === 0) { - expect(root?.weight).toBe(sum("weight")); - expect(root?.occurrences).toBe(sum("occurrences")); + const { unplaced } = artifact.diagnostics; + check(root?.entities === sum("entities") + unplaced.length, () => `the root holds ${root?.entities} entities, the modules ${sum("entities")} and ${unplaced.length} unplaced`); + if (unplaced.length === 0) { + check(Object.is(root?.weight, sum("weight")), () => `the root weighs ${root?.weight}, the modules ${sum("weight")}`); + check(Object.is(root?.occurrences, sum("occurrences")), () => `the root has ${root?.occurrences} occurrences, the modules ${sum("occurrences")}`); } - for (const id of artifact.diagnostics.unplaced) expect(graph.entity(id), id).toBeDefined(); + for (const id of unplaced) check(graph.entity(id) !== undefined, () => `${id}, unplaced, is declared`); + + expect(problems).toEqual([]); }