tjs-lang 0.7.6 → 0.7.8
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 +101 -26
- package/bin/docs.js +4 -1
- package/demo/docs.json +46 -12
- package/demo/src/examples.test.ts +1 -0
- package/demo/src/imports.test.ts +16 -4
- package/demo/src/imports.ts +60 -15
- package/demo/src/playground-shared.ts +9 -8
- package/demo/src/tfs-worker.js +205 -147
- package/demo/src/tjs-playground.ts +34 -10
- package/demo/src/ts-playground.ts +24 -8
- package/dist/index.js +140 -119
- package/dist/index.js.map +4 -4
- package/dist/src/lang/bool-coercion.d.ts +50 -0
- package/dist/src/lang/docs.d.ts +31 -6
- package/dist/src/lang/linter.d.ts +8 -0
- package/dist/src/lang/parser-transforms.d.ts +18 -0
- package/dist/src/lang/parser-types.d.ts +2 -0
- package/dist/src/lang/parser.d.ts +9 -0
- package/dist/src/lang/runtime.d.ts +34 -0
- package/dist/src/lang/types.d.ts +9 -1
- package/dist/src/rbac/index.d.ts +1 -1
- package/dist/src/vm/runtime.d.ts +1 -1
- package/dist/tjs-eval.js +44 -39
- package/dist/tjs-eval.js.map +4 -4
- package/dist/tjs-from-ts.js +20 -20
- package/dist/tjs-from-ts.js.map +3 -3
- package/dist/tjs-lang.js +86 -80
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +50 -45
- package/dist/tjs-vm.js.map +4 -4
- package/llms.txt +79 -0
- package/package.json +3 -2
- package/src/cli/commands/convert.test.ts +16 -21
- package/src/lang/bool-coercion.test.ts +203 -0
- package/src/lang/bool-coercion.ts +314 -0
- package/src/lang/codegen.test.ts +177 -0
- package/src/lang/docs.test.ts +328 -1
- package/src/lang/docs.ts +424 -24
- package/src/lang/emitters/ast.ts +11 -12
- package/src/lang/emitters/dts.test.ts +41 -0
- package/src/lang/emitters/dts.ts +9 -0
- package/src/lang/emitters/js-tests.ts +16 -4
- package/src/lang/emitters/js.ts +208 -2
- package/src/lang/features.test.ts +22 -0
- package/src/lang/inference.ts +54 -0
- package/src/lang/linter.test.ts +104 -1
- package/src/lang/linter.ts +124 -1
- package/src/lang/parser-params.ts +31 -0
- package/src/lang/parser-transforms.ts +539 -6
- package/src/lang/parser-types.ts +2 -0
- package/src/lang/parser.test.ts +73 -1
- package/src/lang/parser.ts +85 -1
- package/src/lang/runtime.ts +98 -0
- package/src/lang/tests.ts +21 -8
- package/src/lang/types.ts +6 -0
- package/src/rbac/index.ts +2 -2
- package/src/rbac/rules.tjs.d.ts +9 -0
- package/src/vm/atoms/batteries.ts +2 -2
- package/src/vm/runtime.ts +10 -3
- package/dist/src/rbac/rules.d.ts +0 -184
- package/src/rbac/rules.js +0 -338
package/src/lang/parser.ts
CHANGED
|
@@ -51,11 +51,58 @@ import {
|
|
|
51
51
|
transformConstBang,
|
|
52
52
|
transformBangAccess,
|
|
53
53
|
transformExtensionCalls,
|
|
54
|
+
transformLetTypeAnnotations,
|
|
54
55
|
} from './parser-transforms'
|
|
55
56
|
|
|
56
57
|
// Re-export transformExtensionCalls for js.ts
|
|
57
58
|
export { transformExtensionCalls } from './parser-transforms'
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Strip single-line comments (//) from source.
|
|
62
|
+
* Replaces comment content with spaces to preserve character offsets.
|
|
63
|
+
* Skips // inside strings and block comments.
|
|
64
|
+
*/
|
|
65
|
+
export function stripLineComments(source: string): string {
|
|
66
|
+
let result = ''
|
|
67
|
+
let i = 0
|
|
68
|
+
while (i < source.length) {
|
|
69
|
+
const ch = source[i]
|
|
70
|
+
// String literals — skip to closing quote
|
|
71
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
72
|
+
const quote = ch
|
|
73
|
+
result += ch
|
|
74
|
+
i++
|
|
75
|
+
while (i < source.length && source[i] !== quote) {
|
|
76
|
+
if (source[i] === '\\') {
|
|
77
|
+
result += source[i++]
|
|
78
|
+
}
|
|
79
|
+
if (i < source.length) result += source[i++]
|
|
80
|
+
}
|
|
81
|
+
if (i < source.length) result += source[i++] // closing quote
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
// Block comment — pass through (may contain //)
|
|
85
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
86
|
+
const end = source.indexOf('*/', i + 2)
|
|
87
|
+
const slice = end === -1 ? source.slice(i) : source.slice(i, end + 2)
|
|
88
|
+
result += slice
|
|
89
|
+
i += slice.length
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
// Line comment — replace with spaces to preserve offsets
|
|
93
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
94
|
+
const nl = source.indexOf('\n', i)
|
|
95
|
+
const end = nl === -1 ? source.length : nl
|
|
96
|
+
result += ' '.repeat(end - i)
|
|
97
|
+
i = end // leave \n for next iteration
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
result += ch
|
|
101
|
+
i++
|
|
102
|
+
}
|
|
103
|
+
return result
|
|
104
|
+
}
|
|
105
|
+
|
|
59
106
|
export function preprocess(
|
|
60
107
|
source: string,
|
|
61
108
|
options: PreprocessOptions = {}
|
|
@@ -74,6 +121,7 @@ export function preprocess(
|
|
|
74
121
|
testErrors: string[]
|
|
75
122
|
polymorphicNames: Set<string>
|
|
76
123
|
extensions: Map<string, Set<string>>
|
|
124
|
+
letAnnotations: Map<string, string>
|
|
77
125
|
} {
|
|
78
126
|
const originalSource = source
|
|
79
127
|
let moduleSafety: 'none' | 'inputs' | 'all' | undefined
|
|
@@ -97,6 +145,7 @@ export function preprocess(
|
|
|
97
145
|
tjsStandard: false,
|
|
98
146
|
tjsSafeEval: false,
|
|
99
147
|
tjsNoVar: false,
|
|
148
|
+
tjsSafeAssign: false,
|
|
100
149
|
}
|
|
101
150
|
: {
|
|
102
151
|
tjsEquals: true,
|
|
@@ -106,6 +155,7 @@ export function preprocess(
|
|
|
106
155
|
tjsStandard: true,
|
|
107
156
|
tjsSafeEval: false, // opt-in only (adds import)
|
|
108
157
|
tjsNoVar: true,
|
|
158
|
+
tjsSafeAssign: true,
|
|
109
159
|
}
|
|
110
160
|
|
|
111
161
|
// Safety: native TJS defaults to 'inputs' (runtime default),
|
|
@@ -134,7 +184,7 @@ export function preprocess(
|
|
|
134
184
|
// TjsCompat disables all TJS modes (useful for native TJS opting out)
|
|
135
185
|
// Individual modes: TjsEquals, TjsClass, TjsDate, TjsNoeval, TjsStandard, TjsSafeEval
|
|
136
186
|
const directivePattern =
|
|
137
|
-
/^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval)\b/
|
|
187
|
+
/^(\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*)\s*(TjsStrict|TjsCompat|TjsEquals|TjsClass|TjsDate|TjsNoeval|TjsNoVar|TjsStandard|TjsSafeEval|TjsSafeAssign)\b/
|
|
138
188
|
|
|
139
189
|
let match
|
|
140
190
|
while ((match = source.match(directivePattern))) {
|
|
@@ -148,6 +198,7 @@ export function preprocess(
|
|
|
148
198
|
tjsModes.tjsNoeval = true
|
|
149
199
|
tjsModes.tjsNoVar = true
|
|
150
200
|
tjsModes.tjsStandard = true
|
|
201
|
+
tjsModes.tjsSafeAssign = true
|
|
151
202
|
} else if (directive === 'TjsCompat') {
|
|
152
203
|
// Disable all TJS modes (JS-compatible)
|
|
153
204
|
tjsModes.tjsEquals = false
|
|
@@ -157,6 +208,7 @@ export function preprocess(
|
|
|
157
208
|
tjsModes.tjsNoVar = false
|
|
158
209
|
tjsModes.tjsStandard = false
|
|
159
210
|
tjsModes.tjsSafeEval = false
|
|
211
|
+
tjsModes.tjsSafeAssign = false
|
|
160
212
|
} else if (directive === 'TjsEquals') {
|
|
161
213
|
tjsModes.tjsEquals = true
|
|
162
214
|
} else if (directive === 'TjsClass') {
|
|
@@ -171,6 +223,8 @@ export function preprocess(
|
|
|
171
223
|
tjsModes.tjsStandard = true
|
|
172
224
|
} else if (directive === 'TjsSafeEval') {
|
|
173
225
|
tjsModes.tjsSafeEval = true
|
|
226
|
+
} else if (directive === 'TjsSafeAssign') {
|
|
227
|
+
tjsModes.tjsSafeAssign = true
|
|
174
228
|
}
|
|
175
229
|
|
|
176
230
|
// Remove the directive from source
|
|
@@ -182,6 +236,11 @@ export function preprocess(
|
|
|
182
236
|
)
|
|
183
237
|
}
|
|
184
238
|
|
|
239
|
+
// Strip single-line comments early — they confuse brace matching,
|
|
240
|
+
// ASI protection, and test extraction (e.g. apostrophes in comments)
|
|
241
|
+
// Preserves line structure by keeping the newline
|
|
242
|
+
source = stripLineComments(source)
|
|
243
|
+
|
|
185
244
|
// TjsStandard mode: insert semicolons to prevent ASI footguns
|
|
186
245
|
// Must happen early before other transformations modify line structure
|
|
187
246
|
if (tjsModes.tjsStandard) {
|
|
@@ -196,6 +255,13 @@ export function preprocess(
|
|
|
196
255
|
// Must happen before acorn parsing since !. is not valid JS
|
|
197
256
|
source = transformBangAccess(source)
|
|
198
257
|
|
|
258
|
+
// Transform `let x: <example>` declarations: strip annotation and record
|
|
259
|
+
// varName -> example. Must happen before paren transforms so the colon
|
|
260
|
+
// is not confused with TS-style annotations on params/returns.
|
|
261
|
+
const letAnnoResult = transformLetTypeAnnotations(source)
|
|
262
|
+
source = letAnnoResult.source
|
|
263
|
+
const letAnnotations = letAnnoResult.annotations
|
|
264
|
+
|
|
199
265
|
// Transform Is/IsNot infix operators to function calls
|
|
200
266
|
// a Is b -> Is(a, b)
|
|
201
267
|
// a IsNot b -> IsNot(a, b)
|
|
@@ -320,6 +386,7 @@ export function preprocess(
|
|
|
320
386
|
testErrors: testResult.errors,
|
|
321
387
|
polymorphicNames: polyResult.polymorphicNames,
|
|
322
388
|
extensions: extResult.extensions,
|
|
389
|
+
letAnnotations,
|
|
323
390
|
}
|
|
324
391
|
}
|
|
325
392
|
|
|
@@ -341,6 +408,8 @@ export function parse(
|
|
|
341
408
|
wasmBlocks: WasmBlock[]
|
|
342
409
|
tests: TestBlock[]
|
|
343
410
|
testErrors: string[]
|
|
411
|
+
letAnnotations: Map<string, string>
|
|
412
|
+
tjsModes: TjsModes
|
|
344
413
|
} {
|
|
345
414
|
const {
|
|
346
415
|
filename = '<source>',
|
|
@@ -361,6 +430,8 @@ export function parse(
|
|
|
361
430
|
wasmBlocks,
|
|
362
431
|
tests,
|
|
363
432
|
testErrors,
|
|
433
|
+
letAnnotations,
|
|
434
|
+
tjsModes,
|
|
364
435
|
} = colonShorthand
|
|
365
436
|
? preprocess(source, { vmTarget })
|
|
366
437
|
: {
|
|
@@ -375,6 +446,17 @@ export function parse(
|
|
|
375
446
|
wasmBlocks: [] as WasmBlock[],
|
|
376
447
|
tests: [] as TestBlock[],
|
|
377
448
|
testErrors: [] as string[],
|
|
449
|
+
letAnnotations: new Map<string, string>(),
|
|
450
|
+
tjsModes: {
|
|
451
|
+
tjsEquals: false,
|
|
452
|
+
tjsClass: false,
|
|
453
|
+
tjsDate: false,
|
|
454
|
+
tjsNoeval: false,
|
|
455
|
+
tjsStandard: false,
|
|
456
|
+
tjsSafeEval: false,
|
|
457
|
+
tjsNoVar: false,
|
|
458
|
+
tjsSafeAssign: false,
|
|
459
|
+
} as TjsModes,
|
|
378
460
|
}
|
|
379
461
|
|
|
380
462
|
try {
|
|
@@ -397,6 +479,8 @@ export function parse(
|
|
|
397
479
|
wasmBlocks,
|
|
398
480
|
tests,
|
|
399
481
|
testErrors,
|
|
482
|
+
letAnnotations,
|
|
483
|
+
tjsModes,
|
|
400
484
|
}
|
|
401
485
|
} catch (e: any) {
|
|
402
486
|
// Convert Acorn error to our error type
|
package/src/lang/runtime.ts
CHANGED
|
@@ -605,6 +605,26 @@ export function TypeOf(value: unknown): string {
|
|
|
605
605
|
return typeof value
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
+
/**
|
|
609
|
+
* Honest boolean coercion. Like `Boolean(x)` but unwraps boxed primitives
|
|
610
|
+
* first, fixing the JS footgun `Boolean(new Boolean(false)) === true`.
|
|
611
|
+
*
|
|
612
|
+
* Under TjsStandard, the source rewriter wraps every truthiness context
|
|
613
|
+
* (if/while/for/do-while conditions, `!`, `&&`, `||`, ternary, and
|
|
614
|
+
* top-level `Boolean(x)` calls) with this function so a boxed `false`
|
|
615
|
+
* actually behaves as `false`.
|
|
616
|
+
*/
|
|
617
|
+
export function toBool(value: unknown): boolean {
|
|
618
|
+
if (
|
|
619
|
+
value instanceof Boolean ||
|
|
620
|
+
value instanceof Number ||
|
|
621
|
+
value instanceof String
|
|
622
|
+
) {
|
|
623
|
+
return Boolean((value as any).valueOf())
|
|
624
|
+
}
|
|
625
|
+
return Boolean(value)
|
|
626
|
+
}
|
|
627
|
+
|
|
608
628
|
export function Eq(a: unknown, b: unknown): boolean {
|
|
609
629
|
// Unwrap boxed primitives
|
|
610
630
|
if (a instanceof String || a instanceof Number || a instanceof Boolean) {
|
|
@@ -844,6 +864,78 @@ type TypeSpec =
|
|
|
844
864
|
| string
|
|
845
865
|
| { check: (v: unknown) => boolean | string; description: string }
|
|
846
866
|
|
|
867
|
+
/**
|
|
868
|
+
* Check that a passed-in function's declared shape matches the expected
|
|
869
|
+
* shape. Returns the function unchanged on a match, or a MonadicError on
|
|
870
|
+
* mismatch. Untyped functions (no `__tjs` metadata — anonymous arrows
|
|
871
|
+
* like `x => false`) pass through unchanged on the assumption that the
|
|
872
|
+
* caller knows what they're doing; they accept any args and return
|
|
873
|
+
* whatever they return.
|
|
874
|
+
*
|
|
875
|
+
* This is a ONE-SHOT check at pass time, NOT a per-call wrapper. The TJS
|
|
876
|
+
* design call: a wrong-shape callback is ONE error at the boundary, not
|
|
877
|
+
* N errors when the receiving function invokes the callback N times.
|
|
878
|
+
*
|
|
879
|
+
* Compatibility rules (deliberately permissive — strict subtyping is a
|
|
880
|
+
* separate, larger feature):
|
|
881
|
+
* - For each expected param: the actual function may declare fewer
|
|
882
|
+
* params (extras simply not used). If both declare a kind, they
|
|
883
|
+
* must match exactly. Either side being `any` always matches.
|
|
884
|
+
* - For the return type: same exact-match rule when both are known.
|
|
885
|
+
*/
|
|
886
|
+
export function checkFnShape(
|
|
887
|
+
fn: unknown,
|
|
888
|
+
expectedParamKinds: string[],
|
|
889
|
+
expectedReturnKind: string,
|
|
890
|
+
path: string
|
|
891
|
+
): unknown {
|
|
892
|
+
if (typeof fn !== 'function') return fn // outer "is callable" check already ran
|
|
893
|
+
const meta = (fn as any).__tjs
|
|
894
|
+
if (!meta || !meta.params) return fn // untyped — let it run
|
|
895
|
+
|
|
896
|
+
const actualEntries = Object.entries(meta.params) as Array<
|
|
897
|
+
[string, { type?: { kind?: string } }]
|
|
898
|
+
>
|
|
899
|
+
for (let i = 0; i < expectedParamKinds.length; i++) {
|
|
900
|
+
const expectedKind = expectedParamKinds[i]
|
|
901
|
+
if (expectedKind === 'any') continue
|
|
902
|
+
const actual = actualEntries[i]
|
|
903
|
+
if (!actual) continue // function takes fewer params, OK
|
|
904
|
+
const actualKind = actual[1]?.type?.kind
|
|
905
|
+
if (!actualKind || actualKind === 'any') continue
|
|
906
|
+
if (actualKind !== expectedKind) {
|
|
907
|
+
return new MonadicError(
|
|
908
|
+
`Expected (...arg${i}: ${expectedKind}, ...) for '${path}', ` +
|
|
909
|
+
`but callback declares arg${i} as ${actualKind}`,
|
|
910
|
+
`${path}(arg${i})`,
|
|
911
|
+
expectedKind,
|
|
912
|
+
actualKind
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (expectedReturnKind !== 'any' && meta.returns) {
|
|
918
|
+
// Metadata's `returns` is `{ type: TypeDescriptor, defaults?: ... }`,
|
|
919
|
+
// but defensively also accept a bare TypeDescriptor.
|
|
920
|
+
const actualReturnKind = meta.returns.type?.kind ?? meta.returns.kind
|
|
921
|
+
if (
|
|
922
|
+
actualReturnKind &&
|
|
923
|
+
actualReturnKind !== 'any' &&
|
|
924
|
+
actualReturnKind !== expectedReturnKind
|
|
925
|
+
) {
|
|
926
|
+
return new MonadicError(
|
|
927
|
+
`Expected callback returning ${expectedReturnKind} for '${path}', ` +
|
|
928
|
+
`but callback returns ${actualReturnKind}`,
|
|
929
|
+
`${path}(return)`,
|
|
930
|
+
expectedReturnKind,
|
|
931
|
+
actualReturnKind
|
|
932
|
+
)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return fn
|
|
937
|
+
}
|
|
938
|
+
|
|
847
939
|
/** Parameter metadata with optional location */
|
|
848
940
|
interface ParamMeta {
|
|
849
941
|
type: TypeSpec
|
|
@@ -1478,6 +1570,7 @@ export function createRuntime() {
|
|
|
1478
1570
|
checkType,
|
|
1479
1571
|
validateArgs,
|
|
1480
1572
|
wrap,
|
|
1573
|
+
checkFnShape,
|
|
1481
1574
|
wrapClass,
|
|
1482
1575
|
compareVersions,
|
|
1483
1576
|
versionsCompatible,
|
|
@@ -1528,6 +1621,8 @@ export function createRuntime() {
|
|
|
1528
1621
|
NotEq,
|
|
1529
1622
|
// Honest typeof (typeof with TjsEquals)
|
|
1530
1623
|
TypeOf,
|
|
1624
|
+
// Honest truthiness (unwraps boxed primitives)
|
|
1625
|
+
toBool,
|
|
1531
1626
|
tjsEquals,
|
|
1532
1627
|
// Extensions
|
|
1533
1628
|
registerExtension: instanceRegisterExtension,
|
|
@@ -1559,6 +1654,7 @@ export const runtime = {
|
|
|
1559
1654
|
checkType,
|
|
1560
1655
|
validateArgs,
|
|
1561
1656
|
wrap,
|
|
1657
|
+
checkFnShape,
|
|
1562
1658
|
wrapClass,
|
|
1563
1659
|
compareVersions,
|
|
1564
1660
|
versionsCompatible,
|
|
@@ -1612,6 +1708,8 @@ export const runtime = {
|
|
|
1612
1708
|
NotEq,
|
|
1613
1709
|
// Honest typeof (used by typeof with TjsEquals)
|
|
1614
1710
|
TypeOf,
|
|
1711
|
+
// Honest truthiness (used in TjsStandard for boxed-primitive coercion)
|
|
1712
|
+
toBool,
|
|
1615
1713
|
}
|
|
1616
1714
|
|
|
1617
1715
|
/**
|
package/src/lang/tests.ts
CHANGED
|
@@ -106,15 +106,17 @@ function extractEmbeddedTests(source: string): ExtractedTest[] {
|
|
|
106
106
|
const tests: ExtractedTest[] = []
|
|
107
107
|
|
|
108
108
|
// Match: /*test 'description' { ... }*/ or /*test { ... }*/
|
|
109
|
+
// Each quote type gets its own alternative so the description can contain
|
|
110
|
+
// the other quote types (e.g. `test 'typeof null is "null"' {`).
|
|
109
111
|
const embeddedRegex =
|
|
110
|
-
/\/\*test\s+(['
|
|
112
|
+
/\/\*test\s+'([^']*)'\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s+"([^"]*)"\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s+`([^`]*)`\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s*\{([\s\S]*?)\}\s*\*\//g
|
|
111
113
|
|
|
112
114
|
let match
|
|
113
115
|
while ((match = embeddedRegex.exec(source)) !== null) {
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const body = (match[
|
|
116
|
+
// Groups: 1/3/5 = description for ' " ` ; 2/4/6 = body for ' " ` ; 7 = body for anonymous
|
|
117
|
+
const desc =
|
|
118
|
+
match[1] || match[3] || match[5] || `embedded test ${tests.length + 1}`
|
|
119
|
+
const body = (match[2] || match[4] || match[6] || match[7] || '').trim()
|
|
118
120
|
|
|
119
121
|
tests.push({
|
|
120
122
|
description: desc,
|
|
@@ -145,8 +147,10 @@ export function extractTests(source: string): TestExtractionResult {
|
|
|
145
147
|
// test { ... } (anonymous test)
|
|
146
148
|
// test 'description' { ... } (canonical TJS)
|
|
147
149
|
// test('description') { ... } (also valid - parenthesized string is still a string)
|
|
150
|
+
// Each quote type has its own alternative so the description can contain
|
|
151
|
+
// the other quote types (e.g. `test 'typeof null is "null"' {`).
|
|
148
152
|
const testRegex =
|
|
149
|
-
/test\s+(['
|
|
153
|
+
/test\s+'([^']*)'\s*\{|test\s+"([^"]*)"\s*\{|test\s+`([^`]*)`\s*\{|test\s*\(\s*'([^']*)'\s*\)\s*\{|test\s*\(\s*"([^"]*)"\s*\)\s*\{|test\s*\(\s*`([^`]*)`\s*\)\s*\{|test\s*\{/g
|
|
150
154
|
const mockRegex = /mock\s*\{/g
|
|
151
155
|
|
|
152
156
|
let cleanCode = source
|
|
@@ -164,8 +168,17 @@ export function extractTests(source: string): TestExtractionResult {
|
|
|
164
168
|
continue
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
//
|
|
168
|
-
|
|
171
|
+
// Groups 1/2/3 = `test 'desc'` / `test "desc"` / `test \`desc\``
|
|
172
|
+
// Groups 4/5/6 = parenthesized variants
|
|
173
|
+
// No group when description is omitted
|
|
174
|
+
const desc =
|
|
175
|
+
match[1] ||
|
|
176
|
+
match[2] ||
|
|
177
|
+
match[3] ||
|
|
178
|
+
match[4] ||
|
|
179
|
+
match[5] ||
|
|
180
|
+
match[6] ||
|
|
181
|
+
`test ${tests.length + 1}`
|
|
169
182
|
const bodyStart = match.index + match[0].length
|
|
170
183
|
|
|
171
184
|
// Find matching closing brace
|
package/src/lang/types.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface TypeDescriptor {
|
|
|
22
22
|
| 'array'
|
|
23
23
|
| 'object'
|
|
24
24
|
| 'union'
|
|
25
|
+
| 'function'
|
|
25
26
|
| 'any'
|
|
26
27
|
nullable?: boolean
|
|
27
28
|
/** For arrays: the element type */
|
|
@@ -32,6 +33,11 @@ export interface TypeDescriptor {
|
|
|
32
33
|
members?: TypeDescriptor[]
|
|
33
34
|
/** For destructured parameters: full parameter descriptors */
|
|
34
35
|
destructuredParams?: Record<string, ParameterDescriptor>
|
|
36
|
+
/** For functions: declared parameters with names and inferred types */
|
|
37
|
+
params?: Array<{ name: string; type: TypeDescriptor }>
|
|
38
|
+
/** For functions: inferred return type. Concise arrow bodies infer from
|
|
39
|
+
* the expression; block bodies and complex expressions stay `any`. */
|
|
40
|
+
returns?: TypeDescriptor
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/** Describes a function parameter */
|
package/src/rbac/index.ts
CHANGED
|
@@ -46,7 +46,7 @@ export {
|
|
|
46
46
|
interpretRuleResult,
|
|
47
47
|
hasRoleLevel,
|
|
48
48
|
buildRuleContext,
|
|
49
|
-
} from './rules.
|
|
49
|
+
} from './rules.tjs'
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Security rule definition
|
|
@@ -125,7 +125,7 @@ import {
|
|
|
125
125
|
validateSchema,
|
|
126
126
|
buildRuleContext,
|
|
127
127
|
interpretRuleResult,
|
|
128
|
-
} from './rules.
|
|
128
|
+
} from './rules.tjs'
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Create an RBAC instance with a store backend
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Ambient declarations for rules.tjs. The bun plugin (bunfig.toml)
|
|
2
|
+
// transpiles .tjs at runtime; tsc needs explicit names to resolve the
|
|
3
|
+
// `from './rules.tjs'` imports in index.ts.
|
|
4
|
+
export function evaluateAccessShortcut(accessRule: any, context: any): any
|
|
5
|
+
export function selectAccessRule(rule: any, context: any): any
|
|
6
|
+
export function validateSchema(schema: any, data: any): any
|
|
7
|
+
export function interpretRuleResult(result: any): any
|
|
8
|
+
export function hasRoleLevel(userRoles: any, requiredRole: any): any
|
|
9
|
+
export function buildRuleContext(options: any): any
|
|
@@ -25,7 +25,7 @@ interface StoreBattery {
|
|
|
25
25
|
interface LLMBattery {
|
|
26
26
|
predict(
|
|
27
27
|
system: string,
|
|
28
|
-
user: string,
|
|
28
|
+
user: string | any[], // string for single-turn, message array for multi-turn
|
|
29
29
|
tools?: any[],
|
|
30
30
|
responseFormat?: any
|
|
31
31
|
): Promise<any>
|
|
@@ -140,7 +140,7 @@ export const llmPredictBattery = defineAtom(
|
|
|
140
140
|
'llmPredictBattery',
|
|
141
141
|
s.object({
|
|
142
142
|
system: s.string.optional,
|
|
143
|
-
user: s.string,
|
|
143
|
+
user: s.union([s.string, s.array(s.any)]), // string or message array for multi-turn
|
|
144
144
|
tools: s.array(s.any).optional,
|
|
145
145
|
responseFormat: s.any.optional,
|
|
146
146
|
}),
|
package/src/vm/runtime.ts
CHANGED
|
@@ -451,7 +451,9 @@ export type ExprNode =
|
|
|
451
451
|
| {
|
|
452
452
|
$expr: 'member'
|
|
453
453
|
object: ExprNode
|
|
454
|
-
|
|
454
|
+
// string for static `obj.foo` and literal-indexed `arr[0]`;
|
|
455
|
+
// ExprNode for variable-indexed `arr[i]` (evaluated at runtime).
|
|
456
|
+
property: string | ExprNode
|
|
455
457
|
computed?: boolean
|
|
456
458
|
optional?: boolean
|
|
457
459
|
}
|
|
@@ -1132,8 +1134,12 @@ export function evaluateExpr(node: ExprNode, ctx: RuntimeContext): any {
|
|
|
1132
1134
|
return undefined
|
|
1133
1135
|
}
|
|
1134
1136
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
+
// Property is either a static string or a computed expression node (e.g. arr[i])
|
|
1138
|
+
const prop =
|
|
1139
|
+
typeof node.property === 'object' && node.property !== null
|
|
1140
|
+
? evaluateExpr(node.property, ctx)
|
|
1141
|
+
: node.property
|
|
1142
|
+
assertSafeProperty(String(prop))
|
|
1137
1143
|
|
|
1138
1144
|
return obj?.[prop]
|
|
1139
1145
|
}
|
|
@@ -1524,6 +1530,7 @@ export const whileLoop = defineAtom(
|
|
|
1524
1530
|
if ((ctx.fuel.current -= 0.1) <= 0) throw new Error('Out of Fuel')
|
|
1525
1531
|
await seq.exec({ op: 'seq', steps: step.body } as any, ctx)
|
|
1526
1532
|
if (ctx.output !== undefined) return
|
|
1533
|
+
if (ctx.error) return // Propagate monadic errors out of the loop
|
|
1527
1534
|
}
|
|
1528
1535
|
},
|
|
1529
1536
|
{ docs: 'While Loop', timeoutMs: 0, cost: 0.1 }
|
package/dist/src/rbac/rules.d.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
export function evaluateAccessShortcut(accessRule: any, context: any): {
|
|
2
|
-
allowed: boolean;
|
|
3
|
-
reason: string;
|
|
4
|
-
} | {
|
|
5
|
-
allowed: boolean;
|
|
6
|
-
reason?: undefined;
|
|
7
|
-
} | null;
|
|
8
|
-
export namespace evaluateAccessShortcut {
|
|
9
|
-
namespace __tjs {
|
|
10
|
-
namespace params {
|
|
11
|
-
namespace accessRule {
|
|
12
|
-
namespace type {
|
|
13
|
-
let kind: string;
|
|
14
|
-
}
|
|
15
|
-
let required: boolean;
|
|
16
|
-
}
|
|
17
|
-
namespace context {
|
|
18
|
-
export namespace type_1 {
|
|
19
|
-
let kind_1: string;
|
|
20
|
-
export { kind_1 as kind };
|
|
21
|
-
}
|
|
22
|
-
export { type_1 as type };
|
|
23
|
-
let required_1: boolean;
|
|
24
|
-
export { required_1 as required };
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
let unsafe: boolean;
|
|
28
|
-
let source: string;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
export function selectAccessRule(rule: any, context: any): any;
|
|
32
|
-
export namespace selectAccessRule {
|
|
33
|
-
export namespace __tjs_1 {
|
|
34
|
-
export namespace params_1 {
|
|
35
|
-
export namespace rule {
|
|
36
|
-
export namespace type_2 {
|
|
37
|
-
let kind_2: string;
|
|
38
|
-
export { kind_2 as kind };
|
|
39
|
-
}
|
|
40
|
-
export { type_2 as type };
|
|
41
|
-
let required_2: boolean;
|
|
42
|
-
export { required_2 as required };
|
|
43
|
-
}
|
|
44
|
-
export namespace context_1 {
|
|
45
|
-
export namespace type_3 {
|
|
46
|
-
let kind_3: string;
|
|
47
|
-
export { kind_3 as kind };
|
|
48
|
-
}
|
|
49
|
-
export { type_3 as type };
|
|
50
|
-
let required_3: boolean;
|
|
51
|
-
export { required_3 as required };
|
|
52
|
-
}
|
|
53
|
-
export { context_1 as context };
|
|
54
|
-
}
|
|
55
|
-
export { params_1 as params };
|
|
56
|
-
let unsafe_1: boolean;
|
|
57
|
-
export { unsafe_1 as unsafe };
|
|
58
|
-
let source_1: string;
|
|
59
|
-
export { source_1 as source };
|
|
60
|
-
}
|
|
61
|
-
export { __tjs_1 as __tjs };
|
|
62
|
-
}
|
|
63
|
-
export function validateSchema(schema: any, data: any): {
|
|
64
|
-
valid: boolean;
|
|
65
|
-
errors: string[];
|
|
66
|
-
};
|
|
67
|
-
export namespace validateSchema {
|
|
68
|
-
export namespace __tjs_2 {
|
|
69
|
-
export namespace params_2 {
|
|
70
|
-
namespace schema {
|
|
71
|
-
export namespace type_4 {
|
|
72
|
-
let kind_4: string;
|
|
73
|
-
export { kind_4 as kind };
|
|
74
|
-
}
|
|
75
|
-
export { type_4 as type };
|
|
76
|
-
let required_4: boolean;
|
|
77
|
-
export { required_4 as required };
|
|
78
|
-
}
|
|
79
|
-
namespace data {
|
|
80
|
-
export namespace type_5 {
|
|
81
|
-
let kind_5: string;
|
|
82
|
-
export { kind_5 as kind };
|
|
83
|
-
}
|
|
84
|
-
export { type_5 as type };
|
|
85
|
-
let required_5: boolean;
|
|
86
|
-
export { required_5 as required };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
export { params_2 as params };
|
|
90
|
-
let unsafe_2: boolean;
|
|
91
|
-
export { unsafe_2 as unsafe };
|
|
92
|
-
let source_2: string;
|
|
93
|
-
export { source_2 as source };
|
|
94
|
-
}
|
|
95
|
-
export { __tjs_2 as __tjs };
|
|
96
|
-
}
|
|
97
|
-
export function interpretRuleResult(result: any): {
|
|
98
|
-
allowed: boolean;
|
|
99
|
-
reason: any;
|
|
100
|
-
};
|
|
101
|
-
export namespace interpretRuleResult {
|
|
102
|
-
export namespace __tjs_3 {
|
|
103
|
-
export namespace params_3 {
|
|
104
|
-
namespace result {
|
|
105
|
-
export namespace type_6 {
|
|
106
|
-
let kind_6: string;
|
|
107
|
-
export { kind_6 as kind };
|
|
108
|
-
}
|
|
109
|
-
export { type_6 as type };
|
|
110
|
-
let required_6: boolean;
|
|
111
|
-
export { required_6 as required };
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
export { params_3 as params };
|
|
115
|
-
let unsafe_3: boolean;
|
|
116
|
-
export { unsafe_3 as unsafe };
|
|
117
|
-
let source_3: string;
|
|
118
|
-
export { source_3 as source };
|
|
119
|
-
}
|
|
120
|
-
export { __tjs_3 as __tjs };
|
|
121
|
-
}
|
|
122
|
-
export function hasRoleLevel(userRoles: any, requiredRole: any): boolean;
|
|
123
|
-
export namespace hasRoleLevel {
|
|
124
|
-
export namespace __tjs_4 {
|
|
125
|
-
export namespace params_4 {
|
|
126
|
-
namespace userRoles {
|
|
127
|
-
export namespace type_7 {
|
|
128
|
-
let kind_7: string;
|
|
129
|
-
export { kind_7 as kind };
|
|
130
|
-
}
|
|
131
|
-
export { type_7 as type };
|
|
132
|
-
let required_7: boolean;
|
|
133
|
-
export { required_7 as required };
|
|
134
|
-
}
|
|
135
|
-
namespace requiredRole {
|
|
136
|
-
export namespace type_8 {
|
|
137
|
-
let kind_8: string;
|
|
138
|
-
export { kind_8 as kind };
|
|
139
|
-
}
|
|
140
|
-
export { type_8 as type };
|
|
141
|
-
let required_8: boolean;
|
|
142
|
-
export { required_8 as required };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
export { params_4 as params };
|
|
146
|
-
let unsafe_4: boolean;
|
|
147
|
-
export { unsafe_4 as unsafe };
|
|
148
|
-
let source_4: string;
|
|
149
|
-
export { source_4 as source };
|
|
150
|
-
}
|
|
151
|
-
export { __tjs_4 as __tjs };
|
|
152
|
-
}
|
|
153
|
-
export function buildRuleContext(options: any): {
|
|
154
|
-
_uid: any;
|
|
155
|
-
_roles: any;
|
|
156
|
-
_isAdmin: any;
|
|
157
|
-
_isAuthor: any;
|
|
158
|
-
_method: any;
|
|
159
|
-
_collection: any;
|
|
160
|
-
_docId: any;
|
|
161
|
-
doc: any;
|
|
162
|
-
newData: any;
|
|
163
|
-
};
|
|
164
|
-
export namespace buildRuleContext {
|
|
165
|
-
export namespace __tjs_5 {
|
|
166
|
-
export namespace params_5 {
|
|
167
|
-
namespace options {
|
|
168
|
-
export namespace type_9 {
|
|
169
|
-
let kind_9: string;
|
|
170
|
-
export { kind_9 as kind };
|
|
171
|
-
}
|
|
172
|
-
export { type_9 as type };
|
|
173
|
-
let required_9: boolean;
|
|
174
|
-
export { required_9 as required };
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
export { params_5 as params };
|
|
178
|
-
let unsafe_5: boolean;
|
|
179
|
-
export { unsafe_5 as unsafe };
|
|
180
|
-
let source_5: string;
|
|
181
|
-
export { source_5 as source };
|
|
182
|
-
}
|
|
183
|
-
export { __tjs_5 as __tjs };
|
|
184
|
-
}
|