---
name: fluent-rule-scaffold
description: Scaffold new Fluent Commerce Rubix rules. Generates Java class, test class, and wires into module.json. Triggers on "create rule", "new rule", "scaffold rule", "add rule class".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: <rule-name> [--entity-type ORDER|FULFILMENT|...] [--package common|fulfilment|order|...]
cursor-tier: manual
planning-gate: mandatory
---

# Rule Scaffolder

Generate new Fluent Commerce Rubix rule classes with proper structure, annotations, test skeletons, and module wiring.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-rule-scaffold.jsonl` exists
2. If yes, read last 20 records
3. Filter to: same entity type/subtype if known, FAILURE or USER_CORRECTED outcomes, confidence = CONFIRMED or PROMOTED
4. Extract top 3 relevant learnings — use as internal "WATCH OUT" guidance during execution
5. Do NOT show raw feedback to the user — silently adjust approach based on past issues

## Pre-flight: Project Knowledge Check

Check for project-specific knowledge that should inform rule scaffolding:

1. Check if `accounts/<PROFILE>/knowledge/` exists
2. If yes, read it and find files where `relevant-skills` includes `fluent-rule-scaffold`
   **Account knowledge:** Read from `accounts/<PROFILE>/knowledge/` when `FLUENT_PROFILE` is set. These contain project-specific conventions.

3. Read matching knowledge files — typically:
   - `knowledge/platform/domain-model.md` — use for entity edges, GraphQL traversal paths in rule code
   - `knowledge/platform/graphql-in-rules.md` — use for DynamicUtils vs Apollo decision, GraphQL API reference, code examples
   - `accounts/<PROFILE>/knowledge/naming-conventions.md` — use for class naming, package conventions
   - `accounts/<PROFILE>/knowledge/coding-standards.md` — use for logging, error handling, test patterns
   - `accounts/<PROFILE>/knowledge/client/business-rules.md` — use for rule logic and validation requirements
   - `accounts/<PROFILE>/knowledge/client/integrations.md` — use if rule calls external systems
4. If `accounts/<PROFILE>/memory/index.md` exists, read the top account-specific rule and deployment gotchas and apply them silently.
5. Apply knowledge silently — do NOT show raw knowledge to user, just follow the conventions

## Planning Gate

### Pre-flight Chain (Three-Level)

Before scaffolding any rule, run the three-level pre-flight check:

1. **Spec check:** If `accounts/<PROFILE>/features/<slug>/status.json` exists for this work:
   - Check `spec: "APPROVED"` — If missing, BLOCK: `→ BLOCKED: PREREQ_MISSING — Run /fluent-use-case-discover first`
   - Check `plan: "APPROVED"` — If missing, BLOCK: `→ BLOCKED: PLAN_REQUIRED — Run /fluent-feature-plan first`
   - If both approved, skip to implementation referencing the plan's §7 (Rules) and §8 (Detailed Design) sections
2. **Multi-artifact work** (this rule + workflow changes + settings): STOP. Invoke `/fluent-feature-plan` first — this skill cannot be used for multi-artifact work without a feature plan
3. **Standalone single-rule work** (no feature context, no multi-artifact):
   - Check `accounts/<PROFILE>/tasks/` for a task plan with `Status: APPROVED`
   - If approved task plan found, skip to implementation
   - If no plan, continue to the Planning Gate below to write a plan for this skill

**Before scaffolding any rule, write a plan using the template from `PLAN_TEMPLATE.md` in the `fluent-feature-plan` skill.** Every table row must carry a Source column (NEW/EXISTING/MODIFIED/REUSED/OOTB). For NEW rules, provide full pseudo logic.

**If this rule is part of a larger feature** (multiple rules + workflow changes + settings), use `/fluent-feature-plan` first to produce the comprehensive plan. Then reference the approved plan during scaffolding.

**Rule-scaffold specific emphasis — ensure these are covered:**

1. **Business Context (§1)** — what business need drives this rule; why custom, not OOTB
2. **State machine (§3.1)** — show the workflow status where this rule fires, with transitions
3. **Sequence diagram (§3.2)** — data flow: READ, VALIDATE, QUERY, BUILD, MUTATE, LOG. Use Mermaid `sequenceDiagram`. Validate syntax per `knowledge/mermaid-syntax-rules.md`
4. **Rulesets (§6)** — which rulesets use this rule, trigger event, from/to status
5. **Rules Inventory (§7)** — this rule classified as NEW with source label
6. **Detailed Design (§8)** — parameters table, pseudo logic, GraphQL operations
7. **Settings (§9)** — settings the rule reads, with context, value type, and shape
8. **GraphQL Operations (§12)** — queries and mutations, marked as NEW or REUSED pattern

**Write the plan to:** `accounts/<PROFILE>/tasks/<YYYY-MM-DD>-rule-scaffold-<slug>.md`. Set `Status: PENDING`.

Present the full plan content to the user and wait for approval before generating any files. On approval, update the file to `Status: APPROVED`. If the user says "just do it", proceed directly (still write the file for audit trail).

## Handoff Protocol

### Signals emitted by this skill

| Signal | Condition | Example |
|--------|-----------|---------|
| `-> READY: <path>` | Rule class + test scaffolded | `-> READY: accounts/MY_PROFILE/SOURCE/backend/.../CancelOrderRule.java` |
| `-> NEXT: /fluent-<skill>` | Next skill in chain | `-> NEXT: /fluent-build` |
| `-> BLOCKED: <reason>` | Prerequisite missing | `-> BLOCKED: PREREQ_MISSING — No module in SOURCE/backend/. Run /fluent-module-scaffold` |
| `-> SKIP: <reason>` | Rule already exists | `-> SKIP: Rule CancelOrderRule already registered in module.json` |

### Error codes

| Code | Condition | Recovery |
|------|-----------|----------|
| `PLAN_REQUIRED` | Multi-artifact work attempted without approved plan | Run `/fluent-feature-plan` first |
| `PREREQ_MISSING` | No module found in `accounts/<PROFILE>/SOURCE/backend/` | Run `/fluent-module-scaffold` or clone repo |
| `VALIDATION_FAILED` | Rule name conflicts or entity type mismatch | Fix input parameters and retry |

## When to Use

- Creating a new custom Rubix rule
- Adding a rule that follows the project's BaseRule pattern
- Generating boilerplate with proper annotations and logging
- Wiring a new rule into `module.json`

## Pre-flight: Module Existence

Before scaffolding any rule, verify a target module exists:

1. **Search** `accounts/<PROFILE>/SOURCE/backend/` recursively for `module.json` files
2. **If module found:** Confirm the module's entity types cover this rule's entity type. Note the module path for downstream scaffolding.
3. **If NO module found:** **STOP.** Do not scaffold. Inform the user: *"No module found in `accounts/<PROFILE>/SOURCE/backend/`. Run `/fluent-module-scaffold` first to create one, or clone an existing repo into `SOURCE/backend/`."*
4. **If module found but source-map.json is missing or stale:** Run `/fluent-custom-code` to analyze the module before adding new rules. This ensures package conventions, base class patterns, and existing rule names are known.

This check prevents scaffolding a rule with nowhere to put it.

## Execution Context: The Rubix User

Rules execute as a per-retailer **Rubix user** (`<ACCOUNT>-<RETAILER_ID>-rubix`, type `API`), NOT the dispatching user. `context.api()` authenticates as this user.

**Implications for scaffolded rules:**
- GraphQL calls via `context.api()` succeed only if the Rubix user has `GRAPHQL` role
- The dispatching user's identity is NOT available by default — if the rule needs to know who clicked the button, add an `@EventAttribute(name = "actionedBy")` and wire the user action attribute with `defaultValue: "{{me.username}}"`
- Permission errors in rule execution → check Rubix user roles first, not the dispatching user

**Discovery:** `{ users(first: 10, type: ["API"]) { edges { node { username primaryRetailer { ref } roles { role { name } } } } } }` — filter for `*-rubix` pattern.

## Pre-Check: Use OOTB or Build Custom?

Before scaffolding, determine if an out-of-the-box (OOTB) rule already covers the requirement.

### Decision Tree

```
User asks: "I need a rule that does X"
│
├── 1. Check module inventory
│   Read: accounts/<PROFILE>/analysis/modules/module-inventory.json
│   If missing → run /fluent-module-validate inventory first
│
├── 2. Search registered rules for matching capability
│   Use MCP tool: plugin.list with name filter
│   Look for rules whose description matches the ask
│   Check both FLUENTRETAIL.* (platform) and <ACCOUNT>.* (deployed)
│
├── 3. Evaluate match
│   ├── EXACT MATCH → Recommend OOTB rule
│   │   Tell user: "Use <RuleName> with parameters: ..."
│   │   Show accepted entity types and parameters
│   │   No scaffolding needed
│   │
│   ├── PARTIAL MATCH → Evaluate gap
│   │   Small gap (missing one parameter) → Consider extending OOTB
│   │   Large gap (fundamentally different) → Build custom
│   │
│   └── NO MATCH → Build custom rule
│       Proceed with scaffolding below
```

### When OOTB Rules Suffice

| Need | OOTB Rule | Why not custom? |
|------|-----------|----------------|
| Change entity status | `SendEvent` / `ChangeStateGQL` | Standard lifecycle — no custom logic needed |
| Send event to another entity | `SendEventToEntity` | Platform handles entity resolution |
| Forward event conditionally | `ForwardIf*` family (dozens exist) | Many condition checks already built |
| Update attributes from event | `SetAttributes` / `UpsertAttributes` | Standard attribute operations |
| Create a fulfilment | `CreateFulfilmentRule` | Standard fulfilment creation |

### When Custom Rules Are Needed

| Need | Why custom? | Real example |
|------|------------|-------------|
| Setting-driven dynamic behavior | OOTB rules have fixed parameters, can't read settings for runtime config | `SendWebhookWithDynamicAttributes`, `UpdateEntityFromSetting` |
| Complex data transformation | Multi-step JSON path extraction + field mapping | `UpsertAttributeFromPath`, `UpdateFulfilmentItemsFromEvent` |
| Cross-entity composition | Query one entity to create/update another atomically | `CreateFulfilmentFromSourcingLocation` |
| Domain-specific validation | Business logic not covered by generic conditions | `CreateMissingVariantProducts` |

### Rule Discovery via MCP

```
# Search all registered rules by keyword
plugin.list name="Fulfilment"     → all fulfilment-related rules
plugin.list name="SendEvent"      → all event-forwarding rules
plugin.list name="Attribute"      → all attribute manipulation rules
plugin.list name="Webhook"        → webhook-related rules
```

Each result includes `description`, `parameters[]`, and `accepts[]` (entity types) — enough to determine fit.

## Rubix Rule Framework

All custom rules in this project follow this architecture:

```
BaseRule (abstract)
  └── extends Rule (Rubix SDK)
  └── wraps Context in ContextWrapper
  └── provides automatic logging via CommonUtils.generateLog()
  └── subclasses implement: run(ContextWrapper context)
