@harperfast/template-react-ts-studio 1.11.3 → 1.11.5

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 (48) hide show
  1. package/.agents/skills/harper-best-practices/AGENTS.md +567 -490
  2. package/.agents/skills/harper-best-practices/rules/automatic-apis.md +46 -78
  3. package/.agents/skills/harper-best-practices/rules/defining-relationships.md +41 -63
  4. package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +59 -45
  5. package/.agents/skills/harper-best-practices/rules/load-env.md +24 -23
  6. package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +62 -78
  7. package/.agents/skills/harper-best-practices/rules/querying-rest-apis.md +92 -65
  8. package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +53 -55
  9. package/.agents/skills/harper-best-practices/rules/v5-upgrade.md +98 -62
  10. package/.agents/skills/harper-best-practices/rules/vector-indexing.md +113 -42
  11. package/.agents/skills/harper-mcp/AGENTS.md +501 -0
  12. package/.agents/skills/harper-mcp/SKILL.md +91 -0
  13. package/.agents/skills/harper-mcp/rules/automatic-verb-tools.md +50 -0
  14. package/.agents/skills/harper-mcp/rules/connecting-clients.md +59 -0
  15. package/.agents/skills/harper-mcp/rules/custom-mcp-prompts.md +52 -0
  16. package/.agents/skills/harper-mcp/rules/custom-mcp-resources.md +64 -0
  17. package/.agents/skills/harper-mcp/rules/custom-mcp-tools.md +72 -0
  18. package/.agents/skills/harper-mcp/rules/durable-quotas.md +73 -0
  19. package/.agents/skills/harper-mcp/rules/enabling-mcp.md +63 -0
  20. package/.agents/skills/harper-mcp/rules/rate-limiting.md +62 -0
  21. package/.agents/skills/harper-mcp/rules/resources-surface.md +44 -0
  22. package/.agents/skills/harper-mcp/rules/security-posture.md +36 -0
  23. package/.agents/skills/harper-mcp/rules.manifest.yaml +203 -0
  24. package/agent/skills/harper-best-practices/AGENTS.md +567 -490
  25. package/agent/skills/harper-best-practices/rules/automatic-apis.md +46 -78
  26. package/agent/skills/harper-best-practices/rules/defining-relationships.md +41 -63
  27. package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +59 -45
  28. package/agent/skills/harper-best-practices/rules/load-env.md +24 -23
  29. package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +62 -78
  30. package/agent/skills/harper-best-practices/rules/querying-rest-apis.md +92 -65
  31. package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +53 -55
  32. package/agent/skills/harper-best-practices/rules/v5-upgrade.md +98 -62
  33. package/agent/skills/harper-best-practices/rules/vector-indexing.md +113 -42
  34. package/agent/skills/harper-mcp/AGENTS.md +501 -0
  35. package/agent/skills/harper-mcp/SKILL.md +83 -0
  36. package/agent/skills/harper-mcp/rules/automatic-verb-tools.md +50 -0
  37. package/agent/skills/harper-mcp/rules/connecting-clients.md +59 -0
  38. package/agent/skills/harper-mcp/rules/custom-mcp-prompts.md +52 -0
  39. package/agent/skills/harper-mcp/rules/custom-mcp-resources.md +64 -0
  40. package/agent/skills/harper-mcp/rules/custom-mcp-tools.md +72 -0
  41. package/agent/skills/harper-mcp/rules/durable-quotas.md +73 -0
  42. package/agent/skills/harper-mcp/rules/enabling-mcp.md +63 -0
  43. package/agent/skills/harper-mcp/rules/rate-limiting.md +62 -0
  44. package/agent/skills/harper-mcp/rules/resources-surface.md +44 -0
  45. package/agent/skills/harper-mcp/rules/security-posture.md +36 -0
  46. package/agent/skills/harper-mcp/rules.manifest.yaml +203 -0
  47. package/package.json +2 -2
  48. package/skills-lock.json +7 -1
@@ -46,13 +46,11 @@ Instructions for the agent to follow when designing Harper schemas, applying cor
46
46
 
47
47
  #### When to Use
48
48
 
49
- Apply this rule when creating or modifying Harper schema files, configuring `graphqlSchema` in `config.yaml`, or deciding which directives to apply to tables and fields. Use it any time a component needs tables, indexes, primary keys, or exported endpoints defined.
49
+ Apply this rule when creating or modifying Harper schema files (`.graphql`), configuring `graphqlSchema` in `config.yaml`, or deciding which directives to apply to types and fields. Use it any time you need to define tables, primary keys, indexes, or exported endpoints.
50
50
 
51
51
  #### How It Works
52
52
 
53
- 1. **Create a schema file** using standard GraphQL type definitions with Harper-specific directives. Name it (e.g., `schema.graphql`) and place it in your component directory.
54
-
55
- 2. **Register the schema** in the component's `config.yaml` using the `graphqlSchema` plugin:
53
+ 1. **Declare the schema file** in the component's `config.yaml` using the `graphqlSchema` plugin:
56
54
 
57
55
  ```yaml
58
56
  graphqlSchema:
@@ -61,7 +59,7 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
61
59
 
62
60
  Both plugins and applications can specify schemas.
63
61
 
64
- 3. **Mark types as tables** with `@table`. The type name becomes the table name by default:
62
+ 2. **Mark types as tables** with `@table`. The type name becomes the table name by default:
65
63
 
66
64
  ```graphql
67
65
  type Dog @table {
@@ -72,13 +70,13 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
72
70
  }
73
71
  ```
74
72
 
75
- 4. **Designate a primary key** with `@primaryKey` on exactly one field per type. Primary keys must be unique; duplicate-key inserts are rejected. If no primary key is provided on insert, Harper auto-generates one:
73
+ 3. **Set a primary key** on every table using `@primaryKey`. Primary keys must be unique; duplicate-key inserts are rejected. If no primary key is provided on insert, Harper auto-generates one based on the field type:
76
74
  - `String` or `ID` → UUID string
77
75
  - `Int`, `Long`, or `Any` → auto-incrementing integer
78
76
 
79
77
  Use `Long` or `Any` for auto-generated numeric keys; `Int` is 32-bit and may be insufficient for large tables.
80
78
 
81
- 5. **Add secondary indexes** with `@indexed` on any field that will be used for filtering in REST queries, SQL, or NoSQL operations:
79
+ 4. **Index fields for querying** with `@indexed`. Required for filtering by an attribute in REST queries, SQL, or NoSQL operations:
82
80
 
83
81
  ```graphql
84
82
  type Breed @table {
@@ -89,7 +87,7 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
89
87
 
90
88
  If the field value is an array, each element is individually indexed. Null values are indexed by default.
91
89
 
92
- 6. **Expose a table as an external endpoint** with `@export`. The optional `name` parameter sets the URL path segment:
90
+ 5. **Expose a table as an external endpoint** with `@export`. Available via REST, MQTT, and other interfaces. The optional `name` parameter sets the URL path segment:
93
91
 
94
92
  ```graphql
95
93
  type MyTable @table @export(name: "my-table") {
@@ -97,9 +95,9 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
97
95
  }
98
96
  ```
99
97
 
100
- Without `name`, the type name is used as the path segment.
98
+ Without `name`, the type name is used.
101
99
 
102
- 7. **Configure `@table` arguments** as needed for database placement, expiration, eviction, and replication:
100
+ 6. **Configure `@table` arguments** as needed. All arguments are optional:
103
101
 
104
102
  | Argument | Type | Default | Description |
105
103
  | -------------- | --------- | ----------------------------- | ------------------------------------------------------------- |
@@ -110,13 +108,15 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
110
108
  | `scanInterval` | `Int` | `(expiration + eviction) / 4` | Seconds between eviction scans |
111
109
  | `replicate` | `Boolean` | `true` | Enable replication of this table |
112
110
 
113
- 8. **Apply additional field directives** where needed:
114
- - `@createdTime` — auto-assigns Unix epoch milliseconds on record creation
115
- - `@updatedTime` — auto-assigns Unix epoch milliseconds on each update
116
- - `@embed(source: "fieldName", model: "modelName")` — computes an embedding vector on write; attribute type must be `[Float]`
117
- - `@hidden` — suppresses the field from MCP tool descriptors and OpenAPI documents (not an access-control mechanism)
111
+ 7. **Apply additional field directives** where needed:
112
+ - `@createdTime` — auto-assigns creation timestamp (Unix epoch ms)
113
+ - `@updatedTime` — auto-assigns update timestamp (Unix epoch ms)
114
+ - `@expiresAt` — marks a field as the record's absolute expiration time (Unix epoch ms)
115
+ - `@embed(source:, model:)` — computes an embedding vector when the source field is written; field type must be `[Float]`
116
+ - `@hidden` — suppresses the field from MCP tool descriptors and OpenAPI document (not an access-control mechanism)
117
+
118
+ 8. **Use `@sealed`** on a type to prevent records from including properties beyond those declared in the schema:
118
119
 
119
- 9. **Restrict extra properties** with `@sealed` if records must not include properties beyond those declared:
120
120
  ```graphql
121
121
  type StrictRecord @table @sealed {
122
122
  id: Long @primaryKey
@@ -124,9 +124,11 @@ Apply this rule when creating or modifying Harper schema files, configuring `gra
124
124
  }
125
125
  ```
126
126
 
127
+ 9. **Use unique database names** in plugins or applications to avoid table naming collisions, since all tables default to the `data` database.
128
+
127
129
  #### Examples
128
130
 
129
- **Minimal two-table schema:**
131
+ **Minimal schema with two tables:**
130
132
 
131
133
  ```graphql
