Computes SPC control limits, zone-based rule violations (Western Electric / Nelson), and - where applicable - process capability/performance indices, for one of the following chart types:

- `individuals` - Individuals (I-MR) chart. Default. Paired with a Moving Range secondary chart.
- `xbar` - X-bar/R chart (subgroup mean + range). Requires subgroupSize 2-10. Paired with a Range secondary chart.
- `xbar_s` - X-bar/S chart (subgroup mean + sample std dev). Any subgroup size >= 2, unlike xbar (R-based), which is limited to the tabulated range n=2-10. Paired with an S secondary chart.
- `p` - Percent defective chart, variable subgroup size.
- `np` - Number defective chart, constant subgroup size.
- `c` - Number of defects chart, constant area of opportunity.
- `u` - Defects-per-unit chart, variable area of opportunity.
- `pre_control` - Pre-Control zones. Spec-limit based - no control limits, no Western Electric/Nelson rules, no capability indices; uses its own run/stop signal instead.

`p`/`np`/`c`/`u` have no secondary chart and no capability indices - there is no subgroup-internal spread statistic to chart, and no within-subgroup sigma to build Cp/Cpk from (attribute-chart limits come directly from the binomial/Poisson variance formula, not from an estimated range/std dev).

Capability is only returned for `individuals`/`xbar`/`xbar_s`, and only when `usl` and/or `lsl` was given: `{cp, cpk, cpu, cpl, pp, ppk, ppu, ppl, mean, sigmaWithin, sigmaOverall}`. `cp`/`pp` are `None` unless BOTH `usl` and `lsl` are given; `cpk`/`ppk` are the min() of whichever one-sided indices exist. `cp`/`cpk` use `sigmaWithin` (short-term, from the same R-bar/d2 or S-bar/c4 estimate the control limits are built from); `pp`/`ppk` use `sigmaOverall` (long-term, the plain sample std dev of every individual reading around the grand mean, ignoring subgrouping).

`pre_control` returns a materially different shape (`zones`/`pairSignals`/`qualifyRunIndex`/`greenLow`/`greenHigh`/`yellowLow`/`yellowHigh` in place of `uclPoints`/`lclPoints`/`violations`/`secondary`/`capability`).

---
### Parameters
**values** `List of Float values`: raw measurements for `individuals`/`xbar`/`xbar_s`/`pre_control` (flat list, subgrouped internally for `xbar`/`xbar_s`); defect/defective count per subgroup for `p`/`u`; defect/defective count per subgroup (constant sample size/area) for `np`/`c`.<br />
**chartType** `String`: `'individuals'`, `'xbar'`, `'xbar_s'`, `'p'`, `'np'`, `'c'`, `'u'`, `'pre_control'`. Can be None (defaults to `'individuals'`).<br />
**ruleSet** `String`: `'both'`, `'western_electric'`, `'nelson'`. Can be None (defaults to `'both'`). Ignored for `pre_control`.<br />
**subgroupSize** `Integer`: required for `xbar` (2-10), `xbar_s` (>= 2), and `np` (constant n). Can be None otherwise.<br />
**usl** `Float`: upper spec limit. Required for `pre_control`, optional elsewhere - if given (with or without `lsl`) for `individuals`/`xbar`/`xbar_s`, one-sided capability indices (`cpu`/`cpk`, `ppu`/`ppk`) are computed. Can be None.<br />
**lsl** `Float`: lower spec limit. Same rules as `usl`, mirrored. Can be None.<br />
**sampleSizes** `List of Integers`: required for `p`/`u` - one entry per subgroup, parallel to `values`. Can be None otherwise.<br />

---
### Returns
**results** `Dictionary`<br />
**outOfControl** `Boolean`<br />
**chartType** `String`<br />
**ruleSet** `String`<br />
**mean** `Float`<br />
**sigma** `Float`: only when control limits are constant across points (None for `p`/`u`, where they vary by sample size - use `uclPoints`/`lclPoints` instead)<br />
**ucl / lcl** `Float`: same constant-limit caveat as `sigma`<br />
**uclPoints / lclPoints** `List of Float values`: per-point limit lists, always populated (repeated scalar for constant-limit chart types; genuinely varying point by point for `p`/`u`)<br />
**usl / lsl** `Float`: pass-through of the input spec limits<br />
**aboveUCL / belowLCL / aboveUSL / belowLSL** `List of Integers`: violating point indices<br />
**violations** `Dictionary`: rule name -> violating point indices<br />
**points** `List of Float values`<br />
**secondary** `Dictionary`: (`individuals`/`xbar`/`xbar_s` only) the paired Moving-Range/Range/S chart - `{label, centerLine, ucl, lcl, points}`<br />
**capability** `Dictionary`: see above<br />

---
### Examples

The shape of `result` changes noticeably by `chartType` - constant vs. per-point control limits, whether `secondary`/`capability` are populated, and (for `pre_control`) an entirely different set of fields. Each example below shows the actual computed values so you can see exactly what differs.

#### individuals

One reading per point, no subgrouping. `sigma`/`ucl`/`lcl` are single constant values; `secondary` is the paired Moving Range chart.

```python
values = [100.4, 98.6, 100.2, 96.0, 109.8, 102.1, 103.2]
result = system.kanoa.quality.spc.spcCheck(values, "individuals", None, None, None, None, None)

# result["points"]  = [100.4, 98.6, 100.2, 96.0, 109.8, 102.1, 103.2]  (unchanged - one point per reading)
# result["mean"]    = 101.471
# result["sigma"]   = 4.462
# result["ucl"]     = 114.858
# result["lcl"]     = 88.085
# result["secondary"] = {"label": "Moving Range", "centerLine": 5.033, "ucl": 16.444, "lcl": 0.0, "points": [1.8, 1.6, 4.2, 13.8, 7.7, 1.1]}
```