```

### Contract-First Design (MANDATORY before writing code)

Before writing ANY rule, classify every input using this decision framework:

**The question:** "Does this value change per event execution, or is it configured once per ruleset?"

| If the input is... | Use | Read via | Example |
|---------------------|------|----------|---------|
| Same every time the ruleset runs | `@ParamString` | `context.getProp("name")` | `eventName`, `creditMemoType`, `fulfilmentType` |
| Different per execution (user input, upstream data) | `@EventAttribute` | `event.getAttributes().get("name")` | `appeasementAmount`, `proposedFulfilment`, `longitude` |
| Entity data already on the context entity | Neither — query it | `DynamicUtils.getJsonPath(context, "field")` | `currency.alphabeticCode`, `ref`, `status` |

Before generating Java files, output this **Rule Contract** block and get confirmation:

| Section | Required content |
|---------|------------------|
| Inputs | `name`, `channel` (`PROP`/`EVENT`/`ENTITY_QUERY`), `required`, `type`, `where-defined` (ruleset props, userActions.attributes, or query path) |
| Outputs | Mutations called, emitted events, entity/event attributes written |
| Validation | Explicit error condition + error message for each required input |
| Compatibility | Event payload read order and any legacy fallback behavior |

**Default event read order:** `event.getAttribute(name, Type.class)` → `event.getAttributes().get(name)` → optional legacy fallback (only if needed for backward compatibility).

**OOTB @ParamString categories** (from fc-plugin-order-2.1.0 analysis, 60+ rules):

| Category | Examples | Pattern |
|----------|---------|---------|
| Event routing | `eventName` | Almost universal (~30 rules). Which event to fire next. |
| Entity type to create | `creditMemoType`, `fulfilmentType`, `invoiceType`, `billingAccountType` | Suffix is always `*Type`. |
| Reference config | `virtualCatalogueRef`, `networkRef`, `sourcingProfileRef` | Suffix is `*Ref`. |
| Status filter lists | `excludedFulfilmentStatuses`, `acceptedStatuses`, `locationStatuses` | Comma-separated values. |
| Branching events | `validAddress`/`invalidAddress`, `BillingAccountExistsEventName` | Two or more event names for conditional routing. |
| Technical defaults | `distanceMeasurementUnit`, `weightMeasurementUnit`, `systemRejectedLocationRef` | Rarely changes, but configurable per deployment. |

**OOTB @EventAttribute categories:**

| Category | Examples | Pattern |
|----------|---------|---------|
| User action form fields | `appeasementAmount`, `appeasementReason`, `comment`, `changeReason` | Simple strings from UI forms. |
| Complex POJOs | `proposedFulfilment`, `packedItems`, `fulfilmentArticles`, `deliveryAddress`, `revisedItems` | Serialized objects passed between rules. |
| Coordinates | `longitude`, `latitude` | From geocoding upstream. |
| Rule-chain accumulation | `locations`, `virtualPositions`, `attributes` | Lists built up across a sourcing chain. |
| Entity references | `entityReference` | Link to triggering entity (e.g., order ID that caused billing account event). |

**Three OOTB rule patterns:**

1. **Pure routing** (ParamString only) — `SendEvent*`, `Forward*` rules. Config: `eventName` + conditions. No event data.
2. **Pure processing** (EventAttribute only) — `Validate*`, `Update*Coordinates`. Consumes dynamic data. No config.
3. **Entity creation** (both) — `Create*` rules. Type/config from props, data from event attrs. **Most custom rules fall here.**

### Concurrent Access Design (MANDATORY for mutating rules)

Multiple users act on entities simultaneously. The UI shows a cached snapshot — by the time a user clicks, another user may have changed the entity. Rules are the safety net.

#### Idempotent rules

If the same event hits twice due to a race condition, the rule should check current state before mutating:

```java
// GOOD: Idempotent — checks before creating
@Override
public void run(RuleContext context) {
    // Check if child entity already exists
    List<CreditMemo> existing = context.getEntity()
        .getCreditMemos().stream()
        .filter(cm -> "APPEASEMENT".equals(cm.getSubtype()))
        .filter(cm -> eventRef.equals(cm.getRef()))
        .collect(Collectors.toList());

    if (!existing.isEmpty()) {
        log.warn("CreditMemo already exists for ref={}, skipping", eventRef);
        return; // Safe no-op
    }
    // Proceed with creation
}

