@holoscript/plugin-manufacturing-qc 2.0.1 → 2.0.2
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/package.json +3 -3
- package/src/__tests__/runtime-integration.test.ts +4 -4
- package/src/__tests__/spc.test.ts +7 -12
- package/src/index.ts +27 -5
- package/src/runtime.ts +3 -7
- package/src/spc.ts +108 -36
- package/src/traits/BOMTrait.ts +34 -6
- package/src/traits/DefectTrackingTrait.ts +51 -11
- package/src/traits/ProductionLineTrait.ts +56 -12
- package/src/traits/QualityGateTrait.ts +42 -10
- package/src/traits/types.ts +22 -4
- package/tsconfig.json +5 -1
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holoscript/plugin-manufacturing-qc",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"peerDependencies": {
|
|
6
|
-
"@holoscript/core": "8.0.
|
|
6
|
+
"@holoscript/core": ">=8.0.0"
|
|
7
7
|
},
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "vitest run --passWithNoTests",
|
|
11
11
|
"test:coverage": "vitest run --coverage --passWithNoTests"
|
|
12
12
|
}
|
|
13
|
-
}
|
|
13
|
+
}
|
|
@@ -80,7 +80,7 @@ describe('manufacturing-qc -> HoloScript runtime integration (spc)', () => {
|
|
|
80
80
|
subgroups: HAND_SUBGROUPS,
|
|
81
81
|
lsl: HAND_LSL,
|
|
82
82
|
usl: HAND_USL,
|
|
83
|
-
}) as never
|
|
83
|
+
}) as never
|
|
84
84
|
);
|
|
85
85
|
await flush();
|
|
86
86
|
|
|
@@ -107,7 +107,7 @@ describe('manufacturing-qc -> HoloScript runtime integration (spc)', () => {
|
|
|
107
107
|
subgroups: HAND_SUBGROUPS,
|
|
108
108
|
lsl: HAND_LSL,
|
|
109
109
|
usl: HAND_USL,
|
|
110
|
-
}) as never
|
|
110
|
+
}) as never
|
|
111
111
|
);
|
|
112
112
|
await flush();
|
|
113
113
|
|
|
@@ -124,7 +124,7 @@ describe('manufacturing-qc -> HoloScript runtime integration (spc)', () => {
|
|
|
124
124
|
subgroups: HAND_SUBGROUPS,
|
|
125
125
|
lsl: HAND_LSL,
|
|
126
126
|
usl: HAND_USL,
|
|
127
|
-
}) as never
|
|
127
|
+
}) as never
|
|
128
128
|
);
|
|
129
129
|
await flush();
|
|
130
130
|
|
|
@@ -155,7 +155,7 @@ describe('manufacturing-qc -> HoloScript runtime integration (spc)', () => {
|
|
|
155
155
|
subgroups: HAND_SUBGROUPS,
|
|
156
156
|
lsl: 10,
|
|
157
157
|
usl: 4,
|
|
158
|
-
}) as never
|
|
158
|
+
}) as never
|
|
159
159
|
);
|
|
160
160
|
await flush();
|
|
161
161
|
|
|
@@ -7,12 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import {
|
|
11
|
-
buildSPCChart,
|
|
12
|
-
computeCapability,
|
|
13
|
-
buildSPCReceipt,
|
|
14
|
-
type Subgroup,
|
|
15
|
-
} from '../spc';
|
|
10
|
+
import { buildSPCChart, computeCapability, buildSPCReceipt, type Subgroup } from '../spc';
|
|
16
11
|
|
|
17
12
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
13
|
|
|
@@ -87,8 +82,7 @@ describe('buildSPCChart — xbar_r', () => {
|
|
|
87
82
|
expect(result.primaryChart.ucl).toBeGreaterThan(result.primaryChart.centerLine);
|
|
88
83
|
expect(result.primaryChart.lcl).toBeLessThan(result.primaryChart.centerLine);
|
|
89
84
|
// At least 80% of subgroups should be in control for a stable reference process
|
|
90
|
-
const inControlFraction =
|
|
91
|
-
1 - result.outOfControlCount / result.subgroupCount;
|
|
85
|
+
const inControlFraction = 1 - result.outOfControlCount / result.subgroupCount;
|
|
92
86
|
expect(inControlFraction).toBeGreaterThanOrEqual(0.8);
|
|
93
87
|
});
|
|
94
88
|
|
|
@@ -158,7 +152,7 @@ describe('buildSPCChart — p', () => {
|
|
|
158
152
|
buildSPCChart('p', [
|
|
159
153
|
{ index: 1, values: [1, 2, 3] },
|
|
160
154
|
{ index: 2, values: [1, 2, 3] },
|
|
161
|
-
])
|
|
155
|
+
])
|
|
162
156
|
).toThrow();
|
|
163
157
|
});
|
|
164
158
|
});
|
|
@@ -191,11 +185,12 @@ describe('buildSPCChart — c', () => {
|
|
|
191
185
|
|
|
192
186
|
describe('computeCapability', () => {
|
|
193
187
|
// Centred process: mean=50, σ≈1, LSL=44, USL=56 → Cpk≈2.0
|
|
194
|
-
const centredValues: number[] = Array.from(
|
|
195
|
-
|
|
188
|
+
const centredValues: number[] = Array.from(
|
|
189
|
+
{ length: 100 },
|
|
190
|
+
(_, i) => 50 + Math.sin(i * 0.7) * 0.8
|
|
196
191
|
);
|
|
197
192
|
const centredSubgroups = makeSubgroups(
|
|
198
|
-
Array.from({ length: 20 }, (_, i) => centredValues.slice(i * 5, i * 5 + 5))
|
|
193
|
+
Array.from({ length: 20 }, (_, i) => centredValues.slice(i * 5, i * 5 + 5))
|
|
199
194
|
);
|
|
200
195
|
|
|
201
196
|
it('computes Cp, Cpk, Pp, Ppk with correct ordering (Cpk ≤ Cp)', () => {
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
export * from './spc';
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export {
|
|
3
|
+
createProductionLineHandler,
|
|
4
|
+
type ProductionLineConfig,
|
|
5
|
+
type Station,
|
|
6
|
+
} from './traits/ProductionLineTrait';
|
|
7
|
+
export {
|
|
8
|
+
createQualityGateHandler,
|
|
9
|
+
type QualityGateConfig,
|
|
10
|
+
type InspectionCriteria,
|
|
11
|
+
} from './traits/QualityGateTrait';
|
|
12
|
+
export {
|
|
13
|
+
createDefectTrackingHandler,
|
|
14
|
+
type DefectTrackingConfig,
|
|
15
|
+
type Defect,
|
|
16
|
+
type DefectSeverity,
|
|
17
|
+
} from './traits/DefectTrackingTrait';
|
|
5
18
|
export { createBOMHandler, type BOMConfig, type BOMItem } from './traits/BOMTrait';
|
|
6
19
|
export * from './traits/types';
|
|
7
20
|
|
|
@@ -26,5 +39,14 @@ export {
|
|
|
26
39
|
type TraitRegistrar,
|
|
27
40
|
} from './runtime';
|
|
28
41
|
|
|
29
|
-
export const pluginMeta = {
|
|
30
|
-
|
|
42
|
+
export const pluginMeta = {
|
|
43
|
+
name: '@holoscript/plugin-manufacturing-qc',
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
traits: ['production_line', 'quality_gate', 'defect_tracking', 'bom', 'spc'],
|
|
46
|
+
};
|
|
47
|
+
export const traitHandlers = [
|
|
48
|
+
createProductionLineHandler(),
|
|
49
|
+
createQualityGateHandler(),
|
|
50
|
+
createDefectTrackingHandler(),
|
|
51
|
+
createBOMHandler(),
|
|
52
|
+
];
|
package/src/runtime.ts
CHANGED
|
@@ -15,11 +15,7 @@
|
|
|
15
15
|
* traits follow the same registrar shape.
|
|
16
16
|
*/
|
|
17
17
|
import { registerPluginTraits } from '@holoscript/core/runtime';
|
|
18
|
-
import {
|
|
19
|
-
computeCapability,
|
|
20
|
-
type Subgroup,
|
|
21
|
-
type ProcessCapability,
|
|
22
|
-
} from './spc';
|
|
18
|
+
import { computeCapability, type Subgroup, type ProcessCapability } from './spc';
|
|
23
19
|
|
|
24
20
|
/** Stable id for this plugin's trait ownership tagging. */
|
|
25
21
|
export const MANUFACTURING_QC_PLUGIN_ID = 'manufacturing-qc' as const;
|
|
@@ -76,7 +72,7 @@ export interface RuntimeTraitHandler {
|
|
|
76
72
|
node: unknown,
|
|
77
73
|
config: SpcTraitConfig,
|
|
78
74
|
context: TraitDispatchContext,
|
|
79
|
-
delta: number
|
|
75
|
+
delta: number
|
|
80
76
|
) => void;
|
|
81
77
|
}
|
|
82
78
|
|
|
@@ -91,7 +87,7 @@ interface SpcNode {
|
|
|
91
87
|
function solveOntoNode(
|
|
92
88
|
node: unknown,
|
|
93
89
|
config: SpcTraitConfig | undefined,
|
|
94
|
-
context: TraitDispatchContext
|
|
90
|
+
context: TraitDispatchContext
|
|
95
91
|
): void {
|
|
96
92
|
const carrier = node as SpcNode;
|
|
97
93
|
const nodeId = carrier.id ?? carrier.name ?? 'unknown';
|
package/src/spc.ts
CHANGED
|
@@ -58,8 +58,8 @@ export interface SPCChartResult {
|
|
|
58
58
|
chartType: ChartType;
|
|
59
59
|
subgroupCount: number;
|
|
60
60
|
totalObservations: number;
|
|
61
|
-
primaryChart: ControlLimits;
|
|
62
|
-
secondaryChart?: ControlLimits;
|
|
61
|
+
primaryChart: ControlLimits; // X̅ or p or c/u
|
|
62
|
+
secondaryChart?: ControlLimits; // R or s (only for variables charts)
|
|
63
63
|
subgroupStats: SubgroupStat[];
|
|
64
64
|
outOfControlCount: number;
|
|
65
65
|
processInControl: boolean;
|
|
@@ -131,48 +131,104 @@ export interface SPCReceipt {
|
|
|
131
131
|
|
|
132
132
|
/** d2 unbiasing constants for range → σ̂ */
|
|
133
133
|
const D2: Record<number, number> = {
|
|
134
|
-
2: 1.128,
|
|
135
|
-
|
|
134
|
+
2: 1.128,
|
|
135
|
+
3: 1.693,
|
|
136
|
+
4: 2.059,
|
|
137
|
+
5: 2.326,
|
|
138
|
+
6: 2.534,
|
|
139
|
+
7: 2.704,
|
|
140
|
+
8: 2.847,
|
|
141
|
+
9: 2.97,
|
|
142
|
+
10: 3.078,
|
|
136
143
|
};
|
|
137
144
|
|
|
138
145
|
/** d3 for range chart LCL */
|
|
139
146
|
const D3: Record<number, number> = {
|
|
140
|
-
2: 0,
|
|
141
|
-
|
|
147
|
+
2: 0,
|
|
148
|
+
3: 0,
|
|
149
|
+
4: 0,
|
|
150
|
+
5: 0,
|
|
151
|
+
6: 0,
|
|
152
|
+
7: 0.076,
|
|
153
|
+
8: 0.136,
|
|
154
|
+
9: 0.184,
|
|
155
|
+
10: 0.223,
|
|
142
156
|
};
|
|
143
157
|
|
|
144
158
|
/** d4 for range chart UCL */
|
|
145
159
|
const D4: Record<number, number> = {
|
|
146
|
-
2: 3.267,
|
|
147
|
-
|
|
160
|
+
2: 3.267,
|
|
161
|
+
3: 2.575,
|
|
162
|
+
4: 2.282,
|
|
163
|
+
5: 2.115,
|
|
164
|
+
6: 2.004,
|
|
165
|
+
7: 1.924,
|
|
166
|
+
8: 1.864,
|
|
167
|
+
9: 1.816,
|
|
168
|
+
10: 1.777,
|
|
148
169
|
};
|
|
149
170
|
|
|
150
171
|
/** c4 unbiasing constants for s → σ̂ */
|
|
151
172
|
const C4: Record<number, number> = {
|
|
152
|
-
2: 0.7979,
|
|
153
|
-
|
|
173
|
+
2: 0.7979,
|
|
174
|
+
3: 0.8862,
|
|
175
|
+
4: 0.9213,
|
|
176
|
+
5: 0.94,
|
|
177
|
+
6: 0.9515,
|
|
178
|
+
7: 0.9594,
|
|
179
|
+
8: 0.965,
|
|
180
|
+
9: 0.9693,
|
|
181
|
+
10: 0.9727,
|
|
154
182
|
};
|
|
155
183
|
|
|
156
184
|
/** A2 constants for X̅-R chart (3σ limits via R̅) */
|
|
157
185
|
const A2: Record<number, number> = {
|
|
158
|
-
2: 1.
|
|
159
|
-
|
|
186
|
+
2: 1.88,
|
|
187
|
+
3: 1.023,
|
|
188
|
+
4: 0.729,
|
|
189
|
+
5: 0.577,
|
|
190
|
+
6: 0.483,
|
|
191
|
+
7: 0.419,
|
|
192
|
+
8: 0.373,
|
|
193
|
+
9: 0.337,
|
|
194
|
+
10: 0.308,
|
|
160
195
|
};
|
|
161
196
|
|
|
162
197
|
/** A3 constants for X̅-s chart (3σ limits via s̅) */
|
|
163
198
|
const A3: Record<number, number> = {
|
|
164
|
-
2: 2.659,
|
|
165
|
-
|
|
199
|
+
2: 2.659,
|
|
200
|
+
3: 1.954,
|
|
201
|
+
4: 1.628,
|
|
202
|
+
5: 1.427,
|
|
203
|
+
6: 1.287,
|
|
204
|
+
7: 1.182,
|
|
205
|
+
8: 1.099,
|
|
206
|
+
9: 1.032,
|
|
207
|
+
10: 0.975,
|
|
166
208
|
};
|
|
167
209
|
|
|
168
210
|
/** B3 / B4 for s-chart limits */
|
|
169
211
|
const B3: Record<number, number> = {
|
|
170
|
-
2: 0,
|
|
171
|
-
|
|
212
|
+
2: 0,
|
|
213
|
+
3: 0,
|
|
214
|
+
4: 0,
|
|
215
|
+
5: 0,
|
|
216
|
+
6: 0.03,
|
|
217
|
+
7: 0.118,
|
|
218
|
+
8: 0.185,
|
|
219
|
+
9: 0.239,
|
|
220
|
+
10: 0.284,
|
|
172
221
|
};
|
|
173
222
|
const B4: Record<number, number> = {
|
|
174
|
-
2: 3.267,
|
|
175
|
-
|
|
223
|
+
2: 3.267,
|
|
224
|
+
3: 2.568,
|
|
225
|
+
4: 2.266,
|
|
226
|
+
5: 2.089,
|
|
227
|
+
6: 1.97,
|
|
228
|
+
7: 1.882,
|
|
229
|
+
8: 1.815,
|
|
230
|
+
9: 1.761,
|
|
231
|
+
10: 1.716,
|
|
176
232
|
};
|
|
177
233
|
|
|
178
234
|
// ─── Statistics helpers ────────────────────────────────────────────────────────────────────
|
|
@@ -266,7 +322,8 @@ function westernElectricViolations(zScores: number[]): string[][] {
|
|
|
266
322
|
// Rule 5: 6 consecutive monotone
|
|
267
323
|
if (i >= 5) {
|
|
268
324
|
const window = zScores.slice(i - 5, i + 1);
|
|
269
|
-
let inc = true;
|
|
325
|
+
let inc = true;
|
|
326
|
+
let dec = true;
|
|
270
327
|
for (let j = 1; j < window.length; j++) {
|
|
271
328
|
if (window[j] <= window[j - 1]) inc = false;
|
|
272
329
|
if (window[j] >= window[j - 1]) dec = false;
|
|
@@ -291,7 +348,10 @@ function westernElectricViolations(zScores: number[]): string[][] {
|
|
|
291
348
|
for (let j = 2; j < window.length; j++) {
|
|
292
349
|
const prevDir = window[j - 1] > window[j - 2];
|
|
293
350
|
const currDir = window[j] > window[j - 1];
|
|
294
|
-
if (currDir === prevDir) {
|
|
351
|
+
if (currDir === prevDir) {
|
|
352
|
+
alternating = false;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
295
355
|
}
|
|
296
356
|
if (alternating) violations[i].push('WE7:alternating14');
|
|
297
357
|
}
|
|
@@ -396,14 +456,15 @@ function buildXbarSChart(subgroups: Subgroup[]): SPCChartResult {
|
|
|
396
456
|
lclFloor: 0,
|
|
397
457
|
};
|
|
398
458
|
|
|
399
|
-
const c4val = C4[n] ?? 0.
|
|
459
|
+
const c4val = C4[n] ?? 0.94;
|
|
400
460
|
const sigma = sBar / c4val;
|
|
401
461
|
const primaryZ = means.map((m) => (sigma > 0 ? (m - xbarBar) / sigma : 0));
|
|
402
462
|
const primaryViolations = westernElectricViolations(primaryZ);
|
|
403
463
|
|
|
404
464
|
const subgroupStats: SubgroupStat[] = subgroups.map((sg, i) => {
|
|
405
465
|
const violations = [...primaryViolations[i]];
|
|
406
|
-
if (stds[i] > secondaryLimits.ucl || stds[i] < secondaryLimits.lcl)
|
|
466
|
+
if (stds[i] > secondaryLimits.ucl || stds[i] < secondaryLimits.lcl)
|
|
467
|
+
violations.push('s:outOfControl');
|
|
407
468
|
return {
|
|
408
469
|
index: sg.index,
|
|
409
470
|
n,
|
|
@@ -485,7 +546,8 @@ function buildPChart(subgroups: Subgroup[]): SPCChartResult {
|
|
|
485
546
|
function buildCChart(subgroups: Subgroup[]): SPCChartResult {
|
|
486
547
|
if (subgroups.length < 2) throw new Error('[spc] c-chart requires ≥ 2 subgroups');
|
|
487
548
|
for (const sg of subgroups) {
|
|
488
|
-
if (sg.defects === undefined)
|
|
549
|
+
if (sg.defects === undefined)
|
|
550
|
+
throw new Error('[spc] c-chart requires .defects on every subgroup');
|
|
489
551
|
}
|
|
490
552
|
|
|
491
553
|
const counts = subgroups.map((sg) => sg.defects ?? 0);
|
|
@@ -537,9 +599,12 @@ function buildCChart(subgroups: Subgroup[]): SPCChartResult {
|
|
|
537
599
|
*/
|
|
538
600
|
export function buildSPCChart(type: ChartType, subgroups: Subgroup[]): SPCChartResult {
|
|
539
601
|
switch (type) {
|
|
540
|
-
case 'xbar_r':
|
|
541
|
-
|
|
542
|
-
case '
|
|
602
|
+
case 'xbar_r':
|
|
603
|
+
return buildXbarRChart(subgroups);
|
|
604
|
+
case 'xbar_s':
|
|
605
|
+
return buildXbarSChart(subgroups);
|
|
606
|
+
case 'p':
|
|
607
|
+
return buildPChart(subgroups);
|
|
543
608
|
case 'np': {
|
|
544
609
|
// np-chart: same as p-chart but plot count instead of proportion
|
|
545
610
|
const result = buildPChart(subgroups);
|
|
@@ -555,7 +620,8 @@ export function buildSPCChart(type: ChartType, subgroups: Subgroup[]): SPCChartR
|
|
|
555
620
|
},
|
|
556
621
|
};
|
|
557
622
|
}
|
|
558
|
-
case 'c':
|
|
623
|
+
case 'c':
|
|
624
|
+
return buildCChart(subgroups);
|
|
559
625
|
case 'u': {
|
|
560
626
|
// u-chart: defects per unit (c-chart normalised by n)
|
|
561
627
|
for (const sg of subgroups) {
|
|
@@ -612,7 +678,7 @@ export function computeCapability(
|
|
|
612
678
|
subgroups: Subgroup[],
|
|
613
679
|
lsl: number,
|
|
614
680
|
usl: number,
|
|
615
|
-
target?: number
|
|
681
|
+
target?: number
|
|
616
682
|
): ProcessCapability {
|
|
617
683
|
if (usl <= lsl) throw new Error('[spc] usl must be > lsl');
|
|
618
684
|
if (allValues.length < 2) throw new Error('[spc] need ≥ 2 values for capability');
|
|
@@ -633,11 +699,11 @@ export function computeCapability(
|
|
|
633
699
|
}
|
|
634
700
|
|
|
635
701
|
const specWidth = usl - lsl;
|
|
636
|
-
const Cp
|
|
637
|
-
const Pp
|
|
702
|
+
const Cp = withinSigma > 0 ? specWidth / (6 * withinSigma) : Infinity;
|
|
703
|
+
const Pp = overallSigma > 0 ? specWidth / (6 * overallSigma) : Infinity;
|
|
638
704
|
|
|
639
|
-
const CpkUpper = withinSigma
|
|
640
|
-
const CpkLower = withinSigma
|
|
705
|
+
const CpkUpper = withinSigma > 0 ? (usl - processMean) / (3 * withinSigma) : Infinity;
|
|
706
|
+
const CpkLower = withinSigma > 0 ? (processMean - lsl) / (3 * withinSigma) : Infinity;
|
|
641
707
|
const Cpk = Math.min(CpkUpper, CpkLower);
|
|
642
708
|
|
|
643
709
|
const PpkUpper = overallSigma > 0 ? (usl - processMean) / (3 * overallSigma) : Infinity;
|
|
@@ -656,11 +722,17 @@ export function computeCapability(
|
|
|
656
722
|
const ppmBelowLSL = ppm(zBelowLSL);
|
|
657
723
|
|
|
658
724
|
return {
|
|
659
|
-
lsl,
|
|
725
|
+
lsl,
|
|
726
|
+
usl,
|
|
727
|
+
target,
|
|
660
728
|
processMean,
|
|
661
729
|
processStdDev: overallSigma,
|
|
662
|
-
Cp,
|
|
663
|
-
|
|
730
|
+
Cp,
|
|
731
|
+
Cpk,
|
|
732
|
+
CpkLower,
|
|
733
|
+
CpkUpper,
|
|
734
|
+
Pp,
|
|
735
|
+
Ppk,
|
|
664
736
|
Cpm,
|
|
665
737
|
ppmAboveUSL,
|
|
666
738
|
ppmBelowLSL,
|
|
@@ -676,7 +748,7 @@ export function buildSPCReceipt(
|
|
|
676
748
|
modelId: string,
|
|
677
749
|
chartResult: SPCChartResult,
|
|
678
750
|
capability?: ProcessCapability,
|
|
679
|
-
options: SPCReceiptOptions = {}
|
|
751
|
+
options: SPCReceiptOptions = {}
|
|
680
752
|
): SPCReceipt {
|
|
681
753
|
const violations: Array<{ criterion: string; message: string }> = [];
|
|
682
754
|
|
package/src/traits/BOMTrait.ts
CHANGED
|
@@ -1,22 +1,50 @@
|
|
|
1
1
|
/** @bom Trait — Bill of Materials management. @trait bom */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export interface BOMItem {
|
|
5
|
-
|
|
4
|
+
export interface BOMItem {
|
|
5
|
+
partNumber: string;
|
|
6
|
+
name: string;
|
|
7
|
+
quantity: number;
|
|
8
|
+
unit: string;
|
|
9
|
+
supplier?: string;
|
|
10
|
+
leadTimeDays: number;
|
|
11
|
+
costPerUnit: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BOMConfig {
|
|
14
|
+
items: BOMItem[];
|
|
15
|
+
revision: string;
|
|
16
|
+
product: string;
|
|
17
|
+
approvedBy?: string;
|
|
18
|
+
}
|
|
6
19
|
|
|
7
20
|
const defaultConfig: BOMConfig = { items: [], revision: '1.0', product: '' };
|
|
8
21
|
|
|
9
22
|
export function createBOMHandler(): TraitHandler<BOMConfig> {
|
|
10
|
-
return {
|
|
23
|
+
return {
|
|
24
|
+
name: 'bom',
|
|
25
|
+
defaultConfig,
|
|
11
26
|
onAttach(n: HSPlusNode, c: BOMConfig, ctx: TraitContext) {
|
|
12
27
|
const totalCost = c.items.reduce((sum, i) => sum + i.costPerUnit * i.quantity, 0);
|
|
13
|
-
n.__bomState = {
|
|
28
|
+
n.__bomState = {
|
|
29
|
+
totalCost,
|
|
30
|
+
itemCount: c.items.length,
|
|
31
|
+
longestLeadTime: Math.max(0, ...c.items.map((i) => i.leadTimeDays)),
|
|
32
|
+
};
|
|
14
33
|
ctx.emit?.('bom:loaded', { items: c.items.length, totalCost });
|
|
15
34
|
},
|
|
16
|
-
onDetach(n: HSPlusNode, _c: BOMConfig, ctx: TraitContext) {
|
|
35
|
+
onDetach(n: HSPlusNode, _c: BOMConfig, ctx: TraitContext) {
|
|
36
|
+
delete n.__bomState;
|
|
37
|
+
ctx.emit?.('bom:unloaded');
|
|
38
|
+
},
|
|
17
39
|
onUpdate() {},
|
|
18
40
|
onEvent(_n: HSPlusNode, c: BOMConfig, ctx: TraitContext, e: TraitEvent) {
|
|
19
|
-
if (e.type === 'bom:check_availability') {
|
|
41
|
+
if (e.type === 'bom:check_availability') {
|
|
42
|
+
const missing = c.items.filter((i) => i.leadTimeDays > 14);
|
|
43
|
+
ctx.emit?.('bom:availability', {
|
|
44
|
+
available: c.items.length - missing.length,
|
|
45
|
+
delayed: missing.length,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
20
48
|
},
|
|
21
49
|
};
|
|
22
50
|
}
|
|
@@ -2,25 +2,65 @@
|
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
4
|
export type DefectSeverity = 'critical' | 'major' | 'minor' | 'cosmetic';
|
|
5
|
-
export interface Defect {
|
|
6
|
-
|
|
5
|
+
export interface Defect {
|
|
6
|
+
id: string;
|
|
7
|
+
severity: DefectSeverity;
|
|
8
|
+
description: string;
|
|
9
|
+
stationId: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
resolved: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface DefectTrackingConfig {
|
|
14
|
+
categories: string[];
|
|
15
|
+
autoEscalateCritical: boolean;
|
|
16
|
+
maxOpenDefects: number;
|
|
17
|
+
}
|
|
7
18
|
|
|
8
|
-
const defaultConfig: DefectTrackingConfig = {
|
|
19
|
+
const defaultConfig: DefectTrackingConfig = {
|
|
20
|
+
categories: ['dimensional', 'surface', 'functional', 'material'],
|
|
21
|
+
autoEscalateCritical: true,
|
|
22
|
+
maxOpenDefects: 50,
|
|
23
|
+
};
|
|
9
24
|
|
|
10
25
|
export function createDefectTrackingHandler(): TraitHandler<DefectTrackingConfig> {
|
|
11
|
-
return {
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
return {
|
|
27
|
+
name: 'defect_tracking',
|
|
28
|
+
defaultConfig,
|
|
29
|
+
onAttach(n: HSPlusNode, _c: DefectTrackingConfig, ctx: TraitContext) {
|
|
30
|
+
n.__defectState = { defects: [], openCount: 0 };
|
|
31
|
+
ctx.emit?.('defect:tracker_ready');
|
|
32
|
+
},
|
|
33
|
+
onDetach(n: HSPlusNode, _c: DefectTrackingConfig, ctx: TraitContext) {
|
|
34
|
+
delete n.__defectState;
|
|
35
|
+
ctx.emit?.('defect:tracker_removed');
|
|
36
|
+
},
|
|
14
37
|
onUpdate() {},
|
|
15
38
|
onEvent(n: HSPlusNode, c: DefectTrackingConfig, ctx: TraitContext, e: TraitEvent) {
|
|
16
|
-
const s = n.__defectState as { defects: Defect[]; openCount: number } | undefined;
|
|
39
|
+
const s = n.__defectState as { defects: Defect[]; openCount: number } | undefined;
|
|
40
|
+
if (!s) return;
|
|
17
41
|
if (e.type === 'defect:log') {
|
|
18
|
-
const defect: Defect = {
|
|
19
|
-
|
|
42
|
+
const defect: Defect = {
|
|
43
|
+
id: `DEF-${s.defects.length + 1}`,
|
|
44
|
+
severity: (e.payload?.severity as DefectSeverity) || 'minor',
|
|
45
|
+
description: (e.payload?.description as string) || '',
|
|
46
|
+
stationId: (e.payload?.stationId as string) || '',
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
resolved: false,
|
|
49
|
+
};
|
|
50
|
+
s.defects.push(defect);
|
|
51
|
+
s.openCount++;
|
|
20
52
|
ctx.emit?.('defect:logged', { id: defect.id, severity: defect.severity });
|
|
21
|
-
if (defect.severity === 'critical' && c.autoEscalateCritical)
|
|
53
|
+
if (defect.severity === 'critical' && c.autoEscalateCritical)
|
|
54
|
+
ctx.emit?.('defect:escalated', { id: defect.id });
|
|
55
|
+
}
|
|
56
|
+
if (e.type === 'defect:resolve') {
|
|
57
|
+
const d = s.defects.find((d) => d.id === e.payload?.id);
|
|
58
|
+
if (d && !d.resolved) {
|
|
59
|
+
d.resolved = true;
|
|
60
|
+
s.openCount--;
|
|
61
|
+
ctx.emit?.('defect:resolved', { id: d.id });
|
|
62
|
+
}
|
|
22
63
|
}
|
|
23
|
-
if (e.type === 'defect:resolve') { const d = s.defects.find(d => d.id === e.payload?.id); if (d && !d.resolved) { d.resolved = true; s.openCount--; ctx.emit?.('defect:resolved', { id: d.id }); } }
|
|
24
64
|
},
|
|
25
65
|
};
|
|
26
66
|
}
|
|
@@ -1,26 +1,70 @@
|
|
|
1
1
|
/** @production_line Trait — Assembly line management. @trait production_line */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export interface Station {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export interface Station {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
cycleTimeS: number;
|
|
8
|
+
status: 'running' | 'idle' | 'maintenance' | 'error';
|
|
9
|
+
}
|
|
10
|
+
export interface ProductionLineConfig {
|
|
11
|
+
stations: Station[];
|
|
12
|
+
targetUnitsPerHour: number;
|
|
13
|
+
shiftDurationH: number;
|
|
14
|
+
product: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ProductionLineState {
|
|
17
|
+
unitsProduced: number;
|
|
18
|
+
currentThroughput: number;
|
|
19
|
+
bottleneckStation: string | null;
|
|
20
|
+
isRunning: boolean;
|
|
21
|
+
}
|
|
7
22
|
|
|
8
|
-
const defaultConfig: ProductionLineConfig = {
|
|
23
|
+
const defaultConfig: ProductionLineConfig = {
|
|
24
|
+
stations: [],
|
|
25
|
+
targetUnitsPerHour: 60,
|
|
26
|
+
shiftDurationH: 8,
|
|
27
|
+
product: '',
|
|
28
|
+
};
|
|
9
29
|
|
|
10
30
|
export function createProductionLineHandler(): TraitHandler<ProductionLineConfig> {
|
|
11
|
-
return {
|
|
12
|
-
|
|
13
|
-
|
|
31
|
+
return {
|
|
32
|
+
name: 'production_line',
|
|
33
|
+
defaultConfig,
|
|
34
|
+
onAttach(n: HSPlusNode, c: ProductionLineConfig, ctx: TraitContext) {
|
|
35
|
+
n.__lineState = {
|
|
36
|
+
unitsProduced: 0,
|
|
37
|
+
currentThroughput: 0,
|
|
38
|
+
bottleneckStation: null,
|
|
39
|
+
isRunning: false,
|
|
40
|
+
};
|
|
41
|
+
ctx.emit?.('line:created', { stations: c.stations.length });
|
|
42
|
+
},
|
|
43
|
+
onDetach(n: HSPlusNode, _c: ProductionLineConfig, ctx: TraitContext) {
|
|
44
|
+
delete n.__lineState;
|
|
45
|
+
ctx.emit?.('line:shutdown');
|
|
46
|
+
},
|
|
14
47
|
onUpdate(n: HSPlusNode, c: ProductionLineConfig, ctx: TraitContext, delta: number) {
|
|
15
|
-
const s = n.__lineState as ProductionLineState | undefined;
|
|
48
|
+
const s = n.__lineState as ProductionLineState | undefined;
|
|
49
|
+
if (!s?.isRunning) return;
|
|
16
50
|
s.unitsProduced += (c.targetUnitsPerHour / 3600) * (delta / 1000);
|
|
17
|
-
const slowest = c.stations.reduce(
|
|
51
|
+
const slowest = c.stations.reduce(
|
|
52
|
+
(a, b) => (a.cycleTimeS > b.cycleTimeS ? a : b),
|
|
53
|
+
c.stations[0]
|
|
54
|
+
);
|
|
18
55
|
s.bottleneckStation = slowest?.id ?? null;
|
|
19
56
|
},
|
|
20
57
|
onEvent(n: HSPlusNode, _c: ProductionLineConfig, ctx: TraitContext, e: TraitEvent) {
|
|
21
|
-
const s = n.__lineState as ProductionLineState | undefined;
|
|
22
|
-
if (
|
|
23
|
-
if (e.type === 'line:
|
|
58
|
+
const s = n.__lineState as ProductionLineState | undefined;
|
|
59
|
+
if (!s) return;
|
|
60
|
+
if (e.type === 'line:start') {
|
|
61
|
+
s.isRunning = true;
|
|
62
|
+
ctx.emit?.('line:started');
|
|
63
|
+
}
|
|
64
|
+
if (e.type === 'line:stop') {
|
|
65
|
+
s.isRunning = false;
|
|
66
|
+
ctx.emit?.('line:stopped', { produced: s.unitsProduced });
|
|
67
|
+
}
|
|
24
68
|
},
|
|
25
69
|
};
|
|
26
70
|
}
|
|
@@ -1,26 +1,58 @@
|
|
|
1
1
|
/** @quality_gate Trait — Inspection checkpoint. @trait quality_gate */
|
|
2
2
|
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
3
|
|
|
4
|
-
export interface InspectionCriteria {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export interface InspectionCriteria {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
type: 'visual' | 'dimensional' | 'functional' | 'electrical';
|
|
8
|
+
tolerance: number;
|
|
9
|
+
unit: string;
|
|
10
|
+
}
|
|
11
|
+
export interface QualityGateConfig {
|
|
12
|
+
criteria: InspectionCriteria[];
|
|
13
|
+
passThreshold: number;
|
|
14
|
+
autoReject: boolean;
|
|
15
|
+
stationId: string;
|
|
16
|
+
}
|
|
17
|
+
export interface QualityGateState {
|
|
18
|
+
inspected: number;
|
|
19
|
+
passed: number;
|
|
20
|
+
failed: number;
|
|
21
|
+
passRate: number;
|
|
22
|
+
}
|
|
7
23
|
|
|
8
|
-
const defaultConfig: QualityGateConfig = {
|
|
24
|
+
const defaultConfig: QualityGateConfig = {
|
|
25
|
+
criteria: [],
|
|
26
|
+
passThreshold: 95,
|
|
27
|
+
autoReject: true,
|
|
28
|
+
stationId: '',
|
|
29
|
+
};
|
|
9
30
|
|
|
10
31
|
export function createQualityGateHandler(): TraitHandler<QualityGateConfig> {
|
|
11
|
-
return {
|
|
12
|
-
|
|
13
|
-
|
|
32
|
+
return {
|
|
33
|
+
name: 'quality_gate',
|
|
34
|
+
defaultConfig,
|
|
35
|
+
onAttach(n: HSPlusNode, _c: QualityGateConfig, ctx: TraitContext) {
|
|
36
|
+
n.__qcState = { inspected: 0, passed: 0, failed: 0, passRate: 100 };
|
|
37
|
+
ctx.emit?.('qc:ready');
|
|
38
|
+
},
|
|
39
|
+
onDetach(n: HSPlusNode, _c: QualityGateConfig, ctx: TraitContext) {
|
|
40
|
+
delete n.__qcState;
|
|
41
|
+
ctx.emit?.('qc:removed');
|
|
42
|
+
},
|
|
14
43
|
onUpdate() {},
|
|
15
44
|
onEvent(n: HSPlusNode, c: QualityGateConfig, ctx: TraitContext, e: TraitEvent) {
|
|
16
|
-
const s = n.__qcState as QualityGateState | undefined;
|
|
45
|
+
const s = n.__qcState as QualityGateState | undefined;
|
|
46
|
+
if (!s) return;
|
|
17
47
|
if (e.type === 'qc:inspect') {
|
|
18
48
|
s.inspected++;
|
|
19
49
|
const pass = (e.payload?.pass as boolean) ?? true;
|
|
20
|
-
if (pass) s.passed++;
|
|
50
|
+
if (pass) s.passed++;
|
|
51
|
+
else s.failed++;
|
|
21
52
|
s.passRate = s.inspected > 0 ? (s.passed / s.inspected) * 100 : 100;
|
|
22
53
|
ctx.emit?.('qc:inspected', { pass, passRate: s.passRate });
|
|
23
|
-
if (s.passRate < c.passThreshold)
|
|
54
|
+
if (s.passRate < c.passThreshold)
|
|
55
|
+
ctx.emit?.('qc:threshold_breach', { passRate: s.passRate, threshold: c.passThreshold });
|
|
24
56
|
}
|
|
25
57
|
},
|
|
26
58
|
};
|
package/src/traits/types.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
export interface HSPlusNode {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export interface HSPlusNode {
|
|
2
|
+
id?: string;
|
|
3
|
+
properties?: Record<string, unknown>;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface TraitContext {
|
|
7
|
+
emit?: (event: string, payload?: unknown) => void;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface TraitEvent {
|
|
11
|
+
type: string;
|
|
12
|
+
payload?: Record<string, unknown>;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface TraitHandler<T = unknown> {
|
|
16
|
+
name: string;
|
|
17
|
+
defaultConfig: T;
|
|
18
|
+
onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void;
|
|
19
|
+
onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void;
|
|
20
|
+
onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void;
|
|
21
|
+
onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void;
|
|
22
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true },
|
|
4
|
+
"include": ["src"]
|
|
5
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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.
|