@collective-intelligence/lint
The Collective Intelligence lint rules, as one Oxlint plugin. Every CI lint
rule lives in this package. The plugin is named ci-solution, so each rule is
enabled as ci-solution/<rule>.
Install
bun add -d oxlint @collective-intelligence/lint
The plugin needs oxlint 1.82.0 or later.
Use
All rules
Extend the recommended config in oxlint.config.ts. It loads the plugin and
enables every rule as an error:
import { recommended } from "@collective-intelligence/lint"
import { defineConfig } from "oxlint"
export default defineConfig({
extends: [recommended]
})
To change one rule, set it after the extend:
export default defineConfig({
extends: [recommended],
rules: { "ci-solution/subpath-index": "warn" }
})
Selected rules
Load the plugin and enable only the rules you want:
import { defineConfig } from "oxlint"
export default defineConfig({
jsPlugins: ["@collective-intelligence/lint"],
rules: {
"ci-solution/package-entrypoint": "error",
"ci-solution/subpath-imports": "error"
}
})
A JSON config works the same way:
{
"jsPlugins": ["@collective-intelligence/lint"],
"rules": { "ci-solution/package-entrypoint": "error" }
}
The RuleName type lists every rule name.
Agent skill
The package ships the agent skill ci-solution-structure at
skills/ci-solution-structure/SKILL.md. In a Harness Intelligence repository,
the collective-intelligence pack installs the skill and adds the plugin with
ci-solution/package-entrypoint to the managed lint config.
Rules
| Rule | Enforces | Group |
|---|---|---|
ci-solution/package-entrypoint | An import of another package targets an entrypoint declared in its package.json exports. | Package boundaries |
ci-solution/subpath-imports | Imports between subpath folders follow the allowed direction. | Solution layout |
ci-solution/subpath-entrypoint | An import from another subpath folder targets the folder or its index file. | Solution layout |
ci-solution/subpath-index | Every subpath folder has an index.ts or index.tsx. | Solution layout |
ci-solution/effect-subpath | An SDK Effect API comes from the SDK's /effect subpath; removed SDK paths are reported. | Effect convention |
ci-solution/no-run-in-view | A view module calls no fetch and runs no Effect. | Effect convention |
ci-solution/effect-actors | XState side-effect actors are fromEffect actors. | Effect convention |
ci-solution/effect-writes | No module takes TanStack Query useMutation; writes run in fromEffect actors. | Effect convention |
Package boundaries
A workspace holds packages that import each other. These rules keep each package behind its declared public entrypoints.
package-entrypoint
Import another package through its root (@scope/pkg) or a subpath listed in
its package.json exports (@scope/pkg/views). * patterns and null
exclusions follow Node resolution. The rule finds the package through
node_modules from the importing file, so workspace packages must be linked by
the package manager.
import { invoiceTotal } from "@acme/billing" // valid
import { invoiceTable } from "@acme/billing/views" // valid when "./views" is exported
import { rate } from "@acme/billing/src/domain/rates" // reported
import { rate } from "../../billing/src/domain/rates" // reported: relative path into another package
A package without an exports field accepts every subpath except src/.
Packages that cannot be found are not checked.
Solution layout
A solution package can split its sources into subpath folders:
src/
ci/ Collective Intelligence entity and capability declarations
domain/ domain model, free of runtime concerns
api/ API contracts
server/ server handlers and runtime wiring
headless/ UI state without rendering
views/ rendered UI
These rules keep those folders apart.
subpath-imports
| Folder | Must not import |
|---|---|
domain/ | server/, views/, headless/, ci/ |
ci/ | views/, headless/ |
headless/ | server/ |
views/ | server/ |
api/ | no limit |
server/ | no limit |
The rule checks relative imports and imports through the package's own name
(@acme/billing/server from src/views/).
subpath-entrypoint
An import from another subpath folder targets the folder or its index file. Imports inside one subpath folder are free.
// src/views/table.ts
import { invoiceTotal } from "../domain" // valid
import { invoiceTotal } from "../domain/index.ts" // valid
import { rate } from "../domain/rates" // reported
subpath-index
Every subpath folder under src/ has an index.ts or index.tsx entrypoint.
The rule reports each file in a folder that has neither.
Effect convention
An app keeps its side effects in XState machines: each side effect is a
fromEffect actor, views read data through TanStack Query, and writes refresh
those reads through the SDK QueryCache. These rules
check the parts of that convention that imports and calls show.
effect-subpath
A Collective Intelligence SDK (@collective-intelligence/*) exposes its Effect
API at <sdk>/effect and <sdk>/effect/<adapter>. The rule reports an SDK path
that names Effect elsewhere: a <sdk>-effect package, or an effect segment
below another subpath. It also reports each SDK path that the release after 0.2.0
removes, with its replacement:
| Removed path | Replacement |
|---|---|
@collective-intelligence/external-apps-server-sdk/runtime/effect | @collective-intelligence/external-apps-server-sdk/effect |
@collective-intelligence/external-apps-server-sdk/persistence/drizzle/effect | @collective-intelligence/external-apps-server-sdk/effect; layerNodePostgres and NodePostgresLayerOptions from @collective-intelligence/external-apps-server-sdk/effect/node-postgres |
@collective-intelligence/external-apps-server-sdk/runtime/next | @collective-intelligence/external-apps-server-sdk/next |
@collective-intelligence/extensions-sdk-backend-effect | @collective-intelligence/extensions-sdk-backend/effect |
import { CiClient } from "@collective-intelligence/external-apps-client-sdk/effect" // valid
import { createClient } from "@collective-intelligence/external-apps-client-sdk" // valid
import { Realtime } from "@collective-intelligence/external-apps-client-sdk/realtime/effect" // reported
import { CiRuntime } from "@collective-intelligence/external-apps-server-sdk/runtime/effect" // reported: removed
no-run-in-view
A view module is a .tsx or .jsx file, or a file in the views/ subpath of a
solution package. The rule reports a call of the global fetch (also
globalThis.fetch and window.fetch) and each use of an Effect run function
(runPromise, runSync, runFork and the other run* functions of Effect
and ManagedRuntime):
- a read from any object, called or passed as a value:
Effect.runPromise(x),runtime.runSync(x),x.pipe(Effect.runPromise); - a key in a destructuring pattern:
const { runFork } = runtime; - a named import from
effect,effect/*or@effect/*:import { runPromise } from "effect/Effect".
A constant name in brackets or quotes counts too: window["fetch"](url),
Effect[`runSync`](x) and const { "runFork": fork } = runtime are reported.
A local binding named fetch, globalThis or window, a method such as
cache.fetch, a name in brackets that only runtime gives (Effect[key]) and an
object literal key such as { runPromise: run } are not reported. To allow the
calls in view tests, turn the rule off for the test files in an overrides
entry.
effect-actors
The rule reports each fromPromise, fromCallback, fromObservable and
fromEventObservable that an import, a re-export or a namespace import
(XState.fromPromise, XState["fromPromise"],
const { fromPromise } = XState) takes from xstate or xstate/*. Make side-effect actors with fromEffect from
@collective-intelligence/external-apps-client-sdk-react/effect in a detached
app, or from @collective-intelligence/extensions-sdk-frontend-react/effect in
a hosted Studio Widget. Machines and
fromTransition actors have no side effects and are not reported. A local name
that shadows the namespace import, such as a parameter named XState, is not
reported.
effect-writes
The rule reports each useMutation that an import, a re-export or a namespace
import (ReactQuery.useMutation, ReactQuery["useMutation"],
const { useMutation } = ReactQuery) takes from @tanstack/react-query. Write through a fromEffect actor from
@collective-intelligence/external-apps-client-sdk-react/effect in a detached
app, or from @collective-intelligence/extensions-sdk-frontend-react/effect in
a hosted Studio Widget; after the write, the actor refreshes reads through the
QueryCache of the same entrypoint (invalidate, setData).
The rule checks every module. useMutation runs only during a React render,
so a module that takes it is a view, a hook or a helper of one of them.
useQuery and the other reads are not reported, and neither are
useMutationState and useIsMutating: they only read the state of a
useMutation. A useMutation from another library, such as
@apollo/client, is not reported. The entity hooks of
@collective-intelligence/external-apps-client-sdk-react/resources
(useEntityCreate, useEntityUpdate, useEntityDelete) are not reported:
the SDK writes and refreshes its own entity reads.
The rule checks no query key. An app that builds the SDK's "ci-entities"
key by hand, for example in invalidateQueries, is not reported: the SDK
owns that key for its entity reads but exports no key or refresh function for
them yet, so a report would have no supported fix.
Adding a rule
Each rule is a plain Oxlint rule in its own folder, src/rules/<rule>/:
-
Write
src/rules/<rule>/<rule>.ts. Export aLintRule(src/rule.ts) with:group: the rule group in the table above (add a group toRuleGroupwhen no group fits);rule: the rule fromdefineRuleof@oxlint/plugins, with amessagesentry inmetafor each finding.src/rule.tsholds the shared helpers:moduleSpecifiersbuilds a visitor that checks every module specifier,staticPropertyNamereads a constant property name, anddeclaredVariablefinds the variable that a name refers to.
A rule reads the workspace through
src/workspace/:packageOfandinstalledPackagefind package manifests,declaresEntrypointchecksexports, andlayoutFilefinds the subpath of a file. -
Add one line to the registry,
src/rules/index.ts. The plugin, therecommendedconfig and theRuleNametype follow from it. -
Write
src/rules/<rule>/<rule>.test.tsagainst the fixture workspace intest/fixtures/workspace/, and add fixture files for the new cases. Add the new diagnostics to the expectations ofsrc/index.test.ts. -
Add the rule to the table above, and to
CHANGELOG.md.
Oxlint calls rule visitors synchronously, so a rule reads files with the
synchronous node:fs API: it cannot wait for a promise or a timer. The
workspace lookups cache each folder they visit, so each package.json is read
once per plugin load.