// BAD: Not idempotent — creates duplicate child entity on retry
@Override
public void run(RuleContext context) {
    // Directly creates without checking existence
    context.action().mutation(createCreditMemoMutation, variables);
}
```

**When idempotency matters most:** Entity creation rules (`Create*`), financial mutation rules (appeasements, invoices, payments), rules that fire downstream events (duplicate events cascade).

#### Precondition validation

Never trust event payload values without validating against current entity state:

```java
@Override
public void run(RuleContext context) {
    // 1. Validate entity is still in expected state
    String currentStatus = context.getEntity().getStatus();
    if (!"BOOKED".equals(currentStatus)) {
        throw new RubixException(400,
            "Expected BOOKED but entity is " + currentStatus);
    }

    // 2. Validate business preconditions
    BigDecimal amount = new BigDecimal(
        event.getAttributes().get("appeasementAmount"));
    BigDecimal orderTotal = context.getEntity().getTotalPrice();
    if (amount.compareTo(orderTotal) > 0) {
        throw new RubixException(400,
            "Appeasement " + amount + " exceeds order total " + orderTotal);
    }

    // 3. Proceed only after validation passes
}
```

**Validate early, fail fast.** Put validation rules before mutation rules in the ruleset ordering. A failed validation rule stops the ruleset — no partial mutations.

#### Entity creation rules: always set a unique ref

When creating child entities (CreditMemo, Fulfilment, etc.), generate a deterministic ref that includes the triggering event context. This enables idempotency checks:

```java
// GOOD: Deterministic ref from event context
String ref = "APPEASE-" + orderId + "-" + System.currentTimeMillis();

