zorya-classifier 0.2.0 → 0.2.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.
Files changed (140) hide show
  1. package/README.md +24 -8
  2. package/dist/app/classifier-config.d.ts +30 -0
  3. package/dist/app/classifier-config.js +46 -0
  4. package/dist/app/classifier-contracts.d.ts +32 -0
  5. package/dist/app/classifier-contracts.js +1 -0
  6. package/dist/app/classifier-representation.d.ts +5 -0
  7. package/dist/app/classifier-representation.js +33 -0
  8. package/dist/app/classifier-runtime.d.ts +7 -0
  9. package/dist/app/classifier-runtime.js +71 -0
  10. package/dist/app/classifier-spatial.d.ts +31 -0
  11. package/dist/app/classifier-spatial.js +78 -0
  12. package/dist/app/cli-arguments.d.ts +2 -2
  13. package/dist/app/create-classifier.d.ts +4 -29
  14. package/dist/app/create-classifier.js +37 -237
  15. package/dist/cli.js +1 -1
  16. package/dist/entities/classification.d.ts +98 -0
  17. package/dist/entities/classification.js +28 -0
  18. package/dist/entities/contracts.d.ts +6 -471
  19. package/dist/entities/contracts.js +6 -97
  20. package/dist/entities/decision.d.ts +211 -0
  21. package/dist/entities/decision.js +33 -0
  22. package/dist/entities/geometry.d.ts +37 -0
  23. package/dist/entities/geometry.js +26 -0
  24. package/dist/entities/image.d.ts +39 -0
  25. package/dist/entities/image.js +15 -0
  26. package/dist/entities/localization.d.ts +125 -0
  27. package/dist/entities/localization.js +13 -0
  28. package/dist/entities/object-proposals.d.ts +4 -1
  29. package/dist/entities/segmentation-metadata.d.ts +70 -0
  30. package/dist/entities/segmentation-metadata.js +21 -1
  31. package/dist/entities/segmentation-verification.d.ts +1 -0
  32. package/dist/entities/segmentation.d.ts +6 -1
  33. package/dist/entities/validation.d.ts +2 -0
  34. package/dist/entities/validation.js +9 -0
  35. package/dist/entities/whole-object.d.ts +4 -0
  36. package/dist/features/choice-beam-layout.js +2 -7
  37. package/dist/features/proposals/presentation.d.ts +21 -0
  38. package/dist/features/proposals/presentation.js +122 -0
  39. package/dist/features/proposals/validation.d.ts +138 -0
  40. package/dist/features/proposals/validation.js +125 -0
  41. package/dist/features/segmentation/box-check.js +10 -23
  42. package/dist/features/segmentation/mask.js +2 -7
  43. package/dist/features/segmentation/region-result.d.ts +18 -0
  44. package/dist/features/segmentation/region-result.js +171 -0
  45. package/dist/features/segmentation/source-mask.d.ts +6 -0
  46. package/dist/features/segmentation/source-mask.js +32 -0
  47. package/dist/features/segmentation/validation.d.ts +9 -0
  48. package/dist/features/segmentation/validation.js +94 -0
  49. package/dist/infrastructure/decision-transport/errors.d.ts +8 -0
  50. package/dist/infrastructure/decision-transport/errors.js +34 -0
  51. package/dist/infrastructure/decision-transport/images.d.ts +1 -0
  52. package/dist/infrastructure/decision-transport/images.js +52 -0
  53. package/dist/infrastructure/{openrouter-common.d.ts → decision-transport/request.d.ts} +6 -15
  54. package/dist/infrastructure/decision-transport/request.js +162 -0
  55. package/dist/infrastructure/decision-transport/response.d.ts +1 -0
  56. package/dist/infrastructure/decision-transport/response.js +72 -0
  57. package/dist/infrastructure/decision-transport/route-identity.d.ts +11 -0
  58. package/dist/infrastructure/decision-transport/route-identity.js +23 -0
  59. package/dist/infrastructure/openrouter-vision.js +1 -1
  60. package/dist/infrastructure/openrouter.js +7 -19
  61. package/dist/infrastructure/raster.js +3 -8
  62. package/dist/infrastructure/segmentation/grabcut/index.d.ts +5 -1
  63. package/dist/infrastructure/segmentation/grabcut/index.js +6 -5
  64. package/dist/infrastructure/segmentation/grabcut/masks.d.ts +49 -0
  65. package/dist/infrastructure/segmentation/grabcut/masks.js +89 -0
  66. package/dist/infrastructure/segmentation/grabcut/model-asset.d.ts +4 -0
  67. package/dist/infrastructure/segmentation/grabcut/model-asset.js +4 -0
  68. package/dist/infrastructure/segmentation/grabcut/runtime.d.ts +70 -0
  69. package/dist/infrastructure/segmentation/grabcut/runtime.js +96 -0
  70. package/dist/infrastructure/segmentation/grabcut/worker-inference.d.ts +4 -43
  71. package/dist/infrastructure/segmentation/grabcut/worker-inference.js +4 -187
  72. package/dist/infrastructure/segmentation/mobile-sam/index.d.ts +3 -58
  73. package/dist/infrastructure/segmentation/mobile-sam/index.js +2 -472
  74. package/dist/infrastructure/segmentation/mobile-sam/input.d.ts +46 -0
  75. package/dist/infrastructure/segmentation/mobile-sam/input.js +83 -0
  76. package/dist/infrastructure/segmentation/mobile-sam/model-assets.d.ts +8 -0
  77. package/dist/infrastructure/segmentation/mobile-sam/model-assets.js +8 -0
  78. package/dist/infrastructure/segmentation/mobile-sam/object-proposals.d.ts +7 -0
  79. package/dist/infrastructure/segmentation/mobile-sam/object-proposals.js +124 -0
  80. package/dist/infrastructure/segmentation/mobile-sam/options.d.ts +43 -0
  81. package/dist/infrastructure/segmentation/mobile-sam/options.js +19 -0
  82. package/dist/infrastructure/segmentation/mobile-sam/proposal-masks.d.ts +26 -0
  83. package/dist/infrastructure/segmentation/mobile-sam/proposal-masks.js +71 -0
  84. package/dist/infrastructure/segmentation/mobile-sam/segmentation-masks.d.ts +112 -0
  85. package/dist/infrastructure/segmentation/mobile-sam/segmentation-masks.js +230 -0
  86. package/dist/infrastructure/segmentation/mobile-sam/segmenter.d.ts +22 -0
  87. package/dist/infrastructure/segmentation/mobile-sam/segmenter.js +268 -0
  88. package/dist/infrastructure/segmentation/mobile-sam/worker-expand.d.ts +7 -0
  89. package/dist/infrastructure/segmentation/mobile-sam/worker-expand.js +188 -0
  90. package/dist/infrastructure/segmentation/mobile-sam/worker-inference.d.ts +6 -149
  91. package/dist/infrastructure/segmentation/mobile-sam/worker-inference.js +6 -845
  92. package/dist/infrastructure/segmentation/mobile-sam/worker-models.d.ts +14 -0
  93. package/dist/infrastructure/segmentation/mobile-sam/worker-models.js +72 -0
  94. package/dist/infrastructure/segmentation/mobile-sam/worker-proposals.d.ts +7 -0
  95. package/dist/infrastructure/segmentation/mobile-sam/worker-proposals.js +159 -0
  96. package/dist/infrastructure/segmentation/mobile-sam/worker-segment.d.ts +4 -0
  97. package/dist/infrastructure/segmentation/mobile-sam/worker-segment.js +126 -0
  98. package/dist/infrastructure/segmentation/native-protocol.d.ts +59 -34
  99. package/dist/infrastructure/segmentation/native-protocol.js +26 -16
  100. package/dist/infrastructure/segmentation/proposal-presentation-input.d.ts +20 -0
  101. package/dist/infrastructure/segmentation/proposal-presentation-input.js +107 -0
  102. package/dist/infrastructure/segmentation/proposal-presentation-pixels.d.ts +8 -0
  103. package/dist/infrastructure/segmentation/proposal-presentation-pixels.js +93 -0
  104. package/dist/infrastructure/segmentation/proposal-presentation.js +10 -201
  105. package/dist/infrastructure/vision-chat/options.d.ts +62 -0
  106. package/dist/infrastructure/vision-chat/options.js +46 -0
  107. package/dist/infrastructure/vision-chat/request.d.ts +23 -0
  108. package/dist/infrastructure/vision-chat/request.js +109 -0
  109. package/dist/infrastructure/vision-chat/response.d.ts +3 -0
  110. package/dist/infrastructure/vision-chat/response.js +105 -0
  111. package/dist/infrastructure/vision-chat.d.ts +4 -61
  112. package/dist/infrastructure/vision-chat.js +8 -271
  113. package/dist/openai-compatible.js +1 -1
  114. package/dist/processes/extract-from-region.js +11 -216
  115. package/dist/processes/extract.d.ts +2 -8
  116. package/dist/processes/extract.js +12 -128
  117. package/dist/processes/proposal-location.js +5 -251
  118. package/docs/0.2.0-plan.md +3 -3
  119. package/docs/README.ru.md +42 -23
  120. package/docs/acceptance.md +5 -5
  121. package/docs/agents.ru.md +95 -0
  122. package/docs/api.md +4 -2
  123. package/docs/architecture.md +33 -5
  124. package/docs/architecture.ru.md +146 -0
  125. package/docs/benchmarks.md +4 -2
  126. package/docs/development.ru.md +141 -0
  127. package/docs/operations.md +4 -4
  128. package/docs/provenance.json +5 -1
  129. package/docs/publishing.md +8 -0
  130. package/docs/refactoring-plan.ru.md +183 -0
  131. package/docs/v02-usage.md +8 -6
  132. package/docs/vision-results.md +2 -0
  133. package/package.json +5 -3
  134. package/dist/infrastructure/boundary-view.d.ts +0 -2
  135. package/dist/infrastructure/boundary-view.js +0 -263
  136. package/dist/infrastructure/openrouter-common.js +0 -317
  137. package/dist/infrastructure/text-representation.d.ts +0 -23
  138. package/dist/infrastructure/text-representation.js +0 -226
  139. package/docs/evidence/v02/categorical-heldout-v4/README.md +0 -35
  140. package/docs/evidence/v02/proposal-resource-soak-261dc23.md +0 -15
