@wowok/agent-mcp 2.3.1 → 2.3.3

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.
@@ -2,28 +2,81 @@ import { z } from 'zod';
2
2
  import { CallEnvSchema, NamedObjectSchema, SubmissionCallSchema } from './base.js';
3
3
  import { GuardNodeSchema } from '../query/index.js';
4
4
  import { DescriptionSchema, GuardTableItemBaseSchema, NameOrAddressSchema } from '../common/index.js';
5
- export const CallGuard_RootSchema = z.discriminatedUnion("type", [
6
- z.object({
7
- type: z.literal("node"),
8
- node: GuardNodeSchema.describe("Guard computational tree root node. MUST return a Bool value (pass/fail)."),
9
- }).strict(),
5
+ import { isBoolReturningQuery } from '../utils/guard-query-utils.js';
6
+ import { ValueType } from '@wowok/wowok';
7
+ export const CallGuard_RootSchema = z.union([
8
+ GuardNodeSchema.refine((node) => {
9
+ const allowedTypes = [
10
+ 'logic_as_u256_greater', 'logic_as_u256_lesser', 'logic_as_u256_equal',
11
+ 'logic_as_u256_greater_or_equal', 'logic_as_u256_lesser_or_equal',
12
+ 'logic_equal', 'logic_not', 'logic_and', 'logic_or',
13
+ 'logic_string_contains', 'logic_string_nocase_contains', 'logic_string_nocase_equal',
14
+ 'calc_string_contains', 'calc_string_nocase_contains', 'calc_string_nocase_equal',
15
+ 'vec_contains_bool', 'vec_contains_address', 'vec_contains_string',
16
+ 'vec_contains_string_nocase', 'vec_contains_number',
17
+ 'query_reward_record_exists',
18
+ 'query',
19
+ 'identifier'
20
+ ];
21
+ if (!allowedTypes.includes(node.type)) {
22
+ return false;
23
+ }
24
+ if (node.type === 'query') {
25
+ const queryNode = node;
26
+ if (!isBoolReturningQuery(queryNode.query)) {
27
+ return false;
28
+ }
29
+ }
30
+ return true;
31
+ }, {
32
+ message: "Guard root node must be an operation that returns Bool. Allowed types: logic_*, calc_string_contains, calc_string_nocase_*, vec_contains_*, reward_record_exist, identifier (must reference Bool type in table). For 'query' type, ONLY queries that return Bool are allowed. Use 'wowok_buildin_info' tool with {\"info\": \"guard instructions\", \"filter\": {\"returnType\": \"Bool\", \"scope\": \"object query\"}} to list all Bool-returning queries. Other nodes (calc_number_*, context, non-Bool queries) must be wrapped with a logic operation."
33
+ }).describe("Guard computational tree root node. MUST be an operation that returns a Bool value (pass/fail). For 'query' type, the specific query must return Bool. For 'identifier' type, the referenced table item must have value_type=Bool."),
10
34
  z.object({
11
35
  type: z.literal("file"),
12
36
  file_path: z.string().describe("Path to JSON or Markdown file containing Guard definition. File can define: namedNew, description, table, root, rely."),
13
37
  format: z.enum(["json", "markdown"]).optional().default("json")
14
38
  .describe("File format: 'json' or 'markdown'. Default is 'json'."),
15
39
  }).strict(),
16
- ]).describe("Guard root definition. Either provide a direct node tree or reference a file to load. When using file type, fields defined in the schema (namedNew, description, table, rely) will OVERRIDE the corresponding fields in the file.");
40
+ ]).describe("Guard root definition. Either provide a direct GuardNode tree (must be a logical operation returning Bool) or reference a file to load. When using file type, fields defined in the schema (namedNew, description, table, rely) will OVERRIDE the corresponding fields in the file.");
41
+ const isBoolValueType = (valueType) => {
42
+ if (typeof valueType === 'number') {
43
+ return valueType === ValueType.Bool;
44
+ }
45
+ if (typeof valueType === 'string') {
46
+ return valueType.toLowerCase() === 'bool' || valueType === '0';
47
+ }
48
+ return false;
49
+ };
17
50
  export const CallGuard_DataSchema = z.object({
18
51
  namedNew: NamedObjectSchema.optional().describe("Name and optional tags for the new Guard object. Set 'onChain: true' to create a public on-chain identity. When using root.type='file', this field OVERRIDES namedNew in the file."),
19
52
  description: DescriptionSchema.optional().describe("Guard description. When using root.type='file', this field OVERRIDES description in the file."),
20
53
  table: z.array(GuardTableItemBaseSchema).optional().describe("Data table of the Guard object. When using root.type='file', this field OVERRIDES table in the file."),
21
- root: CallGuard_RootSchema.describe("Root definition: either a direct node tree (type='node') or a file reference (type='file'). When type='file', the file can define all Guard fields (namedNew, description, table, root, rely), and schema fields override file content."),
54
+ root: CallGuard_RootSchema.describe("Root definition: either a direct GuardNode (must be a logical operation returning Bool) or a file reference (type='file'). When type='file', the file can define all Guard fields (namedNew, description, table, root, rely), and schema fields override file content."),
22
55
  rely: z.object({
23
56
  guards: z.array(NameOrAddressSchema).describe("List of dependent Guard object IDs or names."),
24
57
  logic_or: z.boolean().optional().describe("Whether to use logical OR operator."),
25
58
  }).optional().describe("All Guard objects that the new Guard object depends on. If logic_or is true, the execution result of the new Guard object is the logical OR of the execution results of all dependent Guard objects; otherwise, it is logical AND. When using root.type='file', this field OVERRIDES rely in the file."),
26
- }).strict().describe("On-chain Guard creation. IMPORTANT: All defined data (include all submitted data) must be defined in the 'table' field. USAGE: Set 'namedNew' field with {name, tags?, onChain?} to name the new Guard. The Guard is immutable once created. Define the validation logic in 'root' field. When root.type='file', the file can contain all Guard fields, and any fields defined in the schema will OVERRIDE the file content.");
59
+ }).strict().refine((data) => {
60
+ if (typeof data.root === 'object' && data.root !== null && 'type' in data.root) {
61
+ const rootObj = data.root;
62
+ if (rootObj.type === 'file') {
63
+ return true;
64
+ }
65
+ }
66
+ const rootNode = data.root;
67
+ if (rootNode.type === 'identifier' && rootNode.identifier !== undefined) {
68
+ const tableItem = data.table?.find(item => item.identifier === rootNode.identifier);
69
+ if (!tableItem) {
70
+ return false;
71
+ }
72
+ if (!isBoolValueType(tableItem.value_type)) {
73
+ return false;
74
+ }
75
+ }
76
+ return true;
77
+ }, {
78
+ message: "Invalid Guard root: When using 'identifier' as root, the referenced table item must exist and have value_type='Bool' (or 0)."
79
+ }).describe("On-chain Guard creation. IMPORTANT: All defined data (include all submitted data) must be defined in the 'table' field. USAGE: Set 'namedNew' field with {name, tags?, onChain?} to name the new Guard. The Guard is immutable once created. Define the validation logic in 'root' field. When root.type='file', the file can contain all Guard fields, and any fields defined in the schema will OVERRIDE the file content.");
27
80
  export const CallGuard_InputSchema = z.object({
28
81
  data: CallGuard_DataSchema.describe("Guard data definition."),
29
82
  env: CallEnvSchema.optional(),
@@ -625,58 +625,58 @@ export const GuardNodeSchema = z.lazy(() => z.discriminatedUnion('type', [
625
625
  identifier: GuardIdentifierSchema,
626
626
  }).strict().describe("Reference to the Reward object in the Guard table (must be Address type)."),
627
627
  find: z.enum(['first', 'last']).describe("'first' finds the earliest matching record, 'last' finds the most recent matching record."),
628
- recipient: GuardNodeSchema.describe("GuardNode that returns the recipient address to search for (must be Address type). Typically an 'identifier' node referencing the recipient's address from the Guard table."),
628
+ recipient: z.object({
629
+ identifier: GuardIdentifierSchema,
630
+ }).strict().optional().describe("Optional: Identifier reference to the recipient address in the Guard table (must be Address type). If not specified, uses the transaction signer address."),
629
631
  where: z.object({
630
632
  guard: z.string().optional().describe("Optional filter: Guard object address constant to match."),
631
633
  timeMin: z.string().optional().describe("Optional filter: minimum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
632
634
  timeMax: z.string().optional().describe("Optional filter: maximum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
633
635
  amountMin: z.string().optional().describe("Optional filter: minimum reward amount (inclusive). Use string to represent U64 value."),
634
636
  amountMax: z.string().optional().describe("Optional filter: maximum reward amount (inclusive). Use string to represent U64 value."),
635
- storeFromId: z.union([
636
- z.literal('exists'),
637
- z.literal('not_exists'),
638
- z.object({ eq: z.string().describe("Address string to match") })
639
- ]).optional().describe("Optional filter for store_from_id field: 'exists' means any value exists, 'not_exists' means no value, {eq: string} matches specific address."),
637
+ storeFromId: z.object({
638
+ identifier: GuardIdentifierSchema,
639
+ }).strict().optional().describe("Optional filter: identifier reference to the store_from_id address in the Guard table (must be Address type). If not specified, the store_from_id field is ignored during matching. The server resolves this identifier to the actual address from the Guard table constants."),
640
640
  }).strict().describe("Filter conditions for the reward record query."),
641
- }).strict().describe("Returns U64. Finds the index of the first or last reward record matching the specified recipient and filter conditions. Returns the record index (U64) if found. Throws error if no matching record is found (Guard validation fails)."),
641
+ }).strict().describe("Returns U64. Finds the index of the first or last reward record matching the specified recipient and filter conditions. Returns the record index (U64) if found. Throws error if no matching record is found (Guard validation fails). If recipient is not specified, uses the transaction signer address."),
642
642
  z.object({
643
643
  type: z.literal('query_reward_record_count'),
644
644
  object: z.object({
645
645
  identifier: GuardIdentifierSchema,
646
646
  }).strict().describe("Reference to the Reward object in the Guard table (must be Address type)."),
647
- recipient: GuardNodeSchema.describe("GuardNode that returns the recipient address to count records for (must be Address type). Typically an 'identifier' node referencing the recipient's address from the Guard table."),
647
+ recipient: z.object({
648
+ identifier: GuardIdentifierSchema,
649
+ }).strict().optional().describe("Optional: Identifier reference to the recipient address in the Guard table (must be Address type). If not specified, uses the transaction signer address."),
648
650
  where: z.object({
649
651
  guard: z.string().optional().describe("Optional filter: Guard object address constant to match."),
650
652
  timeMin: z.string().optional().describe("Optional filter: minimum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
651
653
  timeMax: z.string().optional().describe("Optional filter: maximum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
652
654
  amountMin: z.string().optional().describe("Optional filter: minimum reward amount (inclusive). Use string to represent U64 value."),
653
655
  amountMax: z.string().optional().describe("Optional filter: maximum reward amount (inclusive). Use string to represent U64 value."),
654
- storeFromId: z.union([
655
- z.literal('exists'),
656
- z.literal('not_exists'),
657
- z.object({ eq: z.string().describe("Address string to match") })
658
- ]).optional().describe("Optional filter for store_from_id field: 'exists' means any value exists, 'not_exists' means no value, {eq: string} matches specific address."),
656
+ storeFromId: z.object({
657
+ identifier: GuardIdentifierSchema,
658
+ }).strict().optional().describe("Optional filter: identifier reference to the store_from_id address in the Guard table (must be Address type). If not specified, the store_from_id field is ignored during matching. The server resolves this identifier to the actual address from the Guard table constants."),
659
659
  }).strict().describe("Filter conditions for the reward record count query."),
660
- }).strict().describe("Returns U64. Counts the number of reward records matching the specified recipient and filter conditions."),
660
+ }).strict().describe("Returns U64. Counts the number of reward records matching the specified recipient and filter conditions. If recipient is not specified, uses the transaction signer address."),
661
661
  z.object({
662
662
  type: z.literal('query_reward_record_exists'),
663
663
  object: z.object({
664
664
  identifier: GuardIdentifierSchema,
665
665
  }).strict().describe("Reference to the Reward object in the Guard table (must be Address type)."),
666
- recipient: GuardNodeSchema.describe("GuardNode that returns the recipient address to check records for (must be Address type). Typically an 'identifier' node referencing the recipient's address from the Guard table."),
666
+ recipient: z.object({
667
+ identifier: GuardIdentifierSchema,
668
+ }).strict().optional().describe("Optional: Identifier reference to the recipient address in the Guard table (must be Address type). If not specified, uses the transaction signer address."),
667
669
  where: z.object({
668
670
  guard: z.string().optional().describe("Optional filter: Guard object address constant to match."),
669
671
  timeMin: z.string().optional().describe("Optional filter: minimum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
670
672
  timeMax: z.string().optional().describe("Optional filter: maximum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
671
673
  amountMin: z.string().optional().describe("Optional filter: minimum reward amount (inclusive). Use string to represent U64 value."),
672
674
  amountMax: z.string().optional().describe("Optional filter: maximum reward amount (inclusive). Use string to represent U64 value."),
673
- storeFromId: z.union([
674
- z.literal('exists'),
675
- z.literal('not_exists'),
676
- z.object({ eq: z.string().describe("Address string to match") })
677
- ]).optional().describe("Optional filter for store_from_id field: 'exists' means any value exists, 'not_exists' means no value, {eq: string} matches specific address."),
675
+ storeFromId: z.object({
676
+ identifier: GuardIdentifierSchema,
677
+ }).strict().optional().describe("Optional filter: identifier reference to the store_from_id address in the Guard table (must be Address type). If not specified, the store_from_id field is ignored during matching. The server resolves this identifier to the actual address from the Guard table constants."),
678
678
  }).strict().describe("Filter conditions for the reward record existence query."),
679
- }).strict().describe("Returns Bool. Returns true if at least one reward record exists matching the specified recipient and filter conditions, otherwise false."),
679
+ }).strict().describe("Returns Bool. Returns true if at least one reward record exists matching the specified recipient and filter conditions, otherwise false. If recipient is not specified, uses the transaction signer address."),
680
680
  z.object({
681
681
  type: z.literal('query_progress_history_find'),
682
682
  object: z.object({
@@ -1,5 +1,6 @@
1
1
  import { GuardNodeSchema } from '../query/index.js';
2
2
  import { CallGuard_DataSchema } from '../call/guard.js';
3
+ import { isBoolReturningQuery, BOOL_RETURNING_QUERIES } from './guard-query-utils.js';
3
4
  function detectFormat(text) {
4
5
  const trimmed = text.trim();
5
6
  if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
@@ -344,6 +345,23 @@ export function parseGuardFromText(text) {
344
345
  }
345
346
  const result = CallGuard_DataSchema.safeParse(parsedData);
346
347
  if (result.success) {
348
+ const root = result.data.root;
349
+ if (typeof root === 'object' && root !== null && 'type' in root) {
350
+ const rootNode = root;
351
+ if (rootNode.type === 'query' && rootNode.query !== undefined) {
352
+ if (!isBoolReturningQuery(rootNode.query)) {
353
+ const queryStr = typeof rootNode.query === 'number' ? `ID ${rootNode.query}` : `"${rootNode.query}"`;
354
+ const boolQueryExamples = BOOL_RETURNING_QUERIES.slice(0, 5).map(q => `"${q.name}"`).join(', ');
355
+ return {
356
+ success: false,
357
+ errors: [{
358
+ message: `Invalid Guard root: query ${queryStr} does not return Bool. Only queries that return Bool can be used as root. Examples: ${boolQueryExamples}... Use guard2file to see all Bool-returning queries.`,
359
+ path: '/root/query',
360
+ }]
361
+ };
362
+ }
363
+ }
364
+ }
347
365
  return { success: true, data: result.data, errors: [] };
348
366
  }
349
367
  for (const issue of result.error.issues) {
@@ -3,3 +3,9 @@ export declare const isValidGuardQueryName: (name: string) => boolean;
3
3
  export declare const isValidGuardQueryIdOrName: (input: number | string) => boolean;
4
4
  export declare const getGuardQueryByIdOrName: (input: number | string) => import("@wowok/wowok").GuardQuery | undefined;
5
5
  export declare const GUARD_QUERY_NAMES: string[];
6
+ export declare const isBoolReturningQuery: (input: number | string) => boolean;
7
+ export declare const BOOL_RETURNING_QUERIES: {
8
+ id: number;
9
+ name: string;
10
+ description: string;
11
+ }[];
@@ -1,6 +1,8 @@
1
- import { GUARDQUERY } from "@wowok/wowok";
1
+ import { GUARDQUERY, ValueType } from "@wowok/wowok";
2
2
  const validGuardQueryIds = new Set(GUARDQUERY.map(item => item.id));
3
3
  const validGuardQueryNames = new Set(GUARDQUERY.map(item => item.name.toLowerCase()));
4
+ const boolReturningQueryIds = new Set(GUARDQUERY.filter(item => item.return === ValueType.Bool).map(item => item.id));
5
+ const boolReturningQueryNames = new Set(GUARDQUERY.filter(item => item.return === ValueType.Bool).map(item => item.name.toLowerCase()));
4
6
  export const isValidGuardQueryId = (id) => {
5
7
  return validGuardQueryIds.has(id);
6
8
  };
@@ -20,3 +22,12 @@ export const getGuardQueryByIdOrName = (input) => {
20
22
  return GUARDQUERY.find(item => item.name.toLowerCase() === input.toLowerCase());
21
23
  };
22
24
  export const GUARD_QUERY_NAMES = GUARDQUERY.map(item => item.name);
25
+ export const isBoolReturningQuery = (input) => {
26
+ if (typeof input === 'number') {
27
+ return boolReturningQueryIds.has(input);
28
+ }
29
+ return boolReturningQueryNames.has(input.toLowerCase());
30
+ };
31
+ export const BOOL_RETURNING_QUERIES = GUARDQUERY
32
+ .filter(item => item.return === ValueType.Bool)
33
+ .map(item => ({ id: item.id, name: item.name, description: item.description }));
@@ -38,6 +38,51 @@ export function getSchema(name) {
38
38
  return null;
39
39
  }
40
40
  const content = readFileSync(schemaPath, "utf-8");
41
+ const schema = JSON.parse(content);
42
+ return resolveSchemaRefs(schema);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function resolveSchemaRefs(schema, visited = new Set()) {
49
+ if (!schema || typeof schema !== "object") {
50
+ return schema;
51
+ }
52
+ if (Array.isArray(schema)) {
53
+ return schema.map(item => resolveSchemaRefs(item, visited));
54
+ }
55
+ const result = {};
56
+ for (const [key, value] of Object.entries(schema)) {
57
+ if (key === "$ref" && typeof value === "string") {
58
+ const refSchema = loadRefSchema(value, visited);
59
+ if (refSchema) {
60
+ Object.assign(result, resolveSchemaRefs(refSchema, visited));
61
+ }
62
+ }
63
+ else if (typeof value === "object") {
64
+ result[key] = resolveSchemaRefs(value, visited);
65
+ }
66
+ else {
67
+ result[key] = value;
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ function loadRefSchema(ref, visited) {
73
+ if (ref.startsWith("#")) {
74
+ return null;
75
+ }
76
+ if (visited.has(ref)) {
77
+ return { description: `Circular reference to ${ref}` };
78
+ }
79
+ visited.add(ref);
80
+ try {
81
+ const refPath = join(SCHEMAS_DIR, ref);
82
+ if (!existsSync(refPath)) {
83
+ return null;
84
+ }
85
+ const content = readFileSync(refPath, "utf-8");
41
86
  return JSON.parse(content);
42
87
  }
43
88
  catch {
@@ -0,0 +1,199 @@
1
+ # GuardNode Usage Guide
2
+
3
+ This document provides practical examples of GuardNode usage for AI agents.
4
+
5
+ ## Overview
6
+
7
+ A GuardNode is a computational tree node that represents an operation returning a typed value. Nodes can be nested to build complex validation logic. The root node of a Guard MUST return a `Bool` value (pass/fail).
8
+
9
+ ## Common Patterns
10
+
11
+ ### Pattern 1: Verify Transaction Signer is Order Owner
12
+
13
+ ```json
14
+ {
15
+ "type": "logic_equal",
16
+ "nodes": [
17
+ { "type": "context", "context": "Signer" },
18
+ {
19
+ "type": "query",
20
+ "query": 1562,
21
+ "object": { "identifier": 0 },
22
+ "parameters": []
23
+ }
24
+ ]
25
+ }
26
+ ```
27
+
28
+ **Explanation:**
29
+ - `context: Signer` returns the transaction sender's address
30
+ - `query: 1562` gets the order owner (order.owner query)
31
+ - `logic_equal` compares the two addresses
32
+
33
+ ### Pattern 2: Verify Order Progress Node
34
+
35
+ ```json
36
+ {
37
+ "type": "logic_equal",
38
+ "nodes": [
39
+ {
40
+ "type": "query",
41
+ "query": 1253,
42
+ "object": { "identifier": 0, "convert_witness": 100 },
43
+ "parameters": []
44
+ },
45
+ { "type": "identifier", "identifier": 1 }
46
+ ]
47
+ }
48
+ ```
49
+
50
+ **Explanation:**
51
+ - `query: 1253` gets the current progress node name
52
+ - `convert_witness: 100` (TypeOrderProgress) converts the object for progress query
53
+ - Compares with the expected node name stored at identifier 1
54
+
55
+ ### Pattern 3: Verify Reward Not Claimed
56
+
57
+ ```json
58
+ {
59
+ "type": "logic_not",
60
+ "node": {
61
+ "type": "query_reward_record_exists",
62
+ "object": { "identifier": 0 },
63
+ "recipient": { "identifier": 1 },
64
+ "where": {}
65
+ }
66
+ }
67
+ ```
68
+
69
+ **Explanation:**
70
+ - `query_reward_record_exists` checks if a reward record exists for the recipient
71
+ - `logic_not` inverts the result (returns true if NO record exists)
72
+ - This verifies the reward has NOT been claimed
73
+
74
+ ### Pattern 4: Combined Validation (Reward Guard)
75
+
76
+ ```json
77
+ {
78
+ "type": "logic_and",
79
+ "nodes": [
80
+ {
81
+ "type": "logic_equal",
82
+ "nodes": [
83
+ {
84
+ "type": "query",
85
+ "query": 1253,
86
+ "object": { "identifier": 0, "convert_witness": 100 },
87
+ "parameters": []
88
+ },
89
+ { "type": "identifier", "identifier": 1 }
90
+ ]
91
+ },
92
+ {
93
+ "type": "logic_equal",
94
+ "nodes": [
95
+ { "type": "context", "context": "Signer" },
96
+ {
97
+ "type": "query",
98
+ "query": 1562,
99
+ "object": { "identifier": 0 },
100
+ "parameters": []
101
+ }
102
+ ]
103
+ },
104
+ {
105
+ "type": "logic_not",
106
+ "node": {
107
+ "type": "query_reward_record_exists",
108
+ "object": { "identifier": 2 },
109
+ "recipient": { "identifier": 3 },
110
+ "where": {}
111
+ }
112
+ }
113
+ ]
114
+ }
115
+ ```
116
+
117
+ **Explanation:**
118
+ This is a complete Reward Guard that verifies:
119
+ 1. Order is at expected progress node (e.g., "delivered")
120
+ 2. Transaction signer is the order owner
121
+ 3. Order has NOT claimed the reward yet
122
+
123
+ ## Query Instruction Reference
124
+
125
+ | Query ID | Name | Description | Parameters | Returns |
126
+ |----------|------|-------------|------------|---------|
127
+ | 1253 | order.progress.node | Get current progress node name | None | String |
128
+ | 1562 | order.owner | Get order owner address | None | Address |
129
+ | 1563 | order.service | Get order service address | None | Address |
130
+ | 1564 | order.payer | Get order payer address | None | Address |
131
+
132
+ ## Witness Types
133
+
134
+ | Witness ID | Name | Description |
135
+ |------------|------|-------------|
136
+ | 100 | TypeOrderProgress | Access order progress data |
137
+ | 200 | TypeOrder | Access order data |
138
+
139
+ ## Node Return Types
140
+
141
+ ### Logic Nodes (Return Bool)
142
+ - `logic_and`, `logic_or`, `logic_not`
143
+ - `logic_equal`, `logic_as_u256_equal`
144
+ - `logic_as_u256_greater`, `logic_as_u256_lesser`
145
+ - `logic_as_u256_greater_or_equal`, `logic_as_u256_lesser_or_equal`
146
+ - `logic_string_contains`, `logic_string_nocase_contains`, `logic_string_nocase_equal`
147
+
148
+ ### Calculation Nodes (Return Numeric)
149
+ - `calc_number_add`, `calc_number_subtract`, `calc_number_multiply`, `calc_number_divide`, `calc_number_mod` → U256
150
+ - `calc_string_length` → U64
151
+ - `calc_string_contains`, `calc_string_nocase_contains`, `calc_string_nocase_equal` → Bool
152
+ - `calc_string_indexof`, `calc_string_nocase_indexof` → U64
153
+
154
+ ### Conversion Nodes
155
+ - `convert_number_address` → Address
156
+ - `convert_address_number` → U64
157
+ - `convert_number_string` → String
158
+ - `convert_string_number` → U256
159
+ - `convert_safe_u8`, `convert_safe_u16`, `convert_safe_u32`, `convert_safe_u64`, `convert_safe_u128`, `convert_safe_u256` → Respective type
160
+
161
+ ### Query Nodes (Return Varies)
162
+ - `query` → Depends on query instruction
163
+ - `query_reward_record_find`, `query_reward_record_count` → U64
164
+ - `query_reward_record_exists` → Bool
165
+ - `query_progress_history_find`, `query_progress_history_session_find`, `query_progress_history_session_forward_find` → U64
166
+ - `query_progress_history_session_count`, `query_progress_history_session_forward_count`, `query_progress_history_session_forward_retained_submission_count` → U64
167
+
168
+ ### Context Nodes
169
+ - `context: Signer` → Address
170
+ - `context: Clock` → U64 (timestamp)
171
+ - `context: Guard` → Address
172
+
173
+ ### Vector Nodes
174
+ - `vec_length` → U64
175
+ - `vec_contains_bool`, `vec_contains_address`, `vec_contains_string`, `vec_contains_string_nocase`, `vec_contains_number` → Bool
176
+
177
+ ## Best Practices
178
+
179
+ 1. **Root node must return Bool**: Always use a logic node (logic_and, logic_or) as the root.
180
+
181
+ 2. **Use logic_and for multiple conditions**: Most common pattern for combining validations.
182
+
183
+ 3. **Check node return types**: Ensure child nodes return the expected type for the parent node.
184
+ - Logic nodes expect Bool children
185
+ - Calculation nodes expect numeric children
186
+ - Comparison nodes expect matching types
187
+
188
+ 4. **Use identifier references**: Store constants (addresses, strings, numbers) in the Guard table and reference them by identifier.
189
+
190
+ 5. **Query with convert_witness**: When querying object data, use the appropriate witness type to access the data you need.
191
+
192
+ 6. **Handle optional fields**: In where clauses, only include fields you want to filter by. Omitting a field means "don't filter by this field".
193
+
194
+ ## Common Errors
195
+
196
+ 1. **Type mismatch**: Trying to compare incompatible types (e.g., Address with String).
197
+ 2. **Missing identifier**: Referencing an identifier that doesn't exist in the Guard table.
198
+ 3. **Wrong return type**: Using a non-Bool node as the root or as a child of a logic node.
199
+ 4. **Invalid query ID**: Using a query instruction ID that doesn't exist.