// BETTER: Ref from event attribute (retries reuse same ref)
String ref = event.getAttributes().get("appeasementRef");
// If null, generate and store on event for retry safety
```

### Required Annotations

Rubix rules have TWO types of class-level annotations for declaring inputs:

1. **`@ParamString`** (and `@ParamStrings`) -- declares **rule properties** (static config in workflow JSON). Read via `context.getProp("name")`.
2. **`@EventAttribute`** (and `@EventAttributes`) -- declares **event attributes** (dynamic, from user action payload or upstream rule). Read via `event.getAttributes().get("name")` or `event.getAttribute("name", Type.class)`.

**CRITICAL (team standard):** Always declare `@EventAttribute` for every event key your rule consumes. Runtime behavior can vary by rule/plugin/event shape; annotations keep metadata, validation, and behavior explicit and portable across environments.

```java
@RuleInfo(
    name = "MyRuleName",
    description = "What this rule does in one sentence"
)
// Rule props (static workflow config)
@ParamString(name = "paramName", description = "What this parameter controls")
// Event attributes (dynamic from event payload) — MANDATORY for any event attribute the rule reads
@EventAttributes({
    @EventAttribute(name = "attributeName1"),
    @EventAttribute(name = "attributeName2")
})
@Slf4j  // Lombok logging
public class MyRuleName extends BaseRule {
    @Override
    public void run(ContextWrapper context) {
        // Rule implementation
    }
}
```

**Imports for event attribute annotations:**
```java
import com.fluentretail.rubix.rule.meta.EventAttribute;
import com.fluentretail.rubix.rule.meta.multi.EventAttributes;
```

### Key APIs

| API | Usage |
|-----|-------|
| `context.getProp("name")` | Read ruleset prop value (`@ParamString` — always a String) |
| `context.getEvent()` | Get the triggering event |
| `context.getEvent().getAccountId()` | Account ID for logging |
| `context.getEvent().getEntityId()` | Entity ID |
| `context.getEvent().getEntityRef()` | Entity reference |
| `context.getEvent().getEntityType()` | Entity type |
| `context.getEvent().getAttributes()` | Event attributes map (`@EventAttribute` — dynamic from payload) |
| `context.getEvent().getAttribute("name", Type.class)` | Typed event attribute access (preferred over raw map) |
| `context.addLog("message")` | Add to execution log |
| `context.action().mutation()` | Queue a GraphQL mutation (executes AFTER rule completes) |
| `context.action().eventAction()` | Queue an event to fire |
| `DynamicUtils.query(context, paths, queryName, params)` | Execute dynamic GraphQL query |
| `DynamicUtils.mutate(context, values)` | Execute dynamic mutation |
| `DynamicUtils.getJsonPath(context, jsonPath)` | Resolve value from event or entity |
| `SettingUtils.getSettingByRef(context, ref)` | Read a Fluent Setting |

## Rule Class Template

```java
package com.fluentcommerce.rule.<PACKAGE>;

