Skip to content

Workflow and Cell invariants compile as costumes: no lint for variant reachability, pure Cell phases, or wiring-closure placement #395

Description

@systemfsoftware-maker

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

  • The plugin package's typecheck, lint, and RuleTester suite exit 0 (gatekeeper; exact script names to be confirmed from its package.json).
  • Reachability: a fixture declaring an error-union variant never constructed in the file fails lint; the same file with the variant constructed passes.
  • Purity: fixtures covering yield*, an Effect.* constructor, Date.now, and an ioCells-classified import inside decode/decide/encode each fail; a pure Schema-transform decode passes.
  • Wiring closure: ManagedRuntime.make/Layer.provide/Cell.provide inside the package's declared composition root passes and outside it fails; make-per-call and eager module-scope construction fail anywhere at error severity 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 at error with an explicit dated baseline enumerating pre-existing closure sites, and that baseline shrinks monotonically.
  • Every rule's fixture set includes the lawful-shape case, and all pre-existing plugin rules still pass.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions