Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 39 additions & 14 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5093,17 +5093,40 @@ scope per line, the snapshots' form. Size is measured on BroadleafCommerce
**The command.** `codegraph vocabulary <models…> [--level corpus|module|type]
[--scope ID] [--top N] [--class domain|technical|keyword|short|all]
[--weights …] [--technical FILE] [--drift FILE] [--out vocabulary.json]
[--svg FILE] [--json] [--internal-only] [--declared-only]`. No flag: the
corpus root's top 40 `domain` terms as a table — term, class, weight,
occurrences, modules, types. `--level module`: one block per module, its top
N and its `distinct` column, sorted by `distinct` descending — the
cognitive-load report. `--drift FILE`: a plain text file, one business term
per line; each term is split with the universal rules so `Order Line` meets
`order` and `line`; a business term is SPOKEN in a scope when every one of
its words is, and the report lists the business terms no scope speaks and
the `domain` words the code speaks that no business term contains, by
`--level`. The Specy `.domain` file as input is deferred (§20.5): the plain
list first, because it exists in every company.
[--svg FILE] [--json] [--internal-only] [--declared-only] [--no-cache]`
(`--svg` arrives with M19b's layout), a shell over the package like
`analyze` over the analyzer. Every report's
header states the view, the weights and the technical list (its size, the
first 12 hex of its digest, and the file it came from). No flag: the corpus
root's top 40 `domain` terms as a table — term, class, weight, occurrences,
carriers, modules, types. `--level module`: one block per module, its top N
(10 by default) and its `distinct` column, sorted by `distinct` descending,
ties by id — the cognitive-load report. `--scope ID`: a stored module or
type, else an operation counted on demand (`operationScope`: the operation
and every kept entity written inside it, lambdas included, short of a type
or module, indexed by the artifact's terms; an artifact built under other
options is refused); an unknown id, a stub, or a member that is no operation
is a usage error that says what the id is. `--weights KEY=N,…` lays the keys
typed over the defaults (an unknown or repeated key, or a weight below 0, is
a usage error). `--technical FILE` replaces the shared list whole, one word
per line, `#` comments skipped; a line the splitter does not yield as one
word could never match a token and is refused by its number. `--out` writes
the artifact (compact) beside the report. `--drift FILE`: a plain text file,
one business term per line, `#` comments skipped, a term whose words repeat
an earlier one's merged, a line naming no word warned about by its number;
each term is split with the universal rules so `Order Line` meets `order`
and `line`; a business term is SPOKEN in a scope when every one of its words
is, whatever class the code gives it, and the report lists the business
terms no scope of `--level` speaks (with the words no scope says at all, or
"never together"), the ones it speaks and where, and the `domain` words the
code speaks that no business term contains, summed over the level's scopes.
Nothing is stemmed. A term spoken nowhere exits 3, as `analyze --report
cycles` does for a cycle: a CI job can gate on the glossary. `--class` with
`--drift`, and `--level` or `--drift` with `--scope`, are usage errors — a
flag the report would not read is refused, never dropped. `--json` is one
object, `codegraph.vocabularyReport/1`, `report: "words" | "drift"`. The
Specy `.domain` file as input is deferred (§20.5): the plain list first,
because it exists in every company.

**Measured** (the table this milestone fills — the numbers below are what to
collect, not results):
Expand Down Expand Up @@ -5330,10 +5353,12 @@ never looks like a fact.
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`,
- [x] `drift.ts` and `codegraph vocabulary` (§20.1): the report, `--level`,
`--scope` (an operation's scope on demand), `--class`, `--weights`,
`--technical`, `--drift` (exit 3 on a term spoken nowhere), `--out`,
`--json`, the view flags; usage errors for an unknown scope, role or
class.
class; `docs/cli.md`, the website's command and `vocabulary.json`
pages.
- [x] `fixtures/<lang>/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
Expand Down
25 changes: 23 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ share. Task-oriented walkthroughs live in the [README](../README.md).
worked; the input did not). A CI job gating on model quality checks for `3`.
- **`--json`** prints the same information as a machine-readable object.
- **A sibling `model.db`** (the SQLite analysis store) is built on first use and
reused afterwards by `analyze`, `export`, `navigator`, `domain-facts` and
`explain`; on those, `--no-cache` reads the JSONL directly.
reused afterwards by `analyze`, `export`, `navigator`, `vocabulary`,
`domain-facts` and `explain`; on those, `--no-cache` reads the JSONL
directly.