import com.fluentcommerce.common.BaseRule;
import com.fluentcommerce.common.ContextWrapper;
import com.fluentretail.rubix.rule.meta.EventAttribute;
import com.fluentretail.rubix.rule.meta.ParamString;
import com.fluentretail.rubix.rule.meta.RuleInfo;
import com.fluentretail.rubix.rule.meta.multi.EventAttributes;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;

/**
 * <DESCRIPTION>
 */
@RuleInfo(
    name = "<RULE_NAME>",
    description = "<DESCRIPTION>"
)
@ParamString(name = "<PARAM1>", description = "<PARAM1_DESC>")
// Team standard: Declare ALL event attributes this rule reads from the event payload.
@EventAttributes({
    @EventAttribute(name = "<EVENT_ATTR1>"),
    @EventAttribute(name = "<EVENT_ATTR2>")
})
@Slf4j
public class <RULE_NAME> extends BaseRule {

    private static final String CLASS_NAME = <RULE_NAME>.class.getSimpleName();

    @Override
    public void run(ContextWrapper context) {
        String accountId = context.getEvent().getAccountId();

        log.info("[{}] [{}] - Processing event: {}", accountId, CLASS_NAME,
                context.getEvent().getName());
        context.addLog("Processing " + CLASS_NAME);

        // Read rule props (static workflow config via @ParamString)
        String param1 = context.getProp("<PARAM1>");
        if (param1 == null || param1.trim().isEmpty()) {
            log.error("[{}] [{}] - Required prop '<PARAM1>' is missing", accountId, CLASS_NAME);
            context.addLog("Error: <PARAM1> is required");
            return;
        }

        // Read event attributes (dynamic from event payload via @EventAttribute)
        // Prefer Event API first. Only add DynamicUtils JSONPath fallback for legacy payload compatibility.
        Map<String, Object> eventAttributes = context.getEvent().getAttributes();
        Object eventAttr1 = eventAttributes.get("<EVENT_ATTR1>");

        try {
            // === Rule logic here ===

            context.addLog(CLASS_NAME + " completed successfully");
        } catch (Exception e) {
            log.error("[{}] [{}] - Error: {}", accountId, CLASS_NAME, e.getMessage(), e);
            context.addLog("Error in " + CLASS_NAME + ": " + e.getMessage());
        }
    }
}
```

> **When to include `@EventAttribute` vs omit:** If the rule reads ANY data from `event.getAttributes()` or `event.getAttribute(...)`, it MUST declare those attributes with `@EventAttribute`. If the rule only reads rule props (`context.getProp(...)`) and entity data (queries), omit the `@EventAttributes` block entirely.

## Test Class Template

```java
package com.fluentcommerce.rule.<PACKAGE>;

