---
name: fluent-module-validate
description: Discover, classify, and validate Fluent Commerce modules. Inventories all deployed modules (reference vs custom), validates extension module structure, and maps registered rules to their source. Produces cached JSON artifacts with content hashing. Triggers on "validate module", "check module structure", "module health", "what modules are deployed", "list modules", "is my module correct".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: [module-root-path | inventory] [--fix] [--force]
cursor-tier: manual
---

# Module Inventory and Structure Validator

Two capabilities in one skill:

1. **Module Inventory** — discover all deployed modules on an account, classify them (reference / custom extension / custom workflow / custom data), and map registered rules to their source modules.
2. **Structure Validation** — validate a specific custom extension module for structural correctness, version consistency, rule wiring, test coverage, and deployment readiness.

Both produce cached JSON artifacts. Inventory uses TTL-based caching (re-query if older than 1 hour). Structure validation uses content-hash caching (skip if source files unchanged).

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

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

## Ownership Boundary

This skill owns:
- Module inventory and classification for an account
- Structural validation and health reporting for individual modules
- The `module-inventory.json` and per-module `report.json` artifacts

Other skills own:
- Build execution → `/fluent-build`
- Rule scaffolding and OOTB-vs-custom decision → `/fluent-rule-scaffold`
- Module deployment → `/fluent-module-deploy`
- Source code behavior analysis → `/fluent-custom-code`

### Cross-Skill Integration

**`/fluent-module-validate inventory` output is consumed by:**
- `/fluent-custom-code` — Uses `module-inventory.json` to identify custom modules, distinguish `hasSource` cases, and skip reference modules during source analysis
- `/fluent-workflow-analyzer` — Uses deployed module list to annotate workflow rules with module origins
- `/fluent-build` — Uses inventory to determine what needs packaging

