|
|
@@ -0,0 +1,739 @@
|
|
|
+# Plugin API — surfaces, config, trust, compat
|
|
|
+
|
|
|
+**Status:** design, awaiting review (CG-63). Nothing here is implemented yet.
|
|
|
+**Implements:** GH [#1376](https://github.com/colbymchenry/codegraph/issues/1376) · epic CG-62.
|
|
|
+**Gates:** CG-64 (internal registry), CG-65 (config), CG-66 (types package), CG-67
|
|
|
+(loader), CG-68 (end-to-end), CG-69 (example + harness), CG-70 (docs), CG-71
|
|
|
+(validation), CG-72 (language-provider spike).
|
|
|
+**Verified against the tree at:** `feature/CG-62`, 2026-09-02. Every file:line in
|
|
|
+this doc was read; re-check them before trusting a claim.
|
|
|
+
|
|
|
+Goal, in the issue's words: *implement a plugin → install it → list it in config →
|
|
|
+sync*. An internal team ships proprietary framework/route support without forking
|
|
|
+core and without waiting on a release. Built-ins register through the same path, so
|
|
|
+every release exercises the plugin API.
|
|
|
+
|
|
|
+This doc makes the decisions the rest of the epic implements. Where the issue text
|
|
|
+proposed a default, it is either confirmed with a reason or argued down; two
|
|
|
+proposals are argued down (`.codegraph/config.json`, and loading through
|
|
|
+`CodeGraph.open()`). Genuinely open items are collected in
|
|
|
+[Open questions](#open-questions) rather than papered over.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. Two surfaces, phased
|
|
|
+
|
|
|
+**v1 ships one surface: framework / semantic plugins.** A plugin contributes:
|
|
|
+
|
|
|
+| Contribution | Interface | Where it runs today |
|
|
|
+|---|---|---|
|
|
|
+| Framework resolver | `FrameworkResolver` (`src/resolution/types.ts:218`) | parse workers (`extract`), main + resolver workers (`detect`/`resolve`/`claimsReference`/`postExtract`) |
|
|
|
+| Synthesis pass | `SynthPassDef` (`src/resolution/callback-synthesizer.ts:3528`) | resolver workers, merged on the main thread |
|
|
|
+
|
|
|
+`FrameworkResolver` is already plugin-shaped — `detect` / `resolve` /
|
|
|
+`claimsReference` / `extract` / `postExtract`, all pure over a
|
|
|
+`ResolutionContext` — which is why it is the v1 surface and not a new invention.
|
|
|
+`SynthPassDef` is the same idea for whole-graph passes: `run(queries, ctx, yield)`
|
|
|
+returns edges and persists nothing itself, so a pass is a pure function of
|
|
|
+committed graph state. That is ytfh44's `SemanticPlugin.augment(graph) →
|
|
|
+GraphPatch` with the names this codebase already uses.
|
|
|
+
|
|
|
+**Language providers are deferred** to spike CG-72. See
|
|
|
+[§10](#10-why-language-providers-are-deferred).
|
|
|
+
|
|
|
+Deliberately *not* in v1, and why:
|
|
|
+
|
|
|
+- **Node-synthesis hooks** (the Lombok-style `synthesizeMembers` seam inside
|
|
|
+ `extractClass`). A plugin that wants nodes emits them from a framework
|
|
|
+ resolver's `extract()`, which already returns `{ nodes, references }`. Opening a
|
|
|
+ second node-producing seam before the first one has an external user is
|
|
|
+ speculative.
|
|
|
+- **Query/read-time hooks.** No plugin code runs when a graph is *read* — see
|
|
|
+ [§3](#3-the-fact-everything-else-follows-from-plugins-are-a-write-path-concern).
|
|
|
+- **Anything per-request or per-tool-call.** No HTTP hook, no MCP tool
|
|
|
+ contribution, no network capability. Same section.
|
|
|
+
|
|
|
+marcelovani's three-layer model (core registry → official built-ins → user
|
|
|
+plugins) is adopted as-is for layer 1 and 2 semantics: the registry is the only
|
|
|
+registration path, and the ~35 built-in resolvers in
|
|
|
+`src/resolution/frameworks/index.ts` go through it (CG-64). Layer 2 as *separate
|
|
|
+published packages* is explicitly out of scope per the issue ("splitting every
|
|
|
+built-in into its own repo") — built-ins stay in-tree, they just stop being a
|
|
|
+hardcoded array.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. Where the extension points are today
|
|
|
+
|
|
|
+Read this before the rest; the constraints in §3–§9 come from it.
|
|
|
+
|
|
|
+| Thing | Location | Property that matters |
|
|
|
+|---|---|---|
|
|
|
+| `FRAMEWORK_RESOLVERS` | `src/resolution/frameworks/index.ts:42` | plain array, **order is semantic** |
|
|
|
+| `registerFrameworkResolver()` | `src/resolution/frameworks/index.ts:142` | exists, **zero callers**, replaces by name |
|
|
|
+| `detectFrameworks()` | `.../index.ts:116` | already try/catches a throwing `detect` |
|
|
|
+| `SYNTH_PASSES` | `src/resolution/callback-synthesizer.ts:3548` | merge order = array order, **first-seen wins** a duplicate `(source,target)` |
|
|
|
+| Pass dispatch **by name** into a worker | `src/resolution/resolver-worker.ts:101` | `SYNTH_PASSES.find(p => p.name === msg.pass)` |
|
|
|
+| Framework lookup **by name** in a parse worker | `src/extraction/parse-worker.ts:95` | `getAllFrameworkResolvers().filter(r => frameworkNames.includes(r.name))` |
|
|
|
+| Project config | `src/project-config.ts` (`codegraph.json`, root, committed) | mtime-cached, **keyed by project root** |
|
|
|
+| `.codegraph/` ignore rule | `src/directory.ts:652` | `*` + `!.gitignore` — **everything in it is gitignored** |
|
|
|
+| Index staleness stamp | `indexed_with_extraction_version`, `CodeGraph.isIndexStale()` (`src/index.ts:1230`) | the existing "re-index recommended" channel |
|
|
|
+| MCP lazy engine load | `src/mcp/engine.ts:22-27`, `:252` | `CodeGraph.open()` is deliberately **off** the MCP startup path |
|
|
|
+
|
|
|
+Two of these are load-bearing and easy to miss:
|
|
|
+
|
|
|
+**Plugins must load in three process contexts, not one.** The main thread detects
|
|
|
+frameworks and ships their **names** to parse workers, which look the names up in
|
|
|
+their *own* copy of the registry (`parse-worker.ts:95`); resolver workers each
|
|
|
+construct a full `ReferenceResolver` and run synth passes looked up by *name*
|
|
|
+(`resolver-worker.ts:101`). A plugin registered only on the main thread would have
|
|
|
+its `extract()` silently skipped — and worse, `frameworksNeedDecode` would compute
|
|
|
+`false`, sending the file down the kernel raw-buffer fast path, so the failure mode
|
|
|
+is "no error, fewer nodes". Registration therefore happens in **every** context
|
|
|
+that builds a resolver or runs a pass, from one resolved plugin set (§4.4).
|
|
|
+
|
|
|
+**Config is loaded per call site, keyed by root — not threaded through `open()`.**
|
|
|
+`loadExtensionOverrides(root)` and friends are module-level, mtime-cached, and
|
|
|
+root-keyed precisely so the daemon can host several projects in one process
|
|
|
+(`project-config.ts:104`). Plugins follow that pattern (§5.3).
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. The fact everything else follows from: plugins are a write-path concern
|
|
|
+
|
|
|
+> **A plugin's code runs only while the graph is being written — `index`, `sync`,
|
|
|
+> and the watcher's sync. No read path ever loads plugin code.**
|
|
|
+
|
|
|
+Every reader — `codegraph_explore`, `codegraph_node`, the CLI's `query` /
|
|
|
+`context` / `affected`, the `ui` server — reads nodes and edges a plugin already
|
|
|
+wrote. Their shape is the engine's own (`NodeKind`/`EdgeKind` in `src/types.ts`),
|
|
|
+so a reader cannot tell a plugin's edge from a built-in's except by the provenance
|
|
|
+metadata it deliberately carries (§4.3).
|
|
|
+
|
|
|
+This single rule settles four otherwise-hard questions at once:
|
|
|
+
|
|
|
+- **MCP attach latency** (§8): zero plugin work on `serve --mcp` startup, zero on a
|
|
|
+ tool call. The existing lazy-load of the whole CodeGraph chain off the MCP
|
|
|
+ startup path (`engine.ts:22`, worth ~800ms) stays intact because plugins do not
|
|
|
+ hang off `open()`.
|
|
|
+- **ui-server security** (§6): the loopback server never executes plugin code, so
|
|
|
+ the DNS-rebinding boundary in `src/ui-server/security.ts` is untouched and there
|
|
|
+ is nothing new behind it. Plugins get no request hook and no network capability
|
|
|
+ *by construction* — not by a rule someone must remember.
|
|
|
+- **Determinism** (§7): a plugin influences the graph at exactly one point in time
|
|
|
+ (a write run), so "same inputs → same graph" is checkable by re-indexing.
|
|
|
+- **Blast radius of a broken plugin** (§9): it degrades an index run. It can never
|
|
|
+ make a *query* fail, which is what would teach an agent to abandon codegraph.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. The plugin contract
|
|
|
+
|
|
|
+### 4.1 Module shape
|
|
|
+
|
|
|
+A plugin package's entry module default-exports a **factory**:
|
|
|
+
|
|
|
+```ts
|
|
|
+import type { CodeGraphPlugin, PluginContext, PluginContributions } from '@colbymchenry/codegraph-plugin-api';
|
|
|
+
|
|
|
+export default function plugin(ctx: PluginContext): PluginContributions {
|
|
|
+ return {
|
|
|
+ frameworks: [acmeRoutesResolver(ctx.options)],
|
|
|
+ synthPasses: [],
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```ts
|
|
|
+interface PluginContext {
|
|
|
+ /** Absolute, resolved project root. */
|
|
|
+ projectRoot: string;
|
|
|
+ /** The `options` object from this plugin's config entry, verbatim (unvalidated). */
|
|
|
+ options: Record<string, unknown>;
|
|
|
+ /** Running engine version, for a plugin that wants to feature-detect. */
|
|
|
+ engineVersion: string;
|
|
|
+ /** Structured diagnostics. Plugins must not write to stdout — it is the MCP transport. */
|
|
|
+ log: { warn(msg: string, meta?: object): void; debug(msg: string, meta?: object): void };
|
|
|
+}
|
|
|
+
|
|
|
+interface PluginContributions {
|
|
|
+ frameworks?: FrameworkResolver[];
|
|
|
+ synthPasses?: SynthPass[];
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+A factory, not a static object, because options are per-project and the same
|
|
|
+package may be loaded for two roots in one daemon process. The factory must be
|
|
|
+**cheap and side-effect-free** — it constructs resolvers, it does not scan the
|
|
|
+repo. It is called once per (root, plugin) per process.
|
|
|
+
|
|
|
+`ctx.log` exists because `console.log` in a plugin would corrupt the MCP stdio
|
|
|
+transport. This is a hard rule for plugin authors, stated in the authoring guide
|
|
|
+(CG-70), not enforceable at runtime.
|
|
|
+
|
|
|
+### 4.2 Manifest, and why it lives in `package.json`
|
|
|
+
|
|
|
+```jsonc
|
|
|
+// package.json of @acme/codegraph-plugin-internal-routes
|
|
|
+{
|
|
|
+ "name": "@acme/codegraph-plugin-internal-routes",
|
|
|
+ "version": "1.2.0",
|
|
|
+ "main": "dist/index.js",
|
|
|
+ "codegraph": {
|
|
|
+ "id": "acme-internal-routes",
|
|
|
+ "apiVersion": 1,
|
|
|
+ "engines": ">=1.5.0",
|
|
|
+ "capabilities": ["frameworks"]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| Field | Required | Meaning |
|
|
|
+|---|---|---|
|
|
|
+| `id` | yes | Stable identifier. Namespaces every contribution (§7.2), appears in diagnostics and in edge provenance. `[a-z0-9][a-z0-9-]*`, ≤ 64 chars. |
|
|
|
+| `apiVersion` | yes | Integer major of the plugin API this plugin was written against (§9). |
|
|
|
+| `engines` | no | semver range against the **engine** version. Absent = any. |
|
|
|
+| `capabilities` | yes | Declared contribution kinds: `"frameworks"`, `"synthPasses"`. Returning a contribution that was not declared is a load error. |
|
|
|
+
|
|
|
+**The manifest lives in `package.json`, not in the module's exports**, for one
|
|
|
+reason: it is the only version of the check that happens *before* the plugin's code
|
|
|
+executes. `require()` runs module top-level code; a manifest read from an export
|
|
|
+can only gate the factory call, which is already too late. Reading
|
|
|
+`package.json` lets the loader reject an `apiVersion: 2` plugin on a v1 engine
|
|
|
+without ever evaluating it.
|
|
|
+
|
|
|
+*Exception, stated honestly:* a plugin loaded by **path specifier** (a single file
|
|
|
+committed in a non-Node repo, §5.2) may have no `package.json`. Those may export a
|
|
|
+named `manifest` object instead, and the compat check then happens after module
|
|
|
+evaluation. The doc says so out loud rather than pretending the guarantee is
|
|
|
+uniform; the real protection for that case is the trust model (§6), not the check.
|
|
|
+
|
|
|
+**On mismatch — `apiVersion` unsupported, `engines` unsatisfied, manifest missing
|
|
|
+or malformed, `id` colliding — the plugin is skipped with a diagnostic and the run
|
|
|
+continues.** Never a crash, never a partial registration. (§9 covers what a
|
|
|
+mismatch *says*.)
|
|
|
+
|
|
|
+### 4.3 What contributions must guarantee
|
|
|
+
|
|
|
+Promoted from in-tree convention to public contract:
|
|
|
+
|
|
|
+1. **`postExtract` idempotency and id preservation.** The contract already
|
|
|
+ documented at `src/resolution/types.ts:246-255` becomes public verbatim: the
|
|
|
+ node `id` MUST be preserved so existing edges survive; `qualifiedName` SHOULD be
|
|
|
+ preserved so a second run can recover the original in-file form. `postExtract`
|
|
|
+ runs again on **every incremental sync**, so a non-idempotent pass corrupts the
|
|
|
+ graph slowly rather than loudly.
|
|
|
+2. **Node ids must be namespaced.** Built-ins use scheme-prefixed ids
|
|
|
+ (`route:<file>:<line>:<METHOD>:<path>` — `frameworks/express.ts:171`;
|
|
|
+ `expo-module:` for cross-platform pairs). A plugin's synthetic node ids must be
|
|
|
+ prefixed `plugin:<id>:` so two plugins can never collide and so a node can be
|
|
|
+ traced back to what made it.
|
|
|
+3. **Synthesized edges carry provenance.** `provenance: 'heuristic'` plus
|
|
|
+ `metadata.synthesizedBy = '<plugin id>'` and, where the wiring site is known,
|
|
|
+ `metadata.registeredAt = 'file:line'` — the same shape the built-in
|
|
|
+ synthesizers use.
|
|
|
+4. **A plugin edge must say what it means.** `ContextBuilder` renders a
|
|
|
+ synthesized hop from a **closed** `synthesizedBy` switch
|
|
|
+ (`src/context/index.ts:405-421`) whose fallback is the string `event …`. An
|
|
|
+ unknown plugin value would therefore render as a fabricated "event" hop — the
|
|
|
+ exact overclaiming CLAUDE.md forbids. So the contract requires
|
|
|
+ `metadata.label` (a short human phrase, e.g. `"Acme route → handler"`) on any
|
|
|
+ plugin-synthesized edge, and CG-68 must make the renderer prefer
|
|
|
+ `metadata.label` and fall back to `via <plugin id>` — never to `event`.
|
|
|
+5. **Purity.** No wall clock, no randomness, no network, no dependence on file
|
|
|
+ visit order or on state carried between files. See §7.3.
|
|
|
+6. **No direct DB writes.** Contributions return data; the engine persists it.
|
|
|
+ `ResolutionContext` is a read interface and stays one.
|
|
|
+
|
|
|
+### 4.4 One resolved plugin set per run
|
|
|
+
|
|
|
+At the start of a write run the main thread produces a **resolved plugin set**:
|
|
|
+for each config entry, `{ id, version, resolvedPath, options }`, in config order.
|
|
|
+That object — not the config file path — is what parse workers and resolver
|
|
|
+workers receive in their init message.
|
|
|
+
|
|
|
+Consequence: every context loads the *same* modules with the *same* options even
|
|
|
+if `codegraph.json` is edited mid-run, and a worker never re-reads or re-resolves
|
|
|
+config. This is the direct analogue of the main thread resolving the language and
|
|
|
+shipping it to the parse worker (`parse-worker.ts:82-86`) rather than letting each
|
|
|
+worker detect independently.
|
|
|
+
|
|
|
+Workers load only what they need: `"frameworks"` → parse workers and resolver
|
|
|
+workers; `"synthPasses"` → resolver workers only. A plugin declaring neither is
|
|
|
+never loaded in a worker at all.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. Configuration
|
|
|
+
|
|
|
+### 5.1 The file is `codegraph.json` — **not** `.codegraph/config.json`
|
|
|
+
|
|
|
+The issue proposed `.codegraph/config.json` "since the dir already exists".
|
|
|
+Rejected, on evidence:
|
|
|
+
|
|
|
+- **`.codegraph/` is entirely gitignored.** `init` writes `.codegraph/.gitignore`
|
|
|
+ containing `*` + `!.gitignore` (`src/directory.ts:645-657`) so transient files —
|
|
|
+ the DB, `daemon.pid`, sockets — are never committed. A plugin list is the
|
|
|
+ opposite kind of state: it must be committed, reviewed, and shared, or the whole
|
|
|
+ premise ("a teammate clones and gets the same graph") fails. It is also the file
|
|
|
+ a security reviewer must be able to see in a diff (§6).
|
|
|
+- **A committed project config already exists.** `codegraph.json` at the project
|
|
|
+ root (`PROJECT_CONFIG_FILENAME`, `src/project-config.ts:32`) is documented as
|
|
|
+ "committed … that a team shares through version control" and already carries
|
|
|
+ `extensions`, `include`, `exclude`, `includeIgnored`, `deprioritize`. A second
|
|
|
+ config file would be a second answer to "where do I configure codegraph".
|
|
|
+- The `.codegraph/` dir does get one job here, and it is the right one: the
|
|
|
+ machine-local trust stamp (§6.3), which must *not* be committed.
|
|
|
+
|
|
|
+### 5.2 Schema
|
|
|
+
|
|
|
+```jsonc
|
|
|
+{
|
|
|
+ "plugins": [
|
|
|
+ "@acme/codegraph-plugin-internal-routes",
|
|
|
+ {
|
|
|
+ "name": "@acme/codegraph-plugin-rpc",
|
|
|
+ "options": { "protoDir": "api/proto" }
|
|
|
+ },
|
|
|
+ { "name": "./tools/codegraph/legacy-routes.js" }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- Entries are objects `{ name, options?, replaces? }`. A bare string is sugar for
|
|
|
+ `{ name }`, normalized at parse time.
|
|
|
+- `name` is a **module specifier**: a bare package name (resolved from the project
|
|
|
+ root, §7.1) or a project-relative path beginning `./` or `../` that stays inside
|
|
|
+ the root.
|
|
|
+- `options` is passed to the factory verbatim. The engine does not validate it;
|
|
|
+ the plugin does (and reports through `ctx.log`).
|
|
|
+- `replaces?: string[]` — built-in resolver names this entry displaces. See §7.2.
|
|
|
+
|
|
|
+Validation follows `project-config.ts`'s existing posture exactly: **an invalid
|
|
|
+entry is warned-and-skipped, never fatal; a malformed file degrades to the
|
|
|
+zero-config default.** A non-array `plugins`, an entry without a string `name`, an
|
|
|
+absolute path, a path escaping the root — each is one `logWarn` and a skip.
|
|
|
+
|
|
|
+Unknown keys are already ignored by `parseConfig`, so adding `plugins` is
|
|
|
+backward-compatible with older engines — with one honest caveat: an older engine
|
|
|
+silently produces a graph *without* the plugin's edges. The index stamp (§5.4)
|
|
|
+makes that visible on the machine that has the newer engine, not on the old one.
|
|
|
+
|
|
|
+### 5.3 One load path — the existing one
|
|
|
+
|
|
|
+`loadPluginEntries(rootDir): PluginEntry[]` joins `loadExtensionOverrides`,
|
|
|
+`loadExcludePatterns`, … in `src/project-config.ts`, sharing the same
|
|
|
+mtime-cached, root-keyed `loadParsedConfig`.
|
|
|
+
|
|
|
+The issue proposed that "ALL entry points read it through one load path in
|
|
|
+`CodeGraph.open()`". The *intent* is right — one load path — but `open()` is the
|
|
|
+wrong place:
|
|
|
+
|
|
|
+- `open()` is deliberately lazy-required off the MCP startup path
|
|
|
+ (`src/mcp/engine.ts:22-27`) and is not called at all until the first tool call
|
|
|
+ touches a project. Hanging plugin resolution off it puts plugin `require()` cost
|
|
|
+ on the first tool call, which is the latency that matters most (§8).
|
|
|
+- The daemon hosts several roots in one process; root-keyed module-level caching is
|
|
|
+ precisely why `project-config.ts` is built the way it is.
|
|
|
+
|
|
|
+So: **config parsing** goes through the one existing loader, from any entry point,
|
|
|
+at any time. **Plugin loading** happens once per write run, on the write path, from
|
|
|
+the resolved set (§4.4). Both are single paths; they are just not the same path.
|
|
|
+
|
|
|
+### 5.4 When the plugin list changes
|
|
|
+
|
|
|
+Two mechanisms, no hot reload (explicitly out of scope in the issue):
|
|
|
+
|
|
|
+- **Index stamp.** A write run records `indexed_with_plugins` in
|
|
|
+ `project_metadata` — the sorted `id@version` list actually loaded. `codegraph
|
|
|
+ status` compares it to the currently resolved set and, on a difference, prints
|
|
|
+ the same kind of hint `isIndexStale()` already drives: *the plugin set changed
|
|
|
+ since this index was built — run `codegraph index`.*
|
|
|
+- **Live daemon.** Editing `codegraph.json` already triggers
|
|
|
+ `FileWatcher.refreshScope()` and a full rescan (`src/sync/watcher.ts:581,614`).
|
|
|
+ That rescan runs with the plugin set the **process** loaded at start, so a newly
|
|
|
+ listed plugin will not take effect until restart. This is a real, narrow drift
|
|
|
+ (the same class of bug #1590 fixed for scope fields): the rescan's graph and a
|
|
|
+ restarted process's graph differ. v1 handles it by *saying so* — the stamp
|
|
|
+ mismatch above is written by that rescan too, so `status` and the index summary
|
|
|
+ both report "plugin set changed; restart the MCP server / re-index" — rather than
|
|
|
+ by pretending a `require()` cache can be invalidated. Recorded as a known
|
|
|
+ limitation in the authoring guide (CG-70).
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. Trust and security
|
|
|
+
|
|
|
+### 6.1 What is actually true
|
|
|
+
|
|
|
+A plugin is `require()`d into the engine process. It has the full authority of that
|
|
|
+process: filesystem, network, `child_process`. **CodeGraph does not sandbox
|
|
|
+plugins, and v1 does not claim to.** The security property on offer is *consent*,
|
|
|
+not containment: code runs because a human put its name in a version-controlled
|
|
|
+file that shows up in review.
|
|
|
+
|
|
|
+Stating this plainly is the point. Node's `--permission` model could bound a
|
|
|
+plugin's fs/net access, but it applies process-wide and the engine itself needs
|
|
|
+broad fs access; a per-plugin sandbox means a separate process with an IPC-shaped
|
|
|
+contribution API, which is a much larger project than #1376. Out of scope for v1,
|
|
|
+noted in [Open questions](#open-questions) as the natural v2 lever.
|
|
|
+
|
|
|
+### 6.2 The two rules
|
|
|
+
|
|
|
+1. **Explicit listing only.** Only entries present in the project's committed
|
|
|
+ `codegraph.json` are ever loaded. **No auto-discovery** — not by
|
|
|
+ `codegraph-plugin-*` name prefix, not by scanning `node_modules`, not by a
|
|
|
+ `codegraph` key found in an arbitrary installed package. Confirming the issue's
|
|
|
+ proposal, and the reason is worth keeping in the doc: with prefix discovery, any
|
|
|
+ transitive dependency that renames itself becomes an execution vector inside a
|
|
|
+ tool that runs over the user's entire source tree.
|
|
|
+2. **Specifier confinement.** A bare name resolves only through the *project's own*
|
|
|
+ resolution paths (§7.1) — i.e. something the user installed. A path specifier
|
|
|
+ must stay inside the project root; absolute paths and `../` escapes are rejected
|
|
|
+ at parse time, consistent with the engine's existing path-refusal posture
|
|
|
+ (`validatePathWithinRoot`).
|
|
|
+
|
|
|
+### 6.3 The hostile-repo case, and the trust stamp
|
|
|
+
|
|
|
+The uncomfortable corollary of "the config is committed": cloning a hostile
|
|
|
+repository and running `codegraph init` would execute whatever `./tools/evil.js`
|
|
|
+its `codegraph.json` lists. This is the same exposure class as VS Code workspace
|
|
|
+tasks or an ESLint config's plugins, and it deserves an explicit answer rather than
|
|
|
+a footnote.
|
|
|
+
|
|
|
+Proposal — **workspace-trust, machine-local**:
|
|
|
+
|
|
|
+- The first time a project's plugin set is loaded, the engine records a stamp in
|
|
|
+ `.codegraph/plugins-trust.json` (gitignored by construction, per-machine): the
|
|
|
+ hash of the normalized `plugins` array plus the resolved ids/versions.
|
|
|
+- When the stamp is absent or does not match:
|
|
|
+ - **interactive CLI** (`codegraph init` / `index` / `sync` on a TTY) → prompt
|
|
|
+ once, listing exactly what will be loaded, and record the answer;
|
|
|
+ - **non-interactive** (MCP serve, the daemon, CI, `--yes`) → **skip all plugins**
|
|
|
+ with a diagnostic telling the user to run `codegraph plugins trust` (or index
|
|
|
+ once interactively). Never execute unreviewed plugin code from a context that
|
|
|
+ cannot ask.
|
|
|
+- `CODEGRAPH_PLUGINS=0` disables plugin loading entirely, mirroring
|
|
|
+ `CODEGRAPH_KERNEL=0`. A `--no-plugins` flag on `init`/`index`/`sync` does the
|
|
|
+ same for one run.
|
|
|
+
|
|
|
+Note the split: a **bare package** specifier can only load if the user already ran
|
|
|
+`npm install`, which already executes that package's install scripts — the marginal
|
|
|
+risk is small. A **path specifier** ships inside the repo, so it is the case the
|
|
|
+prompt exists for. If the prompt is judged too much UX for v1, the fallback is
|
|
|
+"path specifiers require the stamp, bare specifiers do not" — listed in
|
|
|
+[Open questions](#open-questions).
|
|
|
+
|
|
|
+### 6.4 Interaction with the ui-server loopback boundary
|
|
|
+
|
|
|
+None, by construction (§3): `codegraph ui` is a read surface, it never loads plugin
|
|
|
+code, and no plugin hook runs per HTTP request. The boundary in
|
|
|
+`src/ui-server/security.ts` — loopback `Host` check, no CORS headers,
|
|
|
+`resolveProjectFile` as the single read chokepoint — is unchanged and unextended.
|
|
|
+v1 grants plugins **no network capability of any kind**: no listener, no route, no
|
|
|
+outbound helper in `PluginContext`. A plugin that dials out is doing it with raw
|
|
|
+Node APIs, which is exactly what §6.1 says the trust model is for.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. Module resolution, registry order, determinism
|
|
|
+
|
|
|
+### 7.1 Resolving a specifier inside the bundled runtime
|
|
|
+
|
|
|
+CodeGraph ships its own Node (`scripts/build-bundle.sh`, currently v24.16.0), so
|
|
|
+the engine's own `require` resolves against the *bundle*, not the user's project.
|
|
|
+Plugins must resolve against the **project**:
|
|
|
+
|
|
|
+```ts
|
|
|
+const req = createRequire(path.join(projectRoot, 'package.json'));
|
|
|
+const resolved = req.resolve(specifier); // bare name → project node_modules; './x' → in-repo file
|
|
|
+```
|
|
|
+
|
|
|
+`createRequire` from the project root is the same mechanism the kernel loader
|
|
|
+already uses for our own native binary (`src/extraction/kernel/loader.ts:151`).
|
|
|
+
|
|
|
+- **CJS and ESM both supported.** `require()` first; on `ERR_REQUIRE_ESM` fall back
|
|
|
+ to `await import(pathToFileURL(resolved))`. On the bundled Node 24, `require()`
|
|
|
+ of an ESM graph without top-level await already works, so the fallback is mostly
|
|
|
+ for TLA modules. The default export is unwrapped from `.default` in either case.
|
|
|
+- **Native addons are banned in v1.** A `.node` binding in a resolved specifier is
|
|
|
+ a load error with a specific message. The bundled runtime is Node 24.x; addons in
|
|
|
+ a user's `node_modules` are built against whatever Node they installed with
|
|
|
+ (commonly 20 or 22), and the module ABIs differ — the failure without the ban is
|
|
|
+ an opaque `NODE_MODULE_VERSION` error attributed to codegraph. We control our own
|
|
|
+ kernel binary's build; we cannot control a plugin's.
|
|
|
+- **wasm is allowed** and is the recommended escape hatch for native-speed work —
|
|
|
+ the engine already loads wasm grammars, and a wasm module is ABI-portable across
|
|
|
+ the bundled runtime.
|
|
|
+- Resolution failure (`MODULE_NOT_FOUND`) is a skip-with-diagnostic naming the
|
|
|
+ package and suggesting `npm install`, not a crash.
|
|
|
+
|
|
|
+**Non-Node projects** (a Go or Rails repo with no `node_modules`) get the path
|
|
|
+specifier: commit the plugin — a small built JS file or a vendored package
|
|
|
+directory — anywhere in the repo and list it as `./tools/codegraph/plugin.js`.
|
|
|
+Recommended over the alternatives: `.codegraph/plugins/` with its own
|
|
|
+`package.json` would be gitignored (§5.1) and so invisible to teammates and to
|
|
|
+review; a global `~/.codegraph/plugins` dir is neither per-project nor
|
|
|
+version-controlled. A team that wants dependency management in a non-Node repo can
|
|
|
+put a real `package.json` in `tools/codegraph/` and list
|
|
|
+`./tools/codegraph/node_modules/@acme/plugin` — no new mechanism needed. (Open
|
|
|
+question 2 if the maintainer wants a first-class install dir anyway.)
|
|
|
+
|
|
|
+### 7.2 Registry order and duplicates
|
|
|
+
|
|
|
+- **Built-ins first, in their current array order; then plugins, in config order.**
|
|
|
+ Never interleaved, never sorted. `FRAMEWORK_RESOLVERS` order is semantic (the
|
|
|
+ comments in `frameworks/index.ts` explain which resolver must see a reference
|
|
|
+ first), and `SYNTH_PASSES` order decides which duplicate `(source, target)` edge
|
|
|
+ wins — the cross-tier pass sits before the emitter pass *on purpose*
|
|
|
+ (`callback-synthesizer.ts:3550-3554`).
|
|
|
+- **Duplicate ids are a load error**, not a silent replace. Today
|
|
|
+ `registerFrameworkResolver` splices out a same-named resolver and pushes the new
|
|
|
+ one — fine as a dead export, wrong as a public API: a plugin could shadow
|
|
|
+ `express` by accident and no one would know. CG-64 changes it to reject a
|
|
|
+ duplicate (skip + diagnostic).
|
|
|
+- **Displacing a built-in is explicit and belongs to the user, not the plugin
|
|
|
+ author**: `{ "name": "@acme/drupal", "replaces": ["drupal"] }` in config. The
|
|
|
+ named built-ins are removed from the registry for that project, and `status`
|
|
|
+ reports the displacement. This is what makes CG-71's acceptance test possible —
|
|
|
+ porting `drupalResolver` out-of-tree and diffing the graph requires turning the
|
|
|
+ in-tree one off.
|
|
|
+- **Synth pass names** are namespaced on registration as `<plugin id>:<name>` so
|
|
|
+ the name-keyed dispatch into resolver workers (`resolver-worker.ts:101`) can
|
|
|
+ never collide with a built-in pass.
|
|
|
+- `SYNTH_PROGRESS_STEPS` is pinned by a test to the pass count; with plugin passes
|
|
|
+ the count becomes per-run, so CG-64 must make it a function of the *effective*
|
|
|
+ registry rather than a module constant.
|
|
|
+
|
|
|
+### 7.3 Determinism
|
|
|
+
|
|
|
+**Requirement: same repo + same plugin set → same graph, byte-for-byte in node and
|
|
|
+edge sets.** Two runs that differ mean a plugin is impure, and a non-deterministic
|
|
|
+graph is unreviewable — it breaks incremental-sync convergence (the invariant in
|
|
|
+`.kommandr/memory/sync-rebuild-convergence-invariant.md`) and every A/B measurement
|
|
|
+the project relies on.
|
|
|
+
|
|
|
+What the engine guarantees: fixed registry order; synth-pass results merged in
|
|
|
+registry order regardless of execution order (already true — passes persist
|
|
|
+nothing until the ordered merge, `callback-synthesizer.ts:3730-3738`); the same
|
|
|
+resolved plugin set in every worker (§4.4).
|
|
|
+
|
|
|
+What the plugin must guarantee: no clock, no randomness, no network, no
|
|
|
+cross-file mutable state, no dependence on visit order. The plugin test harness
|
|
|
+(CG-69) enforces it the only way that is honest — index twice and diff — and
|
|
|
+CG-71's validation gate includes a double-index diff on a real repo.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. Performance budget
|
|
|
+
|
|
|
+The constraint that dominates: **MCP attach is already borderline at ~2–3s**
|
|
|
+(CLAUDE.md, retrieval performance), and an agent that starts its first turn before
|
|
|
+codegraph attaches runs the whole task without codegraph.
|
|
|
+
|
|
|
+Commitments:
|
|
|
+
|
|
|
+1. **No plugin work on the MCP startup path, ever.** Plugins load on the write path
|
|
|
+ only (§3), not in `CodeGraph.open()`, not in `MCPEngine.ensureInitialized`, not
|
|
|
+ on a tool call. Attach latency is structurally unchanged — there is no code to
|
|
|
+ run, so there is nothing to measure or regress.
|
|
|
+2. **Plugins load at the start of an index/sync run**, after config resolution and
|
|
|
+ before extraction; the cost is `require()` + one factory call per plugin per
|
|
|
+ context.
|
|
|
+3. **Per-plugin timing is surfaced**: load ms and factory ms per plugin, per
|
|
|
+ context, behind `CODEGRAPH_PLUGIN_TIMINGS=1` (mirroring
|
|
|
+ `CODEGRAPH_SYNTH_TIMINGS`), and a one-line total in the index summary. A plugin
|
|
|
+ whose load exceeds **1s** gets a warning naming it — a slow plugin should be
|
|
|
+ attributable without a profiler.
|
|
|
+4. **The multiplier is explicit.** Load cost is paid once per process/thread that
|
|
|
+ needs the plugin: main thread + N parse workers (frameworks) + M resolver
|
|
|
+ workers. Capability-gated loading (§4.4) keeps a synth-pass-only plugin out of
|
|
|
+ parse workers entirely.
|
|
|
+5. **Gate (CG-71):** with the ported plugin installed, (a) fresh-index wall clock on
|
|
|
+ a control repo within run-to-run noise of the no-plugin build, (b) the explore
|
|
|
+ call budget and Read/Grep counts unchanged on the standard flow questions, (c)
|
|
|
+ MCP attach unchanged. Measured with the harness and model policy CLAUDE.md
|
|
|
+ mandates (Sonnet, ≥2 runs per arm, CLI shim blocked).
|
|
|
+
|
|
|
+Budgeting the *plugin's own* work is the plugin author's problem, with one engine
|
|
|
+guardrail: a plugin's synth pass runs under the same cooperative-yield discipline
|
|
|
+as a built-in (`yieldToLoop` is passed into `run`), because a pass that blocks the
|
|
|
+main thread trips the liveness watchdog and gets the process SIGKILLed — the #1091
|
|
|
+/ #1122 failure class. The authoring guide states this as a rule with the failure
|
|
|
+it prevents.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. Error isolation, diagnostics, and compatibility
|
|
|
+
|
|
|
+### 9.1 Failure policy
|
|
|
+
|
|
|
+| Failure | Behavior |
|
|
|
+|---|---|
|
|
|
+| Manifest missing / malformed / `apiVersion` unsupported / `engines` unsatisfied | Skip the plugin. Diagnostic. Run continues. |
|
|
|
+| Module not found / resolution refused (absolute path, escape, `.node`) | Skip. Diagnostic naming the fix. Run continues. |
|
|
|
+| Throw at module scope or in the factory | Skip. Diagnostic with the message and the plugin id. Run continues. |
|
|
|
+| Throw inside `detect` / `resolve` / `claimsReference` / `extract` / `postExtract` / a synth pass | **Disable that contribution for the session** on the first throw, log once with plugin id + hook + message, continue indexing. |
|
|
|
+| Anything above, in an MCP context | **Never `isError`.** |
|
|
|
+
|
|
|
+Disabling on first throw rather than swallowing every throw is deliberate: a hook
|
|
|
+that throws once per file would produce megabytes of log and a silently
|
|
|
+half-populated graph. Disabling is bounded and honest — the graph is missing that
|
|
|
+plugin's contribution, and `status` says which and why. It matches the existing
|
|
|
+posture around synthesis (a pass that fails on a worker is retried once, then
|
|
|
+skipped with a printed reason, `callback-synthesizer.ts:3778-3788`) and around
|
|
|
+`detect` (already try/caught, `frameworks/index.ts:116-127`).
|
|
|
+
|
|
|
+`isError: true` is reserved for security refusals and genuine malfunctions
|
|
|
+(`src/mcp/tools.ts:78-89`); one or two of them early in a session make an agent
|
|
|
+stop calling codegraph entirely. A plugin problem is a *user configuration*
|
|
|
+problem: it belongs in `status` and in the index summary, never in a tool response.
|
|
|
+
|
|
|
+### 9.2 Where diagnostics surface
|
|
|
+
|
|
|
+- **Index summary** — one line per plugin: `id@version — frameworks: 1, passes: 0
|
|
|
+ (loaded in 34ms)`, and a warning line per skipped/disabled plugin.
|
|
|
+- **`codegraph status`** — a `Plugins:` block after `Backend:`/`Journal:` listing
|
|
|
+ each configured entry with its state (`loaded` / `skipped: <reason>` /
|
|
|
+ `disabled: <hook> threw <message>`) and any built-ins it `replaces`. Because
|
|
|
+ `status` is a *different process* from the index run, the last run's plugin
|
|
|
+ diagnostics are persisted in `project_metadata` under `plugins_last_run` (JSON)
|
|
|
+ alongside `indexed_with_plugins` (§5.4).
|
|
|
+- **Never** in MCP tool output, and never on stdout from a plugin (§4.1).
|
|
|
+
|
|
|
+### 9.3 Versioning and compatibility policy
|
|
|
+
|
|
|
+- **The types package** — `@colbymchenry/codegraph-plugin-api` (CG-66) — is
|
|
|
+ versioned **independently of the engine** and is the only public surface. It
|
|
|
+ exports `CodeGraphPlugin`, `PluginContext`, `PluginContributions`,
|
|
|
+ `FrameworkResolver`, `SynthPass`, `ResolutionContext`, and the graph types
|
|
|
+ (`Node`, `Edge`, `UnresolvedRef`, `NodeKind`, `EdgeKind`, `Language`).
|
|
|
+- **`apiVersion` is an integer major.** The engine declares which majors it
|
|
|
+ accepts. v1 accepts `1` only. When a breaking change ships as major 2, the engine
|
|
|
+ accepts `{1, 2}` for **at least two engine minor releases**, warning in `status`
|
|
|
+ that major 1 is deprecated, then drops it in a major engine release with a
|
|
|
+ CHANGELOG `### Breaking Changes` entry.
|
|
|
+- **In the contract:** the interfaces above, the required members of
|
|
|
+ `ResolutionContext`, the `postExtract` id/idempotency rules, node-id namespacing,
|
|
|
+ edge provenance/label requirements, registry ordering.
|
|
|
+ **Not in the contract:** the *optional* members of `ResolutionContext` (they may
|
|
|
+ come and go; plugins already have to feature-detect them, which is why they are
|
|
|
+ optional in-tree), additive `NodeKind`/`EdgeKind` values, built-in resolver
|
|
|
+ internals, and anything a plugin reaches by importing from
|
|
|
+ `@colbymchenry/codegraph` directly rather than from the types package.
|
|
|
+- **Publishing:** mirror `@colbymchenry/codegraph-ui` — built and asserted in CI,
|
|
|
+ `"private": true`, packed only behind an env flag (`scripts/pack-npm.sh`) — and
|
|
|
+ keep it unpublished until CG-71 proves the API by re-implementing a built-in
|
|
|
+ out-of-tree with an identical graph. Publishing an API we have not used
|
|
|
+ externally is how it gets frozen wrong.
|
|
|
+- **Adding a built-in framework resolver in-tree is not a plugin-API breaking
|
|
|
+ change**, but it does change extraction output, so it keeps bumping
|
|
|
+ `EXTRACTION_VERSION` as today.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. Why language providers are deferred
|
|
|
+
|
|
|
+Deferred to spike **CG-72**, for reasons that are architectural rather than
|
|
|
+schedule-driven:
|
|
|
+
|
|
|
+- **The TS extractor is the fallback path, not the hot path.** Extraction routes
|
|
|
+ through the Rust kernel when `isRouted(language) && kernelSupports(language)`
|
|
|
+ (`src/extraction/kernel/index.ts:155`), with the WASM/TS extractor as the
|
|
|
+ deferral path. Freezing a public plugin API around `LanguageExtractor` would
|
|
|
+ publish the legacy architecture as the contract — ytfh44's point on #1376, and
|
|
|
+ the right one.
|
|
|
+- **The language axis is a closed union, not a registry.** `Language` is
|
|
|
+ `(typeof LANGUAGES)[number]` (`src/types.ts:124`) and `EXTRACTORS` is
|
|
|
+ `Partial<Record<Language, LanguageExtractor>>`
|
|
|
+ (`src/extraction/languages/index.ts:40`). A runtime-registered language means
|
|
|
+ opening that union everywhere it is used — grammars, detection, kernel routing,
|
|
|
+ branch guards, framework `languages` filters. That is ytfh44's phase 1 and 2, and
|
|
|
+ it is a bigger job than everything in this doc combined.
|
|
|
+- **Language knowledge is not only in extraction.** Import rules, member-call
|
|
|
+ syntax, chained-call behavior and type inference live in `src/resolution/`. A
|
|
|
+ language plugin that only supplies extraction still needs core edits, so shipping
|
|
|
+ one would be a promise the seam cannot keep.
|
|
|
+- **The right boundary already exists.** The kernel's ABI-versioned, file/batch-
|
|
|
+ grained buffer contract is a better long-term extension point than a per-AST-node
|
|
|
+ callback API, and it is backend-neutral by construction.
|
|
|
+
|
|
|
+The cost of *not* deciding is visible: #1563 asks, in as many words, "which layer
|
|
|
+should a new language target now?" CG-72 answers it. Until then the honest position
|
|
|
+— stated in the authoring guide (CG-70) — is: **framework and semantic behavior is
|
|
|
+pluggable today; a new language is still a core contribution**, and here is the
|
|
|
+issue tracking the change.
|
|
|
+
|
|
|
+The capability-levels idea from that comment (parser / symbols / imports / local
|
|
|
+calls / cross-file calls / type resolution / framework semantics, each ✓ / partial
|
|
|
+/ —) is worth adopting when the language surface lands. v1's `capabilities` field
|
|
|
+is deliberately the coarse contribution-kind list; it is forward-compatible with a
|
|
|
+richer per-language matrix.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Open questions
|
|
|
+
|
|
|
+1. **Trust prompt in v1?** §6.3 proposes a machine-local trust stamp plus an
|
|
|
+ interactive prompt, with non-interactive contexts skipping plugins. It is the
|
|
|
+ right security posture and it is real UX work. Alternative: require the stamp
|
|
|
+ only for **path** specifiers (code shipped inside the repo) and let bare
|
|
|
+ installed packages load unprompted, on the grounds that installing them already
|
|
|
+ executed their install scripts. *Recommendation: ship the stamp, prompt only for
|
|
|
+ path specifiers in v1.*
|
|
|
+2. **A first-class install dir for non-Node repos?** §7.1 recommends committed path
|
|
|
+ specifiers and no new mechanism. If the maintainer wants `codegraph plugins
|
|
|
+ install` with its own dir, it must live somewhere committed (`tools/codegraph/`,
|
|
|
+ not `.codegraph/`), and it is a new sub-project's worth of npm plumbing.
|
|
|
+ *Recommendation: no; revisit if plugin authorship actually takes off.*
|
|
|
+3. **Do plugin synth passes run in resolver workers in v1?** §4.4 says yes, since
|
|
|
+ parse workers already need plugin loading for `extract()` and the fallback path
|
|
|
+ (a pass that fails on a worker retries on the main thread) already exists.
|
|
|
+ Main-thread-only would be simpler but serializes plugin passes on exactly the
|
|
|
+ repos where synthesis is expensive. *Recommendation: worker-side.*
|
|
|
+4. **`replaces` on the config entry vs the manifest.** Config (§7.2) puts the
|
|
|
+ decision with the person who owns the repo; a manifest field would let a plugin
|
|
|
+ author displace a built-in for every user who installs it. *Recommendation:
|
|
|
+ config only.*
|
|
|
+5. **Publish the types package in v1?** §9.3 recommends prepared-not-published
|
|
|
+ until CG-71 passes, mirroring `codegraph-ui`. The counter-argument is that an
|
|
|
+ unpublished package makes external authorship awkward (plugin authors would
|
|
|
+ vendor the types). *Recommendation: unpublished through CG-71, publish with the
|
|
|
+ release that ships CG-68.*
|
|
|
+6. **Per-plugin sandboxing** (§6.1) is out of scope for v1. If it ever becomes a
|
|
|
+ requirement, the shape is a separate process with an IPC contribution protocol —
|
|
|
+ which the file/batch-grained boundary in CG-72 would also want. Worth keeping
|
|
|
+ the two in the same conversation.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## What each downstream task takes from here
|
|
|
+
|
|
|
+| Task | Consumes |
|
|
|
+|---|---|
|
|
|
+| **CG-64** internal registry | §2 (the three contexts), §7.2 (order, duplicate rejection, `replaces`, namespaced pass names, `SYNTH_PROGRESS_STEPS` becomes per-run) |
|
|
|
+| **CG-65** config | §5 (file choice + schema + `loadPluginEntries` + warn-and-skip posture + `indexed_with_plugins`) |
|
|
|
+| **CG-66** types package | §4 (module shape, manifest, contribution guarantees), §9.3 (what is and is not in the contract, publishing model) |
|
|
|
+| **CG-67** loader + lifecycle | §4.4 (resolved set), §6 (trust), §7.1 (resolution, ESM/CJS, native ban), §9.1–9.2 (failure policy, diagnostics) |
|
|
|
+| **CG-68** end-to-end | §3 (write-path only), §4.3.4 (renderer must honour `metadata.label`, never fall back to `event`), §5.4 |
|
|
|
+| **CG-69** example + harness | §7.3 (double-index determinism check), §4.1 (`ctx.log`, no stdout) |
|
|
|
+| **CG-70** docs | §6.1 (say plainly that plugins are unsandboxed), §5.4 (no hot reload), §8 (cooperative yield), §10 (a new *language* is still a core contribution) |
|
|
|
+| **CG-71** validation | §7.2 (`replaces` makes the built-in port possible), §8.5 (the perf gate) |
|
|
|
+| **CG-72** language spike | §10 |
|
|
|
+
|
|
|
+## References
|
|
|
+
|
|
|
+- GH [#1376](https://github.com/colbymchenry/codegraph/issues/1376) — the issue,
|
|
|
+ plus marcelovani's three-layer comment and ytfh44's two-surface / kernel-boundary
|
|
|
+ comment.
|
|
|
+- GH [#300](https://github.com/colbymchenry/codegraph/pull/300) — the blocked Drupal
|
|
|
+ contribution that motivates the epic; CG-71 ports it out-of-tree.
|
|
|
+- GH [#1563](https://github.com/colbymchenry/codegraph/issues/1563) — "which layer
|
|
|
+ should a new language target now?"; the cost of leaving §10 unanswered.
|
|
|
+- `docs/design/framework-coverage.md` — what a framework resolver has to produce
|
|
|
+ for each of the three pictures (routes, navigation, guards).
|
|
|
+- `docs/design/callback-edge-synthesis.md`,
|
|
|
+ `docs/design/dynamic-dispatch-coverage-playbook.md` — what a synthesis pass is
|
|
|
+ for and the validation bar it is held to.
|
|
|
+- `docs/design/native-extraction-kernel.md`, `docs/design/rust-kernel-migration-plan.md`
|
|
|
+ — the seam §10 defers to.
|