import com.fluentretail.rubix.event.Event;
import com.fluentretail.rubix.rule.meta.RuleInfo;
import com.fluentretail.rubix.v2.context.Context;
import com.fluentretail.rubix.v2.action.Action;
import com.fluentretail.rubix.v2.action.MutationAction;
import com.fluentretail.rubix.v2.action.EventAction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class <RULE_NAME>Test {

    private <RULE_NAME> rule;

    @Mock private Context context;
    @Mock private Event event;
    @Mock private Action action;
    @Mock private MutationAction mutationAction;
    @Mock private EventAction eventAction;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        rule = new <RULE_NAME>();

        when(context.getEvent()).thenReturn(event);
        when(context.action()).thenReturn(action);
        when(action.mutation()).thenReturn(mutationAction);
        when(action.eventAction()).thenReturn(eventAction);
        when(event.getAccountId()).thenReturn("TEST_ACCOUNT");
        when(event.getName()).thenReturn("TestEvent");
    }

    @Test
    @DisplayName("Rule has correct @RuleInfo annotation")
    void hasRuleInfo() {
        RuleInfo info = <RULE_NAME>.class.getAnnotation(RuleInfo.class);
        assertNotNull(info);
        assertEquals("<RULE_NAME>", info.name());
    }

    @Test
    @DisplayName("Handles missing required prop gracefully")
    void missingRequiredProp() {
        Map<String, String> props = new HashMap<>();
        when(context.getProp("<PARAM1>")).thenReturn(null);

        // Should not throw — rule logs error and returns
        assertDoesNotThrow(() -> rule.run(context));
    }

    @Test
    @DisplayName("Processes event successfully with valid props")
    void processesSuccessfully() {
        when(context.getProp("<PARAM1>")).thenReturn("testValue");

        rule.run(context);

        // Verify expected behavior
        // verify(mutationAction).xxx(...);
    }
}
```

## Pre-Check: Load Source Analysis

Before scaffolding, check if `/fluent-custom-code` analysis artifacts exist:

```
accounts/<PROFILE>/analysis/code/source-map.json
accounts/<PROFILE>/analysis/code/constraints.json
```

If found, read them to discover:
- `modules[].patterns.basePackage` — correct package prefix
- `modules[].patterns.subPackages` — available sub-packages
- `modules[].patterns.baseClass` — base class to extend
- `modules[].buildRoot` — where to run Maven
- `modules[].rules[]` — existing rules (avoid name collisions)
- `constraints[].extensionConstraints` — rules to follow

If not found, suggest running `/fluent-custom-code <PROFILE>` first, or fall back to the defaults below. Treat artifact-gate failures as "not found" (for example missing required files, missing hashes, or blocking `missingSources` in `constraints.json`).

## Scaffolding Steps

### 1. Determine rule location

Rules live under the plugin source tree:

```
plugins/rules/<module-alias>/src/main/java/com/fluentcommerce/rule/
├── common/           # Entity-agnostic rules
├── fulfilment/       # Fulfilment-specific rules
├── variantproduct/   # Product-specific rules
├── order/            # Order-specific rules (create if needed)
└── returnorder/      # Return order rules (create if needed)
```

### 2. Create the rule class

Write the Java file at the appropriate package path using the template above. Replace all `<PLACEHOLDERS>`.

### 3. Create the test class

Write the test at the mirror path under `src/test/java/`. Test class name = `<RuleName>Test.java`.

### 4. Wire into module.json

Add the rule name to the `rules` array in the module's `resources/module.json`:

```json
{
  "rules": [
    "ExistingRule1",
    "ExistingRule2",
    "NewRuleName"
  ]
}
```

### 5. Verify compilation

```bash
cd plugins/ && mvn clean install -pl rules/<module-alias> -am
```

If Maven fails, check:
- Node.js version (must be 18 for Apollo codegen): `nvm use 18` on Unix; on Windows with nvm-windows (nvm4w), use the full version number: `nvm use 18.x.x`
- Import paths are correct
- `BaseRule` and `ContextWrapper` are in the `common` package

## GraphQL in Rules — Approach Decision (MANDATORY)

**Before scaffolding any rule that queries or mutates entities, ask the user which approach to use.** Full reference: `knowledge/platform/graphql-in-rules.md`.

### Two Approaches

| Approach | When to Use | Key Trade-off |
|----------|-------------|---------------|
| **DynamicUtils** (dynamic) | Cross-entity queries, rapid prototyping, no Apollo codegen setup, custom extensions | Flexibility — field paths are strings (runtime errors) |
| **Apollo-generated classes** (typed) | OOTB-style rules, complex nested mutations, existing codegen pipeline | Type safety — compile-time checks, IDE autocomplete |

**Ask:** _"This rule needs GraphQL operations. Do you want DynamicUtils (dynamic, no codegen) or Apollo-generated classes (typed, requires codegen pipeline)? Or describe what the rule needs and I'll recommend."_

### DynamicUtils Quick Reference

```java
import com.fluentcommerce.util.dynamic.DynamicUtils;

// Query current entity with POJO response
QueryResult<MyDto> result = DynamicUtils.query(context, MyDto.class);

// Query current entity with field paths
QueryResult<QueryDynamicData> result = DynamicUtils.query(context,
    ImmutableList.of("ref", "status", "items.ref", "items.product.name"));

// Cross-entity query (any GraphQL root)
QueryResult<QueryDynamicData> result = DynamicUtils.query(context,
    ImmutableList.of("username", "roles.role.name"), "users", params);

// Paginated connection (auto-pagination)
QueryListResult<Fulfilment> list = DynamicUtils.queryList(context,
    "fulfilments", Fulfilment.class, null);
for (Fulfilment f : list) { /* ... */ }

// Update current entity
DynamicUtils.mutate(context, ImmutableMap.of("attributes", attributeList));

// Create any entity
DynamicUtils.create(context, "createFulfilment", inputMap);

// Generic mutation (full control)
DynamicUtils.executeMutation(context, "createComment", "CreateCommentInput",
    ImmutableMap.of("input", inputMap), ImmutableList.of("id", "text"));
```

**Path syntax:** Omit `edges` and `node` — DynamicEntityQuery adds them automatically. Use `items.product.name` not `items.edges.node.product.name`.

### Apollo-Generated Quick Reference

```java
// Query (using pre-generated class from .graphql + codegen)
GetCustomerByRefQuery query = GetCustomerByRefQuery.builder().ref(ref).build();
GetCustomerByRefQuery.Data data = (GetCustomerByRefQuery.Data) context.api().query(query);
String name = data.customer().firstName();

// Mutation (using pre-generated class)
CreateBillingAccountMutation mutation = CreateBillingAccountMutation.builder()
    .input(CreateBillingAccountInput.builder().ref(ref).type("CUSTOMER")
        .retailer(RetailerId.builder().id(retailerId).build()).build())
    .build();
context.action().mutation(mutation);

// Connection → List helper
ImmutableList<Item> items = QueryUtils.connectionToList(data.order().items());
```

**Requires:** Apollo codegen pipeline (Node.js 18, `schema.json`, `.graphql` files, Maven build step).

## Rules That Read Settings

```java
import com.fluentcommerce.util.SettingUtils;
import com.fluentretail.rubix.foundation.graphql.type.Setting;