@@ -0,0 +1,171 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { z } from 'zod';
3
+ import { pixelRectSchema } from '../../entities/contracts.js';
4
+ import { decisionMetadataSchema } from '../../entities/decision.js';
5
+ import { containsNonEmptyRect } from '../../entities/geometry.js';
6
+ import { ClassifierError } from '../../entities/errors.js';
7
+ const selectionSchema = z
8
+ .object({
9
+ choice: z.string().min(1),
10
+ nativeChoice: z.string().nullable(),
11
+ probabilities: z.record(z.number().finite()).nullable(),
12
+ ranking: z.array(z.string().min(1)).optional(),
13
+ confidence: z.number().finite().optional(),
14
+ probabilityRoundingStep: z.literal(0.01).optional(),
15
+ tied: z.array(z.string().min(1)),
16
+ })
17
+ .strict();
18
+ const normalizedRectSchema = z
19
+ .object({
20
+ left: z.number().finite().min(0).max(1),
21
+ top: z.number().finite().min(0).max(1),
22
+ width: z.number().finite().positive().max(1),
23
+ height: z.number().finite().positive().max(1),
24
+ })
25
+ .strict();
26
+ const localizationMetadataSchema = z
27
+ .object({
28
+ image: z
29
+ .object({
30
+ width: z.number().int().positive(),
31
+ height: z.number().int().positive(),
32
+ coordinateSpace: z.literal('oriented-source-pixels'),
33
+ })
34
+ .strict(),
35
+ stages: z.array(decisionMetadataSchema.strict()),
36
+ durationMs: z.number().finite().nonnegative(),
37
+ knownCostUsd: z.number().finite().nonnegative(),
38
+ unknownCostCount: z.number().int().nonnegative(),
39
+ })
40
+ .strict();
41
+ const refinementSchema = z.discriminatedUnion('status', [
42
+ z.object({ status: z.literal('refined'), region: pixelRectSchema, method: z.string() }).strict(),
43
+ z.object({ status: z.literal('unknown'), reason: z.string() }).strict(),
44
+ ]);
45
+ const locatedSeedSchema = z
46
+ .object({
47
+ status: z.literal('located'),
48
+ region: pixelRectSchema,
49
+ normalizedRegion: normalizedRectSchema,
50
+ selection: selectionSchema,
51
+ method: z.literal('three-choice-grid-v1'),
52
+ refinement: refinementSchema.optional(),
53
+ metadata: localizationMetadataSchema,
54
+ })
55
+ .strict();
56
+ export function unresolved(seedLocation, segmentation, verification, started, source) {
57
+ const metadata = withMetadataDuration(seedLocation.metadata, started, source, verification);
58
+ return {
59
+ status: 'unknown',
60
+ reason: 'segmentation-uncertain',
61
+ seedLocation: { ...seedLocation, metadata },
62
+ metadata,
63
+ segmentation,
64
+ verification,
65
+ };
66
+ }
67
+ export function snapshotSeedLocation(value, source, roi) {
68
+ const parsed = locatedSeedSchema.safeParse(value);
69
+ if (!parsed.success)
70
+ throw new ClassifierError('INVALID_RESPONSE', 'Region locator returned an invalid result', {
71
+ paths: parsed.error.issues.map((issue) => issue.path.join('.')),
72
+ });
73
+ const seed = parsed.data;
74
+ if (seed.metadata.image.width !== source.width ||
75
+ seed.metadata.image.height !== source.height ||
76
+ !containsNonEmptyRect({ left: 0, top: 0, width: source.width, height: source.height }, seed.region) ||
77
+ !containsNonEmptyRect(roi, seed.region))
78
+ throw new ClassifierError('INVALID_RESPONSE', 'Region locator returned invalid source geometry');
79
+ return {
80
+ status: 'located',
81
+ region: { ...seed.region },
82
+ normalizedRegion: { ...seed.normalizedRegion },
83
+ selection: {
84
+ choice: seed.selection.choice,
85
+ nativeChoice: seed.selection.nativeChoice,
86
+ probabilities: seed.selection.probabilities ? { ...seed.selection.probabilities } : null,
87
+ ...(seed.selection.ranking ? { ranking: [...seed.selection.ranking] } : {}),
88
+ ...(seed.selection.confidence !== undefined ? { confidence: seed.selection.confidence } : {}),
89
+ ...(seed.selection.probabilityRoundingStep !== undefined
90
+ ? { probabilityRoundingStep: seed.selection.probabilityRoundingStep }
91
+ : {}),
92
+ tied: [...seed.selection.tied],
93
+ },
94
+ ...(seed.refinement !== undefined ? { refinement: { ...seed.refinement } } : {}),
95
+ method: 'three-choice-grid-v1',
96
+ metadata: snapshotLocalizationMetadata(seed.metadata),
97
+ };
98
+ }
99
+ function snapshotLocalizationMetadata(value) {
100
+ const parsed = localizationMetadataSchema.safeParse(value);
101
+ if (!parsed.success)
102
+ throw new ClassifierError('INVALID_RESPONSE', 'Region locator returned invalid metadata', {
103
+ paths: parsed.error.issues.map((issue) => issue.path.join('.')),
104
+ });
105
+ return {
106
+ ...parsed.data,
107
+ image: { ...parsed.data.image },
108
+ stages: parsed.data.stages.map((stage) => ({ ...stage })),
109
+ };
110
+ }
111
+ export function withLocalizationDuration(value, started, source) {
112
+ const metadata = withMetadataDuration(value.metadata, started, source);
113
+ return { ...value, metadata };
114
+ }
115
+ export function withMetadataDuration(value, started, source, verification) {
116
+ const metadata = snapshotLocalizationMetadata(value);
117
+ if (source && (metadata.image.width !== source.width || metadata.image.height !== source.height))
118
+ throw new ClassifierError('INVALID_RESPONSE', 'Region locator metadata source dimensions mismatch');
119
+ metadata.durationMs = Math.max(0, performance.now() - started);
120
+ for (const check of verification?.checks ?? []) {
121
+ metadata.stages.push({ ...check.metadata });
122
+ if (check.metadata.costUsd === null)
123
+ metadata.unknownCostCount++;
124
+ else
125
+ metadata.knownCostUsd += check.metadata.costUsd;
126
+ }
127
+ return metadata;
128
+ }
129
+ export function locatedFromMask(seed, box, image, roi, metadata) {
130
+ const touchesRoi = box.left === roi.left ||
131
+ box.top === roi.top ||
132
+ box.left + box.width === roi.left + roi.width ||
133
+ box.top + box.height === roi.top + roi.height;
134
+ return {
135
+ status: 'located',
136
+ kind: 'whole-object',
137
+ seedRegion: { ...seed.region },
138
+ region: { ...box },
139
+ normalizedRegion: {
140
+ left: box.left / image.width,
141
+ top: box.top / image.height,
142
+ width: box.width / image.width,
143
+ height: box.height / image.height,
144
+ },
145
+ bounds: boundsForMask(box),
146
+ completeness: touchesRoi ? 'boundary-limited' : 'visible-object',
147
+ method: 'region-checked-segmentation-v1',
148
+ center: { x: box.left + box.width / 2, y: box.top + box.height / 2 },
149
+ metadata,
150
+ };
151
+ }
152
+ export function boundsFor(roi) {
153
+ const right = roi.left + roi.width;
154
+ const bottom = roi.top + roi.height;
155
+ return {
156
+ left: { start: roi.left, endExclusive: right },
157
+ right: { start: roi.left, endExclusive: right },
158
+ top: { start: roi.top, endExclusive: bottom },
159
+ bottom: { start: roi.top, endExclusive: bottom },
160
+ };
161
+ }
162
+ function boundsForMask(box) {
163
+ const right = box.left + box.width;
164
+ const bottom = box.top + box.height;
165
+ return {
166
+ left: { start: box.left, endExclusive: box.left + 1 },
167
+ right: { start: right - 1, endExclusive: right },
168
+ top: { start: box.top, endExclusive: box.top + 1 },
169
+ bottom: { start: bottom - 1, endExclusive: bottom },
170
+ };
171
+ }
@@ -0,0 +1,6 @@
1
+ import type { PixelRect } from '../../entities/contracts.js';
2
+ import type { RgbaRaster } from '../../entities/representation.js';
3
+ import type { BinaryMask } from '../../entities/segmentation.js';
4
+ export declare function validateSourceMask(mask: BinaryMask, image: RgbaRaster, roi: PixelRect, seed: PixelRect): PixelRect | undefined;
5
+ export declare function clearTransparentForeground(mask: BinaryMask, image: RgbaRaster): void;
6
+ export declare function isEnclosingMask(previous: BinaryMask, previousBox: PixelRect, expanded: BinaryMask, box: PixelRect): boolean;
@@ -0,0 +1,32 @@
1
+ import { containsNonEmptyRect } from '../../entities/geometry.js';
2
+ import { ClassifierError } from '../../entities/errors.js';
3
+ import { validateMask } from './mask.js';
4
+ export function validateSourceMask(mask, image, roi, seed) {
5
+ try {
6
+ return validateMask(mask, image, roi, seed);
7
+ }
8
+ catch {
9
+ throw new ClassifierError('INVALID_RESPONSE', 'Segmenter mask failed binary/source validation');
10
+ }
11
+ }
12
+ export function clearTransparentForeground(mask, image) {
13
+ for (let index = 0; index < mask.data.length; index++)
14
+ if (image.data[index * 4 + 3] === 0)
15
+ mask.data[index] = 0;
16
+ }
17
+ export function isEnclosingMask(previous, previousBox, expanded, box) {
18
+ if (!containsNonEmptyRect(box, previousBox) ||
19
+ (box.left === previousBox.left &&
20
+ box.top === previousBox.top &&
21
+ box.width === previousBox.width &&
22
+ box.height === previousBox.height))
23
+ return false;
24
+ let added = false;
25
+ for (let i = 0; i < previous.data.length; i++) {
26
+ if (previous.data[i] === 1 && expanded.data[i] !== 1)
27
+ return false;
28
+ if (previous.data[i] !== 1 && expanded.data[i] === 1)
29
+ added = true;
30
+ }
31
+ return added;
32
+ }
@@ -0,0 +1,9 @@
1
+ import type { RasterImage } from '../../entities/contracts.js';
2
+ import type { SegmentationResult } from '../../entities/segmentation.js';
3
+ import type { RgbaRaster } from '../../entities/representation.js';
4
+ /** Validates PNG magic bytes and returns an owned copy of the payload. */
5
+ export declare function validateAndSnapshotPng(rawPng: unknown): Uint8Array;
6
+ /** Validates and owns a segmenter's result before any subsequent asynchronous work. */
7
+ export declare function validateSegmentationResult(value: unknown, width: number, height: number): SegmentationResult;
8
+ export declare function assertRasterImage(image: RasterImage): void;
9
+ export declare function validateAndSnapshotRgba(value: unknown, width: number, height: number): RgbaRaster;
@@ -0,0 +1,94 @@
1
+ import { z } from 'zod';
2
+ import { parse } from '../../entities/contracts.js';
3
+ import { ClassifierError } from '../../entities/errors.js';
4
+ import { segmentationMetadataSchema } from '../../entities/segmentation-metadata.js';
5
+ const binaryMaskSchema = z
6
+ .object({
7
+ data: z.instanceof(Uint8Array),
8
+ width: z.number().int().positive(),
9
+ height: z.number().int().positive(),
10
+ format: z.literal('binary-u8'),
11
+ })
12
+ .strict();
13
+ const rgbaSchema = z
14
+ .object({
15
+ data: z.instanceof(Uint8Array),
16
+ width: z.number().int().positive().max(16384),
17
+ height: z.number().int().positive().max(16384),
18
+ format: z.literal('rgba8'),
19
+ })
20
+ .strict();
21
+ const segmentationResultSchema = z.discriminatedUnion('status', [
22
+ z
23
+ .object({
24
+ status: z.literal('segmented'),
25
+ mask: binaryMaskSchema,
26
+ metadata: segmentationMetadataSchema,
27
+ })
28
+ .strict(),
29
+ z
30
+ .object({
31
+ status: z.literal('unknown'),
32
+ reason: z.string().min(1).max(500),
33
+ metadata: segmentationMetadataSchema,
34
+ })
35
+ .strict(),
36
+ ]);
37
+ const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
38
+ /** Validates PNG magic bytes and returns an owned copy of the payload. */
39
+ export function validateAndSnapshotPng(rawPng) {
40
+ if (!(rawPng instanceof Uint8Array) ||
41
+ rawPng.byteLength < 8 ||
42
+ !PNG_SIGNATURE.every((byte, index) => rawPng[index] === byte))
43
+ throw new ClassifierError('INVALID_RESPONSE', 'Cutout raster port returned invalid PNG bytes');
44
+ return new Uint8Array(rawPng);
45
+ }
46
+ /** Validates and owns a segmenter's result before any subsequent asynchronous work. */
47
+ export function validateSegmentationResult(value, width, height) {
48
+ const validation = segmentationResultSchema.safeParse(value);
49
+ if (!validation.success)
50
+ throw new ClassifierError('INVALID_RESPONSE', 'Segmenter returned an invalid result', {
51
+ paths: validation.error.issues.map((issue) => issue.path.join('.')),
52
+ });
53
+ if (validation.data.metadata.input.width !== width ||
54
+ validation.data.metadata.input.height !== height)
55
+ throw new ClassifierError('INVALID_RESPONSE', 'Segmenter metadata dimensions do not match the source image');
56
+ return validation.data.status === 'segmented'
57
+ ? {
58
+ status: 'segmented',
59
+ mask: { ...validation.data.mask, data: new Uint8Array(validation.data.mask.data) },
60
+ metadata: snapshotMetadata(validation.data.metadata),
61
+ }
62
+ : {
63
+ ...validation.data,
64
+ metadata: snapshotMetadata(validation.data.metadata),
65
+ };
66
+ }
67
+ export function assertRasterImage(image) {
68
+ if (!Number.isSafeInteger(image.width) ||
69
+ !Number.isSafeInteger(image.height) ||
70
+ image.width < 1 ||
71
+ image.height < 1 ||
72
+ image.width * image.height > 16_777_216 ||
73
+ image.bytes.byteLength < 1)
74
+ throw new ClassifierError('INVALID_RESPONSE', 'Raster normalizer returned invalid source dimensions');
75
+ }
76
+ export function validateAndSnapshotRgba(value, width, height) {
77
+ const image = parse(rgbaSchema, value, 'INVALID_RESPONSE');
78
+ if (image.format !== 'rgba8' ||
79
+ image.width !== width ||
80
+ image.height !== height ||
81
+ !(image.data instanceof Uint8Array) ||
82
+ image.data.length !== width * height * 4)
83
+ throw new ClassifierError('INVALID_RESPONSE', 'Cutout raster port returned invalid RGBA dimensions');
84
+ return { data: new Uint8Array(image.data), width, height, format: 'rgba8' };
85
+ }
86
+ function snapshotMetadata(metadata) {
87
+ return {
88
+ method: metadata.method,
89
+ backend: metadata.backend,
90
+ durationMs: metadata.durationMs,
91
+ input: { ...metadata.input },
92
+ ...(metadata.qualityScore !== undefined ? { qualityScore: metadata.qualityScore } : {}),
93
+ };
94
+ }
@@ -0,0 +1,8 @@
1
+ import type { z } from 'zod';
2
+ import { ClassifierError } from '../../entities/errors.js';
3
+ import type { ErrorCode } from '../../entities/errors.js';
4
+ export declare function ownedError(code: ErrorCode, message: string, details?: Readonly<Record<string, unknown>>): ClassifierError;
5
+ export declare function invalid(code: 'INVALID_INPUT' | 'INVALID_RESPONSE', message: string): ClassifierError;
6
+ export declare function active(signal: AbortSignal): void;
7
+ export declare function validated<T>(schema: z.ZodType<T, z.ZodTypeDef, unknown>, input: unknown, code: 'INVALID_INPUT' | 'INVALID_RESPONSE'): T;
8
+ export declare function failure(error: unknown, signal: AbortSignal, cleanupFailed?: boolean): ClassifierError;
@@ -0,0 +1,34 @@
1
+ import { ClassifierError } from '../../entities/errors.js';
2
+ const localErrors = new WeakSet();
3
+ export function ownedError(code, message, details = {}) {
4
+ const error = new ClassifierError(code, message, details);
5
+ localErrors.add(error);
6
+ return error;
7
+ }
8
+ export function invalid(code, message) {
9
+ return ownedError(code, message);
10
+ }
11
+ export function active(signal) {
12
+ if (!signal.aborted)
13
+ return;
14
+ const timeout = signal.reason instanceof ClassifierError && signal.reason.code === 'TIMEOUT';
15
+ throw ownedError(timeout ? 'TIMEOUT' : 'ABORTED', timeout ? 'Operation timed out' : 'Operation cancelled');
16
+ }
17
+ export function validated(schema, input, code) {
18
+ const parsed = schema.safeParse(input);
19
+ if (!parsed.success)
20
+ throw invalid(code, 'Decision schema validation failed');
21
+ return parsed.data;
22
+ }
23
+ export function failure(error, signal, cleanupFailed = false) {
24
+ const known = error instanceof ClassifierError && localErrors.has(error);
25
+ const incompleteCleanup = cleanupFailed || (known && error.details.cleanupFailed === true);
26
+ try {
27
+ active(signal);
28
+ }
29
+ catch (aborted) {
30
+ if (aborted instanceof ClassifierError)
31
+ return ownedError(aborted.code, aborted.message, incompleteCleanup ? { cleanupFailed: true } : {});
32
+ }
33
+ return ownedError(known ? error.code : 'PROVIDER', known ? error.message : 'Model provider transport failed', { ...(known ? error.details : {}), ...(incompleteCleanup ? { cleanupFailed: true } : {}) });
34
+ }
@@ -0,0 +1 @@
1
+ export declare function validateImage(url: string): void;
@@ -0,0 +1,52 @@
1
+ import { invalid } from './errors.js';
2
+ function imageSize(bytes, format) {
3
+ if (format === 'png') {
4
+ if (bytes.length < 33 ||
5
+ !bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) ||
6
+ bytes.readUInt32BE(8) !== 13 ||
7
+ bytes.toString('ascii', 12, 16) !== 'IHDR')
8
+ throw invalid('INVALID_INPUT', 'Invalid PNG image header');
9
+ return [bytes.readUInt32BE(16), bytes.readUInt32BE(20)];
10
+ }
11
+ if (bytes[0] !== 255 || bytes[1] !== 216)
12
+ throw invalid('INVALID_INPUT', 'Invalid JPEG image header');
13
+ let offset = 2;
14
+ while (offset + 3 < bytes.length) {
15
+ if (bytes[offset++] !== 255)
16
+ break;
17
+ while (bytes[offset] === 255)
18
+ offset++;
19
+ const marker = bytes[offset++];
20
+ if (marker === undefined || marker === 218 || marker === 217)
21
+ break;
22
+ if (offset + 2 > bytes.length)
23
+ break;
24
+ const length = bytes.readUInt16BE(offset);
25
+ if (length < 2 || offset + length > bytes.length)
26
+ break;
27
+ if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) {
28
+ if (length < 8)
29
+ break;
30
+ return [bytes.readUInt16BE(offset + 5), bytes.readUInt16BE(offset + 3)];
31
+ }
32
+ offset += length;
33
+ }
34
+ throw invalid('INVALID_INPUT', 'Invalid JPEG image dimensions');
35
+ }
36
+ export function validateImage(url) {
37
+ const prefix = /^data:image\/(png|jpeg);base64,/u.exec(url);
38
+ if (!prefix)
39
+ throw invalid('INVALID_INPUT', 'Only inline PNG or JPEG images are supported');
40
+ const encoded = url.slice(prefix[0].length);
41
+ const padding = encoded.endsWith('==') ? 2 : Number(encoded.endsWith('='));
42
+ if (!encoded.length ||
43
+ encoded.length % 4 !== 0 ||
44
+ !/^[A-Za-z0-9+/]+$/u.test(encoded.slice(0, encoded.length - padding)))
45
+ throw invalid('INVALID_INPUT', 'Invalid image base64');
46
+ const bytes = Buffer.from(encoded, 'base64');
47
+ if (bytes.length > 16 * 1024 * 1024 || bytes.toString('base64') !== encoded)
48
+ throw invalid('INVALID_INPUT', 'Invalid image byte representation');
49
+ const [width, height] = imageSize(bytes, prefix[1] ?? '');
50
+ if (width < 280 || height < 280 || width > 1568 || height > 1568)
51
+ throw invalid('INVALID_INPUT', 'Prepared image sides must be 280..1568 pixels');
52
+ }
@@ -1,6 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { ClassifierError } from '../entities/errors.js';
3
- import type { ErrorCode } from '../entities/errors.js';
4
2
  export declare const requestLimit: number;
