tjs-lang 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CONTEXT.md +594 -0
  2. package/LICENSE +190 -0
  3. package/README.md +220 -0
  4. package/bin/benchmarks.ts +351 -0
  5. package/bin/dev.ts +205 -0
  6. package/bin/docs.js +170 -0
  7. package/bin/install-cursor.sh +71 -0
  8. package/bin/install-vscode.sh +71 -0
  9. package/bin/select-local-models.d.ts +1 -0
  10. package/bin/select-local-models.js +28 -0
  11. package/bin/select-local-models.ts +31 -0
  12. package/demo/autocomplete.test.ts +232 -0
  13. package/demo/docs.json +186 -0
  14. package/demo/examples.test.ts +598 -0
  15. package/demo/index.html +91 -0
  16. package/demo/src/autocomplete.ts +482 -0
  17. package/demo/src/capabilities.ts +859 -0
  18. package/demo/src/demo-nav.ts +2097 -0
  19. package/demo/src/examples.test.ts +161 -0
  20. package/demo/src/examples.ts +476 -0
  21. package/demo/src/imports.test.ts +196 -0
  22. package/demo/src/imports.ts +421 -0
  23. package/demo/src/index.ts +639 -0
  24. package/demo/src/module-store.ts +635 -0
  25. package/demo/src/module-sw.ts +132 -0
  26. package/demo/src/playground.ts +949 -0
  27. package/demo/src/service-host.ts +389 -0
  28. package/demo/src/settings.ts +440 -0
  29. package/demo/src/style.ts +280 -0
  30. package/demo/src/tjs-playground.ts +1605 -0
  31. package/demo/src/ts-examples.ts +478 -0
  32. package/demo/src/ts-playground.ts +1092 -0
  33. package/demo/static/favicon.svg +30 -0
  34. package/demo/static/photo-1.jpg +0 -0
  35. package/demo/static/photo-2.jpg +0 -0
  36. package/demo/static/texts/ai-history.txt +9 -0
  37. package/demo/static/texts/coffee-origins.txt +9 -0
  38. package/demo/static/texts/renewable-energy.txt +9 -0
  39. package/dist/index.js +256 -0
  40. package/dist/index.js.map +37 -0
  41. package/dist/tjs-batteries.js +4 -0
  42. package/dist/tjs-batteries.js.map +15 -0
  43. package/dist/tjs-full.js +256 -0
  44. package/dist/tjs-full.js.map +37 -0
  45. package/dist/tjs-transpiler.js +220 -0
  46. package/dist/tjs-transpiler.js.map +21 -0
  47. package/dist/tjs-vm.js +4 -0
  48. package/dist/tjs-vm.js.map +14 -0
  49. package/docs/CNAME +1 -0
  50. package/docs/favicon.svg +30 -0
  51. package/docs/index.html +91 -0
  52. package/docs/index.js +10468 -0
  53. package/docs/index.js.map +92 -0
  54. package/docs/photo-1.jpg +0 -0
  55. package/docs/photo-1.webp +0 -0
  56. package/docs/photo-2.jpg +0 -0
  57. package/docs/photo-2.webp +0 -0
  58. package/docs/texts/ai-history.txt +9 -0
  59. package/docs/texts/coffee-origins.txt +9 -0
  60. package/docs/texts/renewable-energy.txt +9 -0
  61. package/docs/tjs-lang.svg +31 -0
  62. package/docs/tosijs-agent.svg +31 -0
  63. package/editors/README.md +325 -0
  64. package/editors/ace/ajs-mode.js +328 -0
  65. package/editors/ace/ajs-mode.ts +269 -0
  66. package/editors/ajs-syntax.ts +212 -0
  67. package/editors/build-grammars.ts +510 -0
  68. package/editors/codemirror/ajs-language.js +287 -0
  69. package/editors/codemirror/ajs-language.ts +1447 -0
  70. package/editors/codemirror/autocomplete.test.ts +531 -0
  71. package/editors/codemirror/component.ts +404 -0
  72. package/editors/monaco/ajs-monarch.js +243 -0
  73. package/editors/monaco/ajs-monarch.ts +225 -0
  74. package/editors/tjs-syntax.ts +115 -0
  75. package/editors/vscode/language-configuration.json +37 -0
  76. package/editors/vscode/package.json +65 -0
  77. package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
  78. package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
  79. package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
  80. package/package.json +83 -0
  81. package/src/cli/commands/check.ts +41 -0
  82. package/src/cli/commands/convert.ts +133 -0
  83. package/src/cli/commands/emit.ts +260 -0
  84. package/src/cli/commands/run.ts +68 -0
  85. package/src/cli/commands/test.ts +194 -0
  86. package/src/cli/commands/types.ts +20 -0
  87. package/src/cli/create-app.ts +236 -0
  88. package/src/cli/playground.ts +250 -0
  89. package/src/cli/tjs.ts +166 -0
  90. package/src/cli/tjsx.ts +160 -0
  91. package/tjs-lang.svg +31 -0