Setting setting = SettingUtils.getSettingByRef(context, "my.setting.ref");
JsonNode config = JsonUtils.stringToPojo(setting.getLobValue(), JsonNode.class);
```

## GraphQL Schema Pre-flight (MANDATORY)

**Before generating ANY Java code that contains GraphQL operations, validate against the live schema.** Full protocol: `knowledge/platform/graphql-preflight.md`.

This gate applies to BOTH DynamicUtils and Apollo approaches. Do NOT skip — one invalid field path causes silent runtime failures.

### For DynamicUtils rules:

Validate every field path planned for `DynamicUtils.query()`, `DynamicUtils.queryList()`, or `DynamicUtils.mutate()`:

1. **Introspect the root entity type:**
   ```
   graphql.introspect({ type: "<EntityType>" })
   ```
   Example: `graphql.introspect({ type: "Order" })` → confirms available fields and edges.

2. **Walk each field path** — for `"items.product.name"`:
   - `items` → verify exists on Order (connection to OrderItem)
   - `product` → `graphql.introspect({ type: "OrderItem" })` → verify product edge
   - `name` → `graphql.introspect({ type: "VariantProduct" })` → verify name field

3. **For cross-entity queries** (explicit `queryName` parameter):
   ```
   graphql.introspect({ type: "<TargetType>" })
   ```
   Verify the root query field exists and all nested paths resolve.

4. **If ANY path segment is invalid:** STOP. Report the invalid path with the suggested correction from introspection. Do NOT generate code with unvalidated paths.

### For Apollo-generated rules:

Validate the `.graphql` file contents before generating Java code that references Apollo classes:

1. **Build or locate the .graphql query/mutation:**
   ```
   graphql.validate({ query: "<full .graphql file content>" })
   ```

2. **Check `response.valid` (NOT `response.ok`):**
   - `valid: true` → proceed with code generation
   - `valid: false` → read `errors[]`, fix the query, re-validate

3. **Verify schema.json exists** in the module's graphql directory. If missing, warn: "Apollo codegen requires schema.json."

### For mutations (both approaches):

1. **Introspect the mutation input type:**
   ```
   graphql.introspect({ mutation: "updateOrder" })
   ```
   → Returns required input fields, types, and nullability.

2. **Verify every field in the mutation input map** matches the schema input type.

### MCP unavailable fallback:

If MCP is not connected, log: `WARNING: GraphQL schema validation skipped — MCP not available. Validate before deployment.` Add a `// TODO: Validate GraphQL paths against live schema` comment in generated code. Do NOT block code generation.

### Validation checkpoint in plan output:

When writing the rule scaffold plan (§12 GraphQL Operations), include a "Schema Validated" column:

| Operation | Type | Approach | Fields/Paths | Schema Validated |
|-----------|------|----------|-------------|-----------------|
| Query order items | READ | DynamicUtils | items.ref, items.product.name | YES — introspected Order, OrderItem, VariantProduct |
| Create credit memo | WRITE | Apollo | CreateCreditMemoMutation.graphql | YES — graphql.validate passed |

## Post-Scaffold Validation

After generating rule files, verify the output before handing off to `/fluent-build`:

