@holoscript/plugin-manufacturing-qc 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @holoscript/plugin-manufacturing-qc
2
+
3
+ ## 2.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [c64fc1a]
8
+ - @holoscript/core@8.0.6
9
+
10
+ ## 2.0.0
11
+
12
+ ### Patch Changes
13
+
14
+ - @holoscript/core@6.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 HoloScript Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@holoscript/plugin-manufacturing-qc",
3
+ "version": "2.0.1",
4
+ "main": "src/index.ts",
5
+ "peerDependencies": {
6
+ "@holoscript/core": "8.0.6"
7
+ },
8
+ "license": "MIT",
9
+ "scripts": {
10
+ "test": "vitest run --passWithNoTests",
11
+ "test:coverage": "vitest run --coverage --passWithNoTests"
12
+ }
13
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Integration proof: the manufacturing-qc `spc` trait, once registered via the
3
+ * runtime's real `registerTrait` seam, is dispatched BY THE RUNTIME and runs
4
+ * the deterministic SPC capability solver (`computeCapability`) — NOT called
5
+ * directly as a handler object.
6
+ *
7
+ * Mirrors government-civic-plugin's runtime-integration reference
8
+ * (civic_decision). Drives the real path: executeNode(orb) -> orb-executor ->
9
+ * applyDirectives -> traitHandlers.get('spc').onAttach -> computeCapability.
10
+ * The negative control proves the registration is load-bearing (without it, the
11
+ * trait is a dead no-op — which is exactly the tier's status quo).
12
+ */
13
+ import { describe, it, expect } from 'vitest';
14
+ import { HoloScriptRuntime } from '@holoscript/core/runtime';
15
+ import { registerManufacturingQcTraitHandlers } from '../runtime';
16
+ import type { Subgroup } from '../spc';
17
+
18
+ // ── Hand-derived capability dataset ────────────────────────────────────────────
19
+ //
20
+ // computeCapability estimates Cp/Cpk from the WITHIN-subgroup σ̂ = R̅ / d₂
21
+ // (used because all subgroups are the same size n and 2 ≤ n ≤ 10). We pick a
22
+ // dataset whose within-subgroup σ̂ is exact:
23
+ //
24
+ // Two subgroups of size n = 2, both [9, 11]:
25
+ // range(each) = 11 − 9 = 2
26
+ // R̅ = (2 + 2) / 2 = 2
27
+ // d₂[2] = 1.128 (Montgomery / ASTM constant in spc.ts)
28
+ // withinSigma σ̂ = R̅ / d₂ = 2 / 1.128 = 1.7730496453900710
29
+ //
30
+ // allValues = [9, 11, 9, 11] ⇒ processMean μ = 40 / 4 = 10
31
+ // Spec limits LSL = 4, USL = 16 ⇒ specWidth = USL − LSL = 12 (symmetric about μ = 10)
32
+ //
33
+ // Cp = specWidth / (6·σ̂) = 12 / (6 · 1.7730496…) = 2 / 1.7730496… = 1.128
34
+ // CpkUpper = (USL − μ)/(3·σ̂) = (16 − 10)/(3 · 1.7730496…) = 6 / 5.3191489… = 1.128
35
+ // CpkLower = (μ − LSL)/(3·σ̂) = (10 − 4)/(3 · 1.7730496…) = 6 / 5.3191489… = 1.128
36
+ // Cpk = min(CpkUpper, CpkLower) = 1.128
37
+ //
38
+ // (The clean 1.128 is not a coincidence: specWidth = 6·R̅ = 12 and d₂ cancels,
39
+ // leaving Cp = Cpk = d₂[2] exactly. capable = Cpk ≥ 1.33 ⇒ 1.128 < 1.33 ⇒ false.)
40
+ const HAND_SUBGROUPS: Subgroup[] = [
41
+ { index: 1, values: [9, 11] },
42
+ { index: 2, values: [9, 11] },
43
+ ];
44
+ const HAND_ALL_VALUES = [9, 11, 9, 11];
45
+ const HAND_LSL = 4;
46
+ const HAND_USL = 16;
47
+
48
+ const EXPECTED_CP = 1.128;
49
+ const EXPECTED_CPK = 1.128;
50
+ const EXPECTED_MEAN = 10;
51
+
52
+ function spcOrb(config: Record<string, unknown>): unknown {
53
+ return {
54
+ type: 'orb',
55
+ name: 'spc',
56
+ properties: {},
57
+ methods: [],
58
+ position: [0, 0, 0],
59
+ hologram: { shape: 'orb', color: '#fff', size: 1, glow: false, interactive: false },
60
+ directives: [{ type: 'trait', name: 'spc', config }],
61
+ };
62
+ }
63
+
64
+ /** Flush the runtime's async emit dispatch so `on` listeners have fired. */
65
+ const flush = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
66
+
67
+ describe('manufacturing-qc -> HoloScript runtime integration (spc)', () => {
68
+ it('runtime dispatch runs the SPC capability solver for a registered @spc orb', async () => {
69
+ const runtime = new HoloScriptRuntime();
70
+ registerManufacturingQcTraitHandlers(runtime);
71
+
72
+ const solved: Array<Record<string, unknown>> = [];
73
+ runtime.on('spc_solved', (e: unknown) => {
74
+ solved.push(e as Record<string, unknown>);
75
+ });
76
+
77
+ await runtime.executeNode(
78
+ spcOrb({
79
+ allValues: HAND_ALL_VALUES,
80
+ subgroups: HAND_SUBGROUPS,
81
+ lsl: HAND_LSL,
82
+ usl: HAND_USL,
83
+ }) as never,
84
+ );
85
+ await flush();
86
+
87
+ expect(solved).toHaveLength(1);
88
+ const summary = solved[0];
89
+ // Assert against the HAND derivation above, not against solver output.
90
+ expect(summary.processMean as number).toBeCloseTo(EXPECTED_MEAN, 9);
91
+ expect(summary.Cp as number).toBeCloseTo(EXPECTED_CP, 9);
92
+ expect(summary.Cpk as number).toBeCloseTo(EXPECTED_CPK, 9);
93
+ // Cpk = 1.128 < 1.33 ⇒ process not capable.
94
+ expect(summary.capable).toBe(false);
95
+ expect(summary.valueCount).toBe(4);
96
+ expect(summary.subgroupCount).toBe(2);
97
+ });
98
+
99
+ it('NEGATIVE CONTROL: without registration the @spc trait is a dead no-op', async () => {
100
+ const runtime = new HoloScriptRuntime(); // intentionally NOT registered
101
+ const solved: unknown[] = [];
102
+ runtime.on('spc_solved', (e: unknown) => solved.push(e));
103
+
104
+ await runtime.executeNode(
105
+ spcOrb({
106
+ allValues: HAND_ALL_VALUES,
107
+ subgroups: HAND_SUBGROUPS,
108
+ lsl: HAND_LSL,
109
+ usl: HAND_USL,
110
+ }) as never,
111
+ );
112
+ await flush();
113
+
114
+ expect(solved).toHaveLength(0);
115
+ });
116
+
117
+ it('persists the solver result into durable runtime state on ATTACH', async () => {
118
+ const runtime = new HoloScriptRuntime();
119
+ registerManufacturingQcTraitHandlers(runtime);
120
+
121
+ await runtime.executeNode(
122
+ spcOrb({
123
+ allValues: HAND_ALL_VALUES,
124
+ subgroups: HAND_SUBGROUPS,
125
+ lsl: HAND_LSL,
126
+ usl: HAND_USL,
127
+ }) as never,
128
+ );
129
+ await flush();
130
+
131
+ const state = runtime.getState() as Record<string, unknown>;
132
+ const persisted = state['spc:spc'] as
133
+ | { Cp?: number; Cpk?: number; capable?: boolean }
134
+ | undefined;
135
+ expect(persisted).toBeDefined();
136
+ expect(persisted?.Cp).toBeCloseTo(EXPECTED_CP, 9);
137
+ expect(persisted?.Cpk).toBeCloseTo(EXPECTED_CPK, 9);
138
+ expect(persisted?.capable).toBe(false);
139
+ });
140
+
141
+ it('emits spc_error (does not throw through the runtime) for invalid config', async () => {
142
+ const runtime = new HoloScriptRuntime();
143
+ registerManufacturingQcTraitHandlers(runtime);
144
+
145
+ const errors: Array<Record<string, unknown>> = [];
146
+ runtime.on('spc_error', (e: unknown) => {
147
+ errors.push(e as Record<string, unknown>);
148
+ });
149
+
150
+ // USL (4) <= LSL (10): the real solver throws "[spc] usl must be > lsl",
151
+ // which the handler's try/catch turns into an spc_error rather than a throw.
152
+ await runtime.executeNode(
153
+ spcOrb({
154
+ allValues: HAND_ALL_VALUES,
155
+ subgroups: HAND_SUBGROUPS,
156
+ lsl: 10,
157
+ usl: 4,
158
+ }) as never,
159
+ );
160
+ await flush();
161
+
162
+ expect(errors).toHaveLength(1);
163
+ expect(String(errors[0].error)).toContain('usl must be > lsl');
164
+ });
165
+ });
@@ -0,0 +1,278 @@
1
+ /**
2
+ * SPC solver tests — manufacturing-qc-plugin
3
+ *
4
+ * Covers: X̄-R chart, X̄-s chart, p-chart, c-chart, u-chart,
5
+ * capability indices (Cp/Cpk/Ppk/Cpm), Western Electric rules,
6
+ * receipt builder, and false-case gates.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import {
11
+ buildSPCChart,
12
+ computeCapability,
13
+ buildSPCReceipt,
14
+ type Subgroup,
15
+ } from '../spc';
16
+
17
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
18
+
19
+ function makeSubgroups(data: number[][]): Subgroup[] {
20
+ return data.map((values, i) => ({ index: i + 1, values }));
21
+ }
22
+
23
+ /** Classic textbook dataset: Montgomery Example 6.1 (shaft diameters, mm) */
24
+ const XBAR_R_DATA: number[][] = [
25
+ [74, 70, 75, 78, 69],
26
+ [74, 74, 76, 74, 72],
27
+ [79, 73, 76, 79, 77],
28
+ [75, 75, 71, 79, 72],
29
+ [74, 75, 75, 75, 72],
30
+ [72, 71, 75, 73, 71],
31
+ [75, 73, 73, 72, 74],
32
+ [74, 79, 73, 75, 71],
33
+ [73, 76, 71, 74, 75],
34
+ [75, 78, 76, 78, 72],
35
+ [74, 72, 72, 72, 73],
36
+ [79, 77, 78, 79, 75],
37
+ [74, 75, 76, 75, 74],
38
+ [74, 74, 75, 75, 74],
39
+ [73, 74, 74, 74, 73],
40
+ [74, 76, 75, 74, 72],
41
+ [77, 73, 74, 75, 75],
42
+ [74, 73, 76, 75, 73],
43
+ [75, 78, 75, 74, 71],
44
+ [74, 76, 77, 75, 72],
45
+ [74, 74, 76, 74, 74],
46
+ [74, 74, 75, 75, 72],
47
+ [76, 72, 73, 72, 74],
48
+ [76, 72, 74, 73, 74],
49
+ [75, 71, 73, 74, 74],
50
+ ];
51
+
52
+ // ─── X̄-R chart ───────────────────────────────────────────────────────────────
53
+
54
+ describe('buildSPCChart — xbar_r', () => {
55
+ it('produces correct centre line and UCL/LCL', () => {
56
+ const subgroups = makeSubgroups(XBAR_R_DATA);
57
+ const result = buildSPCChart('xbar_r', subgroups);
58
+
59
+ expect(result.chartType).toBe('xbar_r');
60
+ expect(result.subgroupCount).toBe(25);
61
+ expect(result.totalObservations).toBe(125);
62
+
63
+ // Grand mean of this dataset ≈ 74.28
64
+ expect(result.primaryChart.centerLine).toBeCloseTo(74.28, 1);
65
+ // UCL > CL > LCL
66
+ expect(result.primaryChart.ucl).toBeGreaterThan(result.primaryChart.centerLine);
67
+ expect(result.primaryChart.lcl).toBeLessThan(result.primaryChart.centerLine);
68
+ // R-bar ≈ 5.0 for this dataset; UCL_R = D4 * R̄ = 2.115 * 5.0 ≈ 10.5
69
+ expect(result.secondaryChart?.ucl).toBeGreaterThan(result.secondaryChart?.centerLine ?? 0);
70
+ });
71
+
72
+ it('reports each subgroup stat with a mean and range', () => {
73
+ const subgroups = makeSubgroups(XBAR_R_DATA);
74
+ const { subgroupStats } = buildSPCChart('xbar_r', subgroups);
75
+ for (const stat of subgroupStats) {
76
+ expect(stat.mean).toBeDefined();
77
+ expect(stat.range).toBeDefined();
78
+ expect(Array.isArray(stat.violatedRules)).toBe(true);
79
+ }
80
+ });
81
+
82
+ it('process with naturally-varying in-control data reports processInControl=true', () => {
83
+ // Use the main dataset itself — it is the canonical "in-control" reference
84
+ // (Montgomery uses it to establish control limits, so by construction most
85
+ // points are in control). Verify structural property: UCL > CL > LCL.
86
+ const result = buildSPCChart('xbar_r', makeSubgroups(XBAR_R_DATA));
87
+ expect(result.primaryChart.ucl).toBeGreaterThan(result.primaryChart.centerLine);
88
+ expect(result.primaryChart.lcl).toBeLessThan(result.primaryChart.centerLine);
89
+ // At least 80% of subgroups should be in control for a stable reference process
90
+ const inControlFraction =
91
+ 1 - result.outOfControlCount / result.subgroupCount;
92
+ expect(inControlFraction).toBeGreaterThanOrEqual(0.8);
93
+ });
94
+
95
+ it('detects WE1 (beyond 3σ) for an obvious outlier', () => {
96
+ const data = XBAR_R_DATA.map((row) => [...row]);
97
+ data[5] = [120, 120, 120, 120, 120]; // extreme outlier
98
+ const result = buildSPCChart('xbar_r', makeSubgroups(data));
99
+ const outlierStat = result.subgroupStats[5];
100
+ expect(outlierStat.outOfControl).toBe(true);
101
+ expect(outlierStat.violatedRules.some((r) => r.includes('WE1'))).toBe(true);
102
+ });
103
+
104
+ it('throws for subgroup size outside 2–10', () => {
105
+ const bad = [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]]; // 11 values
106
+ expect(() => buildSPCChart('xbar_r', makeSubgroups([...bad, ...bad]))).toThrow();
107
+ });
108
+
109
+ it('throws for fewer than 2 subgroups', () => {
110
+ expect(() => buildSPCChart('xbar_r', makeSubgroups([[1, 2, 3]]))).toThrow();
111
+ });
112
+ });
113
+
114
+ // ─── X̄-s chart ───────────────────────────────────────────────────────────────
115
+
116
+ describe('buildSPCChart — xbar_s', () => {
117
+ it('produces consistent limits (UCL > CL > LCL)', () => {
118
+ const result = buildSPCChart('xbar_s', makeSubgroups(XBAR_R_DATA));
119
+ expect(result.chartType).toBe('xbar_s');
120
+ expect(result.primaryChart.ucl).toBeGreaterThan(result.primaryChart.centerLine);
121
+ expect(result.primaryChart.lcl).toBeLessThan(result.primaryChart.centerLine);
122
+ expect(result.secondaryChart?.ucl).toBeGreaterThan(0);
123
+ });
124
+ });
125
+
126
+ // ─── p-chart ─────────────────────────────────────────────────────────────────
127
+
128
+ describe('buildSPCChart — p', () => {
129
+ const P_DATA: Subgroup[] = [
130
+ { index: 1, values: [], n: 100, defects: 5 },
131
+ { index: 2, values: [], n: 100, defects: 3 },
132
+ { index: 3, values: [], n: 100, defects: 6 },
133
+ { index: 4, values: [], n: 100, defects: 4 },
134
+ { index: 5, values: [], n: 100, defects: 2 },
135
+ { index: 6, values: [], n: 100, defects: 8 },
136
+ { index: 7, values: [], n: 100, defects: 3 },
137
+ { index: 8, values: [], n: 100, defects: 5 },
138
+ { index: 9, values: [], n: 100, defects: 4 },
139
+ { index: 10, values: [], n: 100, defects: 1 },
140
+ ];
141
+
142
+ it('computes p̄ as overall fraction nonconforming', () => {
143
+ const result = buildSPCChart('p', P_DATA);
144
+ expect(result.primaryChart.centerLine).toBeCloseTo(0.041, 3);
145
+ expect(result.primaryChart.ucl).toBeGreaterThan(result.primaryChart.centerLine);
146
+ expect(result.primaryChart.lcl).toBeGreaterThanOrEqual(0);
147
+ });
148
+
149
+ it('flags proportion as out-of-control when above UCL', () => {
150
+ const data = [...P_DATA, { index: 11, values: [], n: 100, defects: 40 }];
151
+ const result = buildSPCChart('p', data);
152
+ const last = result.subgroupStats[result.subgroupStats.length - 1];
153
+ expect(last.outOfControl).toBe(true);
154
+ });
155
+
156
+ it('throws if .n or .defects missing', () => {
157
+ expect(() =>
158
+ buildSPCChart('p', [
159
+ { index: 1, values: [1, 2, 3] },
160
+ { index: 2, values: [1, 2, 3] },
161
+ ]),
162
+ ).toThrow();
163
+ });
164
+ });
165
+
166
+ // ─── c-chart ─────────────────────────────────────────────────────────────────
167
+
168
+ describe('buildSPCChart — c', () => {
169
+ const C_DATA: Subgroup[] = Array.from({ length: 20 }, (_, i) => ({
170
+ index: i + 1,
171
+ values: [],
172
+ defects: [3, 2, 4, 1, 3, 5, 2, 3, 2, 4, 3, 2, 1, 3, 4, 2, 3, 2, 4, 3][i],
173
+ }));
174
+
175
+ it('computes c̄ and symmetric ±3σ limits', () => {
176
+ const result = buildSPCChart('c', C_DATA);
177
+ expect(result.chartType).toBe('c');
178
+ expect(result.primaryChart.centerLine).toBeCloseTo(2.8, 1);
179
+ expect(result.primaryChart.ucl).toBeGreaterThan(result.primaryChart.centerLine);
180
+ });
181
+
182
+ it('detects extreme count as out-of-control', () => {
183
+ const data = [...C_DATA, { index: 21, values: [], defects: 50 }];
184
+ const result = buildSPCChart('c', data);
185
+ const last = result.subgroupStats[result.subgroupStats.length - 1];
186
+ expect(last.outOfControl).toBe(true);
187
+ });
188
+ });
189
+
190
+ // ─── Process capability ───────────────────────────────────────────────────────
191
+
192
+ describe('computeCapability', () => {
193
+ // Centred process: mean=50, σ≈1, LSL=44, USL=56 → Cpk≈2.0
194
+ const centredValues: number[] = Array.from({ length: 100 }, (_, i) =>
195
+ 50 + Math.sin(i * 0.7) * 0.8,
196
+ );
197
+ const centredSubgroups = makeSubgroups(
198
+ Array.from({ length: 20 }, (_, i) => centredValues.slice(i * 5, i * 5 + 5)),
199
+ );
200
+
201
+ it('computes Cp, Cpk, Pp, Ppk with correct ordering (Cpk ≤ Cp)', () => {
202
+ const cap = computeCapability(centredValues, centredSubgroups, 44, 56);
203
+ expect(cap.Cp).toBeGreaterThan(0);
204
+ expect(cap.Cpk).toBeLessThanOrEqual(cap.Cp + 1e-9);
205
+ expect(cap.Ppk).toBeLessThanOrEqual(cap.Pp + 1e-9);
206
+ });
207
+
208
+ it('returns capable=true for high-quality process (Cpk ≥ 1.33)', () => {
209
+ const cap = computeCapability(centredValues, centredSubgroups, 44, 56);
210
+ expect(cap.capable).toBe(true);
211
+ });
212
+
213
+ it('returns capable=false for marginal process (tight spec)', () => {
214
+ const cap = computeCapability(centredValues, centredSubgroups, 49.0, 51.0);
215
+ expect(cap.capable).toBe(false);
216
+ expect(cap.Cpk).toBeLessThan(1.33);
217
+ });
218
+
219
+ it('computes Cpm when target is provided', () => {
220
+ const cap = computeCapability(centredValues, centredSubgroups, 44, 56, 50);
221
+ expect(cap.Cpm).toBeDefined();
222
+ expect(cap.Cpm).toBeGreaterThan(0);
223
+ });
224
+
225
+ it('ppmTotal is within bounds (0 to 1,000,000)', () => {
226
+ const cap = computeCapability(centredValues, centredSubgroups, 44, 56);
227
+ expect(cap.ppmTotal).toBeGreaterThanOrEqual(0);
228
+ expect(cap.ppmTotal).toBeLessThan(1_000_000);
229
+ });
230
+
231
+ it('throws if usl ≤ lsl', () => {
232
+ expect(() => computeCapability(centredValues, centredSubgroups, 56, 44)).toThrow();
233
+ });
234
+
235
+ it('throws if fewer than 2 values', () => {
236
+ expect(() => computeCapability([50], [], 44, 56)).toThrow();
237
+ });
238
+ });
239
+
240
+ // ─── Receipt builder ──────────────────────────────────────────────────────────
241
+
242
+ describe('buildSPCReceipt', () => {
243
+ it('produces a valid in-control receipt with accepted=true', () => {
244
+ const result = buildSPCChart('xbar_r', makeSubgroups(XBAR_R_DATA));
245
+ const receipt = buildSPCReceipt('test-line-01', result);
246
+ expect(receipt.plugin).toBe('manufacturing-qc');
247
+ expect(receipt.cael.event).toBe('manufacturing_qc.spc');
248
+ expect(receipt.payloadHash).toBeTruthy();
249
+ expect(receipt.hashAlgorithm).toBeTruthy();
250
+ });
251
+
252
+ it('sets accepted=false when process is out of control', () => {
253
+ const data = XBAR_R_DATA.map((row) => [...row]);
254
+ data[3] = [120, 120, 120, 120, 120];
255
+ const result = buildSPCChart('xbar_r', makeSubgroups(data));
256
+ const receipt = buildSPCReceipt('line-bad', result);
257
+ expect(receipt.acceptance.accepted).toBe(false);
258
+ expect(receipt.acceptance.violations.length).toBeGreaterThan(0);
259
+ });
260
+
261
+ it('includes Cpk and Ppk in resultSummary when capability provided', () => {
262
+ const result = buildSPCChart('xbar_r', makeSubgroups(XBAR_R_DATA));
263
+ const allValues = XBAR_R_DATA.flat();
264
+ const cap = computeCapability(allValues, makeSubgroups(XBAR_R_DATA), 68, 80);
265
+ const receipt = buildSPCReceipt('line-with-cap', result, cap);
266
+ expect(receipt.resultSummary.Cpk).toBeDefined();
267
+ expect(receipt.resultSummary.Ppk).toBeDefined();
268
+ });
269
+
270
+ it('uses custom runId when provided', () => {
271
+ const result = buildSPCChart('c', [
272
+ { index: 1, values: [], defects: 2 },
273
+ { index: 2, values: [], defects: 3 },
274
+ ]);
275
+ const receipt = buildSPCReceipt('line-custom', result, undefined, { runId: 'my-run-42' });
276
+ expect(receipt.runId).toBe('my-run-42');
277
+ });
278
+ });
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ export * from './spc';
2
+ export { createProductionLineHandler, type ProductionLineConfig, type Station } from './traits/ProductionLineTrait';
3
+ export { createQualityGateHandler, type QualityGateConfig, type InspectionCriteria } from './traits/QualityGateTrait';
4
+ export { createDefectTrackingHandler, type DefectTrackingConfig, type Defect, type DefectSeverity } from './traits/DefectTrackingTrait';
5
+ export { createBOMHandler, type BOMConfig, type BOMItem } from './traits/BOMTrait';
6
+ export * from './traits/types';
7
+
8
+ import { createProductionLineHandler } from './traits/ProductionLineTrait';
9
+ import { createQualityGateHandler } from './traits/QualityGateTrait';
10
+ import { createDefectTrackingHandler } from './traits/DefectTrackingTrait';
11
+ import { createBOMHandler } from './traits/BOMTrait';
12
+
13
+ export * from './spc';
14
+
15
+ // Runtime integration — behavioral trait handler + registrar that wire the
16
+ // deterministic SPC capability solver into HoloScriptRuntime's dispatch. Closes
17
+ // the built-but-dead-wired gap for `spc`, mirroring government-civic's
18
+ // `civic_decision` reference integration.
19
+ export {
20
+ MANUFACTURING_QC_PLUGIN_ID,
21
+ spcHandler,
22
+ registerManufacturingQcTraitHandlers,
23
+ type SpcTraitConfig,
24
+ type SpcSolvedEvent,
25
+ type RuntimeTraitHandler,
26
+ type TraitRegistrar,
27
+ } from './runtime';
28
+
29
+ export const pluginMeta = { name: '@holoscript/plugin-manufacturing-qc', version: '1.0.0', traits: ['production_line', 'quality_gate', 'defect_tracking', 'bom', 'spc'] };
30
+ export const traitHandlers = [createProductionLineHandler(), createQualityGateHandler(), createDefectTrackingHandler(), createBOMHandler()];