tjs-lang 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { coreAtoms, EFFECTFUL_CORE_OPS, type AtomDef } from './runtime'
3
+ import { batteryAtoms } from './atoms/index'
4
+
5
+ const allAtoms = { ...coreAtoms, ...batteryAtoms } as Record<string, AtomDef>
6
+
7
+ describe('atom effects classification (predicate-safety keystone)', () => {
8
+ it('every atom is classified pure | io', () => {
9
+ for (const [op, atom] of Object.entries(allAtoms)) {
10
+ expect(atom.effects, `${op} has no effects tag`).toMatch(/^(pure|io)$/)
11
+ }
12
+ })
13
+
14
+ it('the declared effectful core ops are all tagged io', () => {
15
+ for (const op of EFFECTFUL_CORE_OPS) {
16
+ expect(coreAtoms[op as keyof typeof coreAtoms]?.effects, op).toBe('io')
17
+ }
18
+ })
19
+
20
+ it('every battery atom is io (network calls)', () => {
21
+ for (const atom of Object.values(batteryAtoms) as AtomDef[]) {
22
+ expect(atom.effects, atom.op).toBe('io')
23
+ }
24
+ })
25
+
26
+ it('the pure substrate a predicate relies on is tagged pure', () => {
27
+ // data / control-flow atoms predicates compose from
28
+ const pure = [
29
+ 'map',
30
+ 'filter',
31
+ 'reduce',
32
+ 'find',
33
+ 'len',
34
+ 'split',
35
+ 'join',
36
+ 'keys',
37
+ 'pick',
38
+ 'omit',
39
+ 'merge',
40
+ 'regexMatch',
41
+ 'jsonParse',
42
+ 'callLocal',
43
+ 'if',
44
+ 'while',
45
+ 'return',
46
+ 'scope',
47
+ ]
48
+ for (const op of pure) {
49
+ expect(coreAtoms[op as keyof typeof coreAtoms]?.effects, op).toBe('pure')
50
+ }
51
+ })
52
+
53
+ it('the IO atoms are exactly the ones that may not appear in a predicate', () => {
54
+ const io = Object.values(allAtoms)
55
+ .filter((a) => a.effects === 'io')
56
+ .map((a) => a.op)
57
+ // sanity: the capability-touching atoms are all present
58
+ expect(io).toEqual(
59
+ expect.arrayContaining([
60
+ 'httpFetch',
61
+ 'storeGet',
62
+ 'llmPredict',
63
+ 'agentRun',
64
+ 'runCode',
65
+ 'llmVision',
66
+ ])
67
+ )
68
+ // and none of the pure data ops leaked in
69
+ expect(io).not.toContain('map')
70
+ expect(io).not.toContain('jsonParse')
71
+ })
72
+ })
@@ -229,3 +229,15 @@ export const llmVision = defineAtom(
229
229
  },
230
230
  { docs: 'Analyze images using a vision model', timeoutMs: 120000, cost: 150 }
231
231
  )
232
+
233
+ // Every battery atom is IO (embedding/LLM/vector-store network calls).
234
+ for (const atom of [
235
+ storeVectorize,
236
+ storeCreateCollection,
237
+ storeVectorAdd,
238
+ storeSearch,
239
+ llmPredictBattery,
240
+ llmVision,
241
+ ]) {
242
+ atom.effects = 'io'
243
+ }
package/src/vm/runtime.ts CHANGED
@@ -180,6 +180,18 @@ export interface RuntimeContext {
180
180
 
181
181
  export type AtomExec = (step: any, ctx: RuntimeContext) => Promise<void>
182
182
 
183
+ /**
184
+ * Effect classification of an atom.
185
+ * - `'pure'`: deterministic, no IO, no capability access, no observable side
186
+ * effects — safe inside a synchronous predicate (see the predicate verifier).
187
+ * - `'io'`: touches `ctx.capabilities` (fetch/store/llm/agent/code), or is
188
+ * nondeterministic (random/uuid), or has side effects (console). Not allowed
189
+ * in a predicate.
190
+ * Defaults to `'pure'`; effectful atoms must opt into `'io'`. The invariant
191
+ * "anything touching ctx.capabilities is tagged 'io'" is guarded by a test.
192
+ */
193
+ export type AtomEffects = 'pure' | 'io'
194
+
183
195
  export interface AtomDef {
184
196
  op: OpCode
185
197
  inputSchema: any
@@ -188,6 +200,7 @@ export interface AtomDef {
188
200
  docs?: string
189
201
  timeoutMs?: number
190
202
  cost?: number | ((input: any, ctx: RuntimeContext) => number)
203
+ effects?: AtomEffects
191
204
  }
192
205
 
193
206
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -199,6 +212,8 @@ export interface AtomOptions {
199
212
  docs?: string
200
213
  timeoutMs?: number
201
214
  cost?: number | ((input: any, ctx: RuntimeContext) => number)
215
+ /** Effect class — defaults to `'pure'`; effectful atoms set `'io'`. */
216
+ effects?: AtomEffects
202
217
  }
203
218
 
204
219
  export interface RunResult {
@@ -1347,6 +1362,7 @@ export function defineAtom<I extends Record<string, any>, O = any>(
1347
1362
  docs = '',
1348
1363
  timeoutMs = 1000,
1349
1364
  cost = 1,
1365
+ effects = 'pure',
1350
1366
  } = typeof options === 'string' ? { docs: options } : options
1351
1367
 
1352
1368
  const exec: AtomExec = async (step: any, ctx: RuntimeContext) => {
@@ -1446,6 +1462,7 @@ export function defineAtom<I extends Record<string, any>, O = any>(
1446
1462
  docs,
1447
1463
  timeoutMs,
1448
1464
  cost,
1465
+ effects,
1449
1466
  create: (input: I) => ({ op, ...input }),
1450
1467
  }
1451
1468
  }
@@ -3114,3 +3131,37 @@ export const coreAtoms = {
3114
3131
  releaseProcedure,
3115
3132
  clearExpiredProcedures,
3116
3133
  }
3134
+
3135
+ /**
3136
+ * Effectful core atoms — anything that touches `ctx.capabilities`
3137
+ * (fetch/store/llm/agent/code), is nondeterministic (random/uuid), or has
3138
+ * observable side effects (console). Tagged centrally so the list reads as one
3139
+ * audit surface; a test asserts every capability-touching atom is in here.
3140
+ * Everything else defaults to `effects: 'pure'`.
3141
+ */
3142
+ export const EFFECTFUL_CORE_OPS = [
3143
+ 'httpFetch',
3144
+ 'storeGet',
3145
+ 'storeSet',
3146
+ 'storeQuery',
3147
+ 'storeVectorSearch',
3148
+ 'llmPredict',
3149
+ 'agentRun',
3150
+ 'transpileCode',
3151
+ 'runCode',
3152
+ 'random',
3153
+ 'uuid',
3154
+ 'consoleLog',
3155
+ 'consoleWarn',
3156
+ 'consoleError',
3157
+ 'storeProcedure',
3158
+ 'releaseProcedure',
3159
+ 'clearExpiredProcedures',
3160
+ 'cache',
3161
+ 'memoize',
3162
+ ] as const
3163
+
3164
+ for (const op of EFFECTFUL_CORE_OPS) {
3165
+ const atom = (coreAtoms as Record<string, AtomDef>)[op]
3166
+ if (atom) atom.effects = 'io'
3167
+ }