# Fluxtion golden path (LLM-readable)

The ONE blessed shape for an imperative, AOT, **audited** Fluxtion graph with **named output sinks** and a
**test** — plus the decision points and the gotchas that each cost a debugging cycle if missed. **Pattern-match
the code below; don't infer from prose.**

**Canonical framework rules live in `claude.txt`** (fluxtion repo `docs/claude.txt`) — the mental model, the
source-gen triage, services, build-time generation, `@OnTrigger(dirty)`. That is the framework reference; *this*
guide is the playground-flavoured golden path (portability roots, CheerpJ specifics, the accept/reject
reference). Read both; if they ever disagree, `claude.txt` wins on framework semantics.

This complements two machine-readable resources:

- **Template menu (which shape + which mode + key or keyless):**
  `https://fluxtion-playground.dev/starter-templates/index.json` — canonical shapes (embedded, connector,
  dag-multi-io, aot, audit, service-export, dsl, spring, mongoose) with `mode` (`interpreted` / `aot`) and a
  description that says whether a download needs a key. **Pick the shape here first.** Each entry is a generator
  *spec*, not code.
- **Canonical code:** the playground examples under `static/examples/`. The reference for *this* guide is
  `fluxtion-accept-reject-branch` (builder + imperative nodes + `Decision` + test). Match its shape.

---

## 1. Decision points (resolve these before writing code)

- **Authoring channel.** Imperative (`@OnEventHandler` / `@OnTrigger`) for stateful business logic and
  branching; **DataFlow DSL** (`DataFlowBuilder.subscribe(...).map(...)`) for streaming pipelines; **Spring XML**
  for ops handoff. Mix freely.
