A plugin is what a project adds to Coherence through its config: a language
adapter (for a language Coherence lacks, or wrapping one it has), syntax tables
for the readings, a verifier that proves bullets, and a lexicon layer. One
interface carries all of them: @posthog/coherence/plugin, whose types ship
in the package as dist/plugins/api.d.ts. This page is the author's guide.
Plugins ship two ways:
- Bundled inside
@posthog/coherence, underplugins/<name>. TypeScript, Python, Rust, and the generic language-server adapter that reads the languages a config declares (declared) are bundled plugins that every project runs without naming them (plugins/defaults.jsonlists them, with the default language). Coherence's core names no language: everything it knows of one is asked of the plugin that provides it. Others ship bundled but run only for a project that names them,"<name>": "bundled": Go, Convex and Quint (below). - By path: a folder in the project (or anywhere the project names). This is how a project enhances a bundled plugin: copy it out, change it, and point the config at the copy.
A language with a language server can be checked with no code at all:
declare it under languages in the config (docs/config.md, "A language
Coherence has no adapter for"), and the bundled declared plugin drives its
server. That route stages its refutation as an unsaved document, so it works
only for a server that reports references from documents it has open. Write a
plugin when the server does not (sourcekit-lsp answers only from its on-disk
index), when the language's own tools know more than its server (a package
manifest that says which folders are tests, a compiler that refuses a private
name), or when the readings should read the language too (its declarations,
its entrances, its mass): a declared language gets the chokepoint check
alone.
{
"language": ["typescript"],
"plugins": {
"convex": "bundled",
"sample": "./coherence/plugins/sample"
}
}The key is the plugin's name, which its manifest must carry. The value is
"bundled" or a path to the plugin's folder. The key is the project's own: a
registry's plugins are never inherited by its leaves, since the paths are the
writer's.
A relative path must stay inside the project; one that climbs out of it
("../shared/plugin") is refused. To run a plugin from outside the project,
write its absolute path out: that is the explicit form, and it says on the page
that the code comes from elsewhere.
A plugin's code runs with the project's privileges: whatever the user running
Coherence may read, write or start, it may too. Its module (the adapter and
the verifier) runs in coherence run (so in CI, through the generated
workflow), in refute, and in the warm adapter server that the hooks talk to.
The modules its syntax slots name run wherever a reading reads its language,
a hook's own process included: the economy's prediction at a stop, the
lexicon's member scan and entrance detection read languages there, and the
core's own languages are plugins now. A slot's module is loaded only when a
reading asks for that slot, and should be what a syntax table is: small and
without effects. A session start names, in one line, each plugin the project
brings whose syntax code will run in its hooks. Run a plugin you would run as a dev dependency, and review a
change to one as you would a change to the project's own code. A bundled
plugin ships inside @posthog/coherence and is reviewed with it.
The warm server's code fingerprint holds every plugin folder the project runs, so an edited plugin replaces the server as an edited Coherence does, and a module that fails to load is tried again once it is fixed.
A project without a plugins key reads no plugin of its own. The bundled
plugins that provide its languages are read once per process, as the
built-in languages' tables were compiled in before, and it does exactly the
work it did before plugins existed.
sample/
plugin.json the manifest, read as data
index.ts the module, imported only where its code runs
check.json the project plugin check holds it to (below)
package.json { "type": "module", "private": true }
lexicon.json the lexicon layer (optional)
The modules are TypeScript run as written, as ES modules: Node 22.18 or later
runs them without a flag. The folder's package.json says they are ES
modules, so Node neither guesses nor warns at every load, whatever the
project's own package.json says (plugin eject writes it). Code imports
Coherence only through @posthog/coherence/plugin, whose types ship in the
package as dist/plugins/api.d.ts; a tsconfig that typechecks the folder
needs "allowImportingTsExtensions": true with noEmit, and @types/node
as a dev dependency, since the interface names Node's types.
When Coherence loads a plugin a project names, the plugin's imports of
@posthog/coherence/plugin (and of @posthog/coherence/dist/plugins/api.js)
are answered by the Coherence that is running, whichever copy the plugin's
folder would find on its own: the project's node_modules, while a global
install, npx, another workspace's install or a checkout runs the check.
Several helpers (languageOfFile among them) read what the running
Coherence registered when it read the config, and a second copy has
registered nothing, so a plugin reading through one would answer nothing
without saying why. Only files inside a plugin's folder are redirected; the
project's other files resolve the package as they always did. The installed
copy still gives an editor and a typecheck the interface's types.
The bundled plugins are the fullest examples, shipped in the package under
plugins/: go for a language Coherence lacks, rust for an adapter over a
language server with no npm package, typescript, python (with tach) and
declared. Their sources import the core as ../../src/plugins/api.ts, the
same module the package exports as @posthog/coherence/plugin; an ejected
copy names it the second way, and so should a plugin you write.
{
"name": "sample",
"version": "0.1.0",
"pluginApi": 2,
"provides": { "language": "swift" },
"extensions": [".swift"],
"testFolders": ["Tests"],
"module": "./index.ts",
"ladder": { "top": "reference-choked", "because": "...", "rungs": [] },
"verifiers": [
{ "form": "sample prove", "evidence": "model-checked" },
{ "form": "sample exit", "evidence": "exit-checked", "command": ["node", "check.js", "{name}"], "match": "^proved " }
],
"lexicon": "./lexicon.json"
}| Key | Meaning |
|---|---|
name, version |
Who the plugin is. Every run entry its adapter or verifier answers names both. |
pluginApi |
The interface version it was written for. This release runs 2 and reads plugins written for 1 to 2: a plugin written for 1 loads, and the session start names it with what changed since. Any other version is refused, naming both. |
provides.language |
The language its adapter answers for. A built-in name takes the built-in's place (see the swap rule). A new name needs extensions (or files), and the config's language must name it; the language is then read as a bundled one is, for that project: its files, test files and folders, its entrance rules, its CI steps and every syntax slot it fills. |
extends |
The language whose adapter it is handed to wrap, as base (Convex extends typescript). A plugin for a language of its own extends nothing. |
extensions |
The files a new language is written in. An extension a built-in, a declared language, or another plugin owns is refused. |
module |
The module, relative to the manifest. Required for an adapter, and for a verifier with no command. |
ladder |
The rungs the adapter grades on: top, because, rungs, and optionally whenVacuous, the rung a chokepoint stands on when the refutation could not see its staged uses (absent, the earned rung stands with the verdict not run). Absent, the wrapped adapter's; with none, reference-choked alone, enforced by Coherence's check. |
verifiers |
The via forms it proves (below). A command must name {name}; a command that is never told what to prove is refused. An optional match is a regular expression its output must match for an exit 0 to pass. |
lexicon |
Its lexicon layer, relative to the manifest. |
syntax |
The syntax slots it fills, each "<module beside the manifest>#<export>" (below). |
files |
The files a language is written in, as a regular expression pattern with an optional except (TypeScript, as JSON writes them: \\.(?:[cm]?[tj]sx?)$ except \\.d\\.ts$); extensions is the short form for a plain list. |
source |
except: of the language's files, those the plain source scan leaves out (TypeScript's JavaScript, Python's stubs). |
serverExtensions |
The extensions the language server can report a site in or resolve a name to. |
testFiles |
Regular expressions a test file's base name matches by the language's own naming (pytest's ^test_.*\\.py$), beside .test./.spec. and the test folders. |
testFolders |
Folder names whose files are all tests by the language's own layout, wherever they sit (SwiftPM's Tests), beside the config's test folders. A name matches a folder exactly, case and all. |
members |
How an exported type's body reads for the lexicon's member scan: opens, closes, field, each a regular expression. |
entrances |
The entrance detection rules (rule, plural, detects) and notDetected, what they miss: printed by the query, the page and orient. |
notScanned |
Why the text scans do not read the language. Present, it turns off every reading that scans the language's text, whatever slots are filled: mass, entrance detection and the Structure reading each name the language as not read, with this reason. No bundled plugin carries it, and plugin check fails a plugin that does: every language plugin owes the readings of its text (the reading duties below), and none is waived. It stays so that a plugin still being written says what it does not read rather than measuring as a project of no code. |
environmentFolders |
Folder names no project authors code in, wherever they sit (node_modules, __pycache__). |
markedFolders |
Folders the language's tools make, left out by a marker inside (pyvenv.cfg) or by name beside a file (target beside Cargo.toml), each with its reason. |
ci |
note: what the generated CI workflow says for a project that reads the language. steps: what it runs after the install to set up what the adapter asks (Go's toolchain and gopls), each { "name"?, "uses": "<owner/repo>@<40-hex commit>", "version": "<its release>", "with"? } or { "name"?, "run": "<command>" }. |
wellKnown |
Names the plugin vouches for as well known, beside Coherence's own list. |
The manifest is data so that a hook that only needs to know what a plugin provides (the vocabulary, the spec check, which language a file is written in, the folders a walk leaves out) reads one small file and runs no plugin code, and so that the Scope page, in a browser, can print a language's entrance rules. The module loads in the warm adapter server and in a run, when a check first asks its adapter or verifier.
import type { PluginModule } from "@posthog/coherence/plugin";
const plugin: PluginModule = {
adapter: ({ root, language, base, bounds }) => /* a LanguageAdapter */,
verify: async ({ root, form, name, timeoutMs }) => ({ verdict: "pass", reason: "..." }),
};
export default plugin;adapter returns a LanguageAdapter, exported from
@posthog/coherence/plugin (below, writing an adapter). When the
manifest extends a language, base is that language's adapter: delegate what
you do not change. The core closes base; your adapter closes only what it
started. A module that fails to load makes the adapter unready with the reason,
so every chokepoint it was asked about reads not run, never a pass.
bounds, for a server that can narrow its workspace, is the folders the
component interface reading leaves out.
root is the project's folder as Coherence was handed it. A server may
answer in the real path instead: on macOS /var and /tmp are links into
/private, so a project under one comes back as /private/var/.... Map a
path the server reports against realpathSync(root) as well as root, or
every site falls outside the project and is dropped. (plugin check hands
its scratch project's real path.)
The helpers @posthog/coherence/plugin exports for listing the project
differ in what they leave out:
| Helper | Leaves out |
|---|---|
walkedProjectFiles(walkBounds(root)) |
What git ignores, nested checkouts, hidden, host and environment folders, and what the config's ignore names. |
walkedProjectFiles(walkBounds(root, [])) |
The same, less the config's ignore: the bundled adapters' choice, since a reference from code the adoption leaves out still reaches the thing. |
keepProjectFiles(root, paths) |
Of the paths given, those git ignores and those in nested checkouts; never the config's ignore. |
projectListing(root) |
Nothing more: one listing taken once, handed to keepProjectFiles to answer without asking git again. |
So a fixture copy under an ignored folder is still among an adapter's files.
Resolve a Name in <path> hint with hintedFiles(files, hint): the file at
that exact path when there is one, and every file ending with it only when
there is not, so the copy never makes the name ambiguous.
The readings scan a language by the tables its plugin's manifest names under
syntax, each as "<module>#<export>", loaded synchronously, once, when a
reading first asks for that slot:
| Slot | Read by | Type |
|---|---|---|
entrances |
entrance detection | EntranceSyntax |
interfaces |
the Structure reading | InterfaceSyntax |
kept |
the Structure reading's kept answers, when filled; else interfaces.wildcardModules |
(file, text) => string[] |
source |
the economy and mass | SourceSyntax |
declares |
the observation's interface map | (line) => "type" | "value" | "body" | "unknown" |
notice |
the session start | (root) => string |
lists |
declared mirrors | (text, name) => Literal | undefined |
topLevel |
the spec model's check that a handler, guard or mirror is declared | (text, name) => boolean |
boundaries |
scaffold import and adoption |
BoundarySource: a tool's declaration file read into modules, as tach's |
An EntranceSyntax's manifestCandidates(read, exists, files) is handed the
project's files inside the bounds as its third argument, so a language whose
manifests lie through the tree (every Cargo.toml of a workspace) reads each.
A candidate may carry a name, the one scaffold entrances proposes when
the plugin knows a better one than its symbol gives (a Rust binary by the
name its Cargo.toml gives it), so no language's layout is known in the core.
Every type is exported from @posthog/coherence/plugin. A slot a plugin leaves empty reads as "not read for ", never as
another language: a plugin that fills no source slot is named not read by
mass and every reading that scans text, and one that fills no interfaces
slot by the Structure reading. The kept answers read the interfaces slot's
wildcardModules; the kept slot is still read when a plugin fills it, and
plugin check fails a plugin whose two slots answer with one function or
two spelled alike (its "one answer each" duty).
Two manifest fields name a language's files so the core never does:
basisFiles, regular expressions a base name matches for the files besides
source its answers and entrances rest on (configuration, manifests,
lockfiles: ^Cargo\.toml$), which a kept answer and the Structure reading's
fingerprint are both read against; and moduleFolders, regular expressions a
module file's path matches whose first group is the folder it owns
(^(.*)/__init__\.py$), by which a chokepoint that is a module covers its
folder's files. A plugin answers for the project whose config names it
alone: a process serving several projects asks each one's questions under
forProject(root, ...). A plugin that provides one of the bundled languages answers
each field and slot it declares, and the bundled plugin answers the rest, so
an enhancement of the adapter alone keeps every reading of the language. Its
file rule is one rule: a plugin that names its files, by files or
extensions, owns them whole.
An adapter answers the questions a chokepoint check asks, about one language,
over one project. Most drive a language server over the language server
protocol on stdio (JsonRpcClient, exported from
@posthog/coherence/plugin); plugins/go/go.ts drives gopls in about 600
lines and is the shortest whole example. docs/enforcement.md, shipped
beside this page, explains the grades, the ladder and refutation from the
reader's side; this is the author's side.
| Method | Owes |
|---|---|
ready() |
{ ok: true } once the instrument answers, else { ok: false, reason } naming what is missing and how to get it (gopls was not found (install it with ...)). Never throws for a missing tool: every chokepoint then reads not run with the reason, never pass. |
resolve(name, hint) |
A spec name to one Definition: a file, a range, and the position of the name. Read the name with parseName: a bare symbol, Name in file, or a module path. Several candidates are chosen among by hint (not in a test file, then under hint.component), and a name still ambiguous is refused with candidates, never guessed. |
references(definition) |
Every site in the project's own files (keepProjectFiles) that names the thing, the declaration itself left out, as a ReferenceSite: file, line (1-based), character (0-based), symbol (the enclosing declaration's dotted path, absent at the top level); form, "import" or "re-export" when the site is one; test: true when the adapter knows the site is test code that its path does not show (Rust's #[cfg(test)], a SwiftPM test target read from the package); unread, why, when the adapter cannot tell whether the site reaches the thing. Test files are recognized by the check from the manifest's testFiles and testFolders and the config's test folders; report every site. |
visibility(definition, chokepoint?) |
Whether the language itself refuses a reference from outside the thing's module (enforced), whether the thing is reachable from outside it (visible), the evidence read, and the rung a clean chokepoint earns on it, one of the manifest's ladder.rungs. |
refute(protected, chokepoint?, exempt?, occasion?) |
The refutation, below. |
testFilter(via) |
A bullet's via as the project's test runner filters by it; most return it unchanged. |
forget(files?) |
Make the instrument read the disk again: the named files, or all it may have open. Resolve only once the instrument has taken the change in, since the edit hook checks again right after. |
close() |
Stop what the adapter started. The core closes a base it handed you. |
serverPid() |
Optional: the server's process id, so a reading can bound its memory. |
A check that could never fail proves nothing, so every chokepoint run
refutes itself: the adapter stages uses the check must catch, and the run
reads what it saw. The project's own files are never changed: stage each use
as an unsaved document (textDocument/didOpen, or didChange over a file
the project has), and close or restore every one before refute returns,
whatever it returns. A server that answers only from what is on disk
(sourcekit-lsp reads its index, never an open document) cannot see an
unsaved use: stage it instead in a throwaway copy of the project outside it,
as Python's tach refutation does, ask a server over the copy, map the copy's
paths back to the project's in what you return, and remove the copy.
occasion says who asks: "edit" for the check at an edit, which a hook
waits on inside its latency budget, "run" for a run, refute and plugin check. A refutation that costs seconds (a cold index of a copy) may take a
cheaper path at an edit, such as a copy and an index kept warm between edits
and refreshed with the edited files, or report seen: false at an edit and
leave the proof to the run, which then reads not run at the edit, never pass.
It never reports a use it did not see.
-
When Coherence's check enforces the rung (
reference-choked), stage the uses the check must call bypasses and return them asstaged, each with the site the instrument reported for it (site), absent when it reported none:- a use of the thing in the chokepoint's own file, past the chokepoint's range, when the chokepoint is a symbol;
- a use from a new document beside the protected thing (a re-export, in a language that has them);
- with
exempt, the folder whose references the chokepoint does not govern: no use inside it, and one from a new document outside it (outsideOfFolder).
Never decide yourself whether a staged site is a bypass: the check classifies each with the function it classifies every site with, and the refutation fires only when it calls every one a bypass.
seenis whether the instrument reported every staged use. A use it did not report, or one the check would not call a bypass, makes the check vacuous: the run says which and why, and records not run, never pass. -
When the language enforces the rung (
visibility-choked: Go's unexported names, Rust's privacy), Coherence's check can never be made to fire, since the compiler refuses every outside use first. Stage a use from outside the thing's module, pull the instrument's diagnostics on it, and return the refusal's own text asrefused, withseen: trueand nothing staged. Stage a control use from inside the module too, which must not be refused: a refusal there means the name is wrong, not private, and the refutation proves nothing (seen: false). -
When nothing can be staged (no module path a new package could import, an instrument that cannot open an unsaved document), return
seen: falsewith nothing staged.
account says, in one line, what was staged and what was seen: the run
prints it as the refutation's evidence.
Every language Coherence reads is a bundled plugin, so a project that finds one wanting changes a copy of it:
-
Eject:
coherence plugin eject typescriptcopiesplugins/typescriptfrom the installed package intocoherence-plugins/typescript(or--to <dir>), with itscheck.json, and adds"plugins": { "typescript": "./coherence-plugins/typescript" }to the project's config. The copy imports Coherence only through@posthog/coherence/plugin: the bundled plugins import the core through that one module (src/plugins/api.ts), so the copy reads from the Coherence that runs it what it read bundled. The copy is TypeScript run as written, its imports naming.tsfiles: it needs Node 22.18 or later (which runs TypeScript without a flag), and a project that typechecks it needs"allowImportingTsExtensions": true(withnoEmit) in the tsconfig that includes it. Eject edits only the config'spluginskey, every other byte kept. It refuses, writing nothing, when the folder exists (a link there that leads nowhere included), when it leaves the project once symbolic links are followed, when a link on the way leads nowhere, or when the config already runs a plugin by that name from a path. A plugin the config runs bundled ("go": "bundled") is the one copied, and its copy takes that entry's place.The copy's folder is Coherence's instrument, not your code: no walk reads it (specs, readings, mass, the lexicon's corpus), no reference found there counts against a chokepoint, and a name it declares shadows none of yours. Say
"gradePlugins": truein the config to have your plugin folders walked like any other. -
Enhance: change the copy. It answers for its language in the bundled plugin's place from the next command on; what it does not declare (a data field, a syntax slot) the bundled plugin still answers.
-
Check:
coherence plugin check ./coherence-plugins/typescriptholds it to what every language plugin owes, over the project itscheck.jsoncarries:Duty Holds when file rule the files check.jsonnames as the language's are, and the others are notdeclarations the topLevelslot says which names a file declares, ascheck.jsonsaysreferences the adapter reports every reference to the protected thing, and none else; one that reports none fails here, by name chokepoint the run grades the chokepoint as check.jsonexpects, bypasses named, its refutation seen or refusedmass mass reads the language, counts each file, and the sourceslot's scan finds in each the namescheck.json'sreadings.declaredgivesimports the sourceslot's scan reads every importreadings.importsnames, resolved to its project file, and none else: what the economy's closure walksinterfaces with each folder readings.interfacesnames a component, the component interface reading finds the symbols one uses of another, and none elseentrances entrance detection finds every entrance readings.entrancesnames, by file, handler and rule, and none elseone answer each no two of its slots answer with one function, or two spelled alike Each duty passes, fails with why, or is not run with why (the language server is absent). The command exits 0 only when every one passes. The reading duties run each reading as a project's runs, through the core, so a slot that answers in its type and reads nothing (a
declarationOfthat always saysundefined) fails every reading it feeds, by what each missed, and so does a slot left empty: a slot's success is what the readings that consume it recover. No plugin waives a reading; a check project that names noreadingsfails each reading duty as nothing to check. The bundled plugin'scheck.jsonfor the language, as the installed package ships it, is the minimum: every plugin for that language is held to it, whatever its owncheck.jsonsays. Add cases to the copy'scheck.jsonas you enhance it: they are its tests, held besides the minimum, never in its place. A duty with no case fails as "nothing to check", acheck.jsonthat is no check project ({}) is refused naming each field it lacks, and a duty that throws fails with what it threw while the others still report.plugin check typescriptholds the bundled plugin to the same.A language plugin's
check.jsonis one small project and what the plugin must answer over it.plugins/go/check.jsonis a whole one for a language Coherence does not bundle as a default:Field Holds filesThe project: each file's text by its path from the root. The check writes them into a scratch folder, with a config that runs the plugin, and reads nothing else. language{ "files": [...], "not": [...] }: paths the file rule must take as the language's, and paths it must not. Only the names are read, so a path need not be infiles.declarations[{ "file", "name", "declared" }]: what thetopLevelslot must say of a name in one offiles.protects,chokepointThe thing and its one way in, written as a spec bullet writes them ( Seal,Name in file.go).referencesEvery reference to protectsasfile:line, the line 1-based, test files' included: the adapter must report exactly these, none missed and none more.gradeWhat the run grades the chokepoint ( broken,reference-choked,visibility-choked).bypassesThe references outside the chokepoint the run must name, as file:line; a test file's never is one.readingsWhat each reading of the language's text must recover over files, each compared exactly, each with at least one answer:declared,{ "<file>": ["Name", ...] }for every non-test file of the language;imports,[{ "file", "module", "names" }], one per file and the project file it imports from,"*"for the module whole;interfaces,[{ "from", "to", "symbols" }], component folders and the symbols oftothatfromuses;entrances,[{ "file", "symbol", "rule" }],symbolempty when the file itself is the entrance. Write it for your small project before you write a slot: it is the slot's spec. -
Swap back: remove the
pluginsentry, and the bundled plugin answers again.
A plugin is pinned to the pluginApi it was written for. When Coherence moves
on, the session start names the plugin and what changed since, so an ejected
copy's author knows what to update. pluginApi 2 moved the syntax slots from
the module's syntax export into plugin.json
("syntax": { "<slot>": "<module>#<export>" }), made the member scan
plugin.json data (members), and added the kept, declares, notice,
topLevel and boundaries slots. A module that still exports syntax is
refused at load, by name, rather than read as having none.
- A plugin that provides a language takes the core's place for it: the
bundled plugin's. With
extendsnaming the same language, it is handed the bundled plugin's adapter. This is how an ejected, enhanced built-in replaces ours. - A plugin that extends a language without providing it wraps whichever adapter answers for that language, the core's or a providing plugin's. Several wrap in the config's order. This is how an enhancement such as Convex layers over TypeScript.
- A plugin that extends a language adds to its readings too: the entrance
rules its manifest lists (
entrances.rules) and the names it vouches for (wellKnown) join the language's, and a syntax slot it fills names a function of the table beneath it (the provider's, then each earlier extending plugin's) that returns the table to read by. Convex's"entrances": "convex-entrances.ts#convexEntrances"returns TypeScript's entrance syntax with Convex's shapes added. All of it is for the project that names the plugin alone; one that does not reads no file of it. plugin checkholds a plugin that extends a language to that language's duties, over the bundled minimum for it and the plugin's owncheck.json.- Two plugins that provide one language are refused, both named: two adapters cannot both answer.
A bullet proved by a plugin rather than a test writes its form in brackets:
- the model holds: The work-order model never reopens an ended order.
over: every state the model reaches
via: [sample prove] terminalNeverMoves
The bracketed form is one a loaded plugin declares. The plugin is asked to
prove the name (a module's verify is also told the folder of the bullet's
spec, component, where a name without a file is looked for), by its command (the name in place of {name}, run from the
project root; exit 0 is a pass, and with match only when the output matches
too, an exit 0 whose output does not being a fail that names the expression)
or its module's verify. The verdict is graded
as a test's is, and the run entry records the plugin and the form's evidence
label. A bracket begins no test title, so the two never read as each other. A
form no loaded plugin declares is a spec problem naming the forms that are,
never a test run under that title. Two plugins that prove one form are refused.
A verifier that does not answer within the run's timeout is not run: it proved nothing either way.
refute stages a break and runs the bullet as run does, so a verifier's
bullet is refuted like any other.
plugin check holds a verifier plugin (one that provides no language) to its
own check.json: { "files": {...}, "proofs": [{ "via", "verdict", "reason"?, "component"?, "env"? }] }. Each proof is proved over a scratch copy
as a run proves a bullet, with any environment it names set, and must read the
verdict it expects (its reason matching reason). There is one duty per form:
a form with no proof fails as nothing to check, and a proof expected to pass or
fail that could not run leaves its duty not run with why, never pass. The
report says NOT RUN when no duty failed but one did not pass.
The lexicon is a stack: Coherence's layer, then each plugin's in the config's order, then the project's.
- A plugin's concepts count as declared wherever Coherence's do, and every view
says whose each is:
coherence lexiconprints each layer under its plugin's header, the Scope lexicon view shows each as its own layer, and coverage reports a term's layer asplugin <name>. - A plugin's rejected names govern only its own terms; they are not refused across the project.
- A defect class a plugin's layer declares is a legal
--class. A plugin adds classes as properties of a concept nameddefectwith an empty definition, which carries properties and is not a clash. - A clash is never merged. A plugin concept whose name or alias Coherence or
an earlier plugin already declares stays out of the stack, and the spec check
and
coherence lexiconreport it. The project settles it by declaring the name in its own lexicon, as a concept or as an alias of the sense it means; inside the project its sense wins. - A session start carries a plugin's layer only while its surface is in use: a
language it provides or extends among the project's, or a via form it proves
on some bullet.
coherence lexiconalways prints every layer.
"plugins": { "go": "bundled" } with "language": "go" (or a list naming
it) reads a Go module through gopls:
- Names. A spec names a Go thing as
Name in path/to/file.go, or by its name alone when the module declares it once. Go's ownstore.Writeis prose here: the package qualifier is no part of a spec name. A module is one.gofile; a package folder (internal/audit/) resolves only when it holds one non-test file, since a definition is one file. - Grades. An unexported name is visibility-choked: the compiler
refuses it from any other package, and the refutation is gopls's own
refusal of a use from an unsaved new package, beside a use inside the
package that it must accept. An exported one is reference-choked:
every reference gopls reports in the module must be inside the
chokepoint, and the refutation stages a use beside the protected thing and
one in the chokepoint's own file past the chokepoint. A
_test.gofile is test code. - Read by the readings. Go's files, tests and
vendorfolder, and its top-level declarations, so the spec check reads an entrance's handler or a guard in a Go file. Not yet: entrance detection (declare each under## entrances) and the text scans mass and the economy read by. - gopls is found from
COHERENCE_GOPLS, else thePATH, elsego env GOPATH/bin; with none, every Go chokepoint reads not run with how to install it.ci installsets up Go and gopls in the workflow it writes.
"plugins": { "convex": "bundled" } in a TypeScript project that deploys a
Convex backend. It extends TypeScript:
- References. Convex code reaches a function through the object
convex devgenerates into<functions folder>/_generated/api(internal.telemetry.sendistelemetry.ts'ssend), whose type the language server never links back to the export. The plugin reads each such property chain, from the syntax tree, as a reference to the export it names, so a chokepoint over a Convex function grades the calls real callers make. Its refutation stages one such reference. The functions folder is the nearest one above a file holding both_generated/apiand_generated/server; a path alias to it must end in the folder's own name. Followed as references: a namespace import (import * as g), a module that re-exports the objects (by a relative path or a path alias, relays deep), a const holding part of a chain (const t = internal.telemetry; t.send) and a destructure (const { send: s } = internal.telemetry). - Gaps, never silence. Any other use the plugin cannot read through is
reported as a site not read through, with why, so one outside the
chokepoint keeps it from passing: the object or part of a chain passed on
as a value, spread, read with a computed key or a rest pattern, and a
string naming the function as Convex does (
"telemetry:send"). Names are matched by name, not scope: a local that shadows a followed name reads as that name. - Entrances. A public function (an exported
query,mutationoractionbuilt from the folder's_generated/server), a route aconvex/serverhttpRouterregisters, and the function acronJobstable runs. Internal functions are not entrances. Not detected: a library's routes added withaddHttpRoutes, a custom builder wrapping Convex's, and the functions a library such asconvexAuthexports. - Vocabulary. Its own lexicon layer: Convex function, generated API, functions folder, Convex cron.
It needs the TypeScript compiler (typescript, an optional dependency of
Coherence's that a Convex project has already).
"plugins": { "quint": "bundled" } proves properties of a
Quint model:
- an ended order never moves: The model never reopens an ended order.
over: every state the model reaches within its bound
via: [quint] terminalNeverMoves in Journal.qnt
[quint run] <property>:quint typecheck, thenquint run --invariantover sampled runs. Evidencesimulated. The pass says what was checked: no violation in so many sampled traces, and the deepest one reached, read from quint's trace length statistics; a run that stopped short of the steps asked says so as a limit, and a model with no initial state is not run.[quint] <property>: the same, thenquint verify, Apalache's bounded model check. Evidencebounded-model-checked; the reason states the bound, since it is not every reachable state. Each proof starts its own Apalache server on a free port (--server-endpoint), never one already running.- The model file is named from the bullet's spec folder, or with none named
the folder's one
.qntmodel. - A state that breaks the property fails the bullet with the states quint printed; a refusal (a type error, an unknown name) fails it too.
- What could not run is not run, never a pass: no quint (install
@informalsystems/quint@0.33, or setCOHERENCE_QUINT), a shell that found none,quint verifywithout the Java runtime Apalache needs or without Apalache fetched, and a step out of time. - Tuned under
"quint"in the config:backend(typescript, the default, fetches nothing;rust),maxSamples(1000),maxSteps(20),seed,verifySteps(10). Quint keeps what it fetches underQUINT_HOME. - Its vocabulary is its own lexicon layer (quint model, quint property, bounded model check), carried only while a bullet proves by its forms.
A run entry that a plugin's adapter graded, or a plugin's verifier proved,
carries plugins: [{ name, version }], in the order they wrapped. A verifier's
entry also carries evidence, the label its manifest gives the form.
What the interface does not carry yet:
- Language knowledge still in the core. The vocabulary's code scan reads
many languages' declarations (
src/lifecycle/lexicon-coverage.ts,src/lifecycle/check.ts); entrance detection's package-script rule names the runners it follows, and its pyproject console scripts; the mirror readers parse with the TypeScript compiler and resolve its specifiers; a module's own files (a Python package's__init__, a Rust module's folder) are read insrc/enforcement/check.ts; run routes pytest; the observation reads Python tracebacks. The structural test names each as an exception with its reason, and each is a table a language plugin should own. - A package that is many files. A definition is one file, so a Go package folder stands for a chokepoint or a protected module only when it holds one non-test file.
- Bullets about a plugin's code still live in the core's specs
(
src/adapters/Adapters.spec.md): a bullet's witnessed refutation is keyed by its component, so moving one restages its refutation.