16 KiB
Write a CodeGraph extension (API v1 preview)
Use an extension to add framework extraction, reference resolution or semantic edges for a language CodeGraph already understands. The API does not add parsers, query-time tools, PHP branch guards or browser screen navigation automatically. Extension code runs with the user's permissions in the main process and workers; it is trusted executable code, not a sandbox. Installation never runs npm scripts.
These commands are in this development branch, not a published npm release.
Use a build from the draft PR. From a source checkout, run npm ci, npm run build,
then put dist/bin on PATH (or invoke node /absolute/path/dist/bin/codegraph.js
where this guide says codegraph). Embedded SDK consumers need Node 22.5–24.
Start outside the core repository
Run these commands from a separate working directory with an existing parent:
codegraph extensions create ./python-events --id python-events
codegraph extensions test ./python-events
codegraph extensions pack ./python-events --out ./python-events-0.1.0.cgext
create refuses an existing directory. It produces package.json, index.cjs,
extension.test.json, and a standalone README. There is no runtime dependency,
private core import, source-copying step or bundler requirement for this starter.
Keep the .cgext output outside the source directory. Packages contain UTF-8
JavaScript/JSON/documentation; bundle third-party dependencies before packing.
Native addons, symlinks, arbitrary file extensions and packages over 8 MiB are
rejected. This template keeps its Python/YAML test sources inside fixture JSON.
The starter describes a small Python event-map framework. A *.events.yaml file
has one unquoted event.name: python_function_name per line. The extension adds
event route nodes and labelled links to unique indexed Python function nodes.
Computed, unknown and ambiguous handlers remain unresolved. It is an example
contract for authors to adapt, not support for every Python event library.
Install into a non-Node application
Create a separate directory python-app with these two files:
checkout.events.yaml:
order.created: send_receipt
handlers.py:
def send_receipt():
return "sent"
codegraph extensions install ./python-events-0.1.0.cgext --path ./python-app
codegraph extensions list --path ./python-app
codegraph extensions remove python-events --path ./python-app
The managed installer initializes/indexes the target and pins the package bytes.
The target needs no package.json, npm install, Python execution or Python library.
While installed, inspect the event:order.created route and its Python event dispatch edge to send_receipt in the graph. The author test command below checks
these actual nodes/edges through the public graph API and verifies removal.
Public types and factory
All author types and helpers are exported from @colbymchenry/codegraph.
Do not import src/, dist/plugins/, database classes, or private registries.
For local editor/type checking before release, make a development package with
npm pack --ignore-scripts --pack-destination /absolute/path/to/artifacts in the
built core checkout, then in the author project run
npm install --save-dev /absolute/path/to/artifacts/colbymchenry-codegraph-1.6.0.tgz.
This is local packaging, not npm publication. The installed editor dependency is
excluded when packing the extension.
The CommonJS starter uses a JSDoc CodeGraphPlugin type and module.exports.
A TypeScript author can use import type { CodeGraphPlugin } from '@colbymchenry/codegraph', compile to CommonJS JavaScript, and set main to that
compiled file. ESM factories are also supported. Bundle runtime imports; a managed
package does not inherit the target project's npm dependencies.
| Public contract | Purpose |
|---|---|
CodeGraphPlugin / defineExtension(factory) |
Factory returning PluginContributions, optionally asynchronously |
PluginContext |
Absolute project root, cloned options, engine version, warning/debug logger |
PluginManifest / EXTENSION_API_VERSION |
Package ID, API version, engine range and declared capabilities; current API is 1 |
FrameworkResolver |
Synchronous detection, extraction, reference resolution and optional finalization |
ResolutionContext |
Read-only indexed node/file lookups; optional helpers must be checked before use |
SynthPass |
Async whole-graph pass returning semantic edges; optional language gate |
Node, Edge, NodeKind, EdgeKind, Language |
Graph output shapes and supported kinds/languages |
createExtensionProject, packExtension, testExtension |
The same starter, packer and test harness exposed by the CLI |
AuthorFixtureCase, AuthorTestReport |
Typed fixture assertions and test receipts |
Factories must create independent per-project state. They can be called in the main process and separate parse/resolver workers; do not rely on shared globals, one call per index, worker order, network results, or mutable external state. Use deterministic IDs and output. Keep synchronous hooks synchronous.
Framework hook contract:
detect(graph)returns whether the framework applies.languagesgates per-file extraction. The starter detects*.events.yamlfiles.extract(relativeFile, source)returns{ nodes: [], references: [] }. Every contributed ID starts withplugin:<manifest-id>:. Use a relative file path, supported kind/language, 1-based lines and 0-based columns. References must originate at a node returned by that extraction call.claimsReference(name)admits framework-specific reference names through the core prefilter.resolve(ref, graph)returns a labelledResolvedRefor null. Targets must exist; confidence is between 0 and 1. Never fabricate an endpoint when a name is missing or ambiguous.postExtract(graph)can update previously emitted nodes, preserving both ID and qualified name. It cannot introduce new nodes.
For synthPasses, declare that capability in the manifest and return objects with
name, optional languages, and async run(graph, yieldToLoop). Return Edge[]
whose endpoints exist, kind is supported and metadata.label is meaningful
(1–160 characters). Include metadata.registeredAt as relative/file:line when
known. For long loops call the supplied yield function. Core stamps
provenance: 'heuristic' and metadata.synthesizedBy; core owns persistence and
deterministic merging. Do not open SQLite or write graph files from a plugin.
Author harness and compatibility
codegraph extensions test ./python-events packs and validates the real artifact,
checks the current engine against codegraph.engines, then installs it into fresh
disposable projects through the managed installer. It checks the declared graph
expectations, clean rebuild determinism and removal cleanup. It closes graph
handles and removes its temporary directories on success and ordinary failures.
It executes your code with your permissions. It is not a security sandbox or a
substitute for testing large real applications, process kills and each platform.
Fixtures use format: "codegraph-extension-tests-1" and a nonempty cases array.
Each case has name, files (relative paths to UTF-8 source strings) and expect:
{
"nodes": [{"name": "event:order.created", "kind": "route"}],
"edges": [{"source": "event:order.created", "target": "send_receipt", "label": "Python event dispatch"}],
"absentNodes": [{"name": "event:computed.event"}],
"absentEdges": [{"source": "event:order.missing"}]
}
Node assertions apply only to this extension's contributions; optional kind
and filePath narrow matches. Edge assertions apply only to edges attributed to
this extension. Positive edges require source name, target name and label.
Negative edges can omit target/label to reject any outgoing contribution from
that source. At least one assertion is required per case. Traversal, hidden files,
codegraph.json and a root package.json are refused. Use --fixtures other.json
to select another fixture file inside the extension directory.
Add both successful flows and examples that must remain unresolved. Change an expected handler or label to verify that the harness fails; it must not pass just because an extension loads. Nonzero exit messages name the failing case and assertion or output contract. API versions other than 1 fail before evaluation; incompatible engine ranges report both the required and running versions. Malformed extraction outputs, node IDs and missing edge labels report the field requirements. Correct the extension rather than widening compatibility blindly.
For a compatibility matrix, run the same fixture command using each intended
CodeGraph build and actual operating system, preserving JSON output, engine
version and exit. engines is an assertion about those builds, not proof by itself.
The starter's range defaults to the current preview version through <2; narrow
it if you have not validated that range. The preview author/lifecycle checks
passed on Linux x64, Windows Server 2022 x64 and macOS 15 ARM64 with Node 22.23.2;
see the native validation report
for commands, exact revisions and untested combinations. This does not certify
your extension or every version in its declared range.
Publish through the local marketplace
In the core preview checkout, run node scripts/build-extensions.mjs, then
node marketplace/server/dev.cjs. Open the printed loopback URL, select Publish
an extension, upload the .cgext, and fill in the listing and your HTTPS source
repository URL. The browser signs the submission with its local publisher identity;
retain that browser profile to publish later versions of the same ID. Releases
are immutable. This is publication into your local development registry only.
Use codegraph extensions connect /absolute/path/python-app --marketplace <local-url>
to connect the selected target. Open the printed connection link, allow the local
connection, select the project, and install your extension. Alternatively install
the local registry's /api/download/<id>/<version> URL using the CLI. Remove it
with the managed removal command above. Existing signed publishing and browser
origin/window checks are shared with ordinary extensions.
The hosted HTTPS marketplace and durable production registry are unfinished. Do not deploy or publish to npm as part of this author exercise. Other preview gaps remain: compatible-version fallback, crash/stale-lock recovery and broader Drupal accuracy/performance review. The prior Drupal sample measured about 22–30% rebuild overhead versus the built-in resolver; it is not a general SLA.
Select compatible registry releases
The connected local engine selects the highest stable semantic version matching its own CodeGraph version and extension API, independent of publication time. The marketplace shows that selected version and destination before enabling Install. Switching destinations refreshes the selection. An incompatible latest release does not prevent installation of an older compatible release. A missing, invalid or unsupported API marker is not assumed compatible.
The same selection is available without a browser:
codegraph extensions install python-events --registry http://127.0.0.1:PORT --path /absolute/path/python-app
codegraph extensions update python-events --registry http://127.0.0.1:PORT --path /absolute/path/python-app
codegraph extensions install python-events --registry http://127.0.0.1:PORT --version 0.1.0 --path /absolute/path/python-app
Use the actual registry origin in place of the example URL. Remote registries
must use HTTPS; loopback HTTP is supported for development. --registry makes
the positional argument an extension ID. Without it, the argument remains an
exact package file or URL; --version is rejected for those artifact inputs.
Automatic selection never adopts prereleases or downgrades an installed
version. Opt into a preview using an exact --version 0.2.0-beta.1 pin. Exact
pins may intentionally select an older version, but must still match the
running engine and supported API; they never fall back. Semver engine ranges
use standard prerelease exclusion rules, so an engine preview must be explicitly
allowed by the package range. Build metadata does not change semver precedence;
equal-precedence versions have a deterministic lexical tie break.
No compatible release leaves the current configuration and graph unchanged and reports the rejected ranges/APIs. Catalogs are selection hints: the local installer rechecks the downloaded package ID, version, engine/API compatibility and SHA-256 digest. A changed browser selection must be refreshed before installing. Failed activation still rolls back the configuration and graph.
The companion's authenticated resolve command accepts project, id, and an
optional exact version. Registry install/update commands use the same fields
and may bind the displayed selection via selected: { version, integrity }.
They resolve only against the connected marketplace. Existing exact url plus
integrity commands keep their artifact semantics. All commands retain the
origin, window, local-host and bearer checks.
Interrupted lifecycle operations
Managed install/update/enable/disable/remove writes a recovery journal before changing package trust or project settings. After a killed process, opening or indexing the project, reading extension status/list, or starting another managed operation automatically reconciles that journal. You can also run:
codegraph extensions recover --path /absolute/path/project
Before the graph transaction commits, recovery restores the previous plugin settings and trust and retains the old graph. After that commit, recovery finishes the new state. Recovery does not execute extension code. A failed first install into an uninitialized project returns that project to uninitialized. Previously installed immutable packages remain available; an uncommitted new package and transaction-owned temporary graph files are cleaned up.
A live operation excludes another writer and pending-state readers. Wait for it to finish and retry; an old timestamp is never permission to take its lock. Modern ownership comes from a SQLite transaction, which the OS releases when the process exits, rather than from a PID that could be reused. Legacy live or ambiguous PID locks require operator confirmation, not a timeout override.
Unrelated config fields and trust entries survive recovery. If plugin settings, indexing rules, or transaction-owned trust/package bytes changed independently, recovery stops with the conflicting field and journal path. Keep a copy of your edit and the journal. Restore the indicated recorded plugin/settings or package bytes, run recovery, then reapply the intended edit and reindex. Do not discard a journal or remove a live lock to hide the conflict. Invalid/checksum-failing journals need a verified backup or manual repair; they are never guessed away. The companion returns the recovery error instead of claiming the project is ready.
Process-death checks passed on Linux x64, Windows Server 2022 x64 and macOS 15
ARM64 using local files and SQLite locking, including the default data directory
and a CODEGRAPH_DIR override. Extension package/trust files and recovery records
follow that override. The native report records both POSIX kills and Windows
termination results. Machine power loss, network filesystems, simultaneous
Windows/WSL project use, and mixed older writers bypassing coordination remain
unvalidated.
Direct edits to the graph database, deliberate deletion of the coordination
files, and concurrent project destruction are outside this recovery contract.
Semantic-pass synchronization
API v1 semantic passes can inspect the whole project and do not declare a dependency map. When source files change in a project with a semantic pass, sync builds and atomically commits a full candidate graph. Explicit indexFiles also refreshes the whole project. Scoped path arguments do not restrict that candidate: cross-file registrations must agree with their endpoints. A failed pass preserves the previous graph and reports failure. An unchanged healthy project does not rebuild. This trades incremental latency for correctness; large-project edits can cost a full rebuild. A future dependency/invalidation API is required before making this path incremental safely.