#### xbar (X-bar/R, subgroup size 4)

Readings are grouped into subgroups; `points` become subgroup averages, not raw readings. `secondary` is the paired Range chart instead of Moving Range.

```python
values = [100.4, 98.6, 100.2, 96.0, 109.8, 102.1, 103.2, 97.5, 101.0, 99.8, 100.5, 98.9]
subgroupSize = 4
result = system.kanoa.quality.spc.spcCheck(values, "xbar", None, subgroupSize, None, None, None)

# subgroups formed = [[100.4, 98.6, 100.2, 96.0], [109.8, 102.1, 103.2, 97.5], [101.0, 99.8, 100.5, 98.9]]
# result["points"]  = [98.8, 103.15, 100.05]  (subgroup means, not raw values)
# result["mean"]    = 100.667
# result["ucl"]     = 105.235
# result["lcl"]     = 96.098
# result["secondary"] = {"label": "Range", "centerLine": 6.267, "ucl": 14.301, "lcl": 0.0, "points": [4.4, 12.3, 2.1]}
```

#### xbar_s (X-bar/S, subgroup size 5)

Same idea as `xbar`, but any subgroup size >= 2 (not capped at 10) since it uses the sample standard deviation instead of range.

```python
values = [100.4, 98.6, 100.2, 96.0, 109.8, 102.1, 103.2, 97.5, 101.0, 99.8]
subgroupSize = 5
result = system.kanoa.quality.spc.spcCheck(values, "xbar_s", None, subgroupSize, None, None, None)

# result["points"]  = [101.0, 100.72]  (subgroup means)
# result["mean"]    = 100.860
# result["ucl"]     = 106.158
# result["lcl"]     = 95.562
# result["secondary"] = {"label": "S", "centerLine": 3.712, "ucl": 7.755, "lcl": 0.0, "points": [5.253, 2.170]}
```

#### p (percent defective, sample size varies)

Attribute chart - no `secondary`, no `capability`. `uclPoints`/`lclPoints` genuinely vary per point since `sampleSizes` isn't constant.

```python
values = [3, 5, 2, 8, 4]           # defective counts per subgroup
sampleSizes = [50, 60, 45, 55, 50]
result = system.kanoa.quality.spc.spcCheck(values, "p", None, None, None, None, sampleSizes)

# result["points"]     = [0.06, 0.083, 0.044, 0.145, 0.08]   (proportion defective per subgroup)
# result["mean"]       = 0.0846
# result["uclPoints"]  = [0.203, 0.192, 0.209, 0.197, 0.203]  (different per point - sample size varies)
# result["lclPoints"]  = [0.0, 0.0, 0.0, 0.0, 0.0]
```

#### np (number defective, constant sample size)

`subgroupSize` is the constant sample size here, not a grouping count - `points` stays as the raw defective counts, and control limits are constant (a single `ucl`/`lcl`, same as individuals/xbar).

```python
values = [3, 5, 2, 8, 4]   # defective counts, each out of a subgroup of 50
subgroupSize = 50
result = system.kanoa.quality.spc.spcCheck(values, "np", None, subgroupSize, None, None, None)

# result["points"] = [3, 5, 2, 8, 4]   (raw counts, unchanged)
# result["mean"]   = 4.4
# result["sigma"]  = 2.003
# result["ucl"]    = 10.410
# result["lcl"]    = 0.0
```

#### c (number of defects, constant area of opportunity)

```python
values = [3, 5, 2, 8, 4, 6]   # defect counts, one inspection unit each
result = system.kanoa.quality.spc.spcCheck(values, "c", None, None, None, None, None)

# result["points"] = [3, 5, 2, 8, 4, 6]
# result["mean"]   = 4.667
# result["sigma"]  = 2.160
# result["ucl"]    = 11.147
# result["lcl"]    = 0.0
```

#### u (defects per unit, area of opportunity varies)

Same as `c`, but `sampleSizes` carries the varying area/unit count, so `points` becomes a rate and the limits vary per point.

```python
values = [3, 5, 2, 8, 4]      # defect counts
areas = [10, 12, 9, 11, 10]   # units inspected per subgroup
result = system.kanoa.quality.spc.spcCheck(values, "u", None, None, None, None, areas)

# result["points"]     = [0.3, 0.417, 0.222, 0.727, 0.4]   (defects per unit)
# result["mean"]       = 0.4231
# result["uclPoints"]  = [1.04, 0.986, 1.074, 1.011, 1.04]
# result["lclPoints"]  = [0.0, 0.0, 0.0, 0.0, 0.0]
```

#### pre_control

A materially different return shape - `zones`/`pairSignals`/`qualifyRunIndex`/`greenLow`/`greenHigh`/`yellowLow`/`yellowHigh` in place of `uclPoints`/`lclPoints`/`violations`/`secondary`/`capability`. Requires both `usl` and `lsl`.

```python
values = [99, 101, 98, 102, 100, 106, 107]
usl, lsl = 110.0, 90.0
result = system.kanoa.quality.spc.spcCheck(values, "pre_control", None, None, usl, lsl, None)

# result["greenLow"]  = 95.0   (middle 50% of the 90-110 tolerance)
# result["greenHigh"] = 105.0
# result["zones"]     = ["green", "green", "green", "green", "green", "yellow-high", "yellow-high"]
# the first five points qualify the process (5 consecutive green); the last two
# both landing in yellow-high is what pairSignals flags as a shift, not a random blip
```