1. **Java syntax check** — Read the generated `.java` file and confirm it parses (no unclosed braces, missing imports, or placeholder text like `TODO` or `FIXME` in logic paths)
2. **Test file exists** — Confirm the test class was generated alongside the rule class
3. **module.json wired** — Read `resources/module.json` and verify the new rule name appears in the `rules` array
4. **No duplicate registrations** — Confirm the rule name doesn't appear twice in `module.json`
5. **Compilation check** — Run `mvn compile -pl plugins -q` (or the module's build command) to verify the generated Java compiles without errors. If Maven is unavailable, recommend `/fluent-build` immediately to catch compilation issues early

If any check fails, fix before proceeding. Do not hand off a broken scaffold to `/fluent-build`.

### Post-Scaffold Critique (Automated)

**The implementation critique protocol lives at: `knowledge/platform/implementation-critique-protocol.md`.** Select the **Java Rule Rubric** (JR-1 through JR-7) and follow the protocol exactly.

After generating Java classes and tests, run the adversarial validation loop (max 2 rounds). The critic validates: @ParamString/@EventAttribute annotation contracts, DynamicUtils anti-pattern absence, @RuleInfo name match, null safety on external reads, RubixException usage, and plan pseudo logic alignment.

## Session Tracking

When invoked, log the following to the session tracking protocol (consumed by `/fluent-session`):

**On entry:**
```json
{ "skill": "<this-skill-name>", "timestamp": "<ISO-8601>", "arguments": { "<key>": "<value>", "...": "..." } }
```

**On exit:**
```json
{ "skill": "<this-skill-name>", "outcome": "<completed|failed|skipped>", "changesProduced": ["<seq-numbers>"], "toolCallsProduced": ["<seq-numbers>"], "nextRecommended": "<from-handoff-section>" }
```

All MCP tool calls made during execution should include `"skill": "<this-skill-name>"` in their tracking record for attribution. The `arguments` object should capture the key parameters actually passed (profile, retailer, entity type, etc.) — not a fixed schema. The `nextRecommended` value comes from the Handoff section below.

## Handoff

| Output Artifact | Consumed By | Path |
|----------------|-------------|------|
| Generated Java rule class | `/fluent-build` | `accounts/<PROFILE>/SOURCE/backend/<repo>/plugins/rules/<alias>/src/main/java/.../<RuleName>.java` |
| Generated test class | `/fluent-build` | `accounts/<PROFILE>/SOURCE/backend/<repo>/plugins/rules/<alias>/src/test/java/.../<RuleName>Test.java` |
| Updated module.json | `/fluent-module-validate` | `accounts/<PROFILE>/SOURCE/backend/<repo>/resources/module.json` |

## Error Reporting

Errors from this skill follow the standard format:

| Field | Description |
|-------|-------------|
| `phase` | Skill phase where error occurred (e.g., `pre-flight`, `scaffold`, `validation`, `wiring`) |
| `severity` | `CRITICAL` (blocks downstream), `HIGH` (needs fix), `MEDIUM` (advisory), `LOW` (informational) |
| `message` | Human-readable error description |
| `resolution` | Suggested fix or downstream skill to invoke |

## Post-Flight Verification

After scaffolding completes, verify the generated code:

1. **Compile check**: Run `mvn compile -pl plugins/<module-name>` to verify generated Java compiles without errors.
2. **Test compile check**: Run `mvn test-compile -pl plugins/<module-name>` to verify the test class compiles.
3. **Registration check**: Read `module.json` back and confirm the new rule appears in the `rules` array with correct `name`, `class`, and `type`.
4. **If compilation fails**: Fix the issue (missing imports, incorrect class name, typo) before declaring the scaffold complete. Do not proceed to next steps with broken code.

## Gotchas

- **`@ParamString` names are CASE-SENSITIVE** — `"jsonpath"` and `"jsonPath"` are different props
- **`@EventAttribute` is mandatory by team standard for event data** — any rule that reads from event payloads must declare those keys explicitly for predictable metadata and portability
- **`@ParamString` vs `@EventAttribute` are DIFFERENT input channels** — props come from static workflow JSON config, event attributes come from dynamic event payloads. Never mix them up
- **Do not use `DynamicUtils.getJsonPath(context, "event.X")` as the primary event-reader** — use Event API first; JSONPath is compatibility fallback only when legacy payload shape requires it
- **`context.action().mutation()` only QUEUES** — the mutation executes AFTER the rule returns
- **`DynamicMutation.buildMutationDocument` appends `!`** — never include `!` in type names
- **`updateFulfilmentItem` does NOT exist** — use `updateFulfilment` with nested `items` array
- **BaseRule wraps Context** — implement `run(ContextWrapper context)`, not `run(Context context)`

## Post-Execution: Feedback Capture

After completing this skill (whether success or failure), write a feedback record:

1. **Classify outcome:** `SUCCESS` (clean scaffold), `PARTIAL_SUCCESS` (scaffolded after fixing issues), `FAILURE` (could not complete), `BLOCKED` (no module found), `USER_CORRECTED` (user overrode approach)
2. **Build record:** Create a `feedback-record-v1` JSON object with:
   - `skill`: `"fluent-rule-scaffold"`
   - `feature`: feature slug if applicable
   - `entityType` / `entitySubtype`: from the rule's target entity
   - `phases`: trace of each phase (pre-flight, plan, generate-class, generate-test, wire-module-json, compile-check) with PASS/FAIL/SKIP
   - `learnings`: any gotchas discovered during execution
   - `userCorrections`: any corrections the user provided
3. **Redact secrets.** Keep business identifiers.
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-rule-scaffold.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters (create with `mkdir -p` if missing)

**Skip capture if:** The execution was trivially simple (e.g., user just asked a question, no actual scaffolding happened).

## Known Pitfalls
<!-- feedback-promoted: managed section, updated by feedback loop -->

- **Undeclared event keys create ambiguous behavior** — Even when values appear at runtime, missing `@EventAttribute` weakens contract clarity and makes validation/portability brittle across environments. Declare all consumed event keys. (Source: OOTB + custom rule comparison, CONFIRMED)
- **`@ParamString` vs `@EventAttribute` confusion** — Rule props (`@ParamString`) are static ruleset config; event attributes are per-event payload data. Never read dynamic payload via `context.getProp()`. (Source: framework conventions, CONFIRMED)
- **Use Event API first, JSONPath only as fallback** — Preferred order: `event.getAttribute(...)`/`event.getAttributes()` first, then compatibility fallback (`event.X`/`event.attributes.X`) only when needed for legacy payload shapes. (Source: production debugging, CONFIRMED)

<!-- end feedback-promoted -->
