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.
- package/CLAUDE.md +4 -1
- package/demo/autocomplete.test.ts +37 -0
- package/demo/docs.json +698 -8
- package/demo/src/autocomplete.ts +40 -2
- package/demo/src/introspection-bridge.ts +140 -0
- package/demo/src/introspection-doc.test.ts +63 -0
- package/demo/src/playground-shared.ts +53 -0
- package/demo/src/tjs-playground.ts +21 -0
- package/editors/codemirror/ajs-language.ts +130 -44
- package/editors/codemirror/completion-source.test.ts +114 -0
- package/editors/introspect-value.test.ts +61 -0
- package/editors/introspect-value.ts +86 -0
- package/editors/scope-symbols.test.ts +113 -0
- package/editors/scope-symbols.ts +173 -0
- package/package.json +2 -1
- package/src/lang/index.ts +22 -0
- package/src/lang/predicate-schema.test.ts +97 -0
- package/src/lang/predicate-schema.ts +168 -0
- package/src/lang/predicate.test.ts +184 -0
- package/src/lang/predicate.ts +550 -0
- package/src/lang/suggest.test.ts +84 -0
- package/src/lang/transpiler.ts +26 -0
- package/src/vm/atom-effects.test.ts +72 -0
- package/src/vm/atoms/batteries.ts +12 -0
- package/src/vm/runtime.ts +51 -0
|
@@ -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
|
+
}
|