**Recommended sequencing:**
1. Run `/fluent-module-validate inventory` first (lists what's deployed, classifies modules)
2. Then `/fluent-custom-code` (analyzes local source against deployed inventory)
3. Then `/fluent-workflow-analyzer` (maps workflows to deployed rules)

## When to Use

- "What modules are deployed on this account?"
- "Which modules are custom vs reference?"
- "What rules are available?" (→ inventory + `plugin.list`)
- Before a build to catch structural issues early
- After scaffolding a new rule to verify wiring
- Before a version bump to ensure nothing is missing
- When inheriting an unfamiliar account to map the landscape
- As a pre-deployment gate in CI/CD pipelines

## Required Inputs

- **For inventory**: `PROFILE` name (to run `fluent module list -p <PROFILE>`)
- **For structure validation**: module root path (directory containing `resources/module.json` and `plugins/`)
  - If not provided, search recursively under `accounts/<PROFILE>/SOURCE/backend/` for `resources/module.json`

## Cross-Platform Command Notes

- Fluent CLI syntax is the same across macOS, Linux, and Windows.
- Where shell snippets differ, this skill provides both Bash and PowerShell variants.
- Use the variant that matches the active shell to avoid quoting/redirection issues.

---

# Part 1: Module Inventory

## Classification Algorithm

Every deployed module has a symbolic name. Classification uses this decision tree:

```
Has "fluent-commerce/" prefix?
├── YES → strip prefix to get <baseName>
│   ├── <baseName> in REFERENCE_NAMES set?
│   │   ├── YES → REFERENCE
│   │   └── NO
│   │       ├── starts with "fc-module-"  → CUSTOM_EXTENSION
│   │       ├── starts with "fc-workflow-" → CUSTOM_WORKFLOW
│   │       └── else                       → CUSTOM_DATA
│   │
└── NO (no "fluent-commerce/" prefix)
    ├── starts with "fc-module-"  → CUSTOM_EXTENSION
    └── else                      → CUSTOM_DATA
```

### Reference Module Names

These are the known Fluent Commerce standard modules (OOTB). This list should be updated when Fluent releases new reference modules:

```
core, order, fulfilment, inventory, location, globalinventory,
servicepoint, wave, returns, b2c-sample-data, b2c-sample-data-inventory
```

### Module Types Explained

| Type | Description | Has Rules? | Has Source? | Examples |
|------|-------------|-----------|-------------|---------|
| **REFERENCE** | Standard Fluent Commerce modules from repository. Deployed via `fluent module install <name>`. | Yes (OOTB rules) | No (closed-source) | `fluent-commerce/core`, `fluent-commerce/order` |
| **CUSTOM_EXTENSION** | Account-specific modules with custom Rubix rules. Built from Java source. | Yes (custom rules) | Yes (under `accounts/<PROFILE>/SOURCE/backend/`) | `fluent-commerce/fc-module-custom-extension` |
| **CUSTOM_WORKFLOW** | Workflow definitions packaged as deployable modules. | No | Workflow JSON in module ZIP | `fluent-commerce/fc-workflow-order-hm-updated` |
| **CUSTOM_DATA** | Configuration, settings, sample data, access control. No executable rules. | No | JSON/config files in module ZIP | `HM`, `hm-inventory-data`, `hm/global-access` |

**All types must be deployed** — even reference modules are deployed per-retailer via `fluent module install`.

### Rule Key Format (from `plugin.list`)

Registered rules follow the pattern: `ACCOUNT.context.RuleName`

| Prefix | Meaning |
|--------|---------|
| `FLUENTRETAIL.<context>.*` | Ships with the Fluent platform. Always available regardless of which modules are deployed. |
| `<ACCOUNT>.<context>.*` | Registered by a module deployed to this account. Context maps to the module's rule namespace. |

Known context-to-module mappings:

| Context | Module |
|---------|--------|
| `base` | `fluent-commerce/core` |
| `order`, `commonv2` | `fluent-commerce/order` |
| `fulfilment` | `fluent-commerce/fulfilment` |
| `globalinventory`, `commongi` | `fluent-commerce/inventory` |
| `core` | `fluent-commerce/core` (account-scoped copy) |
| Custom context names | Custom extension modules |

## Inventory Artifact

### Location

```
accounts/<PROFILE>/analysis/modules/
└── module-inventory.json     ← full inventory with classification + rule mapping (persistent)

accounts/<PROFILE>/reports/module-validate/
├── <module-name>.report.json ← per-module structure validation (ephemeral)
└── <module-name>.hash        ← per-module content hash (ephemeral)
```

### Cache Strategy

Inventory data comes from a live API call (`fluent module list`). Use TTL-based caching:

- If `module-inventory.json` exists and is **less than 1 hour old** → read cached, skip re-query
- If older than 1 hour or missing → re-query live, overwrite
- `--force` → always re-query

Check timestamp:
```bash
# Get file age in seconds (cross-platform via Node.js)
node -e "
const fs = require('fs');
const f = process.argv[1];
if (!fs.existsSync(f)) { console.log('missing'); process.exit(0); }
const age = (Date.now() - fs.statSync(f).mtimeMs) / 1000;
console.log(age > 3600 ? 'stale' : 'fresh');
" "accounts/<PROFILE>/analysis/modules/module-inventory.json"
```

### Inventory Schema (`module-inventory.json`)

```json
{
  "schema": "module-inventory-v1",
  "timestamp": "2026-02-22T10:30:00Z",
  "profile": "<PROFILE>",
  "account": "<account-name>",
  "environment": "sandbox",
  "totalModules": 15,
  "modules": {
    "reference": [
      { "name": "fluent-commerce/core", "symbolicName": "fluent-commerce/core", "version": "2.2.1", "deployedAt": "2026-02-19T05:13:25Z", "deployedBy": "<deployer>" },
      { "name": "fluent-commerce/order", "symbolicName": "fluent-commerce/order", "version": "2.1.0", "deployedAt": "2026-02-04T06:03:30Z", "deployedBy": "<deployer>" }
    ],
    "customExtension": [
      {
        "name": "fluent-commerce/fc-module-custom-extension",
        "symbolicName": "fluent-commerce/fc-module-custom-extension",
        "version": "0.0.29",
        "versionSource": "module.json",
        "sourceVersion": "0.0.29",
        "sourceVersionSource": "module.json",
        "versionDrift": false,
        "artifactCoordinates": { "groupId": "com.fluentcommerce", "artifactId": "fc-module-custom-extension", "version": "0.0.29" },
        "osgiSymbolicName": "_.hmextensions",
        "deployedAt": "2026-02-20T14:58:20Z",
        "deployedBy": "<deployer>",
        "sourceDir": "accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-custom-extension",
        "hasSource": true
      },
      {
        "name": "fc-module-upsert-location",
        "symbolicName": "com.example/fc-module-upsert-location",
        "version": "1.2.2",
        "versionSource": "pom.project.version",
        "hasSource": false
      }
    ],
    "customWorkflow": [
      { "name": "fluent-commerce/fc-workflow-order-hm-updated", "version": "1.0.0" }
    ],
    "customData": [
      { "name": "HM", "version": "1.0.13" }
    ]
  },
  "referenceArtifacts": [
    {
      "name": "fluent-commerce/core",
      "version": "2.2.1",
      "sourcePath": "accounts/<PROFILE>/SOURCE/backend/referece modules/fc-module-core-2.2.1",
      "jarFile": "assets/rules/fc-core-plugin-2.2.1.jar",
      "workflowFragments": [],
      "settingsFiles": []
    }
  ],
  "undeployedModules": [
    {
      "name": "fluent-commerce/fc-module-hm-return",
      "version": "0.0.6",
      "moduleType": "configuration_only",
      "sourcePath": "accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-hm-return",
      "deploymentStatus": "not_deployed"
    }
  ],
  "ruleStats": {
    "totalRegistered": 587,
    "byPrefix": {
      "FLUENTRETAIL": 232,
      "<ACCOUNT>": 355
    },
    "byContext": {
      "FLUENTRETAIL.base": 174,
      "<ACCOUNT>.order": 97,
      "<ACCOUNT>.commonv2": 97,
      "<ACCOUNT>.globalinventory": 71
    }
  }
}
```

### Generating the Inventory

```
1. Run:  fluent module list -p <PROFILE>
2. Parse output into module objects (name, symbolicName, version, deployedAt, deployedBy)
3. Classify each module using the decision tree above
4. For CUSTOM_EXTENSION modules, check source availability:
   - Search accounts/<PROFILE>/SOURCE/backend/ recursively for matching module.json name
   - If source repo found (has src/main/java/) → hasSource = true, sourceType = "full_source"
   - If reference module package found (module.json + JAR under assets/rules/, no src/) → hasSource = false, sourceType = "reference_artifact" (read metadata from module.json, no decompilation needed for rule names)
   - If standalone JAR/ZIP found (no module.json alongside) → hasSource = false, sourceType = "standalone_jar" (needs decompilation)
   - If nothing found → hasSource = false, sourceType = "missing"
     **Escalation:** When sourceType is "missing", include actionable guidance in the report:
     - Option A: Clone the source repo → `git clone <repo-url> accounts/<PROFILE>/SOURCE/backend/<repo-name>/`
     - Option B: Provide the compiled JAR → copy to `accounts/<PROFILE>/SOURCE/backend/<jar-name>.jar`, then run `/fluent-custom-code <PROFILE>` to decompile and analyze
     - Option C: Run `/fluent-connect` to trigger full workspace preparation including JAR decompilation
     - Note: Without source or JAR, analysis is limited to deployed metadata from `fluent module list` and `plugin.list` (rule descriptions and parameters only)
   - Handle double-nested git clone dirs (repo-name/repo-name/) by resolving to inner root
   - Set hasSource, sourceType, and sourceDir accordingly
5. For modules with hasSource = true, extract identity from source:
   a. Read resources/module.json → symbolicName (name), sourceVersion (version)
   b. Read plugins/rules/*/pom.xml → artifactCoordinates (groupId, artifactId, version)
   c. Read pom.xml property rubix.osgi.symbolic.name → osgiSymbolicName
   d. Compare sourceVersion with deployed version → set versionDrift flag
6. Scan accounts/<PROFILE>/SOURCE/backend/ for undeployed modules:
   - Find module.json files not matching any deployed module name
   - Classify as config-only (no rules[], no src/main/java/) or extension
   - Add to inventory with deploymentStatus: "not_deployed"
7. Scan for reference module artifacts in SOURCE/backend/:
   - Detect module.json names matching REFERENCE_NAMES set
   - Record version, included workflow fragments, settings, and JAR sizes
   - Add to referenceArtifacts section (separate from deployed reference modules)
8. Optionally query plugin.list (MCP tool) for rule stats
   - Verify MCP is connected to the correct account before using results
   - If MCP points at a different account, skip this step
9. Extract account name and environment from profile base URL:
   - Pattern: https://<account>[.<env>].api.fluentretail.com
   - .test. = test, .sandbox. = sandbox, no qualifier = production
10. Write module-inventory.json
```

### Identity Extraction from Source

Precedence rules (first available wins):

**`symbolicName`:**
1. `resources/module.json` → `name` field (preferred — this is the canonical module identifier)
2. JAR `META-INF/MANIFEST.MF` → `Bundle-SymbolicName` header
3. Fallback: `<groupId>/<artifactId>` from pom.xml

**`version`:**
1. `resources/module.json` → `version` field (preferred — must match what gets deployed)
2. `pom.xml` → `<project><version>` (or `<project><parent><version>` if module inherits)
3. JAR manifest → `Implementation-Version` or `Bundle-Version`

**Maven coordinates** (from pom.xml):
```bash
# Find the rules child POM (has rubix.osgi.symbolic.name property)
# Typically at: plugins/rules/<module-alias>/pom.xml

# Extract groupId, artifactId, version, osgiSymbolicName
node -e "
const fs = require('fs');
const xml = fs.readFileSync(process.argv[1], 'utf8');
const gid = xml.match(/<groupId>([^<]+)<\/groupId>/);
const aid = xml.match(/<artifactId>([^<]+)<\/artifactId>/);
const ver = xml.match(/<version>([^<]+)<\/version>/);
const sym = xml.match(/<rubix\.osgi\.symbolic\.name>([^<]+)<\/rubix/);
console.log(JSON.stringify({
  groupId: gid?.[1], artifactId: aid?.[1],
  version: ver?.[1], osgiSymbolicName: sym?.[1]
}));
" path/to/pom.xml
```

**POM parent version inheritance:** If the rules child POM has no `<version>` element but has a `<parent>` block, read the parent POM's `<version>`. In multi-module Maven projects, the parent POM at `plugins/pom.xml` defines the version and children inherit it.

### Version Drift Detection

After extracting both deployed and source versions, flag mismatches:

```
versionDrift = (deployedVersion !== sourceVersion)
```

| Scenario | Meaning | Action |
|----------|---------|--------|
| `0.0.29` == `0.0.29` | In sync | No action needed |
| `0.0.28` != `0.0.29` | Source ahead of deployed | Deploy pending — run `/fluent-build` + `/fluent-module-deploy` |
| `0.0.30` != `0.0.29` | Deployed ahead of source | Source may be stale — `git pull` or check branch |

Version drift is a WARN, not a FAIL — it's informational for the developer.

---

# Part 2: Structure Validation

Validate a specific custom extension module for correctness.

---

## Artifact: Persistent Report with Content Hash

### Location

```
accounts/<PROFILE>/reports/module-validate/
├── <module-name>.report.json     ← full validation report
└── <module-name>.hash            ← content hash (single hex string)
```

`<module-name>` is derived from `module.json` `name` field with `/` replaced by `--` (e.g., `fluent-commerce--fc-module-custom-extension`).

### Change Detection Protocol

Before running any checks, compute the module's content hash and compare against the stored hash.

**Step 1: Compute current hash**

Use Node.js (always available) to hash all source files:

```bash
node -e "
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const root = process.argv[1];
const h = crypto.createHash('sha256');
const exts = new Set(['.java','.json','.xml','.sh','.ps1']);
const skip = new Set(['dist','target','.git','node_modules','.idea']);
function walk(d) {
  for (const e of fs.readdirSync(d,{withFileTypes:true})) {
    if (skip.has(e.name)) continue;
    const f = path.join(d, e.name);
    if (e.isDirectory()) walk(f);
    else if (exts.has(path.extname(e.name).toLowerCase())) {
      h.update(f.replace(/\\\\/g,'/') + ':');
      h.update(fs.readFileSync(f));
    }
  }
}
walk(root);
console.log(h.digest('hex'));
" "<MODULE_ROOT>"
```

This hashes every `.java`, `.json`, `.xml`, `.sh`, `.ps1` file content (excluding `dist/`, `target/`, `.git/`, `node_modules/`). Any file change — content, rename, add, or delete — produces a different hash.

**Step 2: Compare with stored hash**

```bash
# Read stored hash (empty string if file doesn't exist) — cross-platform via Node.js
node -e "try{console.log(require('fs').readFileSync(process.argv[1],'utf8').trim())}catch{console.log('')}" "accounts/<PROFILE>/reports/module-validate/<module-name>.hash"
```

**Step 3: Decide**

| Condition | Action |
|-----------|--------|
| Stored hash matches current hash | Print "Module unchanged since last validation — report is current." and read the existing `.report.json`. STOP. |
| Stored hash is missing or different | Run full validation. Write new `.report.json` and `.hash`. |
| `--force` flag provided | Always run full validation regardless of hash. |

### Report JSON Schema

```json
{
  "schema": "module-validate-v1",
  "timestamp": "2026-02-22T10:30:00Z",
  "contentHash": "a1b2c3d4...",
  "module": {
    "name": "fluent-commerce/fc-module-custom-extension",
    "version": "0.0.29",
    "root": "accounts/<PROFILE>/SOURCE/backend/.../",
    "rulesCount": 8
  },
  "summary": {
    "pass": 8,
    "warn": 2,
    "fail": 0,
    "info": 1,
    "status": "READY_FOR_BUILD"
  },
  "checks": [
    {
      "id": "manifest",
      "name": "Module Manifest",
      "result": "PASS",
      "message": "Valid JSON with all required fields",
      "details": {}
    },
    {
      "id": "maven",
      "name": "Maven Structure",
      "result": "PASS",
      "message": "Parent POM + 3 child modules",
      "details": { "modules": ["types", "util", "rules"] }
    },
    {
      "id": "versions",
      "name": "Version Consistency",
      "result": "PASS",
      "message": "0.0.29 across all 4 POMs + module.json",
      "details": { "expected": "0.0.29", "found": { "module.json": "0.0.29", "plugins/pom.xml": "0.0.29" } }
    },
    {
      "id": "rule_wiring",
      "name": "Rule Wiring",
      "result": "PASS",
      "message": "8/8 rules have matching classes with @RuleInfo",
      "details": {
        "wired": ["UpdateStatusHistory", "UpsertAttribute"],
        "orphaned": []
      }
    },
    {
      "id": "test_coverage",
      "name": "Test Coverage",
      "result": "WARN",
      "message": "7/8 rules have tests",
      "details": { "missing": ["CreateMissingVariantProducts"] }
    },
    {
      "id": "schema",
      "name": "GraphQL Schema",
      "result": "PASS",
      "message": "global/schema.json present (1.2MB)"
    },
    {
      "id": "settings",
      "name": "Settings",
      "result": "PASS",
      "message": "25 valid JSON files"
    },
    {
      "id": "workflows",
      "name": "Workflow Fragments",
      "result": "PASS",
      "message": "3 valid JSON files"
    },
    {
      "id": "module_config",
      "name": "Module Config",
      "result": "PASS",
      "message": "3 config tokens defined"
    },
    {
      "id": "scripts",
      "name": "Build Scripts",
      "result": "WARN",
      "message": "build-module.sh present, fetch-schema.sh present"
    },
    {
      "id": "dist",
      "name": "Distribution",
      "result": "INFO",
      "message": "dist/ not present (no build artifacts)"
    }
  ]
}
```

### Writing the Artifact

After validation completes:

```bash
# Ensure directories exist (cross-platform via Node.js)
node -e "require('fs').mkdirSync(process.argv[1],{recursive:true})" "accounts/<PROFILE>/analysis/modules"
node -e "require('fs').mkdirSync(process.argv[1],{recursive:true})" "accounts/<PROFILE>/reports/module-validate"

# Write report (use Write tool, not bash echo)
# Write hash (cross-platform via Node.js)
node -e "require('fs').writeFileSync(process.argv[1],process.argv[2]+'\n')" "accounts/<PROFILE>/reports/module-validate/<module-name>.hash" "<computed-hash>"
```

Use the **Write** tool for the `.report.json` file to preserve JSON formatting.

---

## Validation Checklist

Run checks in this order. Report each as PASS, WARN, or FAIL.

### 1. Module Manifest (`resources/module.json`)

| Check | Severity | Detail |
|-------|----------|--------|
| File exists | FAIL | `resources/module.json` must exist |
| Valid JSON | FAIL | Must parse without errors |
| Has `name` | FAIL | Module name (e.g., `"fluent-commerce/fc-module-custom-extension"`) |
| Has `version` | FAIL | Semantic version string |
| Has `rules` array | FAIL | Non-empty array of rule name strings |
| Has `_schema` | WARN | Schema version (e.g., `"1.0.0"`) |
| Has `title` | WARN | Human-readable title |
| Has `description` | WARN | Module description |
| Has `authors` | WARN | At least one author entry |
| Has `dependencies` | WARN | Module dependencies (e.g., `fluent-commerce/core`) |
| Has `compatibility` | WARN | `minRubixVersion`, `rubixPluginSdk`, `fluentApiClient` |
| No duplicate rules | FAIL | Every entry in `rules[]` must be unique |

### 2. Maven Build Structure (`plugins/`)

| Check | Severity | Detail |
|-------|----------|--------|
| `plugins/pom.xml` exists | FAIL | Parent POM must exist |
| Valid XML | FAIL | Must parse without errors |
| Has `<modules>` | FAIL | Must declare child modules |
| Expected children exist | FAIL | Each `<module>` entry must have a matching subdirectory |
| `<packaging>pom</packaging>` | WARN | Parent should be POM packaging |

Standard child module structure:
```
plugins/
├── pom.xml              ← parent POM
├── types/               ← generated types (Apollo codegen)
│   └── <name>/pom.xml
├── util/                ← shared utilities
│   └── <name>/pom.xml
└── rules/               ← rule classes
    └── <name>/pom.xml
```

### 3. Version Consistency

All versions must match the version in `resources/module.json`.

| File | Field to check |
|------|----------------|
| `resources/module.json` | `"version"` (source of truth) |
| `plugins/pom.xml` | `<version>` on the project element |
| `plugins/rules/<name>/pom.xml` | `<version>` on parent and/or project |
| `plugins/types/<name>/pom.xml` | `<version>` on parent and/or project |
| `plugins/util/<name>/pom.xml` | `<version>` on parent and/or project |

Report any mismatch as FAIL with expected vs actual values.

### 4. Rule Class Wiring

For every rule name in `module.json` `rules[]`, verify:

| Check | Severity | Detail |
|-------|----------|--------|
| Java class exists | FAIL | File at `plugins/rules/**/src/main/java/**/<RuleName>.java` |
| `@RuleInfo` present | FAIL | Class has `@RuleInfo` annotation |
| `@RuleInfo.name` matches | FAIL | `name` attribute matches `module.json` entry exactly (case-sensitive) |
| Extends BaseRule or Rule | WARN | Should extend `BaseRule` or `Rule` from Rubix SDK |

Also check for **orphaned rules** — Java classes with `@RuleInfo` that are NOT listed in `module.json.rules[]`. Report as WARN.

### 5. Test Coverage

For every rule class found:

| Check | Severity | Detail |
|-------|----------|--------|
| Test class exists | WARN | Matching `<RuleName>Test.java` under `src/test/java/` |
| Test has at least one `@Test` | WARN | At least one test method |

### 6. GraphQL Schema

| Check | Severity | Detail |
|-------|----------|--------|
| `global/schema.json` exists | WARN | Required for Apollo codegen |
| File is non-empty | WARN | Must be a valid JSON schema dump |
| POM references schema | WARN | `<introspectionFile>` in parent POM points to schema |

### 7. Resources

#### Settings (`resources/settings/`)

| Check | Severity | Detail |
|-------|----------|--------|
| Directory exists | WARN | Optional but common |
| Each `.json` file is valid JSON | FAIL | Every file must parse |
| Each setting has `name` field | WARN | Standard setting structure |

#### Workflows (`resources/workflows/`)

| Check | Severity | Detail |
|-------|----------|--------|
| Directory exists | WARN | Optional — module may not ship workflows |
| Each `.json` file is valid JSON | FAIL | Every file must parse |
| Each workflow has `name` and `type` | WARN | Standard workflow fragment structure |

#### Module Config (`resources/module.config.json`)

| Check | Severity | Detail |
|-------|----------|--------|
| File exists | WARN | Optional — only needed if module uses config tokens |
| Valid JSON | FAIL | Must parse if present |
| Tokens have placeholder values | WARN | Each key should have a `"default:<key>"` pattern or empty string |

### 8. Build Scripts (`scripts/`)

| Check | Severity | Detail |
|-------|----------|--------|
| Directory exists | WARN | Optional but expected |
| Build script exists | WARN | At least one of: `build-module.sh`, `build-module.ps1` |
| Fetch schema script exists | WARN | At least one of: `fetch-schema.sh`, `fetch-schema.ps1` |

### 9. Distribution (`dist/`)

| Check | Severity | Detail |
|-------|----------|--------|
| Directory exists | INFO | Only present after a build |
| ZIP artifact exists | INFO | `dist/*-<version>.zip` |
| ZIP version matches module.json | WARN | If ZIP exists, its version suffix should match |

---

## Execution Protocol

```
1. Locate module root
   └─ find resources/module.json (user path or recursive search)

2. Read module.json
   └─ extract name, version, rules[]

3. Compute content hash (Node.js script above)

4. Check for existing report
   ├─ Hash matches and no --force? → Print "unchanged", read report, STOP
   └─ Hash differs or --force? → Continue to step 5

5. Run all 9 validation checks (sections 1–9 above)

6. Build report JSON (schema above)

7. Write artifacts
   ├─ accounts/<PROFILE>/reports/module-validate/<name>.report.json
   └─ accounts/<PROFILE>/reports/module-validate/<name>.hash

8. Print human-readable summary + status
```

## Console Output Format

```
MODULE VALIDATION REPORT
========================
Module: fluent-commerce/fc-module-custom-extension
Version: 0.0.29
Root: accounts/<PROFILE>/SOURCE/backend/.../
Hash: a1b2c3d4...

RESULTS
-------
[PASS] Module manifest — valid JSON with all required fields
[PASS] Maven structure — parent POM + 3 child modules
[PASS] Version consistency — 0.0.29 across all 4 POMs + module.json
[PASS] Rule wiring — 8/8 rules have matching classes with @RuleInfo
[WARN] Test coverage — 7/8 rules have tests (missing: CreateMissingVariantProducts)
[PASS] GraphQL schema — global/schema.json present
[PASS] Settings — 25 valid JSON files
[PASS] Workflow fragments — 3 valid JSON files
[PASS] Module config — 3 config tokens defined
[WARN] Build scripts — build-module.sh present, fetch-schema.sh present
[INFO] Distribution — dist/ not present (no build artifacts)

SUMMARY: 8 PASS, 2 WARN, 0 FAIL
Status: READY FOR BUILD

Report saved: accounts/<PROFILE>/reports/module-validate/fluent-commerce--fc-module-custom-extension.report.json
```

Status thresholds:
- **READY FOR BUILD** — 0 FAIL
- **NEEDS FIXES** — 1+ FAIL
- Always list WARN items as improvement suggestions

## Consuming the Report from Other Skills

Other skills can check validation state without re-running:

```bash
# Quick status check — cross-platform via Node.js
node -e "
const fs = require('fs');
const hashFile = process.argv[1];
const reportFile = process.argv[2];
let stored = '';
try { stored = fs.readFileSync(hashFile,'utf8').trim(); } catch {}
const current = process.argv[3]; // pass computed hash as argument
if (stored === current) {
  console.log('Validation report is current');
  console.log(fs.readFileSync(reportFile,'utf8'));
} else {
  console.log('Report is stale - re-run validation');
}
" "accounts/<PROFILE>/reports/module-validate/<name>.hash" \
  "accounts/<PROFILE>/reports/module-validate/<name>.report.json" \
  "<CURRENT_HASH>"
```

The report's `summary.status` field gives a machine-readable verdict:
- `READY_FOR_BUILD` — no FAILs, safe to proceed
- `NEEDS_FIXES` — has FAILs, block build/deploy

## Auto-Fix Mode (`--fix`)

When invoked with `--fix`, attempt to resolve common issues:

| Issue | Auto-fix action |
|-------|----------------|
| Missing rule in `module.json` | Add orphaned `@RuleInfo` rule names to `rules[]` |
| Version mismatch in child POM | Update `<version>` to match `module.json` |
| Missing test class | Scaffold via `/fluent-rule-scaffold` test template |
| Invalid JSON in settings | Report parse error location (cannot auto-fix) |

Always preview changes before applying. Never auto-fix without confirmation.

After auto-fix, re-compute the hash and re-run validation to produce a fresh report.

## Cross-Skill Integration

| Scenario | Delegate to |
|----------|-------------|
| Rule class missing entirely | `/fluent-rule-scaffold` to generate |
| Build fails after fix | `/fluent-build` to diagnose |
| Workflow fragments have issues | `/fluent-workflow-analyzer` to validate |
| Need to understand rule behavior | `/fluent-custom-code` to analyze |
| Pre-deploy gate | Read `.report.json` `summary.status` before `/fluent-module-deploy` |

## Troubleshooting

| Issue | Likely Cause | Fix |
|-------|-------------|-----|
| Can't find module root | Non-standard nesting | Search recursively for `resources/module.json` |
| POM version extraction fails | Non-standard POM structure | Read POM manually and locate `<version>` tags |
| Rule class not found | Different package structure | Search by filename across entire `plugins/` tree |
| @RuleInfo mismatch | Typo in annotation vs module.json | Compare exact strings (case-sensitive) |
| Schema file missing | Not downloaded yet | Run `fetch-schema` script or download via API |
| Hash always differs | Line endings (CRLF vs LF) | Hash uses raw bytes; consistent across runs on same OS |
| Report stale after git pull | New files change hash | Normal — validation re-runs automatically |

## Post-Execution: Feedback Capture

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

1. **Classify outcome:** `SUCCESS` (completed cleanly), `PARTIAL_SUCCESS` (completed after fixing issues), `FAILURE` (could not complete), `BLOCKED` (prereq missing), `USER_CORRECTED` (user overrode approach)
2. **Build record:** Create a `feedback-record-v1` JSON object with:
   - `skill`: `"fluent-module-validate"`
   - `feature`: feature slug if applicable, null otherwise
   - `phases`: trace of each phase with PASS/FAIL/SKIP
   - `learnings`: any gotchas discovered during execution
   - `userCorrections`: any corrections the user provided
3. **Redact secrets:** Strip tokens, passwords, API keys. Keep business identifiers.
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-module-validate.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 asked a question, no actual write operation happened).

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

_(No promoted learnings yet.)_

<!-- end feedback-promoted -->
