@callcium/sdk 0.6.0 → 0.8.0

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.
@@ -0,0 +1,97 @@
1
+ import { keccak_256 } from "@noble/hashes/sha3.js";
2
+
3
+ import { bytesToHex } from "./bytes";
4
+ import { CallciumError } from "./errors";
5
+
6
+ import type { Hex } from "./types";
7
+
8
+ ///////////////////////////////////////////////////////////////////////////
9
+ // SignatureParser
10
+ ///////////////////////////////////////////////////////////////////////////
11
+
12
+ /** Parsed components of an ABI function signature. */
13
+ export type ParsedSignature = {
14
+ /** The 4-byte function selector as a 0x-prefixed hex string. */
15
+ selector: Hex;
16
+ /** Comma-separated parameter types, empty string for no-argument functions. */
17
+ types: string;
18
+ };
19
+
20
+ /** Return true if the char code is an ASCII letter. */
21
+ function isAlpha(c: number): boolean {
22
+ return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
23
+ }
24
+
25
+ /** Return true if the char code is an ASCII alphanumeric character. */
26
+ function isAlphanum(c: number): boolean {
27
+ return isAlpha(c) || (c >= 0x30 && c <= 0x39);
28
+ }
29
+
30
+ /**
31
+ * Parse an ABI function signature into its selector and types.
32
+ *
33
+ * Strict mode: no whitespace, name must match `[A-Za-z_][A-Za-z0-9_]*`,
34
+ * structure must be `name(types)` with the final `)` as the last character.
35
+ *
36
+ * @param signature - A function signature string, e.g. `"transfer(address,uint256)"`.
37
+ * @returns The 4-byte selector and the comma-separated types string.
38
+ * @throws {CallciumError} with code `"MALFORMED_SIGNATURE"`, `"SIGNATURE_CONTAINS_WHITESPACE"`, or
39
+ * `"INVALID_FUNCTION_NAME"` if the signature is malformed.
40
+ */
41
+ function parse(signature: string): ParsedSignature {
42
+ if (signature.length < 3) {
43
+ throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: too short, got "${signature}"`);
44
+ }
45
+
46
+ // Scan for whitespace, non-ASCII, and the opening parenthesis in one pass.
47
+ let openParen = -1;
48
+ for (let i = 0; i < signature.length; i++) {
49
+ const c = signature.charCodeAt(i);
50
+ if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) {
51
+ throw new CallciumError("SIGNATURE_CONTAINS_WHITESPACE", "Signature must not contain whitespace");
52
+ }
53
+ if (c > 0x7e) {
54
+ throw new CallciumError("MALFORMED_SIGNATURE", "Signature must contain only ASCII characters");
55
+ }
56
+ if (openParen === -1 && c === 0x28) {
57
+ openParen = i;
58
+ }
59
+ }
60
+
61
+ if (openParen < 1) {
62
+ throw new CallciumError(
63
+ "MALFORMED_SIGNATURE",
64
+ `Invalid signature: must have a function name followed by parentheses, got "${signature}"`,
65
+ );
66
+ }
67
+
68
+ if (!signature.endsWith(")")) {
69
+ throw new CallciumError("MALFORMED_SIGNATURE", `Invalid signature: must end with ")", got "${signature}"`);
70
+ }
71
+
72
+ // Validate function name: [A-Za-z_][A-Za-z0-9_]*.
73
+ const firstChar = signature.charCodeAt(0);
74
+ if (!isAlpha(firstChar) && firstChar !== 0x5f) {
75
+ throw new CallciumError("INVALID_FUNCTION_NAME", "Function name must start with a letter or underscore");
76
+ }
77
+ for (let i = 1; i < openParen; i++) {
78
+ const c = signature.charCodeAt(i);
79
+ if (!isAlphanum(c) && c !== 0x5f) {
80
+ throw new CallciumError(
81
+ "INVALID_FUNCTION_NAME",
82
+ "Function name must contain only alphanumeric characters or underscores",
83
+ );
84
+ }
85
+ }
86
+
87
+ const encoded = new Uint8Array(signature.length);
88
+ for (let i = 0; i < signature.length; i++) encoded[i] = signature.charCodeAt(i);
89
+ const hash = keccak_256(encoded);
90
+
91
+ const selector = bytesToHex(hash.subarray(0, 4));
92
+ const types = signature.slice(openParen + 1, signature.length - 1);
93
+ return { selector, types };
94
+ }
95
+
96
+ /** ABI function signature parser. */
97
+ export const SignatureParser = { parse };
@@ -0,0 +1,205 @@
1
+ import { readU24, writeBE16, writeBE24 } from "./bytes";
2
+ import { DescriptorFormat as DF, TypeCode } from "./constants";
3
+ import { CallciumError } from "./errors";
4
+
5
+ ///////////////////////////////////////////////////////////////////////////
6
+ // Internal helpers
7
+ ///////////////////////////////////////////////////////////////////////////
8
+
9
+ /**
10
+ * Extract the staticWords count from a descriptor node.
11
+ *
12
+ * For elementary types (typeCode < STATIC_ARRAY), returns 1 unless the type is
13
+ * BYTES or STRING, which are ABI-dynamic and return 0. For composite types,
14
+ * reads the 24-bit meta field at offset 1 and shifts right by
15
+ * META_STATIC_WORDS_SHIFT.
16
+ */
17
+ function extractStaticWords(desc: Uint8Array): number {
18
+ const typeCode = desc[0]!;
19
+ if (typeCode < TypeCode.STATIC_ARRAY) {
20
+ return typeCode === TypeCode.BYTES || typeCode === TypeCode.STRING ? 0 : 1;
21
+ }
22
+ return readU24(desc, 1) >> DF.META_STATIC_WORDS_SHIFT;
23
+ }
24
+
25
+ ///////////////////////////////////////////////////////////////////////////
26
+ // Elementary types
27
+ ///////////////////////////////////////////////////////////////////////////
28
+
29
+ /** Build a descriptor node for `address`. */
30
+ export function address(): Uint8Array {
31
+ return new Uint8Array([TypeCode.ADDRESS]);
32
+ }
33
+
34
+ /** Build a descriptor node for `bool`. */
35
+ export function bool(): Uint8Array {
36
+ return new Uint8Array([TypeCode.BOOL]);
37
+ }
38
+
39
+ /** Build a descriptor node for `function`. */
40
+ export function function_(): Uint8Array {
41
+ return new Uint8Array([TypeCode.FUNCTION]);
42
+ }
43
+
44
+ /**
45
+ * Build a descriptor node for `uintN`.
46
+ * @param bits - Bit width, 8–256 in steps of 8.
47
+ * @throws {CallciumError} With code `UNKNOWN_TYPE` if bits is out of range or not a multiple of 8.
48
+ */
49
+ export function uintN(bits: number): Uint8Array {
50
+ if (bits < 8 || bits > 256 || bits % 8 !== 0) {
51
+ throw new CallciumError("UNKNOWN_TYPE", `Invalid uintN bits: ${bits}. Must be 8–256 in steps of 8.`);
52
+ }
53
+ return new Uint8Array([TypeCode.UINT_MIN + (bits / 8 - 1)]);
54
+ }
55
+
56
+ /**
57
+ * Build a descriptor node for `intN`.
58
+ * @param bits - Bit width, 8–256 in steps of 8.
59
+ * @throws {CallciumError} With code `UNKNOWN_TYPE` if bits is out of range or not a multiple of 8.
60
+ */
61
+ export function intN(bits: number): Uint8Array {
62
+ if (bits < 8 || bits > 256 || bits % 8 !== 0) {
63
+ throw new CallciumError("UNKNOWN_TYPE", `Invalid intN bits: ${bits}. Must be 8–256 in steps of 8.`);
64
+ }
65
+ return new Uint8Array([TypeCode.INT_MIN + (bits / 8 - 1)]);
66
+ }
67
+
68
+ /** Build a descriptor node for `bytes` (dynamic). */
69
+ export function bytes(): Uint8Array {
70
+ return new Uint8Array([TypeCode.BYTES]);
71
+ }
72
+
73
+ /** Build a descriptor node for `string`. */
74
+ export function string_(): Uint8Array {
75
+ return new Uint8Array([TypeCode.STRING]);
76
+ }
77
+
78
+ /**
79
+ * Build a descriptor node for `bytesN`.
80
+ * @param n - Byte width, 1–32.
81
+ * @throws {CallciumError} With code `UNKNOWN_TYPE` if n is out of range.
82
+ */
83
+ export function bytesN(n: number): Uint8Array {
84
+ if (n < 1 || n > 32) {
85
+ throw new CallciumError("UNKNOWN_TYPE", `Invalid bytesN size: ${n}. Must be 1–32.`);
86
+ }
87
+ return new Uint8Array([TypeCode.FIXED_BYTES_MIN + (n - 1)]);
88
+ }
89
+
90
+ ///////////////////////////////////////////////////////////////////////////
91
+ // Composite types
92
+ ///////////////////////////////////////////////////////////////////////////
93
+
94
+ /**
95
+ * Build a descriptor node for a dynamic array `T[]` or static array `T[length]`.
96
+ *
97
+ * Dynamic form: `[0x81][meta:3][elemDesc]`
98
+ * Static form: `[0x80][meta:3][elemDesc][length:be16]`
99
+ *
100
+ * @param elemDesc - Descriptor bytes for the element type.
101
+ * @param length - Fixed array length. When omitted, produces a dynamic array.
102
+ * @throws {CallciumError} If length is 0 or exceeds MAX_STATIC_ARRAY_LENGTH.
103
+ */
104
+ export function array(elemDesc: Uint8Array, length?: number): Uint8Array {
105
+ if (length === undefined) {
106
+ // Dynamic array T[].
107
+ const nodeLength = DF.ARRAY_HEADER_SIZE + elemDesc.length;
108
+ if (nodeLength > DF.MAX_NODE_LENGTH) {
109
+ throw new CallciumError(
110
+ "NODE_LENGTH_TOO_LARGE",
111
+ `Dynamic array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}.`,
112
+ );
113
+ }
114
+ const meta24 = (0 << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
115
+ const buf = new Uint8Array(1 + DF.COMPOSITE_META_SIZE + elemDesc.length);
116
+ buf[0] = TypeCode.DYNAMIC_ARRAY;
117
+ writeBE24(buf, 1, meta24);
118
+ buf.set(elemDesc, 4);
119
+ return buf;
120
+ }
121
+
122
+ // Static array T[length].
123
+ if (length <= 0 || length > DF.MAX_STATIC_ARRAY_LENGTH) {
124
+ throw new CallciumError(
125
+ "INVALID_ARRAY_LENGTH",
126
+ `Static array length ${length} out of range. Must be 1–${DF.MAX_STATIC_ARRAY_LENGTH}.`,
127
+ );
128
+ }
129
+ const nodeLength = DF.ARRAY_HEADER_SIZE + elemDesc.length + DF.ARRAY_LENGTH_SIZE;
130
+ if (nodeLength > DF.MAX_NODE_LENGTH) {
131
+ throw new CallciumError(
132
+ "NODE_LENGTH_TOO_LARGE",
133
+ `Static array node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}.`,
134
+ );
135
+ }
136
+ const elemStaticWords = extractStaticWords(elemDesc);
137
+ const staticWords = elemStaticWords === 0 ? 0 : length * elemStaticWords;
138
+ if (staticWords > DF.MAX_STATIC_WORDS) {
139
+ throw new CallciumError(
140
+ "STATIC_WORDS_TOO_LARGE",
141
+ `Static array static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}.`,
142
+ );
143
+ }
144
+ const meta24 = (staticWords << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
145
+ const buf = new Uint8Array(1 + DF.COMPOSITE_META_SIZE + elemDesc.length + DF.ARRAY_LENGTH_SIZE);
146
+ buf[0] = TypeCode.STATIC_ARRAY;
147
+ writeBE24(buf, 1, meta24);
148
+ buf.set(elemDesc, 4);
149
+ writeBE16(buf, 4 + elemDesc.length, length);
150
+ return buf;
151
+ }
152
+
153
+ /**
154
+ * Build a descriptor node for a tuple.
155
+ *
156
+ * Format: `[0x90][meta:3][fieldCount:be16][field0][field1]...`
157
+ *
158
+ * @param fieldDescs - Descriptor bytes for each field, in order.
159
+ * @throws {CallciumError} If fieldDescs is empty, exceeds MAX_TUPLE_FIELDS, or nodeLength overflows MAX_NODE_LENGTH.
160
+ */
161
+ export function tuple(fieldDescs: Uint8Array[]): Uint8Array {
162
+ if (fieldDescs.length === 0 || fieldDescs.length > DF.MAX_TUPLE_FIELDS) {
163
+ throw new CallciumError(
164
+ "INVALID_TUPLE_FIELD_COUNT",
165
+ `Tuple field count ${fieldDescs.length} out of range. Must be 1–${DF.MAX_TUPLE_FIELDS}.`,
166
+ );
167
+ }
168
+ const totalFieldBytes = fieldDescs.reduce((sum, f) => sum + f.length, 0);
169
+ const nodeLength = DF.TUPLE_HEADER_SIZE + totalFieldBytes;
170
+ if (nodeLength > DF.MAX_NODE_LENGTH) {
171
+ throw new CallciumError(
172
+ "NODE_LENGTH_TOO_LARGE",
173
+ `Tuple node length ${nodeLength} exceeds maximum ${DF.MAX_NODE_LENGTH}.`,
174
+ );
175
+ }
176
+
177
+ let anyDynamic = false;
178
+ let staticWordsSum = 0;
179
+ for (const field of fieldDescs) {
180
+ const staticWords = extractStaticWords(field);
181
+ if (staticWords === 0) {
182
+ anyDynamic = true;
183
+ }
184
+ staticWordsSum += staticWords;
185
+ }
186
+ const staticWords = anyDynamic ? 0 : staticWordsSum;
187
+ if (staticWords > DF.MAX_STATIC_WORDS) {
188
+ throw new CallciumError(
189
+ "STATIC_WORDS_TOO_LARGE",
190
+ `Tuple static words ${staticWords} exceeds maximum ${DF.MAX_STATIC_WORDS}.`,
191
+ );
192
+ }
193
+ const meta24 = (staticWords << DF.META_STATIC_WORDS_SHIFT) | nodeLength;
194
+
195
+ const buf = new Uint8Array(1 + DF.COMPOSITE_META_SIZE + DF.TUPLE_FIELDCOUNT_SIZE + totalFieldBytes);
196
+ buf[0] = TypeCode.TUPLE;
197
+ writeBE24(buf, 1, meta24);
198
+ writeBE16(buf, 4, fieldDescs.length);
199
+ let offset = 6;
200
+ for (const field of fieldDescs) {
201
+ buf.set(field, offset);
202
+ offset += field.length;
203
+ }
204
+ return buf;
205
+ }
package/src/types.ts ADDED
@@ -0,0 +1,280 @@
1
+ ///////////////////////////////////////////////////////////////////////////
2
+ // Primitives
3
+ ///////////////////////////////////////////////////////////////////////////
4
+
5
+ /** Hex-encoded byte string, 0x-prefixed. */
6
+ export type Hex = `0x${string}`;
7
+
8
+ /** Ethereum address, 0x-prefixed. */
9
+ export type Address = `0x${string}`;
10
+
11
+ /** Byte range in a source blob. */
12
+ export type Span = { start: number; end: number };
13
+
14
+ ///////////////////////////////////////////////////////////////////////////
15
+ // Enforce
16
+ ///////////////////////////////////////////////////////////////////////////
17
+
18
+ /**
19
+ * Execution context for context-scoped rules.
20
+ * Each property maps to a well-known EVM execution environment value.
21
+ * Only the properties referenced by the policy need to be supplied.
22
+ */
23
+ export type Context = {
24
+ msgSender?: Address;
25
+ msgValue?: bigint;
26
+ blockTimestamp?: bigint;
27
+ blockNumber?: bigint;
28
+ chainId?: bigint;
29
+ txOrigin?: Address;
30
+ baseFee?: bigint;
31
+ gasPrice?: bigint;
32
+ };
33
+
34
+ /** Result of enforcing a policy: pass with matched group index, or fail with one violation per failed group. */
35
+ export type EnforceResult = { ok: true; matchedGroup: number } | { ok: false; violations: Violation[] };
36
+
37
+ /** Subset of violation codes emitted by reader/navigation primitives. */
38
+ export type NavigationViolationCode = "CALLDATA_OUT_OF_BOUNDS" | "ARRAY_INDEX_OUT_OF_BOUNDS";
39
+
40
+ /** Calldata is shorter than the policy's selector slot. */
41
+ export type MissingSelectorViolation = {
42
+ code: "MISSING_SELECTOR";
43
+ };
44
+
45
+ /** Calldata selector does not match the policy's expected selector. */
46
+ export type SelectorMismatchViolation = {
47
+ code: "SELECTOR_MISMATCH";
48
+ /** Selector declared by the policy. */
49
+ expectedValue: Hex;
50
+ /** Selector observed in the calldata. */
51
+ resolvedValue: Hex;
52
+ };
53
+
54
+ /**
55
+ * A context-scoped rule referenced a property not supplied in the execution context.
56
+
57
+ */
58
+ export type MissingContextViolation = {
59
+ code: "MISSING_CONTEXT";
60
+ group: number;
61
+ rule: number;
62
+ scope: number;
63
+ path: Hex;
64
+ /** Declared type of the missing context property. */
65
+ typeCode: number;
66
+ opCode?: number;
67
+ operandData?: Hex;
68
+ };
69
+
70
+ /**
71
+ * A rule's operator returned false against the loaded value.
72
+ *
73
+ * Field combinations:
74
+ * - `resolvedValue` present, `elementIndex` absent — scalar leaf or context value that failed the operator.
75
+ * - `resolvedValue` present, `elementIndex` present — universal-quantifier per-element failure.
76
+ * - `resolvedValue` absent, `elementIndex` absent — existential-aggregate failure (no element satisfied).
77
+ * - `resolvedValue` absent, `elementIndex` present — per-element failure where the leaf could not be loaded.
78
+ *
79
+ * For length operations (`isLengthOp(opCode)`), `resolvedValue` is a hex-encoded count
80
+ * rather than a 32-byte ABI word.
81
+ */
82
+ export type ValueMismatchViolation = {
83
+ code: "VALUE_MISMATCH";
84
+ group: number;
85
+ rule: number;
86
+ scope: number;
87
+ path: Hex;
88
+ /** Operator code with the `Op.NOT` bit intact. */
89
+ opCode: number;
90
+ /** Full untruncated operand bytes declared by the rule. */
91
+ operandData: Hex;
92
+ /** Type code of the failing value's leaf. */
93
+ typeCode: number;
94
+ resolvedValue?: Hex;
95
+ elementIndex?: number;
96
+ };
97
+
98
+ /**
99
+ * Single-code shape for a calldata-navigation failure. Internal helper for de-duplication.
100
+ * @internal
101
+ */
102
+ type CalldataNavigationVariant<C extends NavigationViolationCode> = {
103
+ code: C;
104
+ group: number;
105
+ rule: number;
106
+ scope: number;
107
+ path: Hex;
108
+ opCode?: number;
109
+ operandData?: Hex;
110
+ typeCode?: number;
111
+ elementIndex?: number;
112
+ };
113
+
114
+ /**
115
+ * Calldata structure prevented the rule from being evaluated.
116
+ *
117
+ * The operator was never applied; `opCode`, `operandData`, `typeCode`, and `elementIndex`
118
+ * are diagnostic context describing the failing site, not a constraint claim.
119
+ *
120
+ * A union of per-code variants, narrowable with `Extract<Violation, { code: "..." }>`.
121
+ */
122
+ export type CalldataNavigationViolation =
123
+ | CalldataNavigationVariant<"CALLDATA_OUT_OF_BOUNDS">
124
+ | CalldataNavigationVariant<"ARRAY_INDEX_OUT_OF_BOUNDS">;
125
+
126
+ /** A quantified array exceeded `Limits.MAX_QUANTIFIED_ARRAY_LENGTH`. */
127
+ export type QuantifierLimitExceededViolation = {
128
+ code: "QUANTIFIER_LIMIT_EXCEEDED";
129
+ group: number;
130
+ rule: number;
131
+ scope: number;
132
+ path: Hex;
133
+ /** Hex-encoded element count of the offending array. */
134
+ resolvedValue: Hex;
135
+ };
136
+
137
+ /**
138
+ * A resolved word is not the canonical encoding of its declared type (spec §7.4).
139
+ *
140
+ * `resolvedValue` is the raw 32-byte word as it appeared in calldata, before any masking.
141
+ */
142
+ export type NonCanonicalValueViolation = {
143
+ code: "NON_CANONICAL_VALUE";
144
+ group: number;
145
+ rule: number;
146
+ scope: number;
147
+ path: Hex;
148
+ /** Type code of the target whose encoding was violated. */
149
+ typeCode: number;
150
+ /** Raw 32-byte word as it appeared in calldata, before any masking. */
151
+ resolvedValue: Hex;
152
+ opCode?: number;
153
+ operandData?: Hex;
154
+ elementIndex?: number;
155
+ };
156
+
157
+ /** An `ANY` quantifier was applied to an empty array. */
158
+ export type QuantifierEmptyArrayViolation = {
159
+ code: "QUANTIFIER_EMPTY_ARRAY";
160
+ group: number;
161
+ rule: number;
162
+ scope: number;
163
+ path: Hex;
164
+ };
165
+
166
+ /**
167
+ * Structured details of a single rule failure during enforcement.
168
+ *
169
+ * Carries semantic data only; no message strings.
170
+ * Discriminate on `code` to narrow to the matching variant.
171
+ */
172
+ export type Violation =
173
+ | MissingSelectorViolation
174
+ | SelectorMismatchViolation
175
+ | MissingContextViolation
176
+ | ValueMismatchViolation
177
+ | CalldataNavigationViolation
178
+ | NonCanonicalValueViolation
179
+ | QuantifierLimitExceededViolation
180
+ | QuantifierEmptyArrayViolation;
181
+
182
+ /** Machine-readable reason code for an enforcement violation. Derived from `Violation` to prevent drift. */
183
+ export type ViolationCode = Violation["code"];
184
+
185
+ ///////////////////////////////////////////////////////////////////////////
186
+ // Canonical policy types
187
+ ///////////////////////////////////////////////////////////////////////////
188
+
189
+ /** Canonical structured representation of a policy. */
190
+ export type PolicyData = {
191
+ isSelectorless: boolean;
192
+ selector: Hex;
193
+ descriptor: Hex;
194
+ groups: Constraint[][];
195
+ span?: Span;
196
+ };
197
+
198
+ /** A collection of operators targeting a specific value. */
199
+ export type Constraint = {
200
+ scope: number;
201
+ path: Hex;
202
+ operators: Hex[];
203
+ /** Compiled hint block as carried on the wire; absent when the constraint has no encoding. */
204
+ hint?: Hex;
205
+ span?: Span;
206
+ };
207
+
208
+ ///////////////////////////////////////////////////////////////////////////
209
+ // Structural inspection types
210
+ ///////////////////////////////////////////////////////////////////////////
211
+
212
+ /** A decoded value with its byte position in the source blob. */
213
+ export type Field<T> = { value: T; span: Span };
214
+
215
+ /** Structural representation of a decoded parameter within the descriptor. */
216
+ export type DecodedParam = {
217
+ index: number;
218
+ typeCode: number;
219
+ isDynamic: boolean;
220
+ staticSize: number;
221
+ path: Hex;
222
+ span: Span;
223
+ };
224
+
225
+ /** Structural representation of a decoded rule with per-field spans. */
226
+ export type DecodedRule = {
227
+ ruleSize: Field<number>;
228
+ scope: Field<number>;
229
+ pathDepth: Field<number>;
230
+ path: Field<Hex>;
231
+ /** Compiled hint block; present for calldata rules only. */
232
+ hint?: Field<Hex>;
233
+ opCode: Field<number>;
234
+ dataLength: Field<number>;
235
+ data: Field<Hex>;
236
+ span: Span;
237
+ };
238
+
239
+ /** Structural representation of a decoded group with metadata spans. */
240
+ export type DecodedGroup = {
241
+ ruleCount: Field<number>;
242
+ groupSize: Field<number>;
243
+ rules: DecodedRule[];
244
+ span: Span;
245
+ };
246
+
247
+ /** Structural representation of a decoded policy with full byte-level spans. */
248
+ export type DecodedPolicy = {
249
+ header: Field<number>;
250
+ selector: Field<Hex>;
251
+ descLength: Field<number>;
252
+ descriptor: { raw: Hex; header: Field<{ version: number; paramCount: number }>; params: DecodedParam[]; span: Span };
253
+ groupCount: Field<number>;
254
+ groups: DecodedGroup[];
255
+ span: Span;
256
+ version: number;
257
+ isSelectorless: boolean;
258
+ };
259
+
260
+ ///////////////////////////////////////////////////////////////////////////
261
+ // Validation issues
262
+ ///////////////////////////////////////////////////////////////////////////
263
+
264
+ /** Severity of a validation issue. */
265
+ export type IssueSeverity = "info" | "warning" | "error";
266
+
267
+ /** Category of a validation issue. */
268
+ export type IssueCategory = "typeMismatch" | "contradiction" | "redundancy" | "vacuity" | "compatibility";
269
+
270
+ /** A single validation issue found during policy analysis. */
271
+ export type Issue = {
272
+ severity: IssueSeverity;
273
+ category: IssueCategory;
274
+ groupIndex: number;
275
+ constraintIndex: number;
276
+ code: string;
277
+ value1: Hex;
278
+ value2: Hex;
279
+ message: string;
280
+ };