5
3
  export declare const responseLimit: number;
6
4
  export declare const probability: z.ZodNumber;
@@ -19,29 +17,29 @@ export declare const requestSchema: z.ZodObject<{
19
17
  detail: "high";
20
18
  }>;
21
19
  }, "strict", z.ZodTypeAny, {
22
- type: "image_url";
23
20
  image_url: {
24
21
  url: string;
25
22
  detail: "high";
26
23
  };
27
- }, {
28
24
  type: "image_url";
25
+ }, {
29
26
  image_url: {
30
27
  url: string;
31
28
  detail: "high";
32
29
  };
33
- }>]>, "many">, (string | {
34
30
  type: "image_url";
31
+ }>]>, "many">, (string | {
35
32
  image_url: {
36
33
  url: string;
37
34
  detail: "high";
38
35
  };
39
- })[], (string | {
40
36
  type: "image_url";
37
+ })[], (string | {
41
38
  image_url: {
42
39
  url: string;
43
40
  detail: "high";
44
41
  };
42
+ type: "image_url";
45
43
  })[]>;
46
44
  questions: z.ZodEffects<z.ZodRecord<z.ZodEffects<z.ZodString, string, string>, z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
47
45
  type: z.ZodLiteral<"choice">;
@@ -86,11 +84,11 @@ export declare const requestSchema: z.ZodObject<{
86
84
  }>>;
