Workflow and Cell invariants compile as costumes: no lint for variant reachability, pure Cell phases, or wiring-closure placement
Problem Statement
Three invariants of the cell/workflow architecture are enforced by review alone, so violations compile cleanly. A workflow may declare error variants it never constructs — the type marker checks that the channel is inhabited, not that any code path reaches the variant. decode/decide/encode phases in Cell.layer specs may perform I/O, read clocks, or yield* — their purity is convention. (The plugin's existing make-body-purity rule covers Workflow.make bodies; Cell.layer spec bodies are not covered by it.) And a runtime or layer graph may be closed in any module, though the architecture requires the wiring closure to happen once, in the composition root; process-level cardinality and edge placement of interpretation are review-gated (SF2), not lint-gated, because neither is statically decidable. Each gap turns a rule into a costume that passes CI; the oxlint plugin already enforces a neighboring invariant (one Workflow.make per *.workflow.ts file), so these belong beside it.
Goal
The oxlint plugin ships rules that fail CI when (a) a variant declared in a *.workflow.ts decision or error union is never constructed in that file, (b) a Cell.layer decode/decide/encode spec body references effectful APIs — Effect.* constructors, yield*, Date.now, Math.random, or any module the cell-types vocabulary table classifies as I/O — or (c) a wiring closure happens where it must not: ManagedRuntime.make, Layer.provide, or Cell.provide called inside a function body, or eagerly at module scope with import-time effects, or outside the package's declared composition root. Each rule ships fixture tests proving it fires on the costume shape and stays silent on the lawful shape.
Evidence
@systemfsoftware/effect-cell-types@6.0.1 dist/index.d.ts:52-54 — the brand checks inhabitedness of the channels, not construction of their variants:
type Workflow<Command, Decision, DecisionError> = [Decision] extends [never] ? UninhabitedDecision
: [DecisionError] extends [never] ? UninhabitedError
: ((command: Command) => Result<Decision, DecisionError>) & WorkflowBrand;
dist/index.d.ts:189-201 — the purity facts exist as exported data; the plugin's make-body-purity rule consumes them for Workflow.make bodies, but nothing covers Cell.layer spec bodies:
interface Vocabulary {
readonly module: typeof DESCRIPTION_MODULE;
readonly ioCells: IoCellClassification;
readonly byKind: { readonly pure: readonly PhaseName[] };
readonly composer: 'layer';
}
declare const vocabulary: Vocabulary;
Orientation
Affected: the oxlint plugin package (oxlint-plugin-effect-workflow owns the make-file-location rule; the new rules are siblings). The plugin's existing RuleTester setup and the vocabulary export quoted above are the integration points.
Non-Counting Outcomes
- A reachability rule that inspects only the union declaration, not constructor call sites in the file — declared then equals constructed by fiat.
- A purity rule built on a three-identifier denylist that misses
Effect.runPromise, Effect.sync, or indirect I/O through imported helpers; the vocabulary table must be the source of truth.
- Rules that fire on lawful shapes:
andThen composition, pure Schema transforms in decode, or test files.
- Fixtures asserting message text but never that the rule actually fails the lint run.
- A placement rule keyed on a filename convention (
runtime.ts, bootstrap.ts) or on process cardinality; neither is statically decidable and both rot into allowlists. Undecidable properties belong to review (SF2), not lint.
- Shipping any of these rules at
warn or "advisory" severity as a resting state; the corpus's warn-dominance ruling requires error plus a dated baseline, named review, or off — never a severity a runner cannot enforce.
Acceptance Criteria
Workflow and Cell invariants compile as costumes: no lint for variant reachability, pure Cell phases, or wiring-closure placement
Problem Statement
Three invariants of the cell/workflow architecture are enforced by review alone, so violations compile cleanly. A workflow may declare error variants it never constructs — the type marker checks that the channel is inhabited, not that any code path reaches the variant.
decode/decide/encodephases inCell.layerspecs may perform I/O, read clocks, oryield*— their purity is convention. (The plugin's existingmake-body-purityrule coversWorkflow.makebodies;Cell.layerspec bodies are not covered by it.) And a runtime or layer graph may be closed in any module, though the architecture requires the wiring closure to happen once, in the composition root; process-level cardinality and edge placement of interpretation are review-gated (SF2), not lint-gated, because neither is statically decidable. Each gap turns a rule into a costume that passes CI; the oxlint plugin already enforces a neighboring invariant (oneWorkflow.makeper*.workflow.tsfile), so these belong beside it.Goal
The oxlint plugin ships rules that fail CI when (a) a variant declared in a
*.workflow.tsdecision or error union is never constructed in that file, (b) aCell.layerdecode/decide/encodespec body references effectful APIs —Effect.*constructors,yield*,Date.now,Math.random, or any module the cell-typesvocabularytable classifies as I/O — or (c) a wiring closure happens where it must not:ManagedRuntime.make,Layer.provide, orCell.providecalled inside a function body, or eagerly at module scope with import-time effects, or outside the package's declared composition root. Each rule ships fixture tests proving it fires on the costume shape and stays silent on the lawful shape.Evidence
@systemfsoftware/effect-cell-types@6.0.1dist/index.d.ts:52-54— the brand checks inhabitedness of the channels, not construction of their variants:dist/index.d.ts:189-201— the purity facts exist as exported data; the plugin'smake-body-purityrule consumes them forWorkflow.makebodies, but nothing coversCell.layerspec bodies:Orientation
Affected: the oxlint plugin package (
oxlint-plugin-effect-workflowowns themake-file-locationrule; the new rules are siblings). The plugin's existing RuleTester setup and thevocabularyexport quoted above are the integration points.Non-Counting Outcomes
Effect.runPromise,Effect.sync, or indirect I/O through imported helpers; thevocabularytable must be the source of truth.andThencomposition, pure Schema transforms in decode, or test files.runtime.ts,bootstrap.ts) or on process cardinality; neither is statically decidable and both rot into allowlists. Undecidable properties belong to review (SF2), not lint.warnor "advisory" severity as a resting state; the corpus's warn-dominance ruling requireserrorplus a dated baseline, named review, oroff— never a severity a runner cannot enforce.Acceptance Criteria
package.json).yield*, anEffect.*constructor,Date.now, and anioCells-classified import insidedecode/decide/encodeeach fail; a pure Schema-transform decode passes.ManagedRuntime.make/Layer.provide/Cell.provideinside the package's declared composition root passes and outside it fails; make-per-call and eager module-scope construction fail anywhere aterrorseverity with no baseline (no lawful instances exist). The root is a declared package-metadata field, never a filename convention. Until that field exists in the package taxonomy the placement property is gated by named review (the two-tier taxonomy's human tier) and the lint rule does not exist yet; when it ships it ships aterrorwith an explicit dated baseline enumerating pre-existing closure sites, and that baseline shrinks monotonically.