## Synopsis

Expand Down Expand Up @@ -65,6 +66,11 @@ codegraph city [model.jsonl] [--layout] [--out FILE]
codegraph navigator [model.jsonl] [--out FILE]
[--name STR] [--internal-only] [--declared-only]

codegraph vocabulary [model.jsonl] [--level corpus|module|type] [--scope ID]
[--top N] [--class domain|technical|keyword|short|all]
[--weights KEY=N,…] [--technical FILE] [--drift FILE]
[--out FILE] [--json] [--internal-only] [--declared-only]

codegraph domain-facts [model.jsonl] [--framework spring] [--out FILE]
[--internal-only] [--declared-only]

Expand Down Expand Up @@ -213,6 +219,21 @@ Writes the navigator artifact (`navigator.json`): the tree, one classified
dependency row per base edge, and the cycle/coupling reports. Type and module
nodes carry their entity id, which is how the city's buildings address them.

### `vocabulary`
The words the model's names are made of (never source): each `TNamed`
entity's name split into words, classed `domain`, `technical`, `keyword` or
`short`, and weighted by the role of what carries it. A type's or module's
name weighs 8, an attribute's 4, an operation's 3, a parameter's or local's
1, and the qualifier of a qualified name (`com.acme` of `com.acme.order`) 1.
No flag prints the corpus's 40 heaviest domain words; `--level module|type`
one block per scope, ranked by distinct terms (the cognitive-load report);
`--scope ID` one module or type, or an operation counted on demand.
`--weights` and `--technical FILE` change the scheme, and the header says
which one counted. `--drift FILE` compares with a glossary, one business term
per line: the terms no scope of `--level` says (exit `3`), the ones it says
and where, and the domain words no term contains. `--out FILE` writes the
`vocabulary.json` artifact (`codegraph.vocabulary/1`) beside the report.