package/CONTEXT.md ADDED
@@ -0,0 +1,594 @@
1
+ <!--{"section": "ajs", "group": "docs", "order": 1, "navTitle": "Technical Context"}-->
2
+
3
+ # tjs-lang Technical Context
4
+
5
+ **Note:** This document provides a technical deep-dive into tjs-lang's architecture and security model. For a general overview, installation instructions, and usage examples, please refer to the main [README.md](./README.md).
6
+
7
+ **tjs-lang** is a secure, environment-agnostic runtime for executing AI agents and logic chains defined as JSON ASTs.
8
+
9
+ **Bundle Size:** ~17KB gzipped. Expressions are evaluated via lightweight AST nodes at runtime, eliminating the need for a parser library (the previous JSEP-based approach was ~50KB gzipped).
10
+
11
+ ## 1. Architecture
12
+
13
+ ### The Builder (`TypedBuilder`)
14
+
15
+ A fluent TypeScript API that generates a portable JSON AST. It uses a `Proxy` to dynamically infer methods from the registered Atoms, providing a strongly-typed developer experience.
16
+
17
+ It is important to understand that the builder is only for constructing the AST; it does not contain any of the actual implementation logic for the atoms. All execution is handled by the Runtime.
18
+
19
+ **Usage Pattern:**
20
+
21
+ - All logic chains **must** start with `Agent.take()` to define the input schema for the agent.
22
+ - Subsequent atom calls are chained together fluently (e.g., `.varSet(...)`, `.httpFetch(...)`). This creates an implicit `seq` (sequence) of operations.
23
+ - The chain is terminated by calling `.toJSON()` to produce the serializable AST.
24
+
25
+ You can access the builder via `Agent` (for core atoms) or `vm.Agent` (the recommended way to access both core and any custom atoms registered with the VM instance).
26
+
27
+ ```typescript
28
+ import { Agent, s, AgentVM } from 'tjs-lang'
29
+
30
+ // Global Builder (Core Atoms)
31
+ const logic = Agent.take(s.object({ input: s.string }))
32
+ .varSet({ key: 'sum', value: { $expr: 'binary', op: '+', left: { $expr: 'literal', value: 1 }, right: { $expr: 'literal', value: 1 } } })
33
+
34
+ // VM Builder (Custom Atoms)
35
+ const vm = new AgentVM({ myAtom })
36
+ const customLogic = vm.Agent
37
+ .myAtom({ ... })
38
+ .varSet({ ... })
39
+ ```
40
+
41
+ ### The Runtime (`AgentVM`)
42
+
43
+ A stateless Virtual Machine that executes the AST. The runtime contains all the actual implementation logic for the atoms.
44
+
45
+ - **Sandboxed:** No `eval()`. Math/Logic is evaluated safely via AST expression nodes.
46
+ - **Resource Limited:** Enforces `fuel` (gas) limits and execution timeouts per atom.
47
+ - **Capability-Based:** All IO (Network, DB, AI) must be injected via `capabilities` object.
48
+
49
+ ```typescript
50
+ import { AgentVM } from 'tjs-lang'
51
+ const vm = new AgentVM()
52
+ const { result, fuelUsed } = await vm.run(ast, args, {
53
+ capabilities,
54
+ fuel: 1000,
55
+ })
56
+ ```
57
+
58
+ ## 2. Expression Syntax (ExprNode)
59
+
60
+ Expressions use AST expression nodes (`$expr`) for safe, sandboxed evaluation. Conditions in `if` and `while` atoms use expression strings that are parsed at transpile time.
61
+
62
+ For security, expressions are sandboxed and cannot directly access the agent's state. Use the `vars` parameter to explicitly pass variables from state into the expression scope.
63
+
64
+ ### ExprNode Types
65
+
66
+ - **literal:** `{ $expr: 'literal', value: 5 }` - A constant value
67
+ - **ident:** `{ $expr: 'ident', name: 'x' }` - A variable reference
68
+ - **member:** `{ $expr: 'member', object: {...}, property: 'foo' }` - Property access
69
+ - **binary:** `{ $expr: 'binary', op: '+', left: {...}, right: {...} }` - Binary operations
70
+ - **unary:** `{ $expr: 'unary', op: '-', argument: {...} }` - Unary operations
71
+ - **conditional:** `{ $expr: 'conditional', test: {...}, consequent: {...}, alternate: {...} }` - Ternary
72
+
73
+ ### Supported Operators
74
+
75
+ - **Binary ops:** `+`, `-`, `*`, `/`, `%`, `**`
76
+ - **Logic:** `&&`, `||`
77
+ - **Comparison:** `==`, `!=`, `>`, `<`, `>=`, `<=`
78
+ - **Member Access:** `obj.prop`, `arr[0]`
79
+
80
+ ### Security
81
+
82
+ - **Forbidden:** Function calls, `new`, `this`, global access
83
+ - **Blocked:** Prototype access (`__proto__`, `constructor`)
84
+
85
+ ### Fuel Consumption
86
+
87
+ Each expression node evaluation consumes **0.01 fuel**. This prevents deeply nested or recursive expressions from running unchecked. A simple `a + b` costs ~0.03 fuel (two identifiers + one binary op), while complex nested expressions accumulate cost proportionally.
88
+
89
+ ## 3. Security Model
90
+
91
+ - **Capabilities:** The VM has no default IO. You must provide `fetch`, `store`, etc., allowing you to mock, proxy, or limit access.
92
+ - **Fuel:** Every atom consumes "fuel". Execution aborts if fuel reaches 0.
93
+ - **Execution Timeout:** The VM enforces a global timeout based on fuel budget (see below).
94
+ - **Atom Timeouts:** Individual atoms have a default timeout (1s) to prevent hangs.
95
+ - **State Isolation:** Each run creates a fresh context. Scopes (loops/maps) use prototype inheritance to isolate local variables.
96
+
97
+ ### Execution Timeout
98
+
99
+ The VM enforces a hard timeout on execution to prevent hung agents—safeguarding against code that effectively halts by waiting on slow or non-responsive IO.
100
+
101
+ **How it works:**
102
+
103
+ 1. **Automatic Safety Net:** By default, timeout = `fuel × 10ms`. So 1000 fuel = 10 seconds. _For IO-heavy agents with low fuel costs, explicitly set `timeoutMs` to prevent premature timeouts._
104
+ 2. **Explicit SLA:** Pass `timeoutMs` to enforce a strict time limit regardless of fuel.
105
+ 3. **External Cancellation:** Pass an `AbortSignal` to integrate with external controllers (user cancellation, HTTP timeouts, etc.).
106
+
107
+ ```typescript
108
+ // Default: 1000 fuel = 10 second timeout
109
+ await vm.run(ast, args, { fuel: 1000 })
110
+
111
+ // Explicit timeout: 5 seconds regardless of fuel
112
+ await vm.run(ast, args, { fuel: 10000, timeoutMs: 5000 })
113
+
114
+ // External abort signal
115
+ const controller = new AbortController()
116
+ setTimeout(() => controller.abort(), 3000) // Cancel after 3s
117
+ await vm.run(ast, args, { signal: controller.signal })
118
+ ```
119
+
120
+ **Resource Cleanup:** When a timeout occurs, the VM passes the abort signal to the currently executing atom via `ctx.signal`. Loop atoms (`while`, `map`, `filter`, `reduce`, `find`) check the signal between iterations. `httpFetch` passes the signal to `fetch` for immediate request cancellation.
121
+
122
+ **Timeout vs Fuel:**
123
+
124
+ - **Fuel** protects against CPU-bound abuse (tight loops burning compute)
125
+ - **Timeout** protects against IO-bound abuse (slow network calls, hung promises)
126
+
127
+ Both work together to ensure the VM cannot be held hostage by untrusted code.
128
+
129
+ **Trust Boundary:** The sandbox protects against malicious _agents_ (untrusted AST), not malicious _atom implementations_. Atoms are registered by the host and are trusted to:
130
+
131
+ 1. Be non-blocking (no synchronous CPU-heavy work)
132
+ 2. Respect `ctx.signal` for cancellation
133
+ 3. Clean up resources when aborted
134
+
135
+ If you write custom atoms, ensure they check `ctx.signal?.aborted` in loops and pass `ctx.signal` to any async operations like `fetch`.
136
+
137
+ ### Cost Overrides
138
+
139
+ Default atom costs are guesses. Override them per-run to match your deployment reality:
140
+
141
+ ```typescript
142
+ await vm.run(ast, args, {
143
+ costOverrides: {
144
+ // Static: fixed cost per invocation
145
+ httpFetch: 50,
146
+ llmPredict: 500,
147
+
148
+ // Dynamic: cost based on input
149
+ storeSet: (input) => JSON.stringify(input.value).length * 0.001,
150
+ llmPredict: (input) => (input.model?.includes('gpt-4') ? 1000 : 100),
151
+ },
152
+ })
153
+ ```
154
+
155
+ Use cases:
156
+
157
+ - **API rate limits:** Make external API calls expensive to stay under quota
158
+ - **Metered billing:** Reflect actual dollar costs in fuel consumption
159
+ - **Resource protection:** Make database writes cost more than reads
160
+ - **Testing:** Set all costs to 0 to focus on logic, not budgeting
161
+
162
+ ### Request Context
163
+
164
+ The `context` option passes request-scoped metadata to atoms. Unlike `args` (agent input) or `capabilities` (IO implementations), context carries ambient data like auth, permissions, and request tracing.
165
+
166
+ ```typescript
167
+ await vm.run(ast, args, {
168
+ context: {
169
+ userId: 'user-123',
170
+ role: 'admin',
171
+ permissions: ['read:data', 'write:data', 'fetch:external'],
172
+ requestId: 'req-abc-123',
173
+ },
174
+ })
175
+ ```
176
+
177
+ Atoms access it via `ctx.context`:
178
+
179
+ ```typescript
180
+ const secureFetch = defineAtom(
181
+ 'secureFetch',
182
+ s.object({ url: s.string }),
183
+ s.any,
184
+ async (input, ctx) => {
185
+ // Check permissions
186
+ if (!ctx.context?.permissions?.includes('fetch:external')) {
187
+ throw new Error('Not authorized for external fetch')
188
+ }
189
+ return ctx.capabilities.fetch(input.url)
190
+ }
191
+ )
192
+ ```
193
+
194
+ **Design rationale:**
195
+
196
+ - **Immutable:** Context is read-only; agents cannot modify their own permissions
197
+ - **Separate from args:** Auth data doesn't pollute the agent's input schema
198
+ - **Separate from capabilities:** Same capability implementation, different authorization
199
+ - **Composable:** Works with cost overrides for user-tier-based fuel costs
200
+
201
+ **Production patterns:**
202
+
203
+ ```typescript
204
+ // Firebase/Express integration
205
+ app.post('/run-agent', async (req, res) => {
206
+ const ast = req.body.ast
207
+ const args = req.body.args
208
+
209
+ // Extract auth from request
210
+ const user = await verifyToken(req.headers.authorization)
211
+
212
+ const result = await vm.run(ast, args, {
213
+ context: {
214
+ userId: user.id,
215
+ role: user.role,
216
+ permissions: user.permissions,
217
+ requestId: req.id,
218
+ },
219
+ // User-tier-based costs
220
+ costOverrides: {
221
+ llmPredict: user.tier === 'premium' ? 10 : 100,
222
+ },
223
+ })
224
+
225
+ res.json(result)
226
+ })
227
+ ```
228
+
229
+ ## 4. Stored Procedures
230
+
231
+ The procedure store provides a built-in mechanism for storing ASTs as callable tokens. This enables function-pointer-like patterns where behavior can be passed as data.
232
+
233
+ ### Storage Model
234
+
235
+ ```typescript
236
+ // Module-level storage in runtime.ts
237
+ const procedureStore = new Map<
238
+ string,
239
+ {
240
+ ast: any
241
+ createdAt: number
242
+ expiresAt: number
243
+ }
244
+ >()
245
+ ```
246
+
247
+ **Constants:**
248
+
249
+ - `PROCEDURE_TOKEN_PREFIX`: `'proc_'` - All tokens start with this prefix
250
+ - `DEFAULT_PROCEDURE_TTL`: 3,600,000ms (1 hour)
251
+ - `DEFAULT_MAX_AST_SIZE`: 102,400 bytes (100KB)
252
+
253
+ ### Token Resolution
254
+
255
+ Tokens can be used anywhere an AST is accepted:
256
+
257
+ 1. **`vm.run(token, args)`** - Direct execution via VM
258
+ 2. **`agentRun({ agentId: token, input })`** - Execution from within an agent
259
+ 3. **`agentRun({ agentId: ast, input })`** - Raw AST also accepted (no storage needed)
260
+
261
+ Resolution happens at runtime. If a string starts with `proc_`, the VM looks it up in the store. Expired or missing tokens throw clear errors.
262
+
263
+ ### Fuel Costs
264
+
265
+ | Atom | Cost | Notes |
266
+ | ------------------------ | ---- | ------------------------------- |
267
+ | `storeProcedure` | 1.0 | Plus 0.001 per byte of AST |
268
+ | `releaseProcedure` | 0.5 | Constant |
269
+ | `clearExpiredProcedures` | 0.5 | Plus 0.01 per procedure scanned |
270
+
271
+ ### Security Considerations
272
+
273
+ **Memory bounds:** The `maxSize` parameter prevents storing arbitrarily large ASTs. Default 100KB is generous for most agents.
274
+
275
+ **Expiry:** TTL prevents memory leaks from abandoned procedures. The store is in-memory, so procedures don't survive process restarts.
276
+
277
+ **No capability escalation:** Stored procedures inherit the capabilities of the calling context, not the storing context. A malicious agent cannot store a procedure that later executes with elevated privileges.
278
+
279
+ **Token predictability:** Tokens are UUIDs, not sequential. They cannot be enumerated or guessed.
280
+
281
+ ### Use Cases
282
+
283
+ **Dynamic dispatch (strategy pattern):**
284
+
285
+ ```typescript
286
+ const strategies = ajs`
287
+ function dispatch({ strategyToken, data }) {
288
+ let result = agentRun({ agentId: strategyToken, input: { data } })
289
+ return result
290
+ }
291
+ `
292
+ ```
293
+
294
+ **Worker pool:**
295
+
296
+ ```typescript
297
+ const orchestrator = ajs`
298
+ function orchestrate({ workers, tasks }) {
299
+ let results = []
300
+ for (let i = 0; i < tasks.length; i = i + 1) {
301
+ let workerToken = workers[i % workers.length]
302
+ let r = agentRun({ agentId: workerToken, input: tasks[i] })
303
+ results.push(r)
304
+ }
305
+ return { results }
306
+ }
307
+ `
308
+ ```
309
+
310
+ **Callback registration:**
311
+
312
+ ```typescript
313
+ const registerCallback = ajs`
314
+ function register({ handler }) {
315
+ let token = storeProcedure({ ast: handler, ttl: 300000 })
316
+ return { callbackId: token }
317
+ }
318
+ `
319
+ ```
320
+
321
+ ## 5. Production Considerations
322
+
323
+ ### Recursive Agent Fuel
324
+
325
+ When an agent calls sub-agents via `agentRun`, each sub-agent gets its own fuel budget (passed via the capability). Fuel is **not shared** across the call tree by default.
326
+
327
+ **Why:** The `agentRun` atom delegates to `ctx.capabilities.agent.run`, which the host implements. This gives operators full control over sub-agent resource allocation.
328
+
329
+ **Patterns for shared fuel:**
330
+
331
+ ```typescript
332
+ // Option 1: Pass remaining fuel to children
333
+ const sharedFuel = { current: 1000 }
334
+
335
+ const caps = {
336
+ agent: {
337
+ run: async (agentId, input) => {
338
+ if (sharedFuel.current <= 0) throw new Error('Out of shared fuel')
339
+ const result = await vm.run(agents[agentId], input, {
340
+ fuel: sharedFuel.current,
341
+ capabilities: caps,
342
+ })
343
+ sharedFuel.current -= result.fuelUsed
344
+ return result.result
345
+ },
346
+ },
347
+ }
348
+
349
+ // Option 2: Fixed budget per recursion depth
350
+ const caps = {
351
+ agent: {
352
+ run: async (agentId, input) => {
353
+ // Each child gets 10% of parent's budget
354
+ return vm.run(agents[agentId], input, {
355
+ fuel: 100, // Fixed small budget
356
+ capabilities: caps,
357
+ })
358
+ },
359
+ },
360
+ }
361
+ ```
362
+
363
+ ### Streaming and Long-Running Agents
364
+
365
+ The VM returns results only after complete execution. For long-running agents:
366
+
367
+ - Use `timeoutMs` to enforce SLAs
368
+ - Use `AbortSignal` for user-initiated cancellation
369
+ - Use `trace: true` for post-hoc debugging
370
+
371
+ **For real-time streaming**, implement a custom atom that emits intermediate results:
372
+
373
+ ```typescript
374
+ const streamingAtom = defineAtom(
375
+ 'streamChunk',
376
+ s.object({ data: s.any }),
377
+ s.null,
378
+ async ({ data }, ctx) => {
379
+ // ctx.context contains your streaming callback
380
+ await ctx.context?.onChunk?.(data)
381
+ return null
382
+ }
383
+ )
384
+
385
+ // Usage
386
+ await vm.run(ast, args, {
387
+ context: {
388
+ onChunk: (data) => res.write(JSON.stringify(data) + '\n'),
389
+ },
390
+ })
391
+ ```
392
+
393
+ ### Condition String Syntax
394
+
395
+ The condition parser in `if`/`while` atoms supports a subset of expression syntax:
396
+
397
+ | Supported | Example |
398
+ | ------------- | --------------------------------- |
399
+ | Comparisons | `a > b`, `x == 'hello'`, `n != 0` |
400
+ | Logical | `a && b`, `a \|\| b`, `!a` |
401
+ | Arithmetic | `a + b * c`, `(a + b) / c` |
402
+ | Member access | `obj.foo.bar` |
403
+ | Literals | `42`, `"string"`, `true`, `null` |
404
+
405
+ | **Unsupported** | Alternative |
406
+ | ---------------------- | ---------------------------------- |
407
+ | Ternary `a ? b : c` | Use nested `if` atoms |
408
+ | Array index `a[0]` | Use ExprNode with `computed: true` |
409
+ | Function calls `fn(x)` | Use atoms |
410
+ | Chained `a > b > c` | Use `a > b && b > c` |
411
+
412
+ Unsupported syntax now throws a clear error at build time with suggestions.
413
+
414
+ ### State Semantics
415
+
416
+ Agents are **not transactional**. If an atom fails mid-execution:
417
+
418
+ - Previous state changes persist
419
+ - No automatic rollback
420
+ - Error is captured in monadic flow (`ctx.error`)
421
+
422
+ This is by design—agents are stateful pipelines, not database transactions. If you need atomicity, implement checkpoint/restore in your capabilities:
423
+
424
+ ```typescript
425
+ const caps = {
426
+ store: {
427
+ set: async (key, value) => {
428
+ await db.runTransaction(async (tx) => {
429
+ await tx.set(key, value)
430
+ })
431
+ },
432
+ },
433
+ }
434
+ ```
435
+
436
+ ### Error Handling Granularity
437
+
438
+ The `try/catch` atom catches all errors in the try block. There's no selective catch by error type.
439
+
440
+ **Pattern for error type handling:**
441
+
442
+ ```typescript
443
+ Agent.take(s.object({})).try({
444
+ try: (b) => b.httpFetch({ url: '...' }).as('response'),
445
+ catch: (b) =>
446
+ b
447
+ .varSet({ key: 'errorType', value: 'unknown' })
448
+ // Check error message patterns
449
+ .if(
450
+ 'msg.includes("timeout")',
451
+ { msg: 'error.message' },
452
+ (then) => then.varSet({ key: 'errorType', value: 'timeout' }),
453
+ (el) =>
454
+ el.if('msg.includes("404")', { msg: 'error.message' }, (then) =>
455
+ then.varSet({ key: 'errorType', value: 'not_found' })
456
+ )
457
+ ),
458
+ })
459
+ ```
460
+
461
+ ## 9. Test Coverage
462
+
463
+ **Summary (as of January 2025):**
464
+
465
+ | Metric | Value |
466
+ | --------- | ------ |
467
+ | Tests | 508 |
468
+ | Functions | 84.77% |
469
+ | Lines | 80.36% |
470
+
471
+ ### Coverage by Component
472
+
473
+ **Core Runtime (security-critical):**
474
+
475
+ | File | Functions | Lines | Notes |
476
+ | ------------------------- | --------- | ------- | ---------------------------- |
477
+ | `src/runtime.ts` | 100% | 100% | Re-exports |
478
+ | `src/vm.ts` | 100% | 100% | Re-exports |
479
+ | `src/vm/runtime.ts` | 84% | **98%** | Atoms, expression eval, fuel |
480
+ | `src/vm/vm.ts` | 90% | 94% | VM entry point |
481
+ | `src/transpiler/index.ts` | 100% | 100% | AJS transpiler |
482
+ | `src/builder.ts` | 92% | 90% | Fluent builder |
483
+
484
+ **Language/Transpiler:**
485
+
486
+ | File | Functions | Lines | Notes |
487
+ | -------------------------- | --------- | ----- | ------------------------------- |
488
+ | `src/lang/emitters/ast.ts` | 94% | 83% | TJS → AST |
489
+ | `src/lang/parser.ts` | 92% | 82% | TJS parser |
490
+ | `src/lang/inference.ts` | 57% | 60% | Type inference (lower priority) |
491
+
492
+ **Test Categories:**
493
+
494
+ | Category | Tests | Coverage |
495
+ | -------------------------- | ----- | ---------------------------------------------------- |
496
+ | Security (malicious actor) | 10 | Prototype access, SSRF, ReDoS, path traversal |
497
+ | Runtime core | 25+ | Fuel, timeout, tracing, expressions |
498
+ | Stress/Memory | 6 | Large arrays, deep nesting, memory pressure |
499
+ | Capability failures | 10 | Network errors, store failures, partial capabilities |
500
+ | Allocation fuel | 4 | Proportional charging for strings/arrays |
501
+ | Transpiler | 50+ | Language features, edge cases |
502
+ | Use cases | 100+ | RAG, orchestration, client-server patterns |
503
+
504
+ ### Running Tests
505
+
506
+ ```bash
507
+ # Full suite
508
+ bun test
509
+
510
+ # Fast (skip LLM and benchmarks)
511
+ SKIP_LLM_TESTS=1 AGENT99_TESTS_SKIP_BENCHMARKS=1 bun test
512
+
513
+ # With coverage
514
+ bun test --coverage
515
+ ```
516
+
517
+ ## 10. Dependencies
518
+
519
+ ### Runtime Dependencies
520
+
521
+ These ship with the library and affect bundle size and security posture.
522
+
523
+ | Package | Version | Size | Purpose | Risk |
524
+ | --------------- | ------- | ----- | --------------------------------------- | ---------------------------------------------------- |
525
+ | `acorn` | ^8.15.0 | ~30KB | JavaScript parser for AJS transpilation | **Low** - Mature, widely audited, Mozilla-maintained |
526
+ | `tosijs-schema` | ^1.2.0 | ~5KB | JSON Schema validation | **Low** - Our library, 96.6% coverage, zero deps |
527
+ | `@codemirror/*` | various | ~50KB | Editor syntax highlighting (optional) | **Low** - Only loaded for editor integration |
528
+
529
+ **Total runtime footprint:** ~33KB gzipped (core), ~83KB with editor support.
530
+
531
+ ### tosijs-schema 1.2.0 Coverage
532
+
533
+ Our validation dependency maintains comprehensive test coverage:
534
+
535
+ - **98.25% function coverage, 96.62% line coverage** (146 tests, 349 assertions)
536
+ - **Edge cases tested:** NaN, Infinity, -0, sparse arrays, unicode, deeply nested structures
537
+ - **Complex unions:** nested unions, discriminated unions, union of arrays, union with null/undefined
538
+ - **Format validators:** email, ipv4 boundaries, url protocols, datetime variants
539
+ - **Strict mode:** `validate(data, schema, { strict: true })` validates every item
540
+
541
+ Because tosijs-schema schemas are JSON data (not code), library coverage extends to user-defined schemas—unlike Zod where user schema compositions are untested code.
542
+
543
+ ### Development Dependencies
544
+
545
+ Not shipped to users. Used for building, testing, and development.
546
+
547
+ | Package | Purpose | Notes |
548
+ | ---------------------- | ----------------------------- | ----------------------- |
549
+ | `typescript` | Type checking and compilation | Standard |
550
+ | `bun` | Runtime, bundler, test runner | Fast, modern |
551
+ | `eslint` / `prettier` | Code quality | Standard |
552
+ | `acorn-walk` | AST traversal for transpiler | Only used at build time |
553
+ | `codemirror` | Editor components for demo | Demo only |
554
+ | `tosijs` / `tosijs-ui` | Demo UI framework | Demo only |
555
+ | `happy-dom` | DOM mocking for tests | Test only |
556
+ | `vitest` | Alternative test runner | Optional |
557
+
558
+ ### Dependency Risk Assessment
559
+
560
+ **Supply Chain:**
561
+
562
+ | Risk | Mitigation |
563
+ | ------------------------ | -------------------------------------------------------- |
564
+ | Acorn compromise | Mature project (10+ years), Mozilla backing, widely used |
565
+ | tosijs-schema compromise | We control this library |
566
+ | Transitive dependencies | Minimal—acorn has 0 deps, tosijs-schema has 0 deps |
567
+
568
+ **Version Pinning:**
569
+
570
+ - Production dependencies use caret (`^`) for patch updates
571
+ - Consider using exact versions or lockfile for production deployments
572
+
573
+ **Audit:**
574
+
575
+ ```bash
576
+ # Check for known vulnerabilities
577
+ bun audit
578
+ # or
579
+ npm audit
580
+ ```
581
+
582
+ ### What We Don't Depend On
583
+
584
+ Notably absent from our dependency tree:
585
+
586
+ | Common Dependency | Why We Don't Use It |
587
+ | ----------------- | --------------------------------------- |
588
+ | lodash | Native JS methods suffice |
589
+ | axios | Native fetch + capability injection |
590
+ | moment/dayjs | Built-in Date wrapper in expressions |
591
+ | zod/yup | tosijs-schema is lighter and sufficient |
592
+ | jsep | Replaced with acorn + custom AST nodes |
593
+
594
+ This minimal dependency approach reduces supply chain risk and bundle size.