insights.jsonl
The side-car explain writes: one explanation record per operation, type and module, in the vocabulary of the Specy domain metamodel. It lives beside the model — <model>.insights.jsonl by default — and never inside it, so it can be regenerated, pointed at another model, or deleted without touching model.jsonl or model.db.
packages/insights/src/schema.ts and the vocabularies in packages/insights/src/ddd.ts. Unlike the other artifact pages it carries no excerpt from a real run: producing one requires a model provider and an API key.File layout
header → i* → f* → eoff lines are the units a run asked for and could not explain; a side-car with none has no f line. A reader that only wants explanations keeps the t:"i" lines.
Records are sorted by (level, id), with operation before type before module, and are re-serialized through their Zod schema, so a record built in memory and one read back from disk are the same bytes. The body carries no timestamp; only the trailer does. Two runs with the same records are therefore diffable: what changed is what was re-explained.
During a run, finished records are appended one per line to <out>.journal and the sorted side-car is rewritten from base plus journal at each layer boundary, so an interrupted run resumes where it stopped. Reading a journal is lenient (its last line may be cut); reading the side-car proper is strict.
header
| Field | Type | Meaning |
|---|---|---|
t | "header" | |
kind | "codegraph.insights/1" | |
generatedBy | string | @codegraph/insights |
promptVersion | string | bumped whenever the prompt or a block shape changes; every fingerprint includes it |
metamodel | string | the vocabulary the blocks speak — specy.domain/3 |
models | {leaf, rollup} | the model slug used for operations, and for types and modules |
provider | string | which client served the calls (openrouter, cloudflare); absent on files written before it was recorded |
depth | integer ≥ 0 | how many levels of dependency explanations each prompt carried |
source | {paths, langs, view} | the models explained, their languages, and the view |
i — one insight record
| Field | Type | Meaning |
|---|---|---|
t | "i" | |
id | non-empty string | the rendered entity id — compared as an opaque token, never parsed |
key | {lang, module, symbol, disambiguator?} | the natural key, carried so a re-extraction re-joins without parsing ids |
level | "operation" | "type" | "module" | the discriminant; decides which block shape follows |
kind | string | the entity kind |
name | string | |
file | string | the anchor file, root-relative |
scc | string[] | present when the unit sits in a dependency cycle: every member, sorted, this id included |
origin | "llm" | "template" | |
block | OperationBlock | TypeBlock | ModuleBlock | the explanation itself |
fingerprint | 64-char hex | sha256 of everything the explanation was computed from |
model | string | the model slug that answered |
usage | {promptTokens, completionTokens, cost?} | |
metadata | Record<string, string> | the metamodel convention’s free key/value map |
f — one failure record
A unit left without an insight record, and why. It exists exactly as long as the gap: a run that explains the unit drops it, a run that fails again replaces it with attempts + 1, a run that did not attempt the unit (--scope, --max-calls) carries it over. explain --retry-failed takes its scope from these lines. Sorted like the records; no timestamp.
| Field | Type | Meaning |
|---|---|---|
t | "f" | |
id | string | the unit id — its first member’s rendered entity id, opaque |
key | {lang, module, symbol, disambiguator?} | that entity’s natural key, when known |
level | operation | type | module | |
members | string[] | every entity the unit would have explained, sorted; more than one means a dependency cycle |
model | string | the model that was asked |
reason.kind | provider | invalid-answer | error | the call was refused or never answered · two answers in a row failed validation · anything else |
reason.message | string | the provider’s or the validator’s own words, verbatim |
reason.status | integer, optional | the HTTP status, when the provider gave one (400 the prompt does not fit, 429 rate-limited after every back-off…). 401/402/403 also abort the run: only the unit that received it gets a record |
reason.retryable | boolean, optional | the client’s verdict on asking again at once; a later run may well succeed (credits added) |
attempts | integer ≥ 1 | consecutive runs that attempted this unit and failed |
calls | integer ≥ 0 | model calls the last attempt made |
usage | {promptTokens, completionTokens, cost?}, optional | what those calls spent |
{"t":"f","id":"java:org.broadleafcommerce.core.web.expression.checkout","key":{"lang":"java","module":"org.broadleafcommerce.core.web.expression.checkout","symbol":""},"level":"module","members":["java:org.broadleafcommerce.core.web.expression.checkout"],"model":"openai/gpt-5.6-luna","reason":{"kind":"provider","message":"This request requires more credits, or fewer max_tokens. …","status":402,"retryable":false},"attempts":1,"calls":0}eof
| Field | Type | Meaning |
|---|---|---|
t | "eof" | |
counts | {records, llm, template, reused, failed} | all integers ≥ 0; failed is the number of f lines |
usage | {promptTokens, completionTokens, cost?} | the run’s total |
generatedAt | ISO-8601 string | the ONLY timestamp in the file |
Blocks
Every block carries a name — the ubiquitous-language name the model proposes — a description, and a confidence number clamped to [0,1] on receipt.
OperationBlock
| Field | Type |
|---|---|
name, description | string |
safe | boolean — read-only (no state mutation) or not |
idempotent | boolean | null |
owner | one of OPERATION_OWNERS |
handlesCommand | string | null — the command this operation handles, when it is a command handler |
emits | {name, kind}[] where kind is an EVENT_KINDS member |
preconditions | {name, predicate, violationReason}[] |
postconditions | {name, predicate}[] |
invariantsEnforced | string[] |
usesSpi | {name, capability}[] — external capabilities the operation needs, as SPIs |
domainTerms | string[] |
words | WordEntry[], from prompt 2 — the words of its name and its parameters' |
confidence | number |
TypeBlock
| Field | Type |
|---|---|
name, description | string |
concept | one of DOMAIN_CONCEPTS |
eventKind | EVENT_KINDS member | null |
interfaceRole | INTERFACE_ROLES member | null |
aggregateRoot | boolean | null |
containedIn | string | null |
syncPattern | SYNC_PATTERNS member | null |
identity | string | null — the identity field, for entities |
fields | {name, type, kind}[] where kind is a FIELD_KINDS member |
invariants | {name, predicate, enforcement}[] where enforcement is an ENFORCEMENTS member |
stateMachine | {states, transitions: {from, to, operation}[]} | null |
relatesTo | {name, concept}[] |
exposes | string[] — operations that form this type’s public surface |
dependsOn | {name, role}[] — ports this type needs from others |
domainTerms | string[] |
words | WordEntry[], from prompt 2 — the words of its name and its fields' |
confidence | number |
ModuleBlock
| Field | Type |
|---|---|
name, description | string |
apis | {name, operations: string[]}[] |
spis | {name, capability}[] |
dependsOn | string[] |
concepts | {name, concept}[] |
boundedContextHint | {name, rationale} | null |
sharedKernelHint | string | null — for a module in a dependency cycle: why the group is inseparable, or how to split it |
ubiquitousLanguage | {term, definition}[] — terms the model proposes, in its own words |
words | WordEntry[], from prompt 2 — the words of its name |
glossary | GlossaryEntry[], from prompt 2 — its types’ senses of the words they spell, reconciled |
confidence | number |
WordEntry and GlossaryEntry
explain asks each unit to define the words its names spell, split as codegraph vocabulary splits them; short words and keywords are not asked. The words are given, never chosen: on receipt an entry for a word the prompt did not list is dropped, a word given twice keeps its first entry, and in is restated from the split. A record explained under prompt 1, and a templated one, has no words.
WordEntry field | Type |
|---|---|
word | string — as the splitter spelled it, lowercase |
in | string[] — the identifiers of this unit it appears in |
concern | one of WORD_CONCERNS — ambiguous when this unit uses it in two senses |
sense | string — a short label: what the word signifies here |
meaning | string — one sentence in this unit’s context |
confidence | number, clamped to [0,1] on receipt |
A module is shown what its types said of the words they spell, heaviest first, cut at --terms N (default 60), and reconciles them:
GlossaryEntry field | Type |
|---|---|
word, concern, sense, meaning | as in WordEntry, at the module’s altitude |
polysemous | boolean — its types disagree on the word |
senses | {in, sense}[] — the disagreeing types and their senses, when they do |
Vocabularies
Closed enums, restated as literals in ddd.ts with the source cited.
| Vocabulary | Members |
|---|---|
DOMAIN_CONCEPTS | boundedContext, module, interface, operation, command, query, reaction, entity, readOnlyEntity, aggregate, stateMachine, repository, event, valueType, enum, domainService, applicationService, infrastructureService, invariant, precondition, postcondition, agreement, reconciliation, notDomain, unknown |
EVENT_KINDS | internal, external, error, temporal |
INTERFACE_ROLES | API, SPI |
OPERATION_OWNERS | entity, aggregate, domainService, applicationService, infrastructureService, repository, valueType, unknown |
ENFORCEMENTS | rejection, compensation, alert |
SYNC_PATTERNS | synchronous-query, asynchronous-projection |
FIELD_KINDS | primitive, valueType, entityReference, enum, collection, unknown |
WORD_CONCERNS | domain, technical, ambiguous |
Strict structured output
Providers’ strict structured-output mode requires every property to be required and no additional properties, so “optional” is expressed as .nullable(), never .optional(), and no numeric or string constraints are emitted. confidence is clamped on receipt rather than constrained in the schema.
One exception, by version: words and glossary arrived with prompt 2, so a record may lack them — one explained under prompt 1 decodes and re-encodes to its own bytes — while an answer may not: the schema sent to the provider requires them, and an answer without them is asked again, then recorded as a failure.
Fingerprints
Every record carries a sha256 over what it was computed from: the prompt version, the model slug, the level, the source slices, the comments, the signatures, a digest of the facts shown, the words its prompt asked to define (and a module’s glossary words), the fingerprints of the units it depended on, and the ids of dependencies that had no record. Merkle-style: change one leaf’s source and exactly its transitive dependents and containers change.
The explanation text is deliberately not hashed — a non-deterministic answer must never cascade re-runs. A re-run plans reuse for every unit whose members’ records match; --force overrides. Because a missing dependency is part of the fingerprint, a dependent explained while its callee had failed or was out of scope is redone once the callee exists.
Templated records
A getter, setter, equals/hashCode/toString/compareTo, or a field-assigning constructor is described by a template with origin: "template", confidence 1, no words and no model call — decided from facts only: no throw site, no corpus call, cyclomatic ≤ 1, and at most one field touched. Anything with a throws fact or a corpus call is never trivial, however short.