132
134
  type Dog @table {
@@ -142,7 +144,7 @@ type Breed @table {
142
144
  }
143
145
  ```
144
146
 
145
- **Table with expiration, eviction, and custom scan interval:**
147
+ **Table with expiration, eviction, and scan tuning:**
146
148
 
147
149
  ```graphql
148
150
  # Expire after 5 minutes, evict after 1 hour, scan every 10 minutes
@@ -152,63 +154,59 @@ type WeatherCache @table(expiration: 300, eviction: 3300, scanInterval: 600) {
152
154
  }
153
155
  ```
154
156
 
155
- **Exported table with timestamps and a hidden field:**
157
+ **Table with multiple `@table` arguments combined:**
156
158
 
157
159
  ```graphql
158
- type Customer @table @export(name: "customers") {
160
+ type Event @table(database: "analytics", expiration: 86400) {
159
161
  id: Long @primaryKey
160
162
  name: String @indexed
161
- createdAt: Long @createdTime
162
- updatedAt: Long @updatedTime
163
-
164
- """
165
- Internal — do not surface to external consumers.
166
- """
167
- creditScore: Int @hidden
168
163
  }
169
164
  ```
170
165
 
171
- **Multiple `@table` argument combinations:**
166
+ **Exported table with overridden table name:**
172
167
 
173
168
  ```graphql
174
- # Override table name
175
- type Product @table(table: "products") {
169
+ type Product @table(table: "products") @export(name: "products") {
176
170
  id: Long @primaryKey
171
+ category: String @indexed
172
+ price: Float @indexed
177
173
  }
174
+ ```
178
175
 
179
- # Place in a specific database
180
- type Order @table(database: "commerce") {
181
- id: Long @primaryKey
182
- }
176
+ **Table with timestamps and per-record expiration:**
183
177
 
184
- # Auto-expire records after 1 hour
185
- type Session @table(expiration: 3600) {
186
- id: Long @primaryKey
187
- userId: String
178
+ ```graphql
179
+ type Session @table {
180
+ id: ID @primaryKey
181
+ token: String
182
+ createdAt: Long @createdTime
183
+ updatedAt: Long @updatedTime
184
+ expiresAt: Long @expiresAt
188
185
  }
186
+ ```
189
187
 
190
- # Disable replication
191
- type LocalRecord @table(replicate: false) {
192
- id: Long @primaryKey
193
- value: String
194
- }
188
+ **Table with a hidden internal field:**
195
189
 
196
- # Combine multiple arguments
197
- type Event @table(database: "analytics", expiration: 86400) {
190
+ ```graphql
191
+ type Customer @table {
198
192
  id: Long @primaryKey
199
- name: String @indexed
193
+ name: String
194
+
195
+ """
196
+ Internal — do not surface to external consumers.
197
+ """
198
+ creditScore: Int @hidden
200
199
  }
201
200
  ```
202
201
 
203
202
  #### Notes
204
203
 
205
- - All tables default to the `"data"` database. When designing plugins or applications, use unique database names to avoid table naming collisions.
206
- - Schemas are flexible by default — records may include properties beyond those declared. Use `@sealed` to prevent this.
207
- - `expiration` marks a record stale; `eviction` controls how long after expiration the record is physically removed. Eviction does not remove records from secondary indexes — Harper fetches the full record on demand if an evicted record matches a query.
208
- - `scanInterval` is clock-aligned to the server's local timezone, not startup-aligned. The eviction schedule is deterministic across restarts.
209
- - If replication is disabled on a table and later re-enabled, writes made during the disabled period are not replicated retroactively.
210
- - `@hidden` (on types or fields) is a metadata-visibility directive only. Use `attribute_permissions` on roles to enforce data access control.
211
- - Disabling replication (`replicate: false`) and re-enabling it later will not catch up on writes made while replication was disabled.
204
+ - `@table`, `@export`, `@sealed`, and `@hidden` are type-level directives; `@primaryKey`, `@indexed`, `@embed`, `@createdTime`, `@updatedTime`, `@expiresAt`, and `@hidden` are field-level directives.
205
+ - `eviction` removes non-indexed record data but does **not** remove a record from its secondary indexes. Indexes remain functional for evicted records; Harper fetches the full record on demand when a query matches an evicted entry.
206
+ - `scanInterval` is clock-aligned to the server's local timezone, not startup-aligned. The server's startup time does not affect when eviction runs.
207
+ - Replication is enabled by default. If you disable replication on a table and re-enable it later, it will not catch up on writes made while replication was disabled.
208
+ - `@hidden` is a metadata-visibility directive only. Use `attribute_permissions` on roles to enforce data access control.
209
+ - A full-record `put` that omits an `@expiresAt` field clears it; a `patch` of other fields preserves it.
212
210
 
213
211
  ### 1.3 Defining Relationships Between Tables in Harper
214
212
 
@@ -216,17 +214,17 @@ Instructions for the agent to follow when defining and querying relationships be
216
214
 
217
215
  #### When to Use
218
216
 
219
- Apply this rule whenever a schema requires linking two tables via a foreign key — for example, modeling shows and networks, products and brands, or orders and items. Use it when queries need to filter or select nested related records using dot-syntax.
217
+ Apply this rule when adding foreign key relationships between schema tables, enabling join queries, or returning nested related records in query results. Use it any time a schema type needs to reference records in another table via a foreign key attribute.
220
218
 
221
219
  #### How It Works
222
220
 
223
- 1. **Use `@relationship(from: attribute)` for many-to-one or many-to-many**: Place this on a field in the current table when the foreign key lives in this table and references the primary key of the target table.
221
+ 1. **Use `@relationship(from: attribute)` for many-to-one or many-to-many**: Place this on the field in the table that holds the foreign key. The `from` parameter names the attribute on this table that stores the foreign key referencing the target table's primary key.
224
222
 
225
223
  ```graphql
226
224
  type RealityShow @table @export {
227
225
  id: Long @primaryKey
228
226
  networkId: Long @indexed
229
- network: Network @relationship(from: networkId)
227
+ network: Network @relationship(from: networkId) # many-to-one
230
228
  title: String @indexed
231
229
  }
232
230
 
@@ -236,7 +234,7 @@ Apply this rule whenever a schema requires linking two tables via a foreign key
236
234
  }
237
235
  ```
238
236
 
239
- For a many-to-many relationship, make the foreign key an array:
237
+ If the foreign key attribute is an array, the relationship becomes many-to-many:
240
238
 
241
239
  ```graphql
242
240
  type RealityShow @table @export {
@@ -246,24 +244,24 @@ Apply this rule whenever a schema requires linking two tables via a foreign key
246
244
  }
247
245
  ```
248
246
 
249
- 2. **Use `@relationship(to: attribute)` for one-to-many or many-to-many**: Place this on a field in the current table when the foreign key lives in the target table and references the primary key of this table. The result type must be an array.
247
+ 2. **Use `@relationship(to: attribute)` for one-to-many or many-to-many**: Place this on the table whose primary key is referenced by the foreign key in the target table. The `to` parameter names the attribute on the target table that holds the foreign key. The result type **must** be an array.
250
248
 
251
249
  ```graphql
252
250
  type Network @table @export {
253
251
  id: Long @primaryKey
254
252
  name: String @indexed
255
- shows: [RealityShow] @relationship(to: networkId)
253
+ shows: [RealityShow] @relationship(to: networkId) # one-to-many
256
254
  }
257
255
  ```
258
256
 
259
- 3. **Use `@relationship(from: attribute, to: attribute)` for foreign key to foreign key joins**: Specify both `from` and `to` when neither side uses the primary key. This is useful for joining on non-primary-key attributes.
257
+ 3. **Use `@relationship(from: attribute, to: attribute)` for foreign key to foreign key joins**: Specify both `from` and `to` when neither side uses the primary key. Harper resolves the relationship by searching the target table's `to` attribute for matches using this record's `from` attribute value. The result type must be an array.
260
258
 
261
259
  ```graphql
262
260
  type OrderItem @table @export {
263
261
  id: Long @primaryKey
264
262
  orderId: Long @indexed
265
263
  productSku: Long @indexed
266
- product: Product @relationship(from: productSku, to: sku)
264
+ products: [Product] @relationship(from: productSku, to: sku)
267
265
  }
268
266
 
269
267
  type Product @table @export {
@@ -273,15 +271,14 @@ Apply this rule whenever a schema requires linking two tables via a foreign key
273
271
  }
274
272
  ```
275
273
 
276
- 4. **Query across relationships using dot-syntax**: Filter by related table attributes using chained dot notation. This behaves as an INNER JOIN.
274
+ 4. **Query across relationships using dot-syntax**: Filter records by related table attributes using chained dot notation. This behaves as an INNER JOIN — only records with a matching related record are returned.
277
275
 
278
276
  ```
279
- GET /RealityShow?network.name=Bravo
280
277
  GET /Product/?brand.name=Microsoft
281
278
  GET /Brand/?products.name=Keyboard
282
279
  ```
283
280
 
284
- 5. **Select nested relationship fields with `select()`**: Relationship attributes are not included by default. Use `select()` to include them in results. When selecting without a filter on the related table, this acts as a LEFT JOIN — the relationship property is omitted if the foreign key is null or references a non-existent record.
281
+ 5. **Include relationship fields in results using `select()`**: Relationship attributes are not returned by default. Use `select()` to include them, optionally specifying nested fields with `{}`.
285
282
 
286
283
  ```
287
284
  GET /Product/?brand.name=Microsoft&select(name,brand)
@@ -289,80 +286,59 @@ Apply this rule whenever a schema requires linking two tables via a foreign key
289
286
  GET /Product/?name=Keyboard&select(name,brand{name,id})
290
287
  ```
291
288
 
292
- #### Examples
289
+ When selecting a relationship without filtering on it, Harper performs a LEFT JOIN — the relationship property is omitted if the foreign key is null or references a non-existent record.
293
290
 
294
- **Many-to-one relationship** — a show belongs to a network:
295
-
296
- ```graphql
297
- type RealityShow @table @export {
298
- id: Long @primaryKey
299
- networkId: Long @indexed
300
- network: Network @relationship(from: networkId)
301
- title: String @indexed
302
- }
291
+ 6. **Model many-to-many without a junction table**: Store an array of foreign key values and use `@relationship(from: ...)` pointing to that array attribute. The array order of the foreign key values is preserved when resolving the relationship.
303
292
 
304
- type Network @table @export {
305
- id: Long @primaryKey
306
- name: String @indexed
307
- }
308
- ```
293
+ ```graphql
294
+ type Product @table @export {
295
+ id: Long @primaryKey
296
+ name: String
297
+ resellerIds: [Long] @indexed
298
+ resellers: [Reseller] @relationship(from: "resellerIds")
299
+ }
300
+ ```
309
301
 
310
- Query:
302
+ 7. **Define self-referential relationships** for parent-child hierarchies by pointing `@relationship` back at the same table type.
311
303
 
312
- ```
313
- GET /RealityShow?network.name=Bravo
314
- ```
304
+ #### Examples
315
305
 
316
- **One-to-many relationship** — a network has many shows:
306
+ **Full schema with bidirectional relationships:**
317
307
 
318
308
  ```graphql
319
- type Network @table @export {
309
+ type Product @table @export {
320
310
  id: Long @primaryKey
321
- name: String @indexed
322
- shows: [RealityShow] @relationship(to: networkId)
311
+ name: String
312
+ brandId: Long @indexed
313
+ brand: Brand @relationship(from: "brandId")
323
314
  }
324
- ```
325
-
326
- **Many-to-many with array foreign keys** — a product has multiple resellers:
327
315
 
328
- ```graphql
329
- type Product @table @export {
316
+ type Brand @table @export {
330
317
  id: Long @primaryKey
331
318
  name: String
332
- resellerIds: [Long] @indexed
333
- resellers: [Reseller] @relationship(from: resellerIds)
319
+ products: [Product] @relationship(to: "brandId")
334
320
  }
335
321
  ```
336
322
 
337
- Query with nested select:
323
+ **Querying with joins and nested select:**
338
324
 
339
325
  ```
340
- GET /Product/?resellers.name=Cool Shop&select(id,name,resellers{name,id})
326
+ GET /Product/?brand.name=Microsoft&select(name,brand{name,id})
327
+ GET /Brand/?products.name=Keyboard
341
328
  ```
342
329
 
343
- **Foreign key to foreign key join** — order item joined on SKU:
344
-
345
- ```graphql
346
- type OrderItem @table @export {
347
- id: Long @primaryKey
348
- orderId: Long @indexed
349
- productSku: Long @indexed
350
- product: Product @relationship(from: productSku, to: sku)
351
- }
330
+ **Many-to-many query with nested select:**
352
331
 
353
- type Product @table @export {
354
- id: Long @primaryKey
355
- sku: Long @indexed
356
- name: String
357
- }
332
+ ```
333
+ GET /Product/?resellers.name=Cool Shop&select(id,name,resellers{name,id})
358
334
  ```
359
335
 
360
336
  #### Notes
361
337
 
362
- - The `@relationship` directive requires the referenced attribute to be `@indexed` on the foreign key side.
363
- - Self-referential relationships are supported, enabling parent-child hierarchies within a single table.
364
- - The array order of foreign key values (e.g., `resellerIds`) is preserved when resolving many-to-many relationships.
365
- - When using `select()` without a filter on the related table, the join behaves as a LEFT JOIN — missing or null foreign keys result in the relationship property being omitted rather than causing an error.
338
+ - Every attribute named in `from` or `to` must exist on the respective table and be annotated with `@indexed` to support join queries.
339
+ - The `to`-only and `from`+`to` forms both require the result field type to be an array (e.g., `[RealityShow]`).
340
+ - The `from`-only form on a non-array attribute produces a many-to-one relationship; on an array attribute it produces many-to-many.
341
+ - Self-referential relationships are supported for hierarchical data within a single table.
366
342
 
367
343
  ### 1.4 Vector Indexing
368
344
 
@@ -370,11 +346,11 @@ Instructions for the agent to enable HNSW vector indexes on table fields and que
370
346
 
371
347
  #### When to Use
372
348
 
373
- Apply this rule when adding a vector similarity search capability to a Harper table — for example, storing text embeddings and querying for nearest neighbors, filtering by distance threshold, or tuning index construction and search parameters. Use it alongside [adding-tables-with-schemas.md](adding-tables-with-schemas.md) when defining the schema that hosts the vector field.
349
+ Apply this rule when adding a vector similarity search capability to a Harper table — for example, storing text embeddings and querying for nearest neighbors, filtering by distance threshold, or combining vector search with record-level access control. See [adding-tables-with-schemas.md](adding-tables-with-schemas.md) for how to define the surrounding table schema.
374
350
 
375
351
  #### How It Works
376
352
 
377
- 1. **Declare the vector index on a field**: Add `@indexed(type: "HNSW")` to a `[Float]` field inside a `@table` type. This creates an HNSW (Hierarchical Navigable Small World) index for approximate nearest-neighbor search.
353
+ 1. **Declare the vector index** on a `[Float]` field using `@indexed(type: "HNSW")`:
378
354
 
379
355
  ```graphql
380
356
  type Document @table {
@@ -383,7 +359,7 @@ Apply this rule when adding a vector similarity search capability to a Harper ta
383
359
  }
384
360
  ```
385
361
 
386
- 2. **Query by nearest neighbors using `sort`**: Call `.search()` with a `sort` descriptor that specifies the indexed `attribute` and a `target` vector. Use `limit` to cap results.
362
+ 2. **Query nearest neighbors** using the `sort` parameter with `attribute` and `target`:
387
363
 
388
364
  ```javascript
389
365
  let results = Document.search({
@@ -392,7 +368,7 @@ Apply this rule when adding a vector similarity search capability to a Harper ta
392
368
  });
393
369
  ```
394
370
 
395
- 3. **Combine with filter conditions**: Add a `conditions` array alongside `sort` to pre-filter records before ranking by similarity.
371
+ 3. **Combine with filter conditions** to narrow results before or during traversal:
396
372
 
397
373
  ```javascript
398
374
  let results = Document.search({
@@ -402,7 +378,25 @@ Apply this rule when adding a vector similarity search capability to a Harper ta
402
378
  });
403
379
  ```
404
380
 
405
- 4. **Filter by distance threshold**: To return only records within a similarity cutoff (without ranking), place `target` directly on the condition alongside `comparator` and `value`. This bounds result quality rather than ranking by similarity.
381
+ Conditions are evaluated _during_ graph traversal (predicate-aware search), not after. Very selective conditions are automatically diverted to an exact-scan strategy.
382
+
383
+ 4. **Use a `vectorFilter` function** for predicates not expressible as conditions (JavaScript API only):
384
+
385
+ ```javascript
386
+ let results = Document.search(
387
+ {
388
+ sort: { attribute: 'textEmbeddings', target: searchVector },
389
+ vectorFilter: (record) =>
390
+ record.tenantId === context.user.tenantId && record.status === 'published',
391
+ limit: 10,
392
+ },
393
+ context,
394
+ );
395
+ ```
396
+
397
+ The function receives a frozen candidate record and must return a boolean synchronously. It must be side-effect free and fast — it can run once per candidate visited during traversal (verdicts are memoized per query).
398
+
399
+ 5. **Filter by distance threshold** using `target` on a condition instead of `sort`:
406
400
 
407
401
  ```javascript
408
402
  let results = Document.search({
@@ -415,7 +409,7 @@ Apply this rule when adding a vector similarity search capability to a Harper ta
415
409
  });
416
410
  ```
417
411
 
418
- 5. **Include computed distance in results**: Use the special `$distance` field in `select` to return the distance from the target vector. Available in both `sort`-based and threshold-based queries.
412
+ 6. **Include computed distance in results** using the `$distance` field in `select`:
419
413
 
420
414
  ```javascript
421
415
  let results = Document.search({
@@ -425,7 +419,9 @@ Apply this rule when adding a vector similarity search capability to a Harper ta
425
419
  });
426
420
  ```
427
421
 
428
- 6. **Tune per-query search options**: Pass `distance` and `ef` directly on the `sort` descriptor to override index defaults for a single query.
422
+ `$distance` works in both `sort`-based and threshold-based queries.
423
+
424
+ 7. **Override the distance function or exploration budget per query** via options on the `sort` descriptor:
429
425
 
430
426
  ```javascript
431
427
  let results = Document.search({
@@ -434,10 +430,25 @@ Apply this rule when adding a vector similarity search capability to a Harper ta
434
430
  });
435
431
  ```
436
432
 
437
- - `distance` — overrides the distance function for this query: `"cosine"`, `"euclidean"`, or `"dotProduct"`.
438
- - `ef` — overrides the search exploration budget. Higher values improve recall at the cost of latency.
433
+ - `distance` — `"cosine"`, `"euclidean"`, or `"dotProduct"`.
434
+ - `ef` — overrides the search exploration budget for this query. Higher values improve recall at the cost of latency.
439
435
 
440
- 7. **Configure HNSW index parameters**: Pass parameters directly in the `@indexed` directive. Structural parameters (`distance`, `M`, `efConstruction`, `quantization`) trigger an index rebuild when changed; `efConstructionSearch` does not.
436
+ 8. **Tune filtered traversal** when a `vectorFilter` is very selective by raising `ef` or `filterExpansion`:
437
+
438
+ ```javascript
439
+ let results = Document.search(
440
+ {
441
+ sort: { attribute: 'textEmbeddings', target: searchVector, ef: 200, filterExpansion: 40 },
442
+ vectorFilter: (record) => record.category === 'rare',
443
+ limit: 10,
444
+ },
445
+ context,
446
+ );
447
+ ```
448
+
449
+ Filtered traversal visits at most `ef * filterExpansion` nodes (`filterExpansion` defaults to `24`). If the budget is exhausted before results fill, the search returns what was found rather than erroring.
450
+
451
+ 9. **Configure HNSW index parameters** directly on the directive:
441
452
 
442
453
  ```graphql
443
454
  type Document @table {
@@ -447,18 +458,48 @@ Apply this rule when adding a vector similarity search capability to a Harper ta
447
458
  }
448
459
  ```
449
460
 
450
- 8. **Enable vector quantization**: Use `quantization: "int8"` to store vectors as 8-bit integers, reducing index size and memory usage. Harper re-ranks nearest-neighbor `sort` results against full-precision vectors automatically.
461
+ | Parameter | Default | Description |
462
+ | ---------------------- | ----------------- | ------------------------------------------------------------------------------------------------ |
463
+ | `distance` | `"cosine"` | Distance function: `"cosine"`, `"euclidean"`, or `"dotProduct"` |
464
+ | `efConstruction` | `100` | Max nodes explored during index construction. Higher = better recall, lower = better performance |
465
+ | `M` | `16` | Preferred connections per graph layer |
466
+ | `optimizeRouting` | `0.5` | Heuristic aggressiveness for omitting redundant connections (0 = off, 1 = most aggressive) |
467
+ | `mL` | computed from `M` | Normalization factor for level generation |
468
+ | `efConstructionSearch` | auto-scaled | Max nodes explored during search. When unset, auto-scales with index size |
469
+ | `quantization` | — | `"int8"` stores vectors quantized to int8 |
470
+ | `filterExpansion` | `24` | Visit-budget multiplier for filtered search |
451
471
 
452
- ```graphql
453
- type Document @table {
454
- id: Long @primaryKey
455
- textEmbeddings: [Float] @indexed(type: "HNSW", quantization: "int8")
456
- }
457
- ```
472
+ Changing `efConstructionSearch` on an existing index does not trigger a rebuild. Changing structural parameters (`distance`, `M`, `efConstruction`, `quantization`) does rebuild the index.
473
+
474
+ 10. **Enable int8 quantization** to reduce index size and memory usage:
475
+
476
+ ```graphql
477
+ type Document @table {
478
+ id: Long @primaryKey
479
+ textEmbeddings: [Float] @indexed(type: "HNSW", quantization: "int8")
480
+ }
481
+ ```
482
+
483
+ Graph navigation uses quantized distances. For `sort` queries, Harper re-ranks results against full-precision vectors, restoring exact ordering and exact `$distance` values. Distance-threshold queries filter on the approximate distance.
484
+
485
+ 11. **Implement record-level access control** by overriding `allowRead(user, target, context)` on the table resource. During vector queries the check participates in graph traversal, so a restricted user receives the k nearest records they are allowed to see:
486
+
487
+ ```javascript
488
+ export class Reports extends tables.Reports {
489
+ allowRead(user, target, context) {
490
+ if (!super.allowRead(user, target, context)) return false;
491
+ if (user.role.permission.super_user) return true;
492
+ if (this.ownerId == null) return true;
493
+ return this.ownerId === user.id;
494
+ }
495
+ }
496
+ ```
497
+
498
+ The check must be synchronous, side-effect free, and fast. `this` is the frozen record during per-record evaluation. A thrown exception denies that record (fail closed).
458
499
 
459
500
  #### Examples
460
501
 
461
- Full schema with custom HNSW parameters and a nearest-neighbor query with distance output:
502
+ **Full schema with custom HNSW parameters:**
462
503
 
463
504
  ```graphql
464
505
  type Document @table {
@@ -468,16 +509,33 @@ type Document @table {
468
509
  }
469
510
  ```
470
511
 
512
+ **Nearest-neighbor search with distance output:**
513
+
471
514
  ```javascript
472
- // Nearest-neighbor search with distance scores
473
515
  let results = Document.search({
474
516
  select: ['name', '$distance'],
475
517
  sort: { attribute: 'textEmbeddings', target: searchVector },
476
518
  limit: 5,
477
519
  });
520
+ ```
521
+
522
+ **Filtered vector search with tuned traversal budget:**
523
+
524
+ ```javascript
525
+ let results = Document.search(
526
+ {
527
+ sort: { attribute: 'textEmbeddings', target: searchVector, ef: 200, filterExpansion: 40 },
528
+ vectorFilter: (record) => record.category === 'rare',
529
+ limit: 10,
530
+ },
531
+ context,
532
+ );
533
+ ```
534
+
535
+ **Distance threshold query (no ranking, cutoff only):**
478
536
 
479
- // Distance-threshold query (no ranking)
480
- let closeMatches = Document.search({
537
+ ```javascript
538
+ let results = Document.search({
481
539
  conditions: {
482
540
  attribute: 'textEmbeddings',
483
541
  comparator: 'lt',
@@ -489,24 +547,13 @@ let closeMatches = Document.search({
489
547
 
490
548
  #### Notes
491
549
 
492
- ##### HNSW Parameters
493
-
494
- | Parameter | Default | Description |
495
- | ---------------------- | ----------------- | ------------------------------------------------------------------------------------------------------ |
496
- | `distance` | `"cosine"` | Distance function: `"cosine"`, `"euclidean"`, or `"dotProduct"` |
497
- | `efConstruction` | `100` | Max nodes explored during index construction. Higher = better recall, lower = better performance |
498
- | `M` | `16` | Preferred connections per graph layer. Higher = more space, better recall for high-dimensional data |
499
- | `optimizeRouting` | `0.5` | Heuristic aggressiveness for omitting redundant connections (0 = off, 1 = most aggressive) |
500
- | `mL` | computed from `M` | Normalization factor for level generation |
501
- | `efConstructionSearch` | auto-scaled | Max nodes explored during search. When unset, auto-scales with index size; setting it fixes the budget |
502
- | `quantization` | — | `"int8"` stores vectors quantized to int8 |
503
-
504
- - The `distance` option on a per-query `sort` descriptor accepts `"cosine"`, `"euclidean"`, or `"dotProduct"`.
505
- - When no `ef` is passed and `efConstructionSearch` (or `efConstruction`) is not explicitly set on the index, the search budget auto-scales with index size.
506
- - `efConstruction` seeds the initial value of `efConstructionSearch`; setting either one fixes the search budget.
507
- - The correct parameter name is `efConstructionSearch` (not `efSearchConstruction`).
508
- - `$distance` is available in both `sort`-based ranking and `conditions`-based threshold queries.
509
- - For `quantization: "int8"`, distance-threshold (`lt`/`le`) queries filter on approximate distance; `sort` queries re-rank against full-precision vectors.
550
+ - Use `@indexed(type: "HNSW")` on a `[Float]` field — not on scalar fields.
551
+ - The default `distance` is `"cosine"`. Override it per-index via the directive or per-query via the `sort` descriptor.
552
+ - `efConstruction` seeds the initial value of `efConstructionSearch`. When neither is set, the search budget auto-scales with index size.
553
+ - `vectorFilter` is available from the JavaScript API only; it cannot be expressed in a REST query string.
554
+ - The parameter name Harper reads is `efConstructionSearch` (not `efSearchConstruction`).
555
+ - `$distance` must be listed explicitly in `select` to appear in results.
556
+ - For record-level `allowRead` overrides: collection-scope calls (where `this.ownerId` is `null`) should return `true` to open the connection; per-record filtering happens during query execution and event delivery.
510
557
 
511
558
  ### 1.5 Using the Blob Data Type
512
559
 
@@ -739,13 +786,13 @@ Apply this rule when adding REST or WebSocket API access to Harper tables or cus
739
786
 
740
787
  #### How It Works
741
788
 
742
- 1. **Enable the REST plugin**: Add `rest: true` to your application's `config.yaml`. This activates the HTTP REST interface and enables WebSocket support by default.
789
+ 1. **Enable the REST plugin**: Add `rest: true` to your application's `config.yaml`. This activates the HTTP REST interface on the application server port (default `9926`) and enables WebSocket support automatically.
743
790
 
744
791
  ```yaml
745
792
  rest: true
746
793
  ```
747
794
 
748
- To configure optional behavior:
795
+ To configure options explicitly:
749
796
 
750
797
  ```yaml
751
798
  rest:
@@ -753,69 +800,26 @@ Apply this rule when adding REST or WebSocket API access to Harper tables or cus
753
800
  webSocket: false # disables automatic WebSocket support (enabled by default)
754
801
  ```
755
802
 
756
- 2. **Export your resource in the schema**: Tables are not exposed by default. Use the `@export` directive in your schema definition to make a table available as a REST endpoint. The exported name defines the base URL path, served on the application HTTP server port (default `9926`).
757
-
758
- 3. **Use the correct URL structure**: The REST interface follows a consistent path convention.
759
-
760
- | Path | Description |
761
- | -------------------------------------------- | ---------------------------------------------------------------------------------- |
762
- | `/my-resource` | Returns a description of the resource (e.g., table metadata) |
763
- | `/my-resource/` | Trailing slash — represents the full collection; append query parameters to search |
764
- | `/my-resource/record-id` | A specific record identified by its primary key |
765
- | `/my-resource/record-id/` | Trailing slash — collection of records with the given id prefix |
766
- | `/my-resource/record-id/with/multiple/parts` | Record id with multiple path segments |
767
-
768
- 4. **Map HTTP methods to operations**: Each HTTP method maps to a resource method and operation.
769
- - **GET** — Retrieve a record or search. Calls `get()`.
770
-
771
- ```
772
- GET /MyTable/123
773
- GET /MyTable/?name=Harper
774
- GET /MyTable/123.propertyName
775
- ```
776
-
777
- Responses include an `ETag` header. Clients may send `If-None-Match` to receive `304 Not Modified` when the record is unchanged.
803
+ 2. **Export your resource in the schema**: Tables are not exposed by default. Use the `@export` directive in your schema definition to expose a table as a REST endpoint. The exported name defines the base URL path.
778
804
 
779
- - **PUT** — Create or replace a record (upsert). Calls `put(record)`. Properties not in the body are removed.
805
+ 3. **Use the correct URL structure**: Target resources using these path conventions:
780
806
 
781
- ```
782
- PUT /MyTable/123
783
- Content-Type: application/json
784
-
785
- { "name": "some data" }
786
- ```
807
+ | Path | Description |
808
+ | -------------------------------------------- | ----------------------------------------------------------- |
809
+ | `/my-resource` | Returns resource metadata |
810
+ | `/my-resource/` | Collection — all records; append query parameters to search |
811
+ | `/my-resource/record-id` | Specific record by primary key |
812
+ | `/my-resource/record-id/` | Collection of records with the given id prefix |
813
+ | `/my-resource/record-id/with/multiple/parts` | Record id with multiple path segments |
787
814
 
788
- - **POST** — Create a new record without specifying a primary key. Calls `post(data)`. The assigned key is returned in the `Location` response header.
789
-
790
- ```
791
- POST /MyTable/
792
- Content-Type: application/json
815
+ 4. **Map operations to HTTP methods**: Each HTTP method maps to a resource method:
816
+ - **GET** — Retrieve a record or search. Calls `get()`. Responses include an `ETag` header; send `If-None-Match` on subsequent requests to receive `304 Not Modified` when unchanged.
817
+ - **PUT** — Create or replace a record (upsert). Calls `put(record)`. The stored record exactly matches the submitted body; omitted properties are removed.
818
+ - **POST** — Create a record without specifying a primary key. Calls `post(data)`. The assigned key is returned in the `Location` response header.
819
+ - **PATCH** — Partially update a record, merging only provided top-level properties. Calls the resource's patch handler. Merge is **shallow** — nested objects are replaced entirely, not deep-merged.
820
+ - **DELETE** — Delete a record by id or all records matching a query.
793
821
 
794
- { "name": "some data" }
795
- ```
796
-
797
- - **PATCH** — Partially update a record, merging only provided properties. Unspecified properties are preserved.
798
-
799
- ```
800
- PATCH /MyTable/123
801
- Content-Type: application/json
802
-
803
- { "status": "active" }
804
- ```
805
-
806
- - **DELETE** — Delete a record or all records matching a query.
807
- ```
808
- DELETE /MyTable/123
809
- DELETE /MyTable/?status=archived
810
- ```
811
-
812
- 5. **Access the auto-generated OpenAPI spec**: Harper generates an OpenAPI specification for all exported resources. Retrieve it at:
813
-
814
- ```
815
- GET /openapi
816
- ```
817
-
818
- 6. **Connect via WebSocket**: When `rest` is enabled, WebSocket support is on by default. Connect to a resource URL to subscribe to change events for that resource.
822
+ 5. **Connect via WebSocket**: A WebSocket connection to a resource URL subscribes to that resource and streams change events. See [real-time-apps.md](real-time-apps.md) for full real-time patterns.
819
823
 
820
824
  ```javascript
821
825
  let ws = new WebSocket('wss://server/my-resource/341');
@@ -824,13 +828,17 @@ Apply this rule when adding REST or WebSocket API access to Harper tables or cus
824
828
  };
825
829
  ```
826
830
 
827
- Connecting to `wss://server/my-resource/341` accesses the `my-resource` resource with record id `341` and subscribes to it. When the record changes or a message is published to it, the WebSocket connection receives the update.
831
+ 6. **Implement a custom `connect()` handler** on a resource class to control WebSocket behavior. The method receives `incomingMessages` and must return an async iterable producing messages to send to the client.
828
832
 
829
- 7. **Implement a custom `connect()` handler**: Override `connect(incomingMessages)` on a resource class to control WebSocket behavior. The method must return an async iterable or generator that produces messages to send to the client.
833
+ 7. **Retrieve the OpenAPI spec**: Harper auto-generates an OpenAPI specification for all exported resources, available at:
834
+
835
+ ```
836
+ GET /openapi
837
+ ```
830
838
 
831
839
  #### Examples
832
840
 
833
- **Simple echo server using an async generator**:
841
+ **Simple echo WebSocket server**:
834
842
 
835
843
  ```javascript
836
844
  export class Echo extends Resource {
@@ -842,7 +850,7 @@ export class Echo extends Resource {
842
850
  }
843
851
  ```
844
852
 
845
- **Using the default `connect()` with event-style access and a timer**:
853
+ **Custom `connect()` using the default iterable with `send()` and `close` event**:
846
854
 
847
855
  ```javascript
848
856
  export class Example extends Resource {
@@ -866,30 +874,37 @@ export class Example extends Resource {
866
874
  }
867
875
  ```
868
876
 
869
- **Minimal `config.yaml` enabling REST with WebSocket disabled**:
877
+ **Common REST operations**:
870
878
 
871
- ```yaml
872
- rest:
873
- webSocket: false
879
+ ```
880
+ GET /MyTable/123
881
+ GET /MyTable/?name=Harper
882
+ PUT /MyTable/123
883
+ PATCH /MyTable/123
884
+ DELETE /MyTable/?status=archived
885
+ ```
886
+
887
+ ```json
888
+ { "name": "some data" }
874
889
  ```
875
890
 
876
891
  #### Notes
877
892
 
878
- - Tables must be explicitly exported using `@export` in the schema — they are not exposed by default.
879
- - `rest: true` is the minimal configuration to enable both REST and WebSocket support. See [real-time-apps.md](real-time-apps.md) for patterns around real-time WebSocket usage.
880
- - For full query syntax on `GET` and `DELETE` with query parameters, see [querying-rest-apis.md](querying-rest-apis.md).
881
- - The default `connect()` returns an iterable with a `send(message)` method and a `close` event for cleanup on disconnect.
882
- - For MQTT over WebSockets, set the sub-protocol header `Sec-WebSocket-Protocol: mqtt`.
883
- - In distributed environments, non-retained messages are delivered in the order received per node; retained messages (PUT/updated records) keep only the latest-timestamp version as the winning record across the cluster.
884
- - Use the `Content-Type` request header to specify body format and the `Accept` header to request a specific response format.
893
+ - `rest: true` is the minimal config to enable both REST and WebSocket support. Set `webSocket: false` under the `rest` key to disable WebSocket only.
894
+ - The `@export` directive in the schema is required for any table to appear as a REST endpoint — tables are not exported by default.
895
+ - PATCH merges are shallow (top-level only). Nested objects in the request body replace the entire existing sub-object. Dot-path keys (e.g., `"settings.theme"`) are stored as literal keys, not interpreted as paths.
896
+ - For MQTT over WebSocket, set the sub-protocol header `Sec-WebSocket-Protocol: mqtt`.
897
+ - In distributed environments, non-retained messages are delivered in arrival order; retained messages (PUT/updated records) keep only the latest timestamp as the winning record.
898
+ - For full query syntax on GET and DELETE, see [querying-rest-apis.md](querying-rest-apis.md).
899
+ - For building real-time features with WebSocket subscriptions, see [real-time-apps.md](real-time-apps.md).
885
900
 
886
- ### 2.2 Querying REST APIs
901
+ ### 2.2 Querying Harper REST APIs
887
902
 
888
- Instructions for the agent to filter, sort, select, and paginate Harper REST API collections using URL query parameters.
903
+ Instructions for the agent to filter, sort, select, and paginate records using Harper's URL-based query language on REST collection endpoints.
889
904
 
890
905
  #### When to Use
891
906
 
892
- Apply this rule when building or modifying code that queries Harper REST endpoints with filtering, sorting, field selection, or pagination. Use it whenever constructing URLs against collection paths exposed by Harper's automatic REST interface (see [automatic-apis.md](automatic-apis.md)).
907
+ Apply this rule when building or debugging REST API calls against Harper tables that require filtering by attribute values, comparison ranges, sorting, field selection, or result pagination. This rule also covers OR logic, grouping, and querying across related tables via dot-syntax joins. See [automatic-apis.md](automatic-apis.md) for how Harper exposes tables as REST endpoints.
893
908
 
894
909
  #### How It Works
895
910
 
@@ -900,21 +915,27 @@ Apply this rule when building or modifying code that queries Harper REST endpoin
900
915
  GET /Product/?category=software&inStock=true
901
916
  ```
902
917
 
903
- 2. **Apply comparison operators (FIQL syntax)**: Use FIQL operators directly in query parameter values.
918
+ 2. **Filter for null values**: Use `=null` as the value to match null or non-null records.
904
919
 
905
- | Operator | Meaning |
906
- | ------------ | -------------------------------------- |
907
- | `==` | Equal |
908
- | `=lt=` | Less than |
909
- | `=le=` | Less than or equal |
910
- | `=gt=` | Greater than |
911
- | `=ge=` | Greater than or equal |
912
- | `=ne=`, `!=` | Not equal |
913
- | `=ct=` | Contains (strings) |
914
- | `=sw=` | Starts with (strings) |
915
- | `=ew=` | Ends with (strings) |
916
- | `=`, `===` | Strict equality (no type conversion) |
917
- | `!==` | Strict inequality (no type conversion) |
920
+ ```
921
+ GET /Product/?discount=null
922
+ ```
923
+
924
+ 3. **Apply comparison operators (FIQL syntax)**: Use FIQL operators in query parameters for range and string matching.
925
+
926
+ | Operator | Meaning |
927
+ | -------------------- | -------------------------------------- |
928
+ | `==` | Equal |
929
+ | `=lt=` | Less than |
930
+ | `=le=` | Less than or equal |
931
+ | `=gt=` | Greater than |
932
+ | `=ge=` | Greater than or equal |
933
+ | `=ne=`, `!=` | Not equal |
934
+ | `=ct=` | Contains (strings) |
935
+ | `=sw=`, `==<value>*` | Starts with (strings) |
936
+ | `=ew=` | Ends with (strings) |
937
+ | `=`, `===` | Strict equality (no type conversion) |
938
+ | `!==` | Strict inequality (no type conversion) |
918
939
 
919
940
  ```
920
941
  GET /Product/?price=gt=100
@@ -929,32 +950,52 @@ Apply this rule when building or modifying code that queries Harper REST endpoin
929
950
  GET /Product/?listDate=gt=2017-03-08T09%3A30%3A00.000Z
930
951
  ```
931
952
 
932
- 3. **Chain conditions for range queries**: Omit the attribute name on the second condition to apply it to the same attribute. Only `gt`/`ge` combined with `lt`/`le` is supported.
953
+ 4. **Chain conditions for range queries**: Omit the attribute name on the second condition to apply it to the same attribute. Only `gt`/`ge` combined with `lt`/`le` is supported for chaining.
933
954
 
934
955
  ```
935
956
  GET /Product/?price=gt=100&lt=200
936
957
  ```
937
958
 
938
- 4. **Combine conditions with OR logic**: Use `|` instead of `&`.
959
+ 5. **Apply type conversion**: For FIQL comparators, Harper converts values automatically. Use explicit prefixes to force a type.
960
+
961
+ | Syntax | Behavior |
962
+ | ----------------------------------------- | ------------------------------------------- |
963
+ | `name==null` | Converts to `null` |
964
+ | `name==123` | Converts to number if attribute is untyped |
965
+ | `name==true` | Converts to boolean if attribute is untyped |
966
+ | `name==number:123` | Explicit number conversion |
967
+ | `name==boolean:true` | Explicit boolean conversion |
968
+ | `name==string:some%20text` | Keep as string with URL decode |
969
+ | `name==date:2024-01-05T20%3A07%3A27.955Z` | Explicit Date conversion |
970
+
971
+ For strict operators (`=`, `===`, `!==`), no automatic type conversion is applied.
972
+
973
+ 6. **Combine conditions with OR logic**: Use `|` instead of `&` to express OR between conditions.
939
974
 
940
975
  ```
941
976
  GET /Product/?rating=5|featured=true
942
977
  ```
943
978
 
944
- 5. **Group conditions**: Use parentheses or square brackets to control order of operations. Prefer square brackets when constructing queries from user input, since standard URI encoding safely encodes `[` and `]`.
979
+ 7. **Group conditions**: Use parentheses or square brackets to control evaluation order. Prefer square brackets when building queries from user input, since `[` and `]` are safely URI-encoded.
945
980
 
946
981
  ```
947
982
  GET /Product/?rating=5|(price=gt=100&price=lt=200)
948
983
  GET /Product/?rating=5&[tag=fast|tag=scalable|tag=efficient]
949
984
  ```
950
985
 
951
- Construct grouped queries from JavaScript:
986
+ Build grouped queries in JavaScript:
952
987
 
953
988
  ```javascript
954
989
  let url = `/Product/?rating=5&[${tags.map(encodeURIComponent).join('|')}]`;
955
990
  ```
956
991
 
957
- 6. **Select specific properties with `select(`**: Use `select()` to control which fields are returned.
992
+ Nest groups for complex conditions:
993
+
994
+ ```
995
+ GET /Product/?price=lt=100|[rating=5&[tag=fast|tag=scalable|tag=efficient]&inStock=true]
996
+ ```
997
+
998
+ 8. **Select specific properties with `select(`**: Append `select(...)` as a query function separated by `&` to control which fields are returned.
958
999
 
959
1000
  | Syntax | Returns |
960
1001
  | -------------------------------------- | ------------------------------------------- |
@@ -969,111 +1010,112 @@ Apply this rule when building or modifying code that queries Harper REST endpoin
969
1010
  GET /Product/?brand.name=Microsoft&select(name,brand{name})
970
1011
  ```
971
1012
 
972
- 7. **Limit results with `limit(`**: Use `limit(end)` or `limit(start,end)` to paginate.
1013
+ 9. **Paginate results with `limit(`**: Use `limit(end)` or `limit(start,end)` to restrict the number of records returned.
973
1014
 
974
1015
  ```
975
1016
  GET /Product/?rating=gt=3&inStock=true&select(rating,name)&limit(20)
976
1017
  GET /Product/?rating=gt=3&limit(10,30)
977
1018
  ```
978
1019
 
979
- 8. **Sort results with `sort(`**: Use `sort(property)` or `sort(+property,-property,...)`. Prefix `+` or no prefix = ascending; `-` = descending.
1020
+ 10. **Sort results with `sort(`**: Use `sort(property)` or `sort(+property,-property,...)` to order results. Prefix `+` or no prefix = ascending; `-` = descending.
980
1021
 
981
- ```
982
- GET /Product/?rating=gt=3&sort(+name)
983
- GET /Product/?sort(+rating,-price)
984
- ```
1022
+ ```
1023
+ GET /Product/?rating=gt=3&sort(+name)
1024
+ GET /Product/?sort(+rating,-price)
1025
+ ```
985
1026
 
986
- 9. **Query across relationships**: Use dot-syntax to filter by related table attributes. Relationships must be defined in the schema using `@relation`.
1027
+ 11. **Query across relationships using dot-syntax**: Filter on related table attributes using dot-chained property names. Relationships must be defined in the schema with `@relationship`.
987
1028
 
988
- ```
989
- GET /Product/?brand.name=Microsoft
990
- GET /Brand/?products.name=Keyboard
991
- ```
1029
+ ```
1030
+ GET /Product/?brand.name=Microsoft
1031
+ GET /Brand/?products.name=Keyboard
1032
+ ```
992
1033
 
993
- Use `select()` to include relationship attributes in the response (they are not included by default):
1034
+ Use `select()` to include relationship attributes in the response (they are excluded by default):
994
1035
 
995
- ```
996
- GET /Product/?brand.name=Microsoft&select(name,brand{name})
997
- ```
1036
+ ```
1037
+ GET /Product/?brand.name=Microsoft&select(name,brand)
1038
+ GET /Product/?brand.name=Microsoft&select(name,brand{name})
1039
+ ```
998
1040
 
999
- 10. **Access a specific property by URL**: Append the property name with dot syntax to the record ID. Only works for properties declared in the schema.
1041
+ 12. **Access a specific property by URL**: Append `.propertyName` to a record ID in the URL path. Only works for properties declared in the schema.
1000
1042
  ```
1001
1043
  GET /MyTable/123.propertyName
1002
1044
  ```
1003
1045
 
1004
1046
  #### Examples
1005
1047
 
1006
- **Range filter with select and limit:**
1048
+ **Range filter with select and limit**:
1007
1049
 
1008
1050
  ```
1009
1051
  GET /Product/?category=software&price=gt=100&price=lt=200&select(name,price)&limit(20)
1010
1052
  ```
1011
1053
 
1012
- **Sort descending with multiple fields:**
1054
+ **Sort and paginate**:
1013
1055
 
1014
1056
  ```
1015
- GET /Product/?sort(+rating,-price)
1057
+ GET /Product/?rating=gt=3&sort(+rating,-price)&limit(10,30)
1016
1058
  ```
1017
1059
 
1018
- **OR logic with grouping:**
1060
+ **OR with grouping**:
1019
1061
 
1020
1062
  ```
1021
1063
  GET /Product/?price=lt=100|[rating=5&[tag=fast|tag=scalable|tag=efficient]&inStock=true]
1022
1064
  ```
1023
1065
 
1024
- **Relationship join with nested select:**
1025
-
1026
- ```
1027
- GET /Product/?brand.name=Microsoft&select(name,brand{name,id})
1028
- ```
1029
-
1030
- **Schema defining a relationship for join queries:**
1066
+ **Join query with nested select** — schema first:
1031
1067
 
1032
1068
  ```graphql
1033
1069
  type Product @table @export {
1034
1070
  id: Long @primaryKey
1035
1071
  name: String
1036
1072
  brandId: Long @indexed
1037
- brand: Brand @relation(from: "brandId")
1073
+ brand: Brand @relationship(from: "brandId")
1038
1074
  }
1039
1075
  type Brand @table @export {
1040
1076
  id: Long @primaryKey
1041
1077
  name: String
1042
- products: [Product] @relation(to: "brandId")
1078
+ products: [Product] @relationship(to: "brandId")
1043
1079
  }
1044
1080
  ```
1045
1081
 
1046
- **Many-to-many relationship query:**
1082
+ Then query:
1083
+
1084
+ ```
1085
+ GET /Product/?brand.name=Microsoft&select(name,brand{name,id})
1086
+ ```
1087
+
1088
+ **Many-to-many relationship** — schema:
1047
1089
 
1048
1090
  ```graphql
1049
1091
  type Product @table @export {
1050
1092
  id: Long @primaryKey
1051
1093
  name: String
1052
1094
  resellerIds: [Long] @indexed
1053
- resellers: [Reseller] @relation(from: "resellerId")
1095
+ resellers: [Reseller] @relationship(from: "resellerIds")
1054
1096
  }
1055
1097
  ```
1056
1098
 
1099
+ Query:
1100
+
1057
1101
  ```
1058
1102
  GET /Product/?resellers.name=Cool Shop&select(id,name,resellers{name,id})
1059
1103
  ```
1060
1104
 
1061
- **Type conversion with explicit prefix:**
1105
+ **Date range with URL-encoded colons**:
1062
1106
 
1063
1107
  ```
1064
- GET /Product/?price==number:123
1065
- GET /Product/?active==boolean:true
1066
- GET /Product/?listDate==date:2024-01-05T20%3A07%3A27.955Z
1108
+ GET /Product/?listDate=gt=2017-03-08T09%3A30%3A00.000Z
1067
1109
  ```
1068
1110
 
1069
1111
  #### Notes
1070
1112
 
1071
- - Only indexed attributes can be used as the primary filter; additional unindexed attributes can be combined with `&` once at least one indexed attribute is present.
1072
- - For null value queries, use `?attribute=null`. Indexes must have been created with null indexing support; existing indexes must be removed and re-added to support null queries.
1073
- - FIQL comparators (`==`, `!=`, `=gt=`, etc.) apply automatic type conversion based on value syntax or schema-declared type. Strict operators (`=`, `===`, `!==`) skip automatic type conversion.
1074
- - Filtering by a related attribute produces INNER JOIN behavior (only records with a matching related record are returned). Using `select()` on a relationship without a filter produces LEFT JOIN behavior.
1075
- - The array order of foreign key values in many-to-many relationships is preserved when resolving the relationship.
1076
- - See [automatic-apis.md](automatic-apis.md) for how Harper tables are automatically exposed as REST endpoints.
1113
+ - All filtered attributes must be indexed unless at least one other attribute in the same query is indexed.
1114
+ - Null queries (`?attr=null`) require indexes created after null indexing support was added. Rebuild existing indexes (remove and re-add) to enable null queries on them.
1115
+ - When selecting a related attribute without filtering on it, the join behaves as a LEFT JOIN — the relationship property is omitted if the foreign key is null or references a non-existent record.
1116
+ - The array order of foreign key values (e.g., `resellerIds`) is preserved when resolving many-to-many relationships.
1117
+ - Square brackets (`[`, `]`) are preferred over parentheses for grouping when constructing queries programmatically, because standard URI encoding safely encodes them.
1118
+ - `directURLMapping: true` can be set on a resource to change URL path handling semantics; see your schema configuration for details.
1077
1119
 
1078
1120
  ### 2.3 Real-Time Apps with WebSockets and Pub/Sub
1079
1121
 
@@ -1608,11 +1650,11 @@ Instructions for the agent to interact with Harper tables programmatically using
1608
1650
 
1609
1651
  #### When to Use
1610
1652
 
1611
- Apply this rule when writing server-side Harper code that reads from or writes to tables directly — for example, in request handlers, background jobs, or SSR entry points — instead of going through the REST API. Use it whenever you need to construct queries with `conditions`, paginate results, select specific fields, or perform CRDT-safe mutations with `addTo`.
1653
+ Apply this rule when writing server-side code that reads from or writes to Harper tables directly — for example, in request handlers, background jobs, or SSR rendering — without going through the REST API. Use it whenever you need to construct queries with `conditions`, `select`, `sort`, or `search(`.
1612
1654
 
1613
1655
  #### How It Works
1614
1656
 
1615
- 1. **Import `tables`**: Import from the `harper` package. Each table defined in `schema.graphql` with `@table` is available as a named property.
1657
+ 1. **Import `tables`**: Pull `tables` from the `harper` package. Each property on `tables` corresponds to a table defined in `schema.graphql`.
1616
1658
 
1617
1659
  ```javascript
1618
1660
  import { tables } from 'harper';
@@ -1620,10 +1662,9 @@ Apply this rule when writing server-side Harper code that reads from or writes t
1620
1662
  // same as: databases.data.Product
1621
1663
  ```
1622
1664
 
1623
- 2. **Define your schema**: Declare tables in `schema.graphql` using `@table` and `@primaryKey`.
1665
+ 2. **Define your schema**: Declare tables with `@table` in `schema.graphql`. Each type becomes a property on `tables`.
1624
1666
 
1625
1667
  ```graphql
1626
- # schema.graphql
1627
1668
  type Product @table {
1628
1669
  id: Long @primaryKey
1629
1670
  name: String
@@ -1631,57 +1672,75 @@ Apply this rule when writing server-side Harper code that reads from or writes t
1631
1672
  }
1632
1673
  ```
1633
1674
 
1634
- 3. **Create and mutate records**: Use `create`, `patch`, `get`, and `update` on the table class.
1675
+ 3. **Create and modify records**: Use `create`, `patch`, and `get` for basic CRUD.
1635
1676
 
1636
1677
  ```javascript
1637
- // Create a new record (id auto-generated)
1638
1678
  const created = await Product.create({ name: 'Shirt', price: 9.5 });
1639
-
1640
- // Modify the record
1641
1679
  await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
1642
-
1643
- // Retrieve by primary key
1644
1680
  const record = await Product.get(created.id);
1645
1681
  ```
1646
1682
 
1647
- 4. **Query with `search(` and `conditions`**: Pass a query object to `search()` to filter records. Iterate the async result.
1683
+ 4. **Query with `search(`**: Pass a query object to `Product.search(query)`. It returns an async iterable.
1648
1684
 
1649
1685
  ```javascript
1650
1686
  const query = {
1651
1687
  conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
1652
1688
  };
1653
1689
  for await (const record of Product.search(query)) {
1654
- // ...
1690
+ // process record
1655
1691
  }
1656
1692
  ```
1657
1693
 
1658
- 5. **Use `select` to shape results**: Pass a `select` array to return only specific properties, including nested relationship fields.
1694
+ 5. **Build `conditions`**: Each condition object filters records. Nest conditions with `operator` for boolean logic.
1695
+
1696
+ | Property | Description |
1697
+ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
1698
+ | `attribute` | Property name, or array for chained/joined properties (e.g. `['brand', 'name']`) |
1699
+ | `value` | The value to match |
1700
+ | `comparator` | `equals` (default), `greater_than`, `greater_than_equal`, `less_than`, `less_than_equal`, `starts_with`, `contains`, `ends_with`, `between`, `not_equal` |
1701
+ | `conditions` | Nested conditions array |
1702
+ | `operator` | `and` (default) or `or` for the nested `conditions` |
1703
+
1704
+ 6. **Use `select`** to control which properties are returned. Accepts an array of property names, a string for a single property, or nested objects for relationships.
1659
1705
 
1660
1706
  ```javascript
1661
- const book = await Book.get({ id: 42, select: ['id', 'title', 'author'] });
1662
- book.author.name; // full related Author record
1707
+ // Array of names
1708
+ Product.search({ select: ['name', 'price'] });
1663
1709
 
1664
- // Partial related record
1665
- const book = await Book.get({
1666
- id: 42,
1667
- select: ['id', 'title', { name: 'author', select: ['name'] }],
1668
- });
1710
+ // Nested relationship select
1711
+ Book.get({ id: 42, select: ['id', 'title', { name: 'author', select: ['name'] }] });
1669
1712
  ```
1670
1713
 
1671
- 6. **Use `addTo` for concurrent-safe increments**: Call `addTo` on a mutable resource instance obtained via `update()`. This uses CRDT incrementation, safe across threads and nodes.
1714
+ Special `select` values:
1715
+ - `$id` — returns the primary key regardless of its name
1716
+ - `$updatedtime` — returns the last-updated timestamp
1717
+ - `$distance` — returns the computed distance when querying a vector index
1718
+
1719
+ 7. **Use `addTo`** for concurrent-safe numeric increments (CRDT incrementation, safe across threads and nodes).
1672
1720
 
1673
1721
  ```javascript
1674
- static async post(target, data) {
1675
- const record = await this.update(target.id);
1676
- record.addTo('quantity', -1); // decrement safely across nodes
1677
- }
1722
+ const record = await Product.update(target.id);
1723
+ record.addTo('quantity', -1);
1724
+ ```
1725
+
1726
+ 8. **Apply `sort`, `limit`, and `offset`** for ordering and pagination. A `sort` attribute must be `@indexed`, or at least one `conditions` entry must be present. Pass `allowFullScan: true` to permit an unconditional ordered scan.
1727
+
1728
+ ```javascript
1729
+ Product.search({
1730
+ conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
1731
+ sort: { attribute: 'id' },
1732
+ });
1678
1733
  ```
1679
1734
 
1680
- 7. **Scope destructive operations carefully**: `update`, `patch`, and `delete` operate directly on stored data. Always use specific `conditions`, validate the affected set before writing, and gate behind authorization controls.
1735
+ | Sort property | Description |
1736
+ | ------------- | ---------------------------------------------------------- |
1737
+ | `attribute` | Property name (or array for chained relationship property) |
1738
+ | `descending` | Sort descending if `true` (default: `false`) |
1739
+ | `next` | Secondary sort to resolve ties (same structure) |
1681
1740
 
1682
1741
  #### Examples
1683
1742
 
1684
- ##### Nested conditions query
1743
+ **Nested conditions with `or`:**
1685
1744
 
1686
1745
  ```javascript
1687
1746
  Product.search({
@@ -1698,23 +1757,25 @@ Product.search({
1698
1757
  });
1699
1758
  ```
1700
1759
 
1701
- ##### Chained attribute reference (relationship join)
1760
+ **Chained attribute reference (relationship join):**
1702
1761
 
1703
1762
  ```javascript
1704
1763
  Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
1705
1764
  ```
1706
1765
 
1707
- ##### Deep nested `select`
1766
+ **Deep nested `select` across multiple relationships:**
1708
1767
 
1709
1768
  ```javascript
1710
- select: [
1711
- 'id',
1712
- 'name',
1713
- { name: 'segments', select: ['id', 'name', { name: 'client', select: ['id', 'name'] }] },
1714
- ];
1769
+ Product.search({
1770
+ select: [
1771
+ 'id',
1772
+ 'name',
1773
+ { name: 'segments', select: ['id', 'name', { name: 'client', select: ['id', 'name'] }] },
1774
+ ],
1775
+ });
1715
1776
  ```
1716
1777
 
1717
- ##### SSR usage
1778
+ **SSR usage — read directly from `tables` in a render function:**
1718
1779
 
1719
1780
  ```typescript
1720
1781
  import { tables } from 'harper';
@@ -1727,47 +1788,12 @@ export async function render(url: string): Promise<string> {
1727
1788
 
1728
1789
  #### Notes
1729
1790
 
1730
- ##### `conditions` comparator values
1731
-
1732
- | Comparator | Description |
1733
- | -------------------- | ---------------------- |
1734
- | `equals` | Default equality match |
1735
- | `greater_than` | Strictly greater |
1736
- | `greater_than_equal` | Greater than or equal |
1737
- | `less_than` | Strictly less |
1738
- | `less_than_equal` | Less than or equal |
1739
- | `starts_with` | String prefix match |
1740
- | `contains` | String contains |
1741
- | `ends_with` | String suffix match |
1742
- | `between` | Range match |
1743
- | `not_equal` | Inequality match |
1744
-
1745
- ##### Query object options
1746
-
1747
- | Property | Description |
1748
- | ----------------------- | ----------------------------------------------------------------------- |
1749
- | `conditions` | Array of condition objects to filter records |
1750
- | `operator` | Top-level `and` (default) or `or` for the `conditions` array |
1751
- | `limit` | Maximum number of records to return |
1752
- | `offset` | Number of records to skip (for pagination) |
1753
- | `select` | Properties to include in each returned record |
1754
- | `sort` | Sort order object with `attribute`, `descending`, and `next` properties |
1755
- | `explain` | If `true`, returns conditions reordered as Harper will execute them |
1756
- | `enforceExecutionOrder` | If `true`, forces conditions to execute in the order supplied |
1757
-
1758
- ##### `select` special properties
1759
-
1760
- - `$id` — Returns the primary key regardless of its name
1761
- - `$updatedtime` — Returns the last-updated timestamp
1762
- - `$distance` — Returns the computed distance from the target vector when the query uses a vector index
1763
-
1764
- ##### Relationship join behavior
1765
-
1766
- - Selecting a relationship **without** filtering on it behaves as a **LEFT JOIN** — records with no related row are still returned.
1767
- - Adding a `conditions` entry on a related attribute (e.g. `attribute: ['author', 'name']`) behaves as an **INNER JOIN** — only records with a matching related row are returned.
1768
-
1769
- - Keep `harper` external when bundling for SSR (e.g. `ssr: { external: ['harper'] }` in `vite.config`) so it resolves to the runtime instead of being bundled.
1770
- - `tables`, `databases`, and other Harper APIs are the same live, process-wide objects regardless of whether accessed as globals or via `import { tables } from 'harper'`.
1791
+ - Scope destructive operations (`update`, `patch`, `delete`) with specific `conditions` and validate the affected set before writing. These operate on live data and are not easily reversible.
1792
+ - Sorting by a bare `@primaryKey` with no conditions raises `HdbError: <attribute> is not indexed and not combined with any other conditions`. Add an open-ended condition or pass `allowFullScan: true`.
1793
+ - Selecting a relationship field without filtering on it behaves as a **LEFT JOIN**. Adding a condition on a related attribute (e.g. `attribute: ['author', 'name']`) behaves as an **INNER JOIN**.
1794
+ - A to-many relationship resolves to an array; `await` the property before iterating when needed.
1795
+ - `tables` and `databases.data` reference the same live objects — a record written through one component is immediately visible to all others.
1796
+ - Keep `harper` external when bundling for SSR (e.g. `ssr: { external: ['harper'] }` in `vite.config`) so it resolves to the runtime.
1771
1797
 
1772
1798
  ### 3.4 TypeScript Type Stripping in Harper
1773
1799
 
@@ -1986,22 +2012,26 @@ await fetch('http://localhost:9926/JokeCache/1', {
1986
2012
 
1987
2013
  ### 4.1 Deploying to Harper Fabric
1988
2014
 
1989
- Instructions for the agent to follow when deploying a Harper application to the Harper Fabric cloud using the Harper CLI.
2015
+ Instructions for the agent to follow when deploying a Harper application to a remote Harper Fabric cloud instance.
1990
2016
 
1991
2017
  #### When to Use
1992
2018
 
1993
- Apply this rule when deploying a Harper application to a remote Harper instance or Harper Fabric cluster. This covers interactive deployments, CI/CD pipelines, and any scenario where the agent must push a local or remote package to a target environment.
2019
+ Apply this rule when deploying a Harper application to a remote Harper Fabric cluster or any remote Harper instance. Use it when setting up CI/CD pipelines that push application packages to a target environment, or when deploying from a local directory or external package source to a remote cluster.
1994
2020
 
1995
2021
  #### How It Works
1996
2022
 
1997
- 1. **Authenticate with the remote target**: Run `harper login` once to store an authentication token. The CLI writes `HARPER_CLI_TARGET` to a local `.env` so subsequent commands do not need credentials repeated. Find the **Application URL** on the cluster's **Config → Overview** page (see [creating-a-fabric-account-and-cluster.md](creating-a-fabric-account-and-cluster.md)).
2023
+ 1. **Obtain the target URL**: Get the cluster's **Application URL** from the cluster's **Config → Overview** page. This is the hostname passed to all CLI commands as `target`.
2024
+
2025
+ 2. **Authenticate with `harper login`**: Run `harper login` once to store an authentication token locally. The CLI also writes `HARPER_CLI_TARGET` to a local `.env` for subsequent commands.
1998
2026
 
1999
2027
  ```bash
2000
2028
  harper login <Application URL>
2001
2029
  # Provide cluster username and password when prompted
2002
2030
  ```
2003
2031
 
2004
- 2. **Deploy the application**: Run `harper deploy` with the required parameters. After logging in, no credentials are needed inline.
2032
+ See [creating-a-fabric-account-and-cluster.md](creating-a-fabric-account-and-cluster.md) for setting up a cluster before this step.
2033
+
2034
+ 3. **Deploy with `harper deploy`**: After logging in, deploy without repeating credentials.
2005
2035
 
2006
2036
  ```bash
2007
2037
  harper deploy \
@@ -2012,86 +2042,96 @@ Apply this rule when deploying a Harper application to a remote Harper instance
2012
2042
  replicated=true
2013
2043
  ```
2014
2044
 
2015
- 3. **Choose a package source**: Set the `package` parameter to any valid npm dependency value, or omit it to package and deploy the current local directory.
2016
-
2017
- | Value | Effect |
2018
- | ---------------------------------------------------- | ------------------------------------------------ |
2019
- | _(omitted)_ | Packages and deploys the current local directory |
2020
- | `"@harperdb/status-check"` | npm package |
2021
- | `"HarperDB/status-check"` | GitHub repo (short form) |
2022
- | `"https://github.com/HarperDB/status-check"` | GitHub repo (full URL) |
2023
- | `"git+ssh://git@github.com:HarperDB/secret-app.git"` | Private repo via SSH |
2024
- | `"https://example.com/application.tar.gz"` | Remote tarball |
2045
+ 4. **Use environment variables for CI/CD**: Instead of `harper login`, export credentials as environment variables before running `harper deploy`.
2025
2046
 
2026
- For git tags, use the `semver` directive for reliable versioning:
2027
-
2028
- ```
2029
- HarperDB/application-template#semver:v1.0.0
2047
+ ```bash
2048
+ export HARPER_CLI_USERNAME=<username>
2049
+ export HARPER_CLI_PASSWORD=<password>
2050
+ harper deploy \
2051
+ project=<name> \
2052
+ package=<package> \
2053
+ target=<remote> \
2054
+ restart=true \
2055
+ replicated=true
2030
2056
  ```
2031
2057
 
2032
- 4. **Authenticate for CI/CD pipelines**: Use environment variables instead of interactive login. Set credentials before running `harper deploy`.
2058
+ 5. **Use inline auth parameters for one-off commands**: Pass `auth_username` and `auth_password` directly. These take precedence over environment variables and saved login tokens. Not recommended for production.
2033
2059
 
2034
2060
  ```bash
2035
- export HARPER_CLI_USERNAME=<username>
2036
- export HARPER_CLI_PASSWORD=<password>
2037
2061
  harper deploy \
2038
2062
  project=<name> \
2039
2063
  package=<package> \
2064
+ auth_username=<username> \
2065
+ auth_password=<password> \
2040
2066
  target=<remote> \
2041
2067
  restart=true \
2042
2068
  replicated=true
2043
2069
  ```
2044
2070
 
2045
- 5. **Register SSH keys for private repos**: Before deploying from an SSH-based private repository, use the Add SSH Key operation to register the key with the remote instance.
2071
+ 6. **Choose a package source**: Set the `package` parameter to any valid npm dependency value, or omit it to package and deploy the current local directory.
2072
+
2073
+ | Value | Meaning |
2074
+ | ---------------------------------------------------- | ---------------------------------------------- |
2075
+ | _(omitted)_ | Package and deploy the current local directory |
2076
+ | `"@harperdb/status-check"` | npm package |
2077
+ | `"HarperDB/status-check"` | GitHub shorthand |
2078
+ | `"https://github.com/HarperDB/status-check"` | GitHub full URL |
2079
+ | `"git+ssh://git@github.com:HarperDB/secret-app.git"` | Private repo via SSH |
2080
+ | `"https://example.com/application.tar.gz"` | Remote tarball |
2081
+
2082
+ For pinned git tags, use the `semver` directive:
2083
+
2084
+ ```
2085
+ HarperDB/application-template#semver:v1.0.0
2086
+ ```
2087
+
2088
+ For SSH-based private repos, register keys with the `Add SSH Key` operation before deploying.
2046
2089
 
2047
2090
  #### Examples
2048
2091
 
2049
- **Interactive login then deploy (recommended):**
2092
+ **Interactive login then deploy:**
2050
2093
 
2051
2094
  ```bash
2052
- # Log in once
2053
- harper login <remote>
2054
- # Provide your username and password when prompted
2095
+ harper login https://my-cluster.harperdbcloud.com
2096
+ # Enter username and password when prompted
2055
2097
 
2056
- # Subsequently deploy without credentials
2057
2098
  harper deploy \
2058
- project=<name> \
2059
- package=<package> \
2060
- target=<remote> \
2099
+ project=my-app \
2100
+ package="HarperDB/application-template#semver:v1.0.0" \
2101
+ target=https://my-cluster.harperdbcloud.com \
2061
2102
  restart=true \
2062
2103
  replicated=true
2063
2104
  ```
2064
2105
 
2065
- **Deploy with inline credentials (not recommended for production):**
2106
+ **CI/CD pipeline deploy using environment variables:**
2066
2107
 
2067
2108
  ```bash
2109
+ export HARPER_CLI_USERNAME=admin
2110
+ export HARPER_CLI_PASSWORD=secret
2068
2111
  harper deploy \
2069
- project=<name> \
2070
- package=<package> \
2071
- username=<username> \
2072
- password=<password> \
2073
- target=<remote> \
2112
+ project=my-app \
2113
+ package="@harperdb/status-check" \
2114
+ target=https://my-cluster.harperdbcloud.com \
2074
2115
  restart=true \
2075
2116
  replicated=true
2076
2117
  ```
2077
2118
 
2078
- **Deploy a specific GitHub release by semver tag:**
2119
+ **Deploy current local directory:**
2079
2120
 
2080
2121
  ```bash
2081
2122
  harper deploy \
2082
2123
  project=my-app \
2083
- package="HarperDB/application-template#semver:v1.0.0" \
2084
- target=<remote> \
2124
+ target=https://my-cluster.harperdbcloud.com \
2085
2125
  restart=true \
2086
2126
  replicated=true
2087
2127
  ```
2088
2128
 
2089
2129
  #### Notes
2090
2130
 
2091
- - Always prefer `harper login` for interactive use and environment variables (`HARPER_CLI_USERNAME`, `HARPER_CLI_PASSWORD`) for CI/CD. Avoid inline `username`/`password` parameters in production.
2092
- - Omitting `package` causes the CLI to package the current local directory. Specifying a local file path creates a symlink, so changes are picked up between restarts without redeploying.
2093
- - Harper generates a `package.json` from component configurations and resolves dependencies using a form of `npm install`.
2094
- - For SSH-based private repos, register keys with the Add SSH Key operation before deploying.
2131
+ - Authentication precedence (highest to lowest): inline `auth_username`/`auth_password` parameters → environment variables (`HARPER_CLI_USERNAME`/`HARPER_CLI_PASSWORD`) → saved login token from `harper login`.
2132
+ - `harper login` writes `HARPER_CLI_TARGET` to a local `.env`, so subsequent commands do not need `target` repeated if that file is present.
2133
+ - Harper generates a `package.json` from component configurations and resolves packages via `npm install`. A local file path creates a symlink, so changes are picked up between restarts without redeploying.
2134
+ - For SSH-based private repos, register the SSH key with the `Add SSH Key` operation before running `harper deploy`.
2095
2135
 
2096
2136
  ### 4.2 Creating a Harper Fabric Account and Cluster
2097
2137
 
@@ -2434,18 +2474,18 @@ Instructions for the agent to follow when loading environment variables from `.e
2434
2474
 
2435
2475
  #### When to Use
2436
2476
 
2437
- Apply this rule when a Harper application needs to load secrets or configuration values from `.env` files into `process.env` at startup. Use it whenever hardcoding values must be avoided and environment-specific configuration must be supplied to Harper components.
2477
+ Apply this rule when a Harper application needs to supply secrets, API endpoints, or other configuration values to component code via `process.env` without hardcoding them. Use `loadEnv` any time you need to load one or more `.env` files at application startup.
2438
2478
 
2439
2479
  #### How It Works
2440
2480
 
2441
- 1. **Declare `loadEnv` in `config.yaml`**: Add `loadEnv` as a top-level key. It is built into Harper and requires no installation.
2481
+ 1. **Declare `loadEnv` in `config.yaml`**: Add `loadEnv` as the first entry in `config.yaml`. It is built into Harper and requires no installation.
2442
2482
 
2443
2483
  ```yaml
2444
2484
  loadEnv:
2445
2485
  files: '.env'
2446
2486
  ```
2447
2487
 
2448
- 2. **Place `loadEnv` first**: Always list `loadEnv` before any other components in `config.yaml` so that environment variables are available on `process.env` before dependent components start.
2488
+ 2. **Place `loadEnv` first**: Harper is a single-process application. List `loadEnv` before all other components so that environment variables are available on `process.env` before dependent components start.
2449
2489
 
2450
2490
  ```yaml
2451
2491
  # config.yaml — loadEnv must come first
@@ -2458,9 +2498,9 @@ Apply this rule when a Harper application needs to load secrets or configuration
2458
2498
  files: './src/*.js'
2459
2499
  ```
2460
2500
 
2461
- 3. **Configure the `files` option**: Provide one or more paths or glob patterns pointing to the env files to load. This option is required.
2501
+ 3. **Access loaded values in component code**: After `loadEnv` runs, all loaded values are available on `process.env` and shared across all components.
2462
2502
 
2463
- 4. **Set `override` if needed**: By default, existing environment variables take precedence over values in `.env` files. Set `override: true` to reverse this and have loaded values win.
2503
+ 4. **Control override behavior**: By default, existing environment variables take precedence over values in `.env` files. Set `override: true` to make loaded values win instead.
2464
2504
 
2465
2505
  ```yaml
2466
2506
  loadEnv:
@@ -2468,14 +2508,17 @@ Apply this rule when a Harper application needs to load secrets or configuration
2468
2508
  override: true
2469
2509
  ```
2470
2510
 
2471
- 5. **Load multiple files when required**: Supply a list of files or a glob pattern. Files are loaded in the order specified.
2511
+ 5. **Load multiple files**: Pass an array of paths or a glob pattern to `files`. Files are loaded in the order specified.
2512
+
2472
2513
  ```yaml
2473
2514
  loadEnv:
2474
2515
  files:
2475
2516
  - '.env'
2476
2517
  - '.env.local'
2477
2518
  ```
2478
- or
2519
+
2520
+ or with a glob:
2521
+
2479
2522
  ```yaml
2480
2523
  loadEnv:
2481
2524
  files: 'env-vars/*'
@@ -2490,17 +2533,23 @@ Apply this rule when a Harper application needs to load secrets or configuration
2490
2533
 
2491
2534
  #### Examples
2492
2535
 
2493
- **Minimal setup — single `.env` file:**
2536
+ **Single file, default behavior:**
2494
2537
 
2495
2538
  ```yaml
2539
+ # config.yaml
2496
2540
  loadEnv:
2497
2541
  files: '.env'
2542
+
2543
+ rest: true
2544
+
2545
+ myApp:
2546
+ files: './src/*.js'
2498
2547
  ```
2499
2548
 
2500
- **Full `config.yaml` with load order, multiple files, and override:**
2549
+ **Multiple files with override:**
2501
2550
 
2502
2551
  ```yaml
2503
- # config.yaml — loadEnv must come first
2552
+ # config.yaml
2504
2553
  loadEnv:
2505
2554
  files:
2506
2555
  - '.env'
@@ -2513,114 +2562,142 @@ myApp:
2513
2562
  files: './src/*.js'
2514
2563
  ```
2515
2564
 
2516
- **Glob pattern:**
2517
-
2518
- ```yaml
2519
- loadEnv:
2520
- files: 'env-vars/*'
2521
- ```
2522
-
2523
2565
  #### Notes
2524
2566
 
2525
- - `loadEnv` is built into Harper — do not install it separately; only declare it in `config.yaml`.
2526
- - Because Harper is a single-process application, variables loaded onto `process.env` are shared across all components.
2527
- - Without `override: true`, variables already set in the shell or container environment will not be overwritten by values in `.env` files.
2528
- - `files` is the only required option; omitting it will produce an invalid configuration.
2567
+ - `loadEnv` loads values into `process.env` for **application** code only — it does not configure Harper itself.
2568
+ - Harper's own instance-wide configuration is composed at startup **before** any component's `loadEnv` runs. Variables such as `HARPER_CONFIG`, `HARPER_SET_CONFIG`, and `HARPER_DEFAULT_CONFIG` delivered through a `.env` file are read too late and are ignored. Set Harper configuration directly in the configuration file or export variables in the real process/container environment before Harper starts.
2569
+ - For production credentials, prefer the encrypted secrets store over a committed `.env` file. Secrets are also delivered to components via `process.env`.
2529
2570
 
2530
- ### 4.7 Upgrading a Harper Application to v5
2571
+ ### 4.7 v5 Upgrade: Breaking Changes and Migration Guide
2531
2572
 
2532
- Instructions for the agent to follow when migrating an existing Harper application to version 5, addressing the breaking changes and adopting the recommended patterns.
2573
+ Instructions for the agent to apply when migrating a Harper application to v5, covering all breaking changes and required code updates.
2533
2574
 
2534
2575
  #### When to Use
2535
2576
 
2536
- Apply this rule when upgrading a Harper application from v4.x (or earlier) to v5, or when code written against older APIs behaves differently after a v5 upgrade — for example a record that can no longer be mutated, a query that returns stale data, or a `spawn` call that now throws. Harper v5 introduces breaking changes; applications built on documented APIs need the updates below, while code relying on undocumented behavior or timing may need broader review.
2577
+ Apply this rule when upgrading an existing Harper application to v5, when encountering runtime errors related to renamed packages, changed APIs, or security restrictions introduced in v5, or when reviewing application code for v5 compatibility before deployment.
2537
2578
 
2538
2579
  #### How It Works
2539
2580
 
2540
- 1. **Adopt the `harper` package name**: HarperDB is now Harper, and the package was renamed. Install the open source edition with `npm i -g harper` and the pro edition with `npm i -g @harperfast/harper-pro`. Update application imports from `harperdb` to `harper`:
2581
+ 1. **Update the package import from `harperdb` to `harper`**: All application code must import from `harper` instead of `harperdb`.
2541
2582
 
2542
2583
  ```javascript
2543
2584
  import { tables } from 'harper';
2544
2585
  ```
2545
2586
 
2546
- 2. **Opt in to install scripts when required**: Harper now installs packages with `--ignore-scripts` to guard against accidental script execution, a common security risk. If an application genuinely needs install scripts to run (for example to build native binaries), pass the `allowInstallScripts` option when deploying.
2587
+ 2. **Enable `allowInstallScripts` if packages require install scripts**: Harper v5 uses `--ignore-scripts` by default when installing packages. If your application requires installation scripts (e.g., to install additional binaries), set the `allowInstallScripts` option when deploying.
2547
2588
 
2548
- 3. **Update `Table.get` return-value handling**: `Table.get` now returns a plain, frozen record object rather than an instance of the table class, so instance methods are no longer available on the result. The commonly used `wasLoadedFromSource()` method is gone; cache-vs-origin information now lives on the request `target` as `target.loadedFromSource`. Pass a `RequestTarget` to `Table.get` and read the flag from it. Because the record is frozen, create a copy instead of mutating it in place.
2589
+ 3. **Update `Table.get` usage — return value is now a frozen record object**: `Table.get` now returns a plain record object, not a table class instance. The record is frozen; you cannot mutate it directly.
2590
+ - Replace `wasLoadedFromSource()` with `target.loadedFromSource`:
2549
2591
 
2550
- 4. **Account for automatic transaction context**: With RocksDB, transactions are fully supported by the storage engine, and Harper v5 uses asynchronous context tracking to carry the current transaction across calls automatically. A nested `Table.get` now reads within the current transaction's snapshot, so it will not observe newly written data until you commit. Access the context through `getContext()` and either commit the current transaction (`getContext().transaction.commit()`) or run the read inside a fresh `transaction(...)` to see the latest data. This matters most for code executing outside the context of a Harper request.
2592
+ ```javascript
2593
+ // Old — remove this pattern:
2594
+ const record = await Table.get(id);
2595
+ if (record.wasLoadedFromSource()) { ... }
2551
2596
 
2552
- 5. **Register spawnable commands**: Spawning processes is now tightly controlled. `spawn`, `exec`, and `execFile` may only run executables listed in the `applications.allowedSpawnCommands` configuration, and each call must pass a `name` in its `options` so that only a single named process is started across Harper's multiple threads. Use a distinct `name` when you deliberately need a separate process.
2597
+ // New — use loadedFromSource on the target:
2598
+ const target = new RequestTarget();
2599
+ target.id = id;
2600
+ const record = await Table.get(target);
2601
+ if (target.loadedFromSource) {
2602
+ // record was loaded from origin (not cache)
2603
+ }
2604
+ ```
2553
2605
 
2554
- 6. **Return Response-like objects to set headers**: If a REST method returns an object with a `headers` property, Harper uses it as the response headers.
2606
+ - Replace in-place mutation with object spread, since records are frozen:
2555
2607
 
2556
- 7. **Replace `blob.save()`**: The `blob.save()` method has been removed. Pass the `saveBeforeCommit` flag in the options to the `Blob` constructor instead.
2608
+ ```javascript
2609
+ // Old — throws in v5:
2610
+ const record = await Table.get(id);
2611
+ record.property = 'changed';
2557
2612
 
2558
- 8. **Configure the VM module loader**: v5 loads application modules through Node.js's VM module API, giving each application its own module cache and an application-scoped `harper` module. All of this is controlled by the `applications` section of `harperdb-config.yaml`:
2559
- - `moduleLoader` — `vm-current-context` (default), `vm`, `native`, or `compartment`. The default shares intrinsics with Harper for best compatibility; choose `vm` only if you need per-application intrinsics, and `native` to disable the VM loader entirely (restores pre-v5 loading, but per-app `logger`/`config` context is unavailable).
2560
- - `lockdown` — `freeze-after-load` (default), `freeze`, `ses`, or `none`. Freezing intrinsics prevents prototype-pollution attacks; set `lockdown: none` only as a temporary workaround if a dependency mutates built-in prototypes.
2561
- - `allowedDirectory` — `app` (default) restricts module loading to the application's own directory tree; set `any` if the app must load files from outside it in production.
2562
- - `allowedBuiltinModules` — an optional allowlist restricting which Node.js built-ins the application may import (all are allowed if omitted).
2563
- - `dependencyLoader` — `auto` (default), `app`, or `native`, controlling how npm dependencies are loaded through or around the VM loader.
2613
+ // New — copy instead of mutate:
2614
+ let record = await Table.get(id);
2615
+ record = { ...record, property: 'changed' };
2616
+ ```
2564
2617
 
2565
- #### Examples
2618
+ - `getUpdatedTime` and `getExpiresAt` methods remain available on the record object.
2566
2619
 
2567
- **Updating `Table.get` and its cache check:**
2620
+ 4. **Update transaction and context handling**: Harper v5 uses asynchronous context tracking. `Table.get` and other table calls now automatically inherit the current transaction context. Code that previously omitted context to bypass a transaction will no longer work as expected. Use `getContext` (imported from `harper`) to access and commit the current transaction explicitly when you need to see updated data.
2568
2621
 
2569
- ```javascript
2570
- // Old (v4.x)
2571
- const record = await Table.get(id);
2572
- if (record.wasLoadedFromSource()) {
2573
- // loaded from origin, not cache
2574
- }
2622
+ ```javascript
2623
+ import { setTimeout as delay } from 'node:timers/promises';
2624
+ import { getContext, transaction } from 'harper';
2575
2625
 
2576
- // New (v5)
2577
- const target = new RequestTarget(); // passed in when overriding `get`
2578
- target.id = id;
2579
- const record = await Table.get(target);
2580
- if (target.loadedFromSource) {
2581
- // loaded from origin, not cache
2582
- }
2583
- ```
2626
+ class MyResource {
2627
+ static async get(target) {
2628
+ // Commit the current transaction to read latest data:
2629
+ await getContext().transaction.commit();
2630
+ // Optionally wrap each poll in a new transaction:
2631
+ while ((await transaction(() => Table.get(target))).status !== 'ready') {
2632
+ await delay(100);
2633
+ }
2634
+ return Table.get(target);
2635
+ }
2636
+ }
2637
+ ```
2584
2638
 
2585
- **Committing the transaction to read fresh data:**
2639
+ 5. **Register allowed spawn commands via `allowedSpawnCommands`**: Any use of `spawn`, `exec`, or `execFile` from `node:child_process` must reference executables listed in `applications.allowedSpawnCommands` in `harperdb-config.yaml`. Provide a `name` property in the options argument to ensure only a single named process is started across threads.
2586
2640
 
2587
- ```javascript
2588
- import { setTimeout as delay } from 'node:timers/promises';
2589
- import { getContext, transaction } from 'harper';
2641
+ 6. **Replace `blob.save()` with `saveBeforeCommit`**: The `blob.save()` method has been removed. Use the `saveBeforeCommit` flag in the options passed to the `Blob` constructor instead.
2590
2642
 
2591
- class MyResource {
2592
- static async get(target) {
2593
- // The current transaction is a consistent snapshot; commit it to see updates.
2594
- await getContext().transaction.commit();
2595
- while ((await transaction(() => Table.get(target))).status !== 'ready') {
2596
- delay(100);
2597
- }
2598
- return Table.get(target);
2599
- }
2600
- }
2601
- ```
2643
+ 7. **Configure the `moduleLoader` and `lockdown` settings**: Harper v5 loads application modules through Node.js's VM module API. Control this behavior in `harperdb-config.yaml` under the `applications` key.
2602
2644
 
2603
- **Module loader and security configuration in `harperdb-config.yaml`:**
2645
+ | Setting | Default | Options |
2646
+ | ------------------ | -------------------- | --------------------------------------------------- |
2647
+ | `moduleLoader` | `vm-current-context` | `vm-current-context`, `vm`, `native`, `compartment` |
2648
+ | `lockdown` | `freeze-after-load` | `freeze-after-load`, `freeze`, `ses`, `none` |
2649
+ | `dependencyLoader` | `auto` | `auto`, `app`, `native` |
2650
+ | `allowedDirectory` | `app` | `app`, `any` |
2651
+ - Use `moduleLoader: native` to disable the VM loader entirely and restore pre-v5 behavior (application-specific context such as tagged logging and per-app `config` will not be available).
2652
+ - Use `lockdown: none` as a temporary workaround if a dependency modifies intrinsic prototypes at runtime and throws a `TypeError`.
2653
+
2654
+ #### Examples
2655
+
2656
+ ##### Full `harperdb-config.yaml` `applications` block
2604
2657
 
2605
2658
  ```yaml
2606
2659
  applications:
2607
- lockdown: freeze-after-load # default
2608
- moduleLoader: vm-current-context # vm-current-context (default) | vm | native | compartment
2609
- dependencyLoader: auto # auto (default) | app | native
2610
- allowedDirectory: app # app (default) | any
2660
+ lockdown: freeze-after-load
2661
+ moduleLoader: vm-current-context
2662
+ dependencyLoader: auto
2663
+ allowedDirectory: app
2611
2664
  allowedSpawnCommands:
2612
2665
  - npm
2613
2666
  - node
2614
- # allowedBuiltinModules: [] # if omitted, all Node.js built-ins are allowed
2615
2667
  ```
2616
2668
 
2617
- #### Recommended Changes
2669
+ ##### Restricting allowed built-in modules
2618
2670
 
2619
- Beyond the required fixes above, Harper v5 encourages these patterns for new and migrated code:
2671
+ ```yaml
2672
+ applications:
2673
+ allowedBuiltinModules:
2674
+ - fs
2675
+ - path
2676
+ - http
2677
+ ```
2678
+
2679
+ ##### Disabling the VM loader for package compatibility
2680
+
2681
+ ```yaml
2682
+ applications:
2683
+ moduleLoader: native
2684
+ ```
2685
+
2686
+ ##### Accessing context and committing a transaction
2687
+
2688
+ ```javascript
2689
+ import { getContext } from 'harper';
2690
+
2691
+ await getContext().transaction.commit();
2692
+ ```
2693
+
2694
+ #### Notes
2620
2695
 
2621
- - Implement endpoints with `static` methods on Resources/Tables, reading request information from the request `target` argument (or from `getContext()`).
2622
- - Rely on automatic context propagation rather than threading context through every call manually; access it via `getContext()` exported from `harper`.
2623
- - Access Harper functions and APIs through the `harper` package rather than through global variables.
2696
+ - The `lockdown: freeze-after-load` default freezes `Object`, `Array`, `Promise`, `Map`, `Set`, and other intrinsics after all application code loads. Any code or dependency that mutates intrinsic prototypes after startup will throw a `TypeError`.
2697
+ - Under `lockdown: ses`, the constrained `fetch` (https-only) is applied only in `vm` mode. In `vm-current-context` and `native` modes, application code uses the standard global `fetch`.
2698
+ - In production, `allowedDirectory: app` prevents loading modules from outside the application's own directory tree. Set `allowedDirectory: any` only if your application legitimately requires it.
2699
+ - `dependencyLoader: native` is a narrower alternative to `moduleLoader: native` — it uses native loading only for npm packages while keeping the VM loader for first-party application source files.
2700
+ - Access Harper functions and APIs through the `harper` package rather than through global variables. Use `getContext` from `harper` to access request context without passing it explicitly through every call.
2624
2701
 
2625
2702
  ### 4.8 Delegating to the Built-in Agent
2626
2703