87
85
  }, "strict", z.ZodTypeAny, {
88
86
  state: (string | {
89
- type: "image_url";
90
87
  image_url: {
91
88
  url: string;
92
89
  detail: "high";
93
90
  };
91
+ type: "image_url";
94
92
  })[];
95
93
  questions: Record<string, {
96
94
  type: "choice";
@@ -103,11 +101,11 @@ export declare const requestSchema: z.ZodObject<{
103
101
  }>;
104
102
  }, {
105
103
  state: (string | {
106
- type: "image_url";
107
104
  image_url: {
108
105
  url: string;
109
106
  detail: "high";
110
107
  };
108
+ type: "image_url";
111
109
  })[];
112
110
  questions: Record<string, {
113
111
  type: "choice";
@@ -119,12 +117,5 @@ export declare const requestSchema: z.ZodObject<{
119
117
  instructions: string;
120
118
  }>;
121
119
  }>;
122
- export declare function ownedError(code: ErrorCode, message: string, details?: Readonly<Record<string, unknown>>): ClassifierError;
123
- export declare function invalid(code: 'INVALID_INPUT' | 'INVALID_RESPONSE', message: string): ClassifierError;
124
- export declare function active(signal: AbortSignal): void;
125
- export declare function validated<T>(schema: z.ZodType<T, z.ZodTypeDef, unknown>, input: unknown, code: 'INVALID_INPUT' | 'INVALID_RESPONSE'): T;
126
- export declare function validateImage(url: string): void;
127
120
  /** Bounded owned snapshot; schema parsing never receives caller iterators or accessors. */
128
121
  export declare function preflightJson(input: unknown, limit: number): unknown;
129
- export declare function failure(error: unknown, signal: AbortSignal, cleanupFailed?: boolean): ClassifierError;
130
- export declare function readResponse(response: Response, signal: AbortSignal, limit: number): Promise<string>;
@@ -0,0 +1,162 @@
1
+ import { isProxy } from 'node:util/types';
2
+ import { z } from 'zod';
3
+ import { invalid, ownedError } from './errors.js';
4
+ export const requestLimit = 32 * 1024 * 1024;
5
+ export const responseLimit = 1024 * 1024;
6
+ export const probability = z.number().finite().min(0).max(1);
7
+ export const key = z
8
+ .string()
9
+ .min(1)
10
+ .max(128)
11
+ .refine((value) => value !== '__proto__');
12
+ const text = z.string().min(1).max(64_000);
13
+ const imageSchema = z
14
+ .object({
15
+ type: z.literal('image_url'),
16
+ image_url: z.object({ url: z.string().max(22_369_656), detail: z.literal('high') }).strict(),
17
+ })
18
+ .strict();
19
+ const questionSchema = z.discriminatedUnion('type', [
20
+ z
21
+ .object({
22
+ type: z.literal('choice'),
23
+ rankingSize: z.union([z.literal(1), z.literal(4)]).default(4),
24
+ instructions: text,
25
+ criteria: z
26
+ .record(key, text)
27
+ .refine((value) => Object.keys(value).length >= 2 && Object.keys(value).length <= 255),
28
+ })
29
+ .strict(),
30
+ z.object({ type: z.literal('noul'), instructions: text }).strict(),
31
+ ]);
32
+ export const requestSchema = z
33
+ .object({
34
+ state: z
35
+ .array(z.union([text, imageSchema]))
36
+ .min(1)
37
+ .max(64)
38
+ .refine((value) => value.filter((part) => typeof part !== 'string').length <= 9),
39
+ questions: z
40
+ .record(key, questionSchema)
41
+ .refine((value) => Object.keys(value).length >= 1 && Object.keys(value).length <= 16),
42
+ })
43
+ .strict();
44
+ /** Bounded owned snapshot; schema parsing never receives caller iterators or accessors. */
45
+ export function preflightJson(input, limit) {
46
+ let remaining = limit;
47
+ let nodes = 0;
48
+ const ancestors = new Set();
49
+ function charge(bytes) {
50
+ remaining -= bytes;
51
+ if (remaining < 0)
52
+ throw ownedError('RESOURCE_LIMIT', 'Decision request exceeds byte limit');
53
+ }
54
+ function string(value) {
55
+ charge(2);
56
+ for (let index = 0; index < value.length; index++) {
57
+ const code = value.charCodeAt(index);
58
+ if (code < 32)
59
+ charge([8, 9, 10, 12, 13].includes(code) ? 2 : 6);
60
+ else if (code === 34 || code === 92)
61
+ charge(2);
62
+ else if (code < 128)
63
+ charge(1);
64
+ else if (code < 2048)
65
+ charge(2);
66
+ else if (code >= 0xd800 && code <= 0xdbff) {
67
+ const next = value.charCodeAt(index + 1);
68
+ if (next >= 0xdc00 && next <= 0xdfff) {
69
+ charge(4);
70
+ index++;
71
+ }
72
+ else
73
+ charge(6);
74
+ }
75
+ else
76
+ charge(code >= 0xdc00 && code <= 0xdfff ? 6 : 3);
77
+ }
78
+ }
79
+ function visit(value, depth) {
80
+ if (++nodes > 10_000 || depth > 6)
81
+ throw invalid('INVALID_INPUT', 'Decision structure exceeds supported limits');
82
+ if (typeof value === 'string') {
83
+ string(value);
84
+ return value;
85
+ }
86
+ if (value === null || typeof value === 'boolean') {
87
+ charge(value === false ? 5 : 4);
88
+ return value;
89
+ }
90
+ if (typeof value === 'number' && Number.isFinite(value)) {
91
+ charge(String(value).length);
92
+ return value;
93
+ }
94
+ if (typeof value !== 'object' || value === null)
95
+ throw invalid('INVALID_INPUT', 'Unsupported decision value');
96
+ if (isProxy(value))
97
+ throw invalid('INVALID_INPUT', 'Decision input must contain plain values');
98
+ // Zod reads declared object fields even when they are not enumerable; JSON does not.
99
+ for (const name of [
100
+ 'state',
101
+ 'questions',
102
+ 'type',
103
+ 'image_url',
104
+ 'url',
105
+ 'detail',
106
+ 'instructions',
107
+ 'criteria',
108
+ ]) {
109
+ const descriptor = Object.getOwnPropertyDescriptor(value, name);
110
+ if (descriptor && !descriptor.enumerable)
111
+ throw invalid('INVALID_INPUT', 'Decision fields must be enumerable');
112
+ }
113
+ if (ancestors.has(value))
114
+ throw invalid('INVALID_INPUT', 'Circular decision structure');
115
+ ancestors.add(value);
116
+ charge(2);
117
+ if (Array.isArray(value)) {
118
+ const copy = [];
119
+ if (value.length > 64)
120
+ throw invalid('INVALID_INPUT', 'Decision array exceeds supported limits');
121
+ for (let index = 0; index < value.length; index++) {
122
+ if (index)
123
+ charge(1);
124
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
125
+ if (!descriptor || !Object.hasOwn(descriptor, 'value'))
126
+ throw invalid('INVALID_INPUT', 'Decision input must contain plain values');
127
+ copy.push(visit(descriptor.value, depth + 1));
128
+ }
129
+ ancestors.delete(value);
130
+ return copy;
131
+ }
132
+ else {
133
+ const copy = {};
134
+ const prototype = Object.getPrototypeOf(value);
135
+ if (prototype !== Object.prototype && prototype !== null)
136
+ throw invalid('INVALID_INPUT', 'Decision input must contain plain objects');
137
+ let count = 0;
138
+ for (const name in value) {
139
+ if (!Object.hasOwn(value, name))
140
+ continue;
141
+ if (++count > 255)
142
+ throw invalid('INVALID_INPUT', 'Decision object exceeds supported limits');
143
+ if (count > 1)
144
+ charge(1);
145
+ string(name);
146
+ charge(1);
147
+ const descriptor = Object.getOwnPropertyDescriptor(value, name);
148
+ if (!descriptor || !Object.hasOwn(descriptor, 'value'))
149
+ throw invalid('INVALID_INPUT', 'Decision input must contain plain values');
150
+ Object.defineProperty(copy, name, {
151
+ value: visit(descriptor.value, depth + 1),
152
+ enumerable: true,
153
+ configurable: true,
154
+ writable: true,
155
+ });
156
+ }
157
+ ancestors.delete(value);
158
+ return copy;
159
+ }
160
+ }
161
+ return visit(input, 0);
162
+ }
@@ -0,0 +1 @@
1
+ export declare function readResponse(response: Response, signal: AbortSignal, limit: number): Promise<string>;