Index
Design-plane contract for discovered Crux definitions, relations, source locations, diagnostics, and lint findings.
import type { ProjectIndexSnapshot } from '@crux/core/project-index'
import { serializeProjectIndex } from '@crux/core/project-index/serializers'The Project Index describes what exists in a local Crux project. It is separate from runtime observability: index definitions are prompts, contexts, tools, agents, flows, flow steps, compositions, RAG resources, memory, memory blocks, blackboards, workspaces, safety definitions, suites, evals, and other design-time resources; spans describe what happened during a run.
@crux/core owns the TypeScript contract. The Go devtools backend mirrors the contract manually, builds the service-owned read model, and exposes it to web devtools and the TUI.
Snapshot Shape
interface ProjectIndexSnapshot {
schemaVersion: 1
project?: ProjectIdentity
lint?: CruxLintConfig
indexedAt: string
indexing?: ProjectIndexingStatus
sourceGraph?: ProjectIndexSourceGraph
prompts: PromptMeta[]
contexts: ContextMeta[]
tools: ToolMeta[]
definitions: ProjectDefinition[]
relations: ProjectRelation[]
diagnostics: IndexDiagnostic[]
lintFindings: IndexLintFinding[]
ruleDescriptors: IndexRuleDescriptor[]
sources: IndexSourceFile[]
}prompts, contexts, and tools are runtime snapshot summaries. The canonical authored read model is definitions plus relations, diagnostics, lintFindings, ruleDescriptors, and sources; web devtools and the TUI should render authored primitives from definitions.
indexing is the backend-owned progress/readiness status for the index snapshot. It tells clients whether the index is cold, cached, refreshing, ready, or degraded, and includes separate phase state for the fast AST pass, the semantic enrichment pass, and cache. Clients should render this field directly and should not infer indexing readiness from diagnostics or missing source refs.
sourceGraph is indexer-owned provenance for sources dependency/dependent rows. It records the graph schema version, producer, and capabilities such as source dependencies, reverse dependents, definition ownership, and diagnostic ownership. Incremental planners and future partial index executors should trust source closures only when this marker is present with the required capabilities; older snapshots without it should fall back to full reindex.
Resolved Project Model
ResolvedProjectModel is the config-inspection read model exported from @crux/core/project-index.
It is not a runtime setup API and it is not a central registry requirement. Local tooling uses it to
explain what Crux inferred or explicitly received while keeping policy/config separate from authored
source visibility.
import type { ProjectModelProvenance, ResolvedProjectModel } from '@crux/core/project-index'The model contains the selected root, optional package name, config files, source roots, ignored
paths, discovered definitions, source-visible relations, Quality discovery settings, and
diagnostics. Fields that can be inferred or overridden use ProjectModelField<T> so clients can show
whether a value came from source, runtime evidence, filesystem convention, config, or CLI flags.
Source-discovered prompt and context bundles are represented directly on definitions. For example,
createPrompts({ support: { answer } }) gives the answer prompt a provenance-bearing
path.value of ["support", "answer"]; createContexts(...) does the same for context leaves.
Prompt/context use bindings that Crux can prove from source appear in relations, such as
prompt.uses_context, with inferred visibility and source provenance. Users do not need to repeat
these prompts, contexts, or relationships in crux.config.ts for local inspection.
Tools and skill registries follow the same source-first rule. Exported tool(...) definitions,
registry(...) values, and skill.fromRegistry(registryValue, path) calls appear as inferred
definitions without config registration. Registry-backed skills link to their registry value with
skill.uses_registry; bundled registries such as skillsSh are represented as built-in registry
definitions and are not fetched during discovery.
@crux/indexer produces this model with resolveProjectModel(...), and @crux/local exposes it
through crux config inspect for human or --json output.
type ProjectModelProvenance =
| { kind: 'source'; file: string; exportName?: string }
| { kind: 'runtime'; traceId?: string; attribute: string }
| { kind: 'filesystem'; path: string; convention: string }
| { kind: 'config'; path: string; key: string }
| { kind: 'cli'; flag: string }Diagnostics use stable ProjectModelDiagnosticCode values, such as
project_model.source_only_discovery, project_model.missing_stable_id,
project_model.dynamic_tool_map_unproven,
project_model.prompt_test_dependency_unproven, and
project_model.config_import_failed, plus branded diagnostic ids. Definition
and relation ids are branded separately. The brands disappear in JSON, but
TypeScript callers must construct ids at a boundary with
createProjectModelDefinitionId(), createProjectModelRelationId(), or
createProjectModelDiagnosticId() instead of passing arbitrary strings through
the public contract.
Clients should render Project Model diagnostics as project facts with small suggested fixes. A missing config is allowed: the source-only diagnostic explains the discovery mode, not that prompts, contexts, or tools must be registered in config. Selected source-shape facts, such as missing stable ids, runtime-dependent tool maps, or tested prompts whose context dependencies are only partially proven, are projected into Project Model diagnostics with source provenance.
Producers
The index contract is produced by two packages:
| Producer | Responsibility |
|---|---|
@crux/indexer | Discovers source-authored definitions, relations, source refs, diagnostics, and index lint findings. |
@crux/local | Persists and serves the index read model, enriches it with quality state, and broadcasts updates to UIs. |
Runtime snapshots emitted by withDevtools() can enrich prompt/context/tool metadata, but source files and .crux/quality records are authoritative for authored definitions. See @crux/indexer for the indexing package and @crux/local for CLI/API access.
The indexing pipeline is designed as fast source truth plus background enrichment. During crux dev,
the Go service asks the worker for a bounded static/AST pass first so it can publish useful Project
Index data quickly. That first patch may carry an index.source_only diagnostic as a status marker.
When semantic enrichment succeeds, the service applies the semantic patch and clears that marker; when
semantic enrichment is unavailable or degraded, the marker remains so clients can explain the current
fidelity honestly.
The semantic pass can enrich proven aliases, barrels, imported symbols, schema refs, callbacks, and primitive relations without blocking the first snapshot. The Go service owns stale-while-refresh cache loading, patch merging, final read-model state, and realtime publication; TypeScript workers emit neutral index facts/patches.
Full-Fidelity Definitions
Export definitions from discovered modules for full source visibility. Use crux.config.* for explicit policy, trust, ownership, or overrides rather than duplicating every primitive registration. Inline or dynamically generated definitions may still appear as partial entries, but Crux surfaces a diagnostic when it cannot prove IDs, relations, cases, or source locations.
Static relation extraction currently covers common design-time links such as prompt-to-context, agent-to-prompt, agent-to-tool, skill-to-registry, agent handoffs, flow-to-step, statically visible agent/tool/flow-step memory access, RAG pipeline-to-retriever, memory-to-memory-block, and composition-to-agent/flow. Workspaces carry source, namespace, mounts, tool exposure, and blob-storage hints when those values are literal. Source file entries include produced definition ids plus dependency/dependent file edges when known, and sourceGraph records whether those source rows are complete enough for source-closure planning. The Go service owns these definitions and relations so web devtools and the TUI render the same graph.
Definitions can include supporting source references in sourceRefs. source remains the primary authored call site, while sourceRefs point at separate schema declarations, callback functions, handlers, prompt builders, system prompt constants, validators, policies, config bindings, helper functions, reusable text fragments, and nested schema fragments that Crux can prove from source. The resolver supports same-file identifiers and direct project imports for tool schemas, nested schema identifiers, agent/prompt/context callbacks, prompt use context targets, local context-array constants, prompt/context system constants, direct identifiers and conservative object-property paths injected into static system template strings, safety/scorer callbacks, tool callbacks, Convex Agent config/callback bindings, flow-step callbacks, Convex Agent tool-map contributors, and simple callback-factory arguments. For example, createTool({ parameters: writerSchema, execute: runWriter }) can expose both metadata.inputSchema and refs for writerSchema, nested schemas such as SearchStepSchema, runWriter, and one statically visible helper called by runWriter; prompt({ system: PLANNER_SYSTEM }) can expose the PLANNER_SYSTEM declaration; a context system template that injects formatting.SUPPORTED_ELEMENTS can expose that property declaration with metadata.injected: true and metadata.fragment: true; new Agent(..., { tools, contextHandler, usageHandler }), direct convexAgent({...}), and profile-created crux.convexAgent({...}) can expose prompt, tools, contextHandler, usageHandler, and prepare bindings, tool-map spreads/properties, factory arguments passed into handler or prepare builders, and one helper level.
interface ProjectSourceRef {
id: string
role:
| 'schema'
| 'callback'
| 'handler'
| 'execute'
| 'prompt'
| 'system'
| 'resolver'
| 'validator'
| 'policy'
| 'config'
| 'helper'
property?: string
symbol?: string
source: SourceLocation
snippet?: SourceSnippet
fidelity: 'resolved' | 'partial'
description?: string
metadata?: {
schemaKind?: 'zod' | 'convex-validator' | 'json-schema'
parsedSchema?: boolean
nested?: boolean
injected?: boolean
fragment?: boolean
factoryArg?: boolean
argumentIndex?: number
argumentName?: string
toolMapContributor?: 'spread' | 'property'
referencedDefinitionIds?: string[]
dataAccess?: boolean
}
}Clients should render sourceRefs as supporting source locations and should not use them as runtime graph edges. Authored graph links still come from relations; parsed schemas and primitive intelligence still live on metadata. Source file entries can include dependency/dependent edges derived from source refs so file maps can show "this prompt depends on that schema/fragment file" without turning those source dependencies into Crux runtime relations.
Definitions include metadata.runtimeJoin hints where Crux can derive stable runtime identifiers. The object is typed as ProjectRuntimeJoin from @crux/core/project-index. These hints are for the Go read models to join spans/resources back to authored definitions without UI-side inference.
runtimeJoin separates authored identity from runtime correlation. definitionId is always the index definition id. spanAttributes contains only stable attributes that runtime spans actually emit. Runtime-only correlation fields stay separate: flowId identifies one executing flow instance and stepId identifies one executing flow-step instance, so authored flow definitions join flow.run spans by primitive plus span name, while authored flow-step definitions join flow.step spans by primitive plus stepLabel/span name. Memory blocks join memory spans through sourceDefinitionId, blockDefinitionId, runtime memoryId, and blockId. Blackboards use memory-shaped spans with memoryType: "blackboard"; clients should not expect a separate blackboardId span attribute.
Definitions can also include metadata.intelligence when Crux has source-backed primitive structure. This is the backend-owned design-plane explanation of how a primitive is shaped, not a runtime trace. Normal flow() definitions expose immediate ordered control metadata. Convex flow({ name, args, handler }) definitions additionally expose validator-derived argsSchema, visible suspension points from flow.waitFor() / flow.suspend(), and flow.step.waits_for_signal relations. Agent definitions expose prompt/tool/handoff dependency intelligence when literal and can also expose visible memory, blackboard, and workspace reads/writes in metadata.intelligence.data. Tool definitions and flow step definitions expose the same visible data access shape when authored code calls store helpers. These entries include the source targetVariable, literal key/path when available, and source location; normalized definition links come from relations such as agent.reads_memory, tool.writes_blackboard, flow.step.reads_memory, flow.step.writes_blackboard, and flow.step.writes_workspace. Literal parallel(), pipeline(), consensus(), and swarm() calls expose their visible branches, stages, participants, coordinators, judges/scorers, and shared state through definitions and relations such as composition.parallel.branch, composition.pipeline.stage, parallel.includes_branch, pipeline.includes_stage, pipeline.stage.uses_prompt, pipeline.stage.uses_tool, consensus.includes_agent, consensus.uses_scorer, swarm.includes_agent, swarm.coordinated_by, swarm.uses_blackboard, and swarm.uses_memory. Retrieval pipelines can expose rag.pipeline.stage definitions plus rag.pipeline.includes_stage, rag.pipeline.stage.uses_retriever, and rag.pipeline.stage.uses_scorer relations when stage arrays use literal identifiers. Workspaces can expose literal tools and mount paths through workspace.exposes_tool and workspace.mounts_path; constraints and guardrails can expose literal target coverage through constraint.applies_to and guardrail.applies_to; evals can expose coverage through eval.covers_definition.
The intelligence contract is additive. If Crux cannot prove a fact from source or runtime data, the field is omitted rather than filled with an empty placeholder. Clients should render definitions, relations, and metadata.intelligence directly and should not reconstruct flow, pipeline, parallel, consensus, or swarm structure by parsing source snippets.
When a candidate source file is import-safe, the indexer imports it in CRUX_INDEX=1 mode and upgrades exported rich primitives to resolved definitions. Today that covers agents, flows, RAG retrievers and pipelines, memory, blackboards, constraints, guardrails, and scorers. Static flow.step definitions remain source-derived because flow handles do not expose their internal handler graph at runtime.
Composition index entries are source-derived call sites for the immediate APIs: parallel({ agents }), pipeline({ steps }), consensus({ agents }), and swarm({ agents }). Adapter factory helpers such as createParallel() are not indexed as user-authored composition definitions.
Prompt, context, and tool schemas are exposed on ProjectDefinition.metadata when Crux can resolve or statically project them:
promptdefinitions:metadata.inputSchema,metadata.outputSchema,metadata.hasOutput, andmetadata.settingscontextdefinitions:metadata.inputSchema,metadata.priority,metadata.isStatic, andmetadata.usedBytooldefinitions:metadata.inputSchema
Clients should render these schema fields directly from the definition detail. Partial definitions can still include best-effort schemas when the source uses common inline Zod expressions such as z.object, z.array, z.enum, z.string, z.number, z.boolean, optional, describe, default, min, and max. Tool schemas passed by same-file variable or direct project import are also resolved when the underlying Zod/Convex-validator expression is statically visible; the calling file records a dependency on the imported schema file. Unresolved dynamic schemas are omitted rather than guessed.
Quality Joins
The Go service enriches index definitions with a derived quality summary when eval, RAG eval, flow eval, experiment, baseline, comparison, cassette, or feedback records point at that definition. The summary can include related eval IDs, suite IDs, experiment IDs, baseline IDs, comparison IDs, feedback IDs, cassette paths, run IDs, trace IDs, run counts, case counts, last status/time, pass rate, and changed-since-baseline fingerprint signals and affected eval/suite suggestions.
This field is a read-model join. It is not written into source files or indexer output, and clients should not recompute it by walking eval lists themselves.
Quality insights use the same backend-owned mapping. When an insight references a target, experiment, trace, or cassette path that resolves to index definitions, the response includes linkedDefinitionIds and linkedSources so web devtools and the TUI can navigate to the exact source without client-side matching rules.
Lint Findings
lintFindings are backend-owned authored-graph findings. They are intentionally separate from diagnostics: diagnostics describe indexer health, import failures, parse failures, or index fidelity issues; lint findings describe actionable design observations about the Crux graph the indexer successfully understood.
ruleDescriptors is the companion rule metadata list. It describes available built-in and extension-provided index lint rules even when none of those rules fired a finding in the current snapshot. Clients should use it for rule docs, suppression affordances, fix affordances, option schemas, and rule filtering instead of hard-coding rule knowledge.
The full lint finding, profile, rule metadata, and suppression contract is documented in Index Lint. Rule-specific reference pages live under Index Lints.
Clients should render lintFindings directly and should not recompute lint rules by walking relations client-side. Missing lint findings means "no backend finding for the current index", not that the UI should infer its own warnings.
Prompt/context/injectable assembly can also affect contracts. When the indexer can resolve an injection path, metadata.intelligence.contract.expandedInputSchema describes the effective input shape and inputContributions explains which injected definition contributed each field. Injection lint rules use that same backend-owned projection for findings such as hidden required input, conflicting injected fields, conditional required input, dynamic injection dependencies, and dynamic injected tool surfaces.
Runtime-observed injection data stays outside ProjectIndexSnapshot. The local devtools API exposes GET /api/project/index/observed-injection?limit=250 for trace-derived evidence such as observed source ids, included/excluded/dropped contribution counts, branch labels, injected tool names, prompt ids, compact run refs, and redacted prompt input-key summaries. The endpoint also adds soft comparison metadata such as indexMatch, toolIndex, and top-level drift entries when runtime observed sources or tools that are not present or not predicted by the current authored index. Clients can compose this endpoint with GET /api/project/index to show possible-versus-observed injection and runtime input validation, but should keep the distinction visible: unobserved runtime data does not mean a source-authored branch is impossible, observed input summaries contain key names rather than values, and observed runtime data should not mutate the authored index contract.
Serializers
serializeProjectIndex() converts known runtime prompt/context/tool snapshots into the Project Index shape. It is mainly used by the devtools integration and custom advanced integrations.
const snapshot = serializeProjectIndex({
prompts,
contexts,
tools,
project: { root: process.cwd(), name: 'my-app' },
})Clients should read the Go service through GET /api/project/index or GET /api/index rather than importing user source or running serializers themselves.