- **Compile mode.** *Interpreted* (`.build()`, keyless, in-browser, fast to feel) vs **AOT / builder**
  (a `FluxtionGraphBuilder` generates a dispatcher class; native-ready; the audit step-through + "deploy
  unchanged" story). **For an audited, deployable graph, use the builder/AOT shape below.**
- **Output sink — the single most error-prone choice.** Decision table:

  | Situation | Use | Notes |
  |---|---|---|
  | **imperative node, AOT** | **`@ServiceRegistered` + `MessageSink`** (inject by name) | ✅ blessed. See §2. |
  | DSL flow | `.sink("name")` then `flow.addSink("name", consumer)` | the DSL sink registry. |
  | imperative node, AOT | ❌ hand-wired `SinkPublisher` | **fails source-gen**: `constructor should be pre-generated`. |
  | imperative node, AOT | `getNodeById("x")` + a settable field | works, but couples the host to node ids; prefer `@ServiceRegistered`. |

---

## 2. The canonical shape (copy this)

Goal: a `RiskGate`-style imperative graph, AOT-generated by a builder, audited, with two named output sinks,
runnable + testable. (Full version: `static/examples/fluxtion-accept-reject-branch/`.)

**`fluxtion-builder/MyBuilder.java`** — authors the graph at build time; the playground/Maven generates the
processor. No `Fluxtion.compile` in `Main`, no source/graphml print markers.

```java
package com.example;

import com.telamin.fluxtion.builder.compile.config.FluxtionCompilerConfig;
import com.telamin.fluxtion.builder.compile.config.FluxtionGraphBuilder;
import com.telamin.fluxtion.builder.generation.config.EventProcessorConfig;
import com.telamin.fluxtion.runtime.audit.EventLogControlEvent.LogLevel;

public class MyBuilder implements FluxtionGraphBuilder {
    @Override public void buildGraph(EventProcessorConfig cfg) {
        cfg.addEventAudit(LogLevel.INFO);                 // audit every node invocation
        RiskGate gate = new RiskGate();
        // Register ONLY the roots; Fluxtion discovers the upstream tree via constructor refs.
        cfg.addNode(new Decisions(gate), "decisions");
    }
    @Override public void configureGeneration(FluxtionCompilerConfig cfg) {
        cfg.setWriteSourceToFile(true);
        cfg.setWriteGraphMlToFile(true);                  // GraphML for the step-through
        cfg.setGenerateDescription(true);
        cfg.setFormatSource(false);
        cfg.setPackageName("com.example.generated");
        cfg.setClassName("MyProcessor");
        // PORTABLE: only set output dirs when the host injects a build root.
        // Local Maven leaves them unset -> Fluxtion defaults (target/...).
        String buildDir = System.getProperty("fluxtion.build.dir");
        if (buildDir != null && !buildDir.isEmpty()) {
            cfg.setOutputDirectory(buildDir + "/generated/source/");
            cfg.setResourcesOutputDirectory(buildDir + "/generated/meta/");
            cfg.setBuildOutputDirectory(buildDir + "/");
        }
    }
}
```

**Imperative node** — `@OnEventHandler` entry; `auditLog.info(k,v)` for the business-readable audit;
**output sink injected by name** via `@ServiceRegistered MessageSink`.

```java
package com.example;

import com.telamin.fluxtion.runtime.annotations.OnTrigger;
import com.telamin.fluxtion.runtime.annotations.builder.FluxtionIgnore;
import com.telamin.fluxtion.runtime.annotations.runtime.ServiceRegistered;
import com.telamin.fluxtion.runtime.node.SingleNamedNode;
import com.telamin.fluxtion.runtime.output.MessageSink;          // extends Consumer<T>

public class Decisions extends SingleNamedNode {                 // gives auditLog + a name
    private final RiskGate gate;
    @FluxtionIgnore private MessageSink<String> sink;            // NOT a renderable field

    public Decisions(RiskGate gate) { super("decisions"); this.gate = gate; }

    @ServiceRegistered                                           // injected by name; no getNodeById
    @SuppressWarnings("unchecked")
    public void onSink(MessageSink<?> sink, String name) {
        if ("out".equals(name)) { this.sink = (MessageSink<String>) sink; }
    }

    @OnTrigger
    public boolean onUpdate() {
        auditLog.info("decision", gate.decision()).info("reason", gate.reason());  // audit = business record
        if (sink != null) sink.accept(gate.decision());          // sink is just a Consumer
        return true;
    }
}
```

**`Main.java`** — load the generated processor by name, register sinks by name (an inline lambda *is* a
`MessageSink`), drive. Portable + thin.

```java
package com.example;

import com.telamin.fluxtion.runtime.DataFlow;
import com.telamin.fluxtion.runtime.output.MessageSink;

public class Main {
    public static void main(String[] args) throws Exception {
        DataFlow flow = (DataFlow) Class.forName("com.example.generated.MyProcessor")
                .getDeclaredConstructor().newInstance();          // by name: built earlier in the pipeline

        flow.setAuditLogProcessor(System.out::println);           // NO [FLUXTION_AUDIT] markers needed
        flow.init();
        flow.registerService((MessageSink<String>) System.out::println, MessageSink.class, "out");
        flow.onEvent(/* your event */);
    }
}
```

**Test** — fresh `Class.forName` instance per run (imperative-only → `newInstance` is fine), register a
collecting sink by name, assert. Same `registerService` call the app uses.

```java
package com.example;

import com.telamin.fluxtion.runtime.DataFlow;
import com.telamin.fluxtion.runtime.output.MessageSink;
import org.junit.jupiter.api.Test;
import java.util.ArrayList; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class MyTest {
    @Test void routes() throws Exception {
        DataFlow flow = (DataFlow) Class.forName("com.example.generated.MyProcessor")
                .getDeclaredConstructor().newInstance();
        List<String> out = new ArrayList<>();
        MessageSink<String> sink = out::add;                      // MessageSink is a functional interface
        flow.init();
        flow.registerService(sink, MessageSink.class, "out");
        flow.onEvent(/* ... */);
        assertEquals(1, out.size());
    }
}
```

---

## 2b. Run it fast (jbang — no Maven)

The quickest feedback loop is a single jbang file, no Maven project — and it works for imperative/AOT, not
just DSL. **But it is not offline:** every Fluxtion build (`compile`, `compileAot`, `DataFlowBuilder.build()`,
`interpret`) runs topology inference in the generator — the cloud service by default (key in
`~/.fluxtion/fluxtion.apiKeyFile`), or a local generator if you add `fluxtion-generator-core` to `//DEPS`.

```java
//REPOS central=https://repo1.maven.org/maven2,fluxtion=https://repo.repsy.io/mvn/fluxtion/fluxtion-public
//DEPS com.telamin.fluxtion:fluxtion-builder:1.0.6
//JAVA 21
package demo;                                        // NAMED package required
import com.telamin.fluxtion.Fluxtion;
import com.telamin.fluxtion.runtime.DataFlow;
import com.telamin.fluxtion.runtime.annotations.OnEventHandler;
import com.telamin.fluxtion.runtime.annotations.OnTrigger;

public class Demo {
    public static void main(String[] a) {
        DataFlow flow = (DataFlow) Fluxtion.compileAot("demo.generated", "DemoProcessor",
                new Child(new Parent()));             // add only the child; Parent is discovered
        flow.init();
        flow.onEvent("hi");
    }
    public static class Parent { @OnEventHandler public boolean on(String s){ return true; } }
    public static class Child  { final Parent p; public Child(Parent p){ this.p = p; }
        @OnTrigger public boolean fire(){ return true; } }
}
```

`jbang demo/Demo.java`. Rules that trip people up: helper node classes must be **public** + in a **named
package** (the generated processor is a sub-package and can't see default-package / package-private types), so
make them `public static` **nested** classes (one public top-level class per file). A node CAN be a `record`;
a counter works as `record Counter(LongAdder n)` (final reference, mutable adder) — `int count; count++` cannot.

## 3. Gotcha → fix (each one cost a real debugging cycle)

| Symptom | Cause | Fix |
|---|---|---|
| `cannot find matching constructor for ... fields:[x]` | a `Map`/`List`/`Consumer`/rich field isn't renderable as a Java literal by source-gen | annotate the field `@FluxtionIgnore` (or `transient`) and set it at runtime |
| `use @AssignToField to resolve clashing types [a, b]` | two+ constructor params of the **same type** | annotate each: `@AssignToField("fieldName")` |
| `constructor should be pre-generated` | hand-wired a `SinkPublisher` into an imperative AOT node | don't — use `@ServiceRegistered MessageSink` (§2) |
| downstream `@OnTrigger` never fires | the node has no trigger annotation, or a `.push()` setter target's dirty bit is never set | give it `@OnEventHandler`/`@OnTrigger`; for DSL→imperative use `.flowSupplier()` + an `@OnTrigger` holder |
| `cannot find symbol: class Foo` in generated source | user types in the default package | put `package com.example;` on **every** file |
| `cannot find symbol: method lambda$null$...` | a **capturing lambda** inside `Fluxtion.compile(cfg -> ...)` | use a **named method reference**, not a lambda (inverts for runtime `.build()`, which needs lambdas) |
| assertions read 0/null on your node ref | `compile + addNode(node)` bakes its OWN instance; yours is a bystander | use `getNodeById("name")`, or `compileDispatcher` in tests, or inject via `@ServiceRegistered` |
| (CheerpJ only) `UnsatisfiedLinkError ... getFloatVolatile` | a primitive `float` field | use `double` |
| (CheerpJ only) 2nd `Fluxtion.compile` throws `NoClassDefFoundError $$Lambda` | compile-once per JVM | use the **builder/AOT** shape (no compile in `Main`); tests `new` the generated class |

---

## 4. Portability (playground vs local = injected path roots only)

Write **portable** source; let the host inject the roots:

- **Data output:** `System.getProperty("fluxtion.output.dir", "output")` — real JVM writes `./output/`; the
  playground injects `/files/output`.
- **Generated source:** set `configureGeneration` dirs **only when** `System.getProperty("fluxtion.build.dir")`
  is present (see §2) — real Maven uses Fluxtion defaults; the playground injects `/files`.
- **Audit:** `setAuditLogProcessor(System.out::println)` — the playground recovers the bare `eventLogRecord`
  blocks from stdout; no markers, identical on a real JVM.

Result: one source tree, byte-identical between playground and local, differing only in those injected roots.

---

## 5. Required rules (80% of cold-start failures)

1. `package com.example;` on **every** file (default-package types are unreachable from the generated processor).
2. `flow.init()` does **not** reset user-node fields — only per-event machinery. Use `@Initialise` or an explicit `reset()` if you reuse one processor across batches.
3. **Source-gen is serialisation** — Fluxtion reflects over node fields and emits Java that reconstructs them. Every "renderable field" gotcha follows from this.
4. **Single-threaded by design.** No executors/threads/async inside nodes.
5. **Mutate state through events**, not from outside — external mutation breaks audit/replay.

Reference corpus when unsure: the playground examples (`static/examples/`) and
`https://github.com/telaminai/fluxtion-examples`. Match the closest working example before improvising.
