@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/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
- function validateNode(node, violations) {
839
- const schema = Object.hasOwn(NODE_SCHEMAS, node.type) ? NODE_SCHEMAS[node.type] : undefined;
840
- if (schema) {
841
- const props = node.props || {};
842
- // Check required props
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: 'component',
857
- message: "'component' requires either 'ref' or 'name' prop",
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
- // Cross-prop validation: guard needs expr (assertion) OR kind/type (security guard)
863
- if (node.type === 'guard' && !('expr' in props) && !('kind' in props) && !('type' in props)) {
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: 'guard',
866
- message: "'guard' requires either 'expr' (assertion) or 'kind'/'type' (security guard)",
867
- line: node.loc?.line,
868
- col: node.loc?.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
- // Recurse into children
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
  /**