### `domain-facts`
One JSON artifact (`codegraph.domainFacts/1`) with one dossier per corpus
type, every fact the model holds about it pre-joined: annotations with their
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"@codegraph/navigator": "workspace:*",
"@codegraph/navigator-ui": "workspace:*",
"@codegraph/scm": "workspace:*",
"@codegraph/viz": "workspace:*"
"@codegraph/viz": "workspace:*",
"@codegraph/vocabulary": "workspace:*"
},
"devDependencies": {
"@types/node": "catalog:",
Expand Down
152 changes: 152 additions & 0 deletions packages/cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { basename, resolve } from "node:path";
import { parseArgs } from "node:util";
import { FOLD_LEVELS, FRAMEWORK_PROFILES, type FoldLevel } from "@codegraph/analyzer";
import { METRIC_PREFIXES, SCALES, metricNames } from "@codegraph/city";
import { SCOPE_LEVELS, TERM_CLASSES, WEIGHT_KEYS, type ScopeLevel, type TermClass, type WeightKey } from "@codegraph/vocabulary";
import { defaultDataDir } from "./app/store.js";
import { UsageError } from "./exit.js";

Expand All @@ -26,6 +27,7 @@ export const COMMAND_NAMES = [
"export",
"city",
"navigator",
"vocabulary",
"serve",
"domain-facts",
"explain",
Expand Down Expand Up @@ -372,6 +374,78 @@ export const NAVIGATOR_SPEC: CommandSpec = {
],
};

/** `vocabulary --class`: one of the package's classes, or every one. */
export const VOCABULARY_CLASSES = [...TERM_CLASSES, "all"] as const;
export type VocabularyClass = (typeof VOCABULARY_CLASSES)[number];

/**
* `codegraph vocabulary`: the words the model's names are made of (PLAN.md
* §20.1). The counts are `@codegraph/vocabulary`'s; this command picks the
* scope, the class and the report, and writes the artifact on `--out`.
*/
export const VOCABULARY_SPEC: CommandSpec = {
name: "vocabulary",
summary: "Report the words the model's names are made of, per corpus, module or type; or drift against a glossary.",
positional: MODELS_POSITIONAL_DEFAULTED,
options: [
{
name: "level",
type: "string",
describe: "The corpus's words, or one block per module or type, ranked by distinct terms (the cognitive-load report).",
choices: SCOPE_LEVELS,
defaultValue: "corpus",
},
{
name: "scope",
type: "string",
describe: "Report one scope: a module or type by its entity id, or an operation, counted on demand.",
placeholder: "ID",
},
{
name: "top",
type: "string",
describe: "Show the N heaviest words: 40 by default, 10 per block under --level module|type.",
placeholder: "N",
integer: true,
},
{
name: "class",
type: "string",
describe: "Which words to show.",
choices: VOCABULARY_CLASSES,
defaultValue: "domain",
},
{
name: "weights",
type: "string",
describe: `Override weights, e.g. type=6,qualifier=0. Keys: ${WEIGHT_KEYS.join(", ")}.`,
placeholder: "KEY=N,…",
},
{
name: "technical",
type: "string",
describe: "Replace the shared technical words with this file's, one word per line; each language's own still apply.",
placeholder: "FILE",
},
{
name: "drift",
type: "string",
describe:
"Compare with a glossary, one business term per line: the terms no scope of --level says (exit 3), and the domain words no term contains.",
placeholder: "FILE",
},
{
name: "out",
type: "string",
describe: "Also write the vocabulary.json artifact to this file.",
placeholder: "FILE",
},
JSON_OPTION,
...VIEW_OPTIONS,
NO_CACHE_OPTION,
],
};

/**
* `codegraph serve`: ONE PAGE over a model — the navigator (tree, evidence,
* graph, cycles, coupling) with the 3D city as one of its tabs. Both
Expand Down Expand Up @@ -1063,6 +1137,7 @@ export const COMMAND_SPECS: readonly CommandSpec[] = [
EXPORT_SPEC,
CITY_SPEC,
NAVIGATOR_SPEC,
VOCABULARY_SPEC,
SERVE_SPEC,
DOMAIN_FACTS_SPEC,
EXPLAIN_SPEC,
Expand Down Expand Up @@ -1146,6 +1221,24 @@ export interface NavigatorOptions extends ModelInputOptions, ViewOptions, CacheO
readonly out: string | undefined;
}

export interface VocabularyOptions extends ModelInputOptions, ViewOptions, CacheOptions {
readonly level: ScopeLevel;
/** `--scope ID`: one module, type or operation; undefined = every scope of `level`. */
readonly scope: string | undefined;
/** `--top N`; undefined lets each report pick its own default. */
readonly top: number | undefined;
readonly termClass: TermClass | "all";
/** `--weights`: the keys typed, laid over the defaults by the command. */
readonly weights: Readonly<Partial<Record<WeightKey, number>>>;
/** `--technical FILE`; undefined = the shared list. */
readonly technical: string | undefined;
/** `--drift FILE`: the glossary to compare with. */
readonly drift: string | undefined;
/** `--out FILE`: the artifact, beside the report. */
readonly out: string | undefined;
readonly json: boolean;
}

