@kernlang/core 3.1.8 → 3.2.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/dist/codegen/functions.js +15 -5
- package/dist/codegen/functions.js.map +1 -1
- package/dist/codegen/machines.d.ts +4 -1
- package/dist/codegen/machines.js +12 -4
- package/dist/codegen/machines.js.map +1 -1
- package/dist/codegen/screens.js +76 -63
- package/dist/codegen/screens.js.map +1 -1
- package/dist/codegen/type-system.js +16 -6
- package/dist/codegen/type-system.js.map +1 -1
- package/dist/codegen-core.js +7 -0
- package/dist/codegen-core.js.map +1 -1
- package/dist/config.d.ts +18 -1
- package/dist/config.js +82 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/parser-core.js +32 -3
- package/dist/parser-core.js.map +1 -1
- package/dist/schema.js +667 -47
- package/dist/schema.js.map +1 -1
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +17 -0
- package/dist/spec.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/version-adapters.d.ts +1 -1
- package/dist/version-adapters.js +11 -0
- package/dist/version-adapters.js.map +1 -1
- package/dist/version-detect.d.ts +1 -1
- package/dist/version-detect.js +2 -0
- package/dist/version-detect.js.map +1 -1
- package/package.json +1 -1
package/dist/schema.js
CHANGED
|
@@ -132,6 +132,19 @@ export const NODE_SCHEMAS = {
|
|
|
132
132
|
name: { required: true, kind: 'identifier' },
|
|
133
133
|
initial: { kind: 'rawExpr' },
|
|
134
134
|
type: { kind: 'typeAnnotation' },
|
|
135
|
+
safe: { kind: 'boolean' },
|
|
136
|
+
throttle: { kind: 'number' },
|
|
137
|
+
debounce: { kind: 'number' },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
animation: {
|
|
141
|
+
description: 'Interval-driven state update — generates useEffect with setInterval and auto-cleanup',
|
|
142
|
+
example: 'animation name=frame interval=100 update="(prev) => (prev + 1) % 4"',
|
|
143
|
+
props: {
|
|
144
|
+
name: { required: true, kind: 'identifier' },
|
|
145
|
+
interval: { required: true, kind: 'number' },
|
|
146
|
+
update: { required: true, kind: 'rawExpr' },
|
|
147
|
+
active: { kind: 'rawExpr' },
|
|
135
148
|
},
|
|
136
149
|
},
|
|
137
150
|
transition: {
|
|
@@ -740,9 +753,11 @@ export const NODE_SCHEMAS = {
|
|
|
740
753
|
// ── React / UI element nodes ──────────────────────────────────────────
|
|
741
754
|
screen: {
|
|
742
755
|
description: 'Full-screen container component (minHeight: 100vh flex column)',
|
|
743
|
-
example: 'screen name=Dashboard\n row\n text value="Welcome"',
|
|
756
|
+
example: 'screen name=Dashboard export=default memo=true\n row\n text value="Welcome"',
|
|
744
757
|
props: {
|
|
745
758
|
name: { kind: 'identifier' },
|
|
759
|
+
export: { kind: 'string' },
|
|
760
|
+
memo: { kind: 'rawExpr' },
|
|
746
761
|
},
|
|
747
762
|
},
|
|
748
763
|
row: {
|
|
@@ -820,6 +835,611 @@ export const NODE_SCHEMAS = {
|
|
|
820
835
|
styles: { kind: 'rawExpr' },
|
|
821
836
|
},
|
|
822
837
|
},
|
|
838
|
+
// ── Backend: Stream / Spawn / Timer ───────────────────────────────────
|
|
839
|
+
stream: {
|
|
840
|
+
description: 'Async stream — SSE route (backend), or AsyncGenerator → state with cleanup (Ink). mode=channel for dispatch bridging.',
|
|
841
|
+
example: 'stream name=messages source=session.messages mode=channel dispatch=handleChunk',
|
|
842
|
+
props: {
|
|
843
|
+
name: { kind: 'identifier' },
|
|
844
|
+
source: { kind: 'rawExpr' },
|
|
845
|
+
append: { kind: 'boolean' },
|
|
846
|
+
mode: { kind: 'string' },
|
|
847
|
+
dispatch: { kind: 'rawExpr' },
|
|
848
|
+
},
|
|
849
|
+
allowedChildren: ['spawn', 'handler', 'on', 'timer'],
|
|
850
|
+
},
|
|
851
|
+
spawn: {
|
|
852
|
+
description: 'Child process — spawns a binary with shell:false safety, SIGTERM/SIGKILL escalation, and abort-on-disconnect',
|
|
853
|
+
example: "spawn binary=ffmpeg args=\"['-i',input,'-f','mp3','pipe:1']\" timeout=30",
|
|
854
|
+
props: {
|
|
855
|
+
binary: { required: true, kind: 'string' },
|
|
856
|
+
args: { kind: 'rawExpr' },
|
|
857
|
+
timeout: { kind: 'number' },
|
|
858
|
+
stdin: { kind: 'rawExpr' },
|
|
859
|
+
},
|
|
860
|
+
allowedChildren: ['on', 'env', 'handler'],
|
|
861
|
+
},
|
|
862
|
+
timer: {
|
|
863
|
+
description: 'Request timeout — wraps handler in a deadline with AbortController and configurable timeout handler',
|
|
864
|
+
example: 'timer timeout=15\n handler <<<\n const result = await longRunningTask();\n res.json(result);\n >>>',
|
|
865
|
+
props: {
|
|
866
|
+
timeout: { kind: 'number' },
|
|
867
|
+
name: { kind: 'identifier' },
|
|
868
|
+
},
|
|
869
|
+
allowedChildren: ['handler', 'on'],
|
|
870
|
+
},
|
|
871
|
+
env: {
|
|
872
|
+
description: 'Environment variable — declares a required or optional env var, used in spawn or server config',
|
|
873
|
+
example: 'env name=DATABASE_URL required=true',
|
|
874
|
+
props: {
|
|
875
|
+
name: { required: true, kind: 'identifier' },
|
|
876
|
+
value: { kind: 'rawExpr' },
|
|
877
|
+
required: { kind: 'boolean' },
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
trigger: {
|
|
881
|
+
description: 'Event trigger — fires an action on a named event from a source',
|
|
882
|
+
example: 'trigger kind=webhook on=push from=github',
|
|
883
|
+
props: {
|
|
884
|
+
kind: { kind: 'identifier' },
|
|
885
|
+
on: { kind: 'string' },
|
|
886
|
+
from: { kind: 'string' },
|
|
887
|
+
},
|
|
888
|
+
allowedChildren: ['handler'],
|
|
889
|
+
},
|
|
890
|
+
// ── Next.js production patterns ───────────────────────────────────────
|
|
891
|
+
fetch: {
|
|
892
|
+
description: 'Server-side data fetch — generates an async fetch call in a Next.js server component',
|
|
893
|
+
example: 'fetch name=posts url="/api/posts" options="{ next: { revalidate: 60 } }"',
|
|
894
|
+
props: {
|
|
895
|
+
name: { required: true, kind: 'identifier' },
|
|
896
|
+
url: { required: true, kind: 'rawExpr' },
|
|
897
|
+
options: { kind: 'rawExpr' },
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
generateMetadata: {
|
|
901
|
+
description: 'Next.js generateMetadata export — async function for dynamic page metadata',
|
|
902
|
+
example: 'generateMetadata params="slug:string"',
|
|
903
|
+
props: {
|
|
904
|
+
params: { kind: 'string' },
|
|
905
|
+
},
|
|
906
|
+
allowedChildren: ['handler'],
|
|
907
|
+
},
|
|
908
|
+
notFound: {
|
|
909
|
+
description: 'Next.js notFound() call — triggers 404 page',
|
|
910
|
+
example: 'notFound',
|
|
911
|
+
props: {},
|
|
912
|
+
},
|
|
913
|
+
redirect: {
|
|
914
|
+
description: 'Next.js redirect() call — server-side redirect to another route',
|
|
915
|
+
example: 'redirect to="/login"',
|
|
916
|
+
props: {
|
|
917
|
+
to: { required: true, kind: 'string' },
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
// ── CLI nodes ─────────────────────────────────────────────────────────
|
|
921
|
+
cli: {
|
|
922
|
+
description: 'CLI application root — defines a command-line tool with commands, flags, and imports',
|
|
923
|
+
example: 'cli name=myapp version=1.0.0 description="My CLI tool"\n command name=init description="Initialize project"\n handler <<<\n console.log("Initializing...")\n >>>',
|
|
924
|
+
props: {
|
|
925
|
+
name: { required: true, kind: 'identifier' },
|
|
926
|
+
version: { kind: 'string' },
|
|
927
|
+
description: { kind: 'string' },
|
|
928
|
+
},
|
|
929
|
+
allowedChildren: ['command', 'flag', 'import'],
|
|
930
|
+
},
|
|
931
|
+
command: {
|
|
932
|
+
description: 'CLI subcommand with arguments, flags, and handler',
|
|
933
|
+
example: 'command name=deploy description="Deploy to production" alias=d\n arg name=target type=string required=true\n flag name=dry-run alias=n type=boolean\n handler <<<\n deploy(target, { dryRun })\n >>>',
|
|
934
|
+
props: {
|
|
935
|
+
name: { required: true, kind: 'identifier' },
|
|
936
|
+
description: { kind: 'string' },
|
|
937
|
+
alias: { kind: 'string' },
|
|
938
|
+
},
|
|
939
|
+
allowedChildren: ['arg', 'flag', 'handler', 'import'],
|
|
940
|
+
},
|
|
941
|
+
arg: {
|
|
942
|
+
description: 'CLI positional argument — required args must come before optional ones',
|
|
943
|
+
example: 'arg name=target type=string required=true description="Deploy target"',
|
|
944
|
+
props: {
|
|
945
|
+
name: { required: true, kind: 'identifier' },
|
|
946
|
+
type: { kind: 'identifier' },
|
|
947
|
+
required: { kind: 'boolean' },
|
|
948
|
+
description: { kind: 'string' },
|
|
949
|
+
default: { kind: 'rawExpr' },
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
flag: {
|
|
953
|
+
description: 'CLI flag/option — named with optional short alias',
|
|
954
|
+
example: 'flag name=verbose alias=v type=boolean description="Enable verbose output"',
|
|
955
|
+
props: {
|
|
956
|
+
name: { required: true, kind: 'identifier' },
|
|
957
|
+
alias: { kind: 'string' },
|
|
958
|
+
type: { kind: 'identifier' },
|
|
959
|
+
required: { kind: 'boolean' },
|
|
960
|
+
description: { kind: 'string' },
|
|
961
|
+
default: { kind: 'rawExpr' },
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
// ── React lifecycle hooks (Batch 2) ───────────────────────────────────
|
|
965
|
+
memo: {
|
|
966
|
+
description: 'React useMemo — memoized computation with dependency tracking',
|
|
967
|
+
example: 'memo name=filtered deps="items,filter"\n handler <<<\n return items.filter(i => i.active)\n >>>',
|
|
968
|
+
props: {
|
|
969
|
+
name: { required: true, kind: 'identifier' },
|
|
970
|
+
deps: { kind: 'string' },
|
|
971
|
+
},
|
|
972
|
+
allowedChildren: ['handler'],
|
|
973
|
+
},
|
|
974
|
+
callback: {
|
|
975
|
+
description: 'React useCallback — memoized function reference with dependency tracking',
|
|
976
|
+
example: 'callback name=handleSubmit deps="formData" async=true\n handler <<<\n await api.submit(formData)\n >>>',
|
|
977
|
+
props: {
|
|
978
|
+
name: { required: true, kind: 'identifier' },
|
|
979
|
+
params: { kind: 'string' },
|
|
980
|
+
deps: { kind: 'string' },
|
|
981
|
+
async: { kind: 'boolean' },
|
|
982
|
+
},
|
|
983
|
+
allowedChildren: ['handler'],
|
|
984
|
+
},
|
|
985
|
+
ref: {
|
|
986
|
+
description: 'React useRef — mutable ref object that persists across renders',
|
|
987
|
+
example: 'ref name=inputRef type=HTMLInputElement initial=null',
|
|
988
|
+
props: {
|
|
989
|
+
name: { required: true, kind: 'identifier' },
|
|
990
|
+
type: { kind: 'typeAnnotation' },
|
|
991
|
+
initial: { kind: 'rawExpr' },
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
context: {
|
|
995
|
+
description: 'React useContext — consume a React context by name',
|
|
996
|
+
example: 'context name=theme source=ThemeContext',
|
|
997
|
+
props: {
|
|
998
|
+
name: { required: true, kind: 'identifier' },
|
|
999
|
+
source: { required: true, kind: 'identifier' },
|
|
1000
|
+
},
|
|
1001
|
+
},
|
|
1002
|
+
prop: {
|
|
1003
|
+
description: 'Component prop declaration — name, type, optionality, and default value',
|
|
1004
|
+
example: 'prop name=title type=string\nprop name=count type=number optional=true default=0',
|
|
1005
|
+
props: {
|
|
1006
|
+
name: { required: true, kind: 'identifier' },
|
|
1007
|
+
type: { kind: 'typeAnnotation' },
|
|
1008
|
+
optional: { kind: 'boolean' },
|
|
1009
|
+
default: { kind: 'rawExpr' },
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
returns: {
|
|
1013
|
+
description: 'Return type declaration or return statement for a hook/function',
|
|
1014
|
+
example: 'returns type=AuthState with="{ user, login, logout }"',
|
|
1015
|
+
props: {
|
|
1016
|
+
name: { kind: 'identifier' },
|
|
1017
|
+
type: { kind: 'typeAnnotation' },
|
|
1018
|
+
with: { kind: 'rawExpr' },
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
render: {
|
|
1022
|
+
description: 'Render function — JSX output block for a component or hook',
|
|
1023
|
+
example: 'render\n handler <<<\n return <div>{children}</div>\n >>>',
|
|
1024
|
+
props: {},
|
|
1025
|
+
allowedChildren: ['handler'],
|
|
1026
|
+
},
|
|
1027
|
+
template: {
|
|
1028
|
+
description: 'Reusable template with named slots — defines a composable layout pattern',
|
|
1029
|
+
example: 'template name=PageLayout\n slot name=header\n slot name=content\n slot name=footer optional=true',
|
|
1030
|
+
props: {
|
|
1031
|
+
name: { required: true, kind: 'identifier' },
|
|
1032
|
+
},
|
|
1033
|
+
allowedChildren: ['slot', 'body', 'handler'],
|
|
1034
|
+
},
|
|
1035
|
+
// ── Data layer (Batch 3) ──────────────────────────────────────────────
|
|
1036
|
+
column: {
|
|
1037
|
+
description: 'Database column definition within a model — type, constraints, and default value',
|
|
1038
|
+
example: 'column name=email type=string unique=true\ncolumn name=age type=number optional=true',
|
|
1039
|
+
props: {
|
|
1040
|
+
name: { required: true, kind: 'identifier' },
|
|
1041
|
+
type: { kind: 'typeAnnotation' },
|
|
1042
|
+
optional: { kind: 'boolean' },
|
|
1043
|
+
primary: { kind: 'boolean' },
|
|
1044
|
+
unique: { kind: 'boolean' },
|
|
1045
|
+
default: { kind: 'rawExpr' },
|
|
1046
|
+
},
|
|
1047
|
+
},
|
|
1048
|
+
relation: {
|
|
1049
|
+
description: 'Database relation — defines a foreign key relationship between models',
|
|
1050
|
+
example: 'relation name=author target=User kind=many-to-one',
|
|
1051
|
+
props: {
|
|
1052
|
+
name: { required: true, kind: 'identifier' },
|
|
1053
|
+
target: { required: true, kind: 'identifier' },
|
|
1054
|
+
kind: { kind: 'string' },
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
inject: {
|
|
1058
|
+
description: 'Dependency injection — inject a service or value into the current scope',
|
|
1059
|
+
example: 'inject name=db type=Database from="./database.js"',
|
|
1060
|
+
props: {
|
|
1061
|
+
name: { required: true, kind: 'identifier' },
|
|
1062
|
+
type: { kind: 'typeAnnotation' },
|
|
1063
|
+
from: { kind: 'rawExpr' },
|
|
1064
|
+
with: { kind: 'rawExpr' },
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
entry: {
|
|
1068
|
+
description: 'Cache entry — defines a cached value with key and optional strategy',
|
|
1069
|
+
example: 'entry name=userProfile key="user:{id}"\n strategy name=stale-while-revalidate max=60',
|
|
1070
|
+
props: {
|
|
1071
|
+
name: { required: true, kind: 'identifier' },
|
|
1072
|
+
key: { kind: 'string' },
|
|
1073
|
+
},
|
|
1074
|
+
allowedChildren: ['strategy', 'handler'],
|
|
1075
|
+
},
|
|
1076
|
+
invalidate: {
|
|
1077
|
+
description: 'Cache invalidation rule — trigger cache clearing on an event',
|
|
1078
|
+
example: 'invalidate on=userUpdate tags="user,profile"',
|
|
1079
|
+
props: {
|
|
1080
|
+
on: { required: true, kind: 'string' },
|
|
1081
|
+
tags: { kind: 'string' },
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
signal: {
|
|
1085
|
+
description: 'Reactive signal — named state that triggers updates on change (used in hooks/components)',
|
|
1086
|
+
example: 'signal name=isLoading',
|
|
1087
|
+
props: {
|
|
1088
|
+
name: { required: true, kind: 'identifier' },
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
// ── Structural + UI controls (Batch 4) ────────────────────────────────
|
|
1092
|
+
section: {
|
|
1093
|
+
description: 'Semantic section container — groups related content with optional title',
|
|
1094
|
+
example: 'section title="User Settings"',
|
|
1095
|
+
props: {
|
|
1096
|
+
title: { kind: 'string' },
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
list: {
|
|
1100
|
+
description: 'List container — renders child items as an ordered or unordered list',
|
|
1101
|
+
example: 'list\n item value="First"\n item value="Second"',
|
|
1102
|
+
props: {},
|
|
1103
|
+
allowedChildren: ['item'],
|
|
1104
|
+
},
|
|
1105
|
+
item: {
|
|
1106
|
+
description: 'List item — single entry within a list container',
|
|
1107
|
+
example: 'item value="Buy groceries"',
|
|
1108
|
+
props: {
|
|
1109
|
+
value: { kind: 'string' },
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
option: {
|
|
1113
|
+
description: 'Select option — a selectable choice within a select dropdown',
|
|
1114
|
+
example: 'option value=admin label="Administrator"',
|
|
1115
|
+
props: {
|
|
1116
|
+
value: { required: true, kind: 'string' },
|
|
1117
|
+
label: { kind: 'string' },
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
select: {
|
|
1121
|
+
description: 'Select dropdown — bound to state with child options',
|
|
1122
|
+
example: 'select bind=role\n option value=admin label="Admin"\n option value=user label="User"',
|
|
1123
|
+
props: {
|
|
1124
|
+
bind: { kind: 'identifier' },
|
|
1125
|
+
},
|
|
1126
|
+
allowedChildren: ['option'],
|
|
1127
|
+
},
|
|
1128
|
+
slot: {
|
|
1129
|
+
description: 'Template slot — named insertion point within a template',
|
|
1130
|
+
example: 'slot name=header optional=true default="Default Header"',
|
|
1131
|
+
props: {
|
|
1132
|
+
name: { required: true, kind: 'identifier' },
|
|
1133
|
+
slotType: { kind: 'string' },
|
|
1134
|
+
optional: { kind: 'boolean' },
|
|
1135
|
+
default: { kind: 'rawExpr' },
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
body: {
|
|
1139
|
+
description: 'Body block — raw code content for templates or structural containers',
|
|
1140
|
+
example: 'body <<<\n <main>{children}</main>\n>>>',
|
|
1141
|
+
props: {
|
|
1142
|
+
code: { kind: 'rawBlock' },
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
// ── Phase 3: Remaining node schemas (100% coverage) ───────────────────
|
|
1146
|
+
// Terminal / Ink UI
|
|
1147
|
+
scroll: { description: 'Scrollable container', example: 'scroll', props: {} },
|
|
1148
|
+
progress: {
|
|
1149
|
+
description: 'Progress bar — shows completion status',
|
|
1150
|
+
example: 'progress value=75 max=100 label="Loading"',
|
|
1151
|
+
props: { value: { kind: 'number' }, max: { kind: 'number' }, label: { kind: 'string' } },
|
|
1152
|
+
},
|
|
1153
|
+
divider: { description: 'Visual divider / horizontal rule', example: 'divider', props: {} },
|
|
1154
|
+
codeblock: {
|
|
1155
|
+
description: 'Code block with syntax highlighting',
|
|
1156
|
+
example: 'codeblock lang=typescript <<<\n const x = 1;\n>>>',
|
|
1157
|
+
props: { lang: { kind: 'string' }, code: { kind: 'rawBlock' } },
|
|
1158
|
+
},
|
|
1159
|
+
tab: {
|
|
1160
|
+
description: 'Single tab within a tabs container',
|
|
1161
|
+
example: 'tab label="Settings"\n text value="Settings content"',
|
|
1162
|
+
props: { label: { kind: 'string' } },
|
|
1163
|
+
},
|
|
1164
|
+
separator: { description: 'Ink horizontal rule / separator', example: 'separator', props: {} },
|
|
1165
|
+
thead: { description: 'Table head section', example: 'thead', props: {} },
|
|
1166
|
+
tbody: { description: 'Table body section', example: 'tbody', props: {} },
|
|
1167
|
+
tr: { description: 'Table row', example: 'tr', props: {} },
|
|
1168
|
+
th: { description: 'Table header cell', example: 'th value="Name"', props: { value: { kind: 'string' } } },
|
|
1169
|
+
td: { description: 'Table data cell', example: 'td value="John"', props: { value: { kind: 'string' } } },
|
|
1170
|
+
scoreboard: {
|
|
1171
|
+
description: 'Dashboard scoreboard — container for metric widgets',
|
|
1172
|
+
example: 'scoreboard\n metric label="Users" value=1234',
|
|
1173
|
+
props: {},
|
|
1174
|
+
allowedChildren: ['metric'],
|
|
1175
|
+
},
|
|
1176
|
+
metric: {
|
|
1177
|
+
description: 'Single metric display — label + value pair',
|
|
1178
|
+
example: 'metric label="Active Users" value={{users.length}}',
|
|
1179
|
+
props: { label: { required: true, kind: 'string' }, value: { required: true, kind: 'rawExpr' } },
|
|
1180
|
+
},
|
|
1181
|
+
spinner: {
|
|
1182
|
+
description: 'Loading spinner with optional text',
|
|
1183
|
+
example: 'spinner text="Loading..."',
|
|
1184
|
+
props: { text: { kind: 'string' } },
|
|
1185
|
+
},
|
|
1186
|
+
box: {
|
|
1187
|
+
description: 'Ink box container with border styling',
|
|
1188
|
+
example: 'box borderStyle=round borderColor=green',
|
|
1189
|
+
props: { borderStyle: { kind: 'string' }, borderColor: { kind: 'string' } },
|
|
1190
|
+
},
|
|
1191
|
+
gradient: {
|
|
1192
|
+
description: 'Gradient text effect (Ink)',
|
|
1193
|
+
example: 'gradient text="Hello" colors="red,blue"',
|
|
1194
|
+
props: { text: { kind: 'string' }, colors: { kind: 'string' } },
|
|
1195
|
+
},
|
|
1196
|
+
// Ink-specific input nodes
|
|
1197
|
+
'input-area': { description: 'Ink text input area', example: 'input-area', props: {} },
|
|
1198
|
+
'output-area': { description: 'Ink text output area', example: 'output-area', props: {} },
|
|
1199
|
+
'text-input': {
|
|
1200
|
+
description: 'Ink text input with binding',
|
|
1201
|
+
example: 'text-input value={{query}} onChange={{setQuery}} placeholder="Search..."',
|
|
1202
|
+
props: { value: { kind: 'rawExpr' }, onChange: { kind: 'rawExpr' }, placeholder: { kind: 'string' } },
|
|
1203
|
+
},
|
|
1204
|
+
'select-input': {
|
|
1205
|
+
description: 'Ink select input — choose from a list',
|
|
1206
|
+
example: 'select-input items={{options}} onSelect={{handleSelect}}',
|
|
1207
|
+
props: { items: { kind: 'rawExpr' }, onSelect: { kind: 'rawExpr' } },
|
|
1208
|
+
},
|
|
1209
|
+
'multi-select': {
|
|
1210
|
+
description: 'Ink multi-select — choose multiple options from a list',
|
|
1211
|
+
example: 'multi-select options={{items}} onChange={{handleChange}}',
|
|
1212
|
+
props: { options: { kind: 'rawExpr' }, onChange: { kind: 'rawExpr' }, defaultValue: { kind: 'rawExpr' } },
|
|
1213
|
+
},
|
|
1214
|
+
'confirm-input': {
|
|
1215
|
+
description: 'Ink confirmation prompt — yes/no input',
|
|
1216
|
+
example: 'confirm-input onConfirm={{handleConfirm}} onCancel={{handleCancel}}',
|
|
1217
|
+
props: {
|
|
1218
|
+
onConfirm: { kind: 'rawExpr' },
|
|
1219
|
+
onCancel: { kind: 'rawExpr' },
|
|
1220
|
+
defaultChoice: { kind: 'string' },
|
|
1221
|
+
submitOnEnter: { kind: 'boolean' },
|
|
1222
|
+
},
|
|
1223
|
+
},
|
|
1224
|
+
'password-input': {
|
|
1225
|
+
description: 'Ink password input — masked text entry',
|
|
1226
|
+
example: 'password-input bind=password placeholder="Enter password..."',
|
|
1227
|
+
props: { bind: { kind: 'identifier' }, placeholder: { kind: 'string' }, onChange: { kind: 'rawExpr' } },
|
|
1228
|
+
},
|
|
1229
|
+
'status-message': {
|
|
1230
|
+
description: 'Ink status message — success/error/warning indicator',
|
|
1231
|
+
example: 'status-message variant="success"\n text value="Done!"',
|
|
1232
|
+
props: { variant: { kind: 'string' } },
|
|
1233
|
+
},
|
|
1234
|
+
alert: {
|
|
1235
|
+
description: 'Ink alert — prominent notification box',
|
|
1236
|
+
example: 'alert variant="warning" title="Caution"\n text value="This cannot be undone."',
|
|
1237
|
+
props: { variant: { kind: 'string' }, title: { kind: 'string' } },
|
|
1238
|
+
},
|
|
1239
|
+
'ordered-list': {
|
|
1240
|
+
description: 'Ink ordered list — numbered items',
|
|
1241
|
+
example: 'ordered-list\n text value="First"\n text value="Second"',
|
|
1242
|
+
props: {},
|
|
1243
|
+
},
|
|
1244
|
+
'unordered-list': {
|
|
1245
|
+
description: 'Ink unordered list — bulleted items',
|
|
1246
|
+
example: 'unordered-list\n text value="Item A"\n text value="Item B"',
|
|
1247
|
+
props: {},
|
|
1248
|
+
},
|
|
1249
|
+
focus: {
|
|
1250
|
+
description: 'Ink focus management — useFocus hook',
|
|
1251
|
+
example: 'focus name=emailFocus autoFocus=true',
|
|
1252
|
+
props: { name: { required: true, kind: 'identifier' }, autoFocus: { kind: 'boolean' }, id: { kind: 'string' } },
|
|
1253
|
+
},
|
|
1254
|
+
'app-exit': {
|
|
1255
|
+
description: 'Ink app exit — useApp().exit() triggered by condition',
|
|
1256
|
+
example: 'app-exit on={{complete}}',
|
|
1257
|
+
props: { on: { required: true, kind: 'rawExpr' } },
|
|
1258
|
+
},
|
|
1259
|
+
'static-log': {
|
|
1260
|
+
description: 'Ink Static component — log-style output above dynamic content',
|
|
1261
|
+
example: 'static-log items={{logs}}\n text value={{item.message}}',
|
|
1262
|
+
props: { items: { required: true, kind: 'rawExpr' } },
|
|
1263
|
+
},
|
|
1264
|
+
newline: {
|
|
1265
|
+
description: 'Ink Newline component — insert line breaks',
|
|
1266
|
+
example: 'newline count=2',
|
|
1267
|
+
props: { count: { kind: 'number' } },
|
|
1268
|
+
},
|
|
1269
|
+
'layout-row': {
|
|
1270
|
+
description: 'Ink horizontal layout — Box with flexDirection=row',
|
|
1271
|
+
example: 'layout-row gap=2\n text value="Left"\n text value="Right"',
|
|
1272
|
+
props: { gap: { kind: 'number' }, padding: { kind: 'number' } },
|
|
1273
|
+
},
|
|
1274
|
+
'layout-col': {
|
|
1275
|
+
description: 'Ink vertical column — Box with flexDirection=column and flex grow',
|
|
1276
|
+
example: 'layout-col flex=1\n text value="Content"',
|
|
1277
|
+
props: { flex: { kind: 'number' }, width: { kind: 'number' } },
|
|
1278
|
+
},
|
|
1279
|
+
'layout-stack': {
|
|
1280
|
+
description: 'Ink vertical stack — Box with flexDirection=column (most common layout)',
|
|
1281
|
+
example: 'layout-stack padding=1\n text value="Header"\n text value="Body"',
|
|
1282
|
+
props: { padding: { kind: 'number' }, gap: { kind: 'number' } },
|
|
1283
|
+
},
|
|
1284
|
+
spacer: {
|
|
1285
|
+
description: 'Ink spacer — empty Box with flexGrow=1 for filling space',
|
|
1286
|
+
example: 'spacer',
|
|
1287
|
+
props: {},
|
|
1288
|
+
},
|
|
1289
|
+
'screen-embed': {
|
|
1290
|
+
description: 'Embed another screen component inline with typed props. Use from= for cross-file imports.',
|
|
1291
|
+
example: 'screen-embed screen=Header title="Dashboard"\nscreen-embed screen=SpinnerBlock from="./status.kern"',
|
|
1292
|
+
props: { screen: { required: true, kind: 'identifier' }, from: { kind: 'string' } },
|
|
1293
|
+
},
|
|
1294
|
+
// Control flow / structural
|
|
1295
|
+
repl: {
|
|
1296
|
+
description: 'Read-eval-print loop — interactive terminal command loop',
|
|
1297
|
+
example: 'repl name=shell prompt=">"',
|
|
1298
|
+
props: { name: { kind: 'identifier' }, prompt: { kind: 'string' } },
|
|
1299
|
+
allowedChildren: ['on', 'handler'],
|
|
1300
|
+
},
|
|
1301
|
+
parallel: {
|
|
1302
|
+
description: 'Parallel execution — run children concurrently',
|
|
1303
|
+
example: 'parallel\n dispatch to=worker1\n dispatch to=worker2',
|
|
1304
|
+
props: { name: { kind: 'identifier' } },
|
|
1305
|
+
},
|
|
1306
|
+
dispatch: {
|
|
1307
|
+
description: 'Dispatch an action or message to a target',
|
|
1308
|
+
example: 'dispatch to=worker payload={{data}}',
|
|
1309
|
+
props: { to: { required: true, kind: 'string' }, payload: { kind: 'rawExpr' } },
|
|
1310
|
+
},
|
|
1311
|
+
// biome-ignore lint/suspicious/noThenProperty: `then` is a valid KERN node type, not a Promise thenable
|
|
1312
|
+
then: {
|
|
1313
|
+
description: 'Sequential continuation — runs after parent completes',
|
|
1314
|
+
example: 'then\n handler <<<\n console.log("done")\n >>>',
|
|
1315
|
+
props: {},
|
|
1316
|
+
allowedChildren: ['handler'],
|
|
1317
|
+
},
|
|
1318
|
+
// Lifecycle / structural children
|
|
1319
|
+
singleton: {
|
|
1320
|
+
description: 'Singleton marker — service is instantiated once',
|
|
1321
|
+
example: 'singleton name=cache',
|
|
1322
|
+
props: { name: { kind: 'identifier' } },
|
|
1323
|
+
},
|
|
1324
|
+
constructor: {
|
|
1325
|
+
description: 'Constructor for a service — runs on instantiation',
|
|
1326
|
+
example: 'constructor params="size:number"\n handler <<<\n this.data = new Map();\n >>>',
|
|
1327
|
+
props: { params: { kind: 'string' } },
|
|
1328
|
+
allowedChildren: ['handler'],
|
|
1329
|
+
},
|
|
1330
|
+
cleanup: {
|
|
1331
|
+
description: 'Cleanup handler — runs on teardown (useEffect return, signal dispose)',
|
|
1332
|
+
example: 'cleanup <<<\n controller.abort();\n>>>',
|
|
1333
|
+
props: { code: { kind: 'rawBlock' } },
|
|
1334
|
+
},
|
|
1335
|
+
export: {
|
|
1336
|
+
description: 'Re-export statement — export names from another module',
|
|
1337
|
+
example: 'export from="./utils.js" names="add,subtract"',
|
|
1338
|
+
props: {
|
|
1339
|
+
from: { kind: 'importPath' },
|
|
1340
|
+
names: { kind: 'string' },
|
|
1341
|
+
types: { kind: 'string' },
|
|
1342
|
+
star: { kind: 'boolean' },
|
|
1343
|
+
default: { kind: 'identifier' },
|
|
1344
|
+
},
|
|
1345
|
+
},
|
|
1346
|
+
describe: {
|
|
1347
|
+
description: 'Test suite — groups related test cases',
|
|
1348
|
+
example: 'describe name="UserService"\n it name="creates a user"\n handler <<<\n expect(createUser()).toBeDefined();\n >>>',
|
|
1349
|
+
props: { name: { required: true, kind: 'string' } },
|
|
1350
|
+
allowedChildren: ['it', 'describe', 'handler'],
|
|
1351
|
+
},
|
|
1352
|
+
it: {
|
|
1353
|
+
description: 'Test case — single test assertion',
|
|
1354
|
+
example: 'it name="returns 200 on success"\n handler <<<\n expect(res.status).toBe(200);\n >>>',
|
|
1355
|
+
props: { name: { required: true, kind: 'string' } },
|
|
1356
|
+
allowedChildren: ['handler'],
|
|
1357
|
+
},
|
|
1358
|
+
// Ground layer — semantic reasoning
|
|
1359
|
+
path: {
|
|
1360
|
+
description: 'Decision path — a named branch in a resolve/branch tree',
|
|
1361
|
+
example: 'path value="/api/users"',
|
|
1362
|
+
props: { value: { required: true, kind: 'string' } },
|
|
1363
|
+
},
|
|
1364
|
+
resolve: {
|
|
1365
|
+
description: 'Resolution node — selects among candidates using a discriminator',
|
|
1366
|
+
example: 'resolve name=bestRoute\n candidate name=fast\n candidate name=reliable',
|
|
1367
|
+
props: { name: { kind: 'identifier' } },
|
|
1368
|
+
allowedChildren: ['candidate', 'discriminator', 'handler'],
|
|
1369
|
+
},
|
|
1370
|
+
candidate: {
|
|
1371
|
+
description: 'Candidate option within a resolve block',
|
|
1372
|
+
example: 'candidate name=primary\n handler <<<\n return fastPath();\n >>>',
|
|
1373
|
+
props: { name: { required: true, kind: 'identifier' } },
|
|
1374
|
+
allowedChildren: ['handler'],
|
|
1375
|
+
},
|
|
1376
|
+
discriminator: {
|
|
1377
|
+
description: 'Selection strategy for choosing among candidates',
|
|
1378
|
+
example: 'discriminator method=latency metric=p99',
|
|
1379
|
+
props: { method: { kind: 'identifier' }, metric: { kind: 'string' } },
|
|
1380
|
+
allowedChildren: ['handler'],
|
|
1381
|
+
},
|
|
1382
|
+
pattern: {
|
|
1383
|
+
description: 'Pattern match — structural matching on values',
|
|
1384
|
+
example: 'pattern name=classify on={{input.type}}',
|
|
1385
|
+
props: { name: { kind: 'identifier' }, on: { kind: 'rawExpr' } },
|
|
1386
|
+
allowedChildren: ['path', 'handler'],
|
|
1387
|
+
},
|
|
1388
|
+
apply: {
|
|
1389
|
+
description: 'Apply a transform or function to data',
|
|
1390
|
+
example: 'apply fn=normalize to={{rawData}}',
|
|
1391
|
+
props: { fn: { kind: 'identifier' }, to: { kind: 'rawExpr' } },
|
|
1392
|
+
},
|
|
1393
|
+
expect: {
|
|
1394
|
+
description: 'Assertion — declare an expected condition at runtime',
|
|
1395
|
+
example: 'expect expr={{items.length > 0}} message="Items must not be empty"',
|
|
1396
|
+
props: { expr: { required: true, kind: 'rawExpr' }, message: { kind: 'string' } },
|
|
1397
|
+
},
|
|
1398
|
+
recover: {
|
|
1399
|
+
description: 'Recovery handler — runs when a parent node fails',
|
|
1400
|
+
example: 'recover\n handler <<<\n return fallbackValue;\n >>>',
|
|
1401
|
+
props: {},
|
|
1402
|
+
allowedChildren: ['handler'],
|
|
1403
|
+
},
|
|
1404
|
+
strategy: {
|
|
1405
|
+
description: 'Retry/fallback strategy configuration',
|
|
1406
|
+
example: 'strategy name=exponential-backoff max=3 delay=1000',
|
|
1407
|
+
props: { name: { required: true, kind: 'identifier' }, max: { kind: 'number' }, delay: { kind: 'number' } },
|
|
1408
|
+
allowedChildren: ['handler'],
|
|
1409
|
+
},
|
|
1410
|
+
// Reason layer — metadata children
|
|
1411
|
+
reason: {
|
|
1412
|
+
description: 'Reason annotation — explains why a decision was made',
|
|
1413
|
+
example: 'reason text="Using cache to avoid repeated API calls"',
|
|
1414
|
+
props: { text: { kind: 'string' } },
|
|
1415
|
+
},
|
|
1416
|
+
evidence: {
|
|
1417
|
+
description: 'Evidence annotation — links to supporting data for a decision',
|
|
1418
|
+
example: 'evidence text="Benchmarks show 3x speedup" source="perf-report.md"',
|
|
1419
|
+
props: { text: { kind: 'string' }, source: { kind: 'string' } },
|
|
1420
|
+
},
|
|
1421
|
+
needs: {
|
|
1422
|
+
description: 'Confidence gap — declares what evidence is missing',
|
|
1423
|
+
example: 'needs text="Integration test for concurrent writes"',
|
|
1424
|
+
props: { text: { kind: 'string' } },
|
|
1425
|
+
},
|
|
1426
|
+
// Rule layer — native .kern lint rules
|
|
1427
|
+
rule: {
|
|
1428
|
+
description: 'Custom lint rule definition — matches patterns and emits findings',
|
|
1429
|
+
example: 'rule id=no-console severity=warning category=style\n message template="Avoid console.log in production"',
|
|
1430
|
+
props: {
|
|
1431
|
+
id: { required: true, kind: 'identifier' },
|
|
1432
|
+
severity: { kind: 'string' },
|
|
1433
|
+
category: { kind: 'string' },
|
|
1434
|
+
confidence: { kind: 'number' },
|
|
1435
|
+
},
|
|
1436
|
+
allowedChildren: ['message', 'handler'],
|
|
1437
|
+
},
|
|
1438
|
+
message: {
|
|
1439
|
+
description: 'Rule message template — the text shown when a lint rule matches',
|
|
1440
|
+
example: 'message template="Found {count} unused imports"',
|
|
1441
|
+
props: { template: { kind: 'string' } },
|
|
1442
|
+
},
|
|
823
1443
|
};
|
|
824
1444
|
/**
|
|
825
1445
|
* Validate an IR tree against the schema definitions (required props, allowed children, cross-prop rules).
|
|
@@ -835,63 +1455,63 @@ export function validateSchema(root) {
|
|
|
835
1455
|
validateNode(root, violations);
|
|
836
1456
|
return violations;
|
|
837
1457
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
for (const [propName, propSchema] of Object.entries(schema.props)) {
|
|
844
|
-
if (propSchema.required && !(propName in props)) {
|
|
845
|
-
violations.push({
|
|
846
|
-
nodeType: node.type,
|
|
847
|
-
message: `'${node.type}' requires prop '${propName}'`,
|
|
848
|
-
line: node.loc?.line,
|
|
849
|
-
col: node.loc?.col,
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
// Cross-prop validation: component needs ref or name
|
|
854
|
-
if (node.type === 'component' && !('ref' in props) && !('name' in props)) {
|
|
1458
|
+
const UNIVERSAL_CHILDREN = new Set(['handler', 'cleanup', 'reason', 'evidence', 'needs', 'signal', 'doc']);
|
|
1459
|
+
function checkRequiredProps(node, schema, violations) {
|
|
1460
|
+
const props = node.props || {};
|
|
1461
|
+
for (const [propName, propSchema] of Object.entries(schema.props)) {
|
|
1462
|
+
if (propSchema.required && !(propName in props)) {
|
|
855
1463
|
violations.push({
|
|
856
|
-
nodeType:
|
|
857
|
-
message:
|
|
1464
|
+
nodeType: node.type,
|
|
1465
|
+
message: `'${node.type}' requires prop '${propName}'`,
|
|
858
1466
|
line: node.loc?.line,
|
|
859
1467
|
col: node.loc?.col,
|
|
860
1468
|
});
|
|
861
1469
|
}
|
|
862
|
-
|
|
863
|
-
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function checkCrossProps(node, violations) {
|
|
1473
|
+
const props = node.props || {};
|
|
1474
|
+
if (node.type === 'component' && !('ref' in props) && !('name' in props)) {
|
|
1475
|
+
violations.push({
|
|
1476
|
+
nodeType: 'component',
|
|
1477
|
+
message: "'component' requires either 'ref' or 'name' prop",
|
|
1478
|
+
line: node.loc?.line,
|
|
1479
|
+
col: node.loc?.col,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
if (node.type === 'guard' && !('expr' in props) && !('kind' in props) && !('type' in props)) {
|
|
1483
|
+
violations.push({
|
|
1484
|
+
nodeType: 'guard',
|
|
1485
|
+
message: "'guard' requires either 'expr' (assertion) or 'kind'/'type' (security guard)",
|
|
1486
|
+
line: node.loc?.line,
|
|
1487
|
+
col: node.loc?.col,
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function checkAllowedChildren(node, schema, violations) {
|
|
1492
|
+
if (!schema.allowedChildren || !node.children)
|
|
1493
|
+
return;
|
|
1494
|
+
for (const child of node.children) {
|
|
1495
|
+
if (!schema.allowedChildren.includes(child.type) && !UNIVERSAL_CHILDREN.has(child.type)) {
|
|
864
1496
|
violations.push({
|
|
865
|
-
nodeType:
|
|
866
|
-
message:
|
|
867
|
-
line:
|
|
868
|
-
col:
|
|
1497
|
+
nodeType: node.type,
|
|
1498
|
+
message: `'${node.type}' does not allow child type '${child.type}' (allowed: ${schema.allowedChildren.join(', ')})`,
|
|
1499
|
+
line: child.loc?.line,
|
|
1500
|
+
col: child.loc?.col,
|
|
869
1501
|
});
|
|
870
1502
|
}
|
|
871
|
-
// Check allowed children
|
|
872
|
-
if (schema.allowedChildren && node.children) {
|
|
873
|
-
for (const child of node.children) {
|
|
874
|
-
if (!schema.allowedChildren.includes(child.type)) {
|
|
875
|
-
// Don't flag structural children that are consumed by parents
|
|
876
|
-
// (handler, reason, evidence, needs, etc.)
|
|
877
|
-
const universalChildren = ['handler', 'cleanup', 'reason', 'evidence', 'needs', 'signal', 'doc'];
|
|
878
|
-
if (!universalChildren.includes(child.type)) {
|
|
879
|
-
violations.push({
|
|
880
|
-
nodeType: node.type,
|
|
881
|
-
message: `'${node.type}' does not allow child type '${child.type}' (allowed: ${schema.allowedChildren.join(', ')})`,
|
|
882
|
-
line: child.loc?.line,
|
|
883
|
-
col: child.loc?.col,
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
1503
|
}
|
|
890
|
-
|
|
1504
|
+
}
|
|
1505
|
+
function validateNode(node, violations) {
|
|
1506
|
+
const schema = Object.hasOwn(NODE_SCHEMAS, node.type) ? NODE_SCHEMAS[node.type] : undefined;
|
|
1507
|
+
if (schema) {
|
|
1508
|
+
checkRequiredProps(node, schema, violations);
|
|
1509
|
+
checkCrossProps(node, violations);
|
|
1510
|
+
checkAllowedChildren(node, schema, violations);
|
|
1511
|
+
}
|
|
891
1512
|
if (node.children) {
|
|
892
|
-
for (const child of node.children)
|
|
1513
|
+
for (const child of node.children)
|
|
893
1514
|
validateNode(child, violations);
|
|
894
|
-
}
|
|
895
1515
|
}
|
|
896
1516
|
}
|
|
897
1517
|
/**
|