export interface ServeOptions extends ModelInputOptions, CityBuildOptions, CacheOptions {
/** `--port N`; 0 = an ephemeral port (the default under `--app`). */
readonly port: number;
Expand Down Expand Up @@ -1316,6 +1409,7 @@ export type Invocation =
| { readonly kind: "run"; readonly command: "export"; readonly options: ExportOptions }
| { readonly kind: "run"; readonly command: "city"; readonly options: CityOptions }
| { readonly kind: "run"; readonly command: "navigator"; readonly options: NavigatorOptions }
| { readonly kind: "run"; readonly command: "vocabulary"; readonly options: VocabularyOptions }
| { readonly kind: "run"; readonly command: "serve"; readonly options: ServeOptions }
| { readonly kind: "run"; readonly command: "insights"; readonly options: InsightsOptions }
| { readonly kind: "run"; readonly command: "domain-facts"; readonly options: DomainFactsOptions }
Expand Down Expand Up @@ -1811,6 +1905,8 @@ export function parseInvocation(argv: readonly string[]): Invocation {
out: stringOf(values, "out"),
},
};
case "vocabulary":
return { kind: "run", command: "vocabulary", options: { models, ...vocabularyOf(values) } };
case "serve": {
const app = appOf(values, spec, models);
return {
Expand Down Expand Up @@ -2022,6 +2118,62 @@ export function parseInvocation(argv: readonly string[]): Invocation {
}
}

function vocabularyOf(values: ParsedValues): Omit<VocabularyOptions, "models"> {
const scope = stringOf(values, "scope");
const drift = stringOf(values, "drift");
// A flag the chosen report would not read is refused, never silently dropped.
const unread: readonly (readonly [boolean, string, string])[] = [
[scope !== undefined && stringOf(values, "level") !== undefined, "--level", "--scope"],
[scope !== undefined && drift !== undefined, "--drift", "--scope"],
[drift !== undefined && stringOf(values, "class") !== undefined, "--class", "--drift"],
];
for (const [conflict, flag, report] of unread) {
if (!conflict) continue;
throw new UsageError(
`${flag} does not apply with ${report}`,
report === "--scope"
? "--scope reports one scope; drop --level and --drift, or drop --scope."
: "--drift compares the business terms with every word, and lists the domain words no term contains.",
);
}
return {
level: (stringOf(values, "level") ?? "corpus") as ScopeLevel,
scope,
top: integerOf(values, "top"),
termClass: (stringOf(values, "class") ?? "domain") as TermClass | "all",
weights: weightsOf(stringOf(values, "weights")),
technical: stringOf(values, "technical"),
drift,
out: stringOf(values, "out"),
json: flagOf(values, "json"),
...viewOf(values),
noCache: flagOf(values, "no-cache"),
};
}

/** `--weights type=6,qualifier=0`: a known key, once, weighing a number >= 0. */
function weightsOf(raw: string | undefined): Partial<Record<WeightKey, number>> {
if (raw === undefined) return {};
const weights: Partial<Record<WeightKey, number>> = {};
for (const entry of raw.split(",")) {
const [key = "", value, ...rest] = entry.split("=").map((part) => part.trim());
const weight = Number(value);
if (value === undefined || value === "" || rest.length > 0 || key === "" || !Number.isFinite(weight) || weight < 0) {
throw new UsageError(
`invalid entry '${entry.trim()}' in --weights`,
"Each entry is KEY=N, N a number >= 0: --weights type=6,qualifier=0",
);
}
if (!(WEIGHT_KEYS as readonly string[]).includes(key)) {
throw new UsageError(`unknown role '${key}' in --weights`, `Valid keys: ${WEIGHT_KEYS.join(", ")}.`);
}
const role = key as WeightKey;
if (weights[role] !== undefined) throw new UsageError(`--weights weighs ${role} twice`, "Name each key once.");
weights[role] = weight;
}
return weights;
}

/** `--time`: unix seconds, or anything `Date.parse` reads (ISO dates). */
function timeOf(raw: string | undefined): number | undefined {
if (raw === undefined) return undefined;
Expand Down
8 changes: 1 addition & 7 deletions packages/cli/src/commands/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { EXIT, type ExitCode } from "../exit.js";
import { errLine, errLines, outLines, type IoSink } from "../io.js";
import { benignDuplicateIds } from "../load.js";
import { openAnalysis, type AnalysisSource } from "../source.js";
import { resolveView } from "../view.js";
import { resolveView, viewLabel } from "../view.js";

/**
* `codegraph analyze <model.jsonl...> --report deps|cycles|coupling`.
Expand Down Expand Up @@ -350,12 +350,6 @@ function rankingLine(ranking: Ranking, noun: string): string {

/* ------------------------------------------------------------- shared text */

function viewLabel(view: ViewDescriptor): string {
return view.filters.length === 0
? `${view.name} (nothing filtered — stubs and inferred edges included)`
: `${view.name} (${view.filters.join(", ")})`;
}

/** The header IS part of the artifact: a number without its view is not a fact. */
function headerLines(context: ReportContext): readonly string[] {
return [
Expand Down
Loading
Loading