@almadar/agent 1.0.14 → 1.1.1
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/agent/index.d.ts +1 -1
- package/dist/agent/index.js +684 -3038
- package/dist/agent/index.js.map +1 -1
- package/dist/event-transformer/index.js +13 -1
- package/dist/event-transformer/index.js.map +1 -1
- package/dist/index.d.ts +3 -248
- package/dist/index.js +688 -4760
- package/dist/index.js.map +1 -1
- package/dist/{orbital-subagent-DEb1LXxi.d.ts → orbital-subagent-kKa0EqQM.d.ts} +68 -68
- package/dist/tools/index.d.ts +291 -54
- package/dist/tools/index.js +685 -3034
- package/dist/tools/index.js.map +1 -1
- package/package.json +7 -7
package/dist/agent/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isOrbitalDefinition,
|
|
1
|
+
import { isOrbitalDefinition, isEntityReference, getTraitName } from '@almadar/core/types';
|
|
2
2
|
import { FilesystemBackend, createDeepAgent } from 'deepagents';
|
|
3
3
|
import { MemorySaver } from '@langchain/langgraph';
|
|
4
4
|
export { Command } from '@langchain/langgraph';
|
|
@@ -10,17 +10,34 @@ import { exec, spawn } from 'child_process';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { promisify } from 'util';
|
|
12
12
|
import * as fs4 from 'fs/promises';
|
|
13
|
-
import
|
|
13
|
+
import * as domain_language_star from '@almadar/core/domain-language';
|
|
14
14
|
import * as fs3 from 'fs';
|
|
15
15
|
import crypto, { randomUUID } from 'crypto';
|
|
16
|
-
import { getFullOrbitalPrompt, getRequirementsTraitPrompt, getKeyBehaviorsReference, getSExprQuickRef, getCommonErrorsSection, getArchitectureSection } from '@almadar/skills';
|
|
16
|
+
import { getFullOrbitalPrompt, generateKflowDesignSkill, getRequirementsTraitPrompt, getKeyBehaviorsReference, getSExprQuickRef, getCommonErrorsSection, getArchitectureSection } from '@almadar/skills';
|
|
17
|
+
import { formatRecommendationsForPrompt, buildRecommendationContext, recommendPatterns } from '@almadar/patterns';
|
|
17
18
|
import { GitHubIntegration } from '@almadar/integrations';
|
|
18
19
|
import { BaseCheckpointSaver } from '@langchain/langgraph-checkpoint';
|
|
19
20
|
|
|
21
|
+
var __defProp = Object.defineProperty;
|
|
22
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
20
23
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
24
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
21
25
|
var __esm = (fn, res) => function __init() {
|
|
22
26
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
23
27
|
};
|
|
28
|
+
var __export = (target, all) => {
|
|
29
|
+
for (var name in all)
|
|
30
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
31
|
+
};
|
|
32
|
+
var __copyProps = (to, from, except, desc) => {
|
|
33
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
34
|
+
for (let key of __getOwnPropNames(from))
|
|
35
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
36
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
37
|
+
}
|
|
38
|
+
return to;
|
|
39
|
+
};
|
|
40
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget);
|
|
24
41
|
function getInlineEntity(entity) {
|
|
25
42
|
if (isEntityReference(entity)) {
|
|
26
43
|
return null;
|
|
@@ -640,3042 +657,398 @@ function combineOrbitals(orbitals, options) {
|
|
|
640
657
|
const validation = validate ? { valid: true, errors: [], warnings: [] } : void 0;
|
|
641
658
|
return {
|
|
642
659
|
success: true,
|
|
643
|
-
schema: orbitalSchema,
|
|
644
|
-
validation,
|
|
645
|
-
stats
|
|
646
|
-
};
|
|
647
|
-
} catch (error) {
|
|
648
|
-
return {
|
|
649
|
-
success: false,
|
|
650
|
-
error: error instanceof Error ? error.message : String(error),
|
|
651
|
-
stats
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// src/orbitals/domain-language/tokens.ts
|
|
657
|
-
var KEYWORDS = {
|
|
658
|
-
"a": "A" /* A */,
|
|
659
|
-
"an": "AN" /* AN */,
|
|
660
|
-
"is": "IS" /* IS */,
|
|
661
|
-
"it": "IT" /* IT */,
|
|
662
|
-
"has": "HAS" /* HAS */,
|
|
663
|
-
"belongs": "BELONGS" /* BELONGS */,
|
|
664
|
-
"to": "TO" /* TO */,
|
|
665
|
-
"many": "MANY" /* MANY */,
|
|
666
|
-
"one": "ONE" /* ONE */,
|
|
667
|
-
"as": "AS" /* AS */,
|
|
668
|
-
"can": "CAN" /* CAN */,
|
|
669
|
-
"be": "BE" /* BE */,
|
|
670
|
-
"starts": "STARTS" /* STARTS */,
|
|
671
|
-
"the": "THE" /* THE */,
|
|
672
|
-
"shows": "SHOWS" /* SHOWS */,
|
|
673
|
-
"entity": "ENTITY" /* ENTITY */,
|
|
674
|
-
"purpose": "PURPOSE" /* PURPOSE */,
|
|
675
|
-
"url": "URL" /* URL */,
|
|
676
|
-
"displays": "DISPLAYS" /* DISPLAYS */,
|
|
677
|
-
"users": "USERS" /* USERS */,
|
|
678
|
-
"when": "WHEN" /* WHEN */,
|
|
679
|
-
"on": "WHEN" /* WHEN */,
|
|
680
|
-
// "on EVENT" is equivalent to "when EVENT"
|
|
681
|
-
"accessed": "ACCESSED" /* ACCESSED */,
|
|
682
|
-
"lifecycle": "LIFECYCLE" /* LIFECYCLE */,
|
|
683
|
-
"behavior": "BEHAVIOR" /* BEHAVIOR */,
|
|
684
|
-
"states": "STATES" /* STATES */,
|
|
685
|
-
"initial": "INITIAL" /* INITIAL */,
|
|
686
|
-
"transitions": "TRANSITIONS" /* TRANSITIONS */,
|
|
687
|
-
"from": "FROM" /* FROM */,
|
|
688
|
-
"if": "IF" /* IF */,
|
|
689
|
-
"then": "THEN" /* THEN */,
|
|
690
|
-
"rules": "RULES" /* RULES */,
|
|
691
|
-
"every": "EVERY" /* EVERY */,
|
|
692
|
-
"check": "CHECK" /* CHECK */,
|
|
693
|
-
"and": "AND" /* AND */,
|
|
694
|
-
"or": "OR" /* OR */,
|
|
695
|
-
"not": "NOT" /* NOT */,
|
|
696
|
-
"provided": "PROVIDED" /* PROVIDED */,
|
|
697
|
-
"empty": "EMPTY" /* EMPTY */,
|
|
698
|
-
"user": "USER" /* USER */,
|
|
699
|
-
"owns": "OWNS" /* OWNS */,
|
|
700
|
-
"this": "THIS" /* THIS */,
|
|
701
|
-
"text": "TEXT" /* TEXT */,
|
|
702
|
-
"number": "NUMBER" /* NUMBER */,
|
|
703
|
-
"currency": "CURRENCY" /* CURRENCY */,
|
|
704
|
-
"date": "DATE" /* DATE */,
|
|
705
|
-
"timestamp": "TIMESTAMP" /* TIMESTAMP */,
|
|
706
|
-
"required": "REQUIRED" /* REQUIRED */,
|
|
707
|
-
"unique": "UNIQUE" /* UNIQUE */,
|
|
708
|
-
"auto": "AUTO" /* AUTO */,
|
|
709
|
-
"default": "DEFAULT" /* DEFAULT */,
|
|
710
|
-
"true": "BOOLEAN" /* BOOLEAN */,
|
|
711
|
-
"false": "BOOLEAN" /* BOOLEAN */
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
// src/orbitals/domain-language/lexer.ts
|
|
715
|
-
var Lexer = class {
|
|
716
|
-
constructor(input) {
|
|
717
|
-
this.pos = 0;
|
|
718
|
-
this.line = 1;
|
|
719
|
-
this.column = 1;
|
|
720
|
-
this.indentStack = [0];
|
|
721
|
-
this.input = input;
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* Tokenize the entire input
|
|
725
|
-
*/
|
|
726
|
-
tokenize() {
|
|
727
|
-
const tokens = [];
|
|
728
|
-
while (!this.isAtEnd()) {
|
|
729
|
-
const token = this.nextToken();
|
|
730
|
-
if (token) {
|
|
731
|
-
tokens.push(token);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
while (this.indentStack.length > 1) {
|
|
735
|
-
this.indentStack.pop();
|
|
736
|
-
tokens.push(this.makeToken("DEDENT" /* DEDENT */, ""));
|
|
737
|
-
}
|
|
738
|
-
tokens.push(this.makeToken("EOF" /* EOF */, ""));
|
|
739
|
-
return tokens;
|
|
740
|
-
}
|
|
741
|
-
nextToken() {
|
|
742
|
-
if (this.column === 1) {
|
|
743
|
-
const indentToken = this.handleIndentation();
|
|
744
|
-
if (indentToken) {
|
|
745
|
-
return indentToken;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
this.skipWhitespace();
|
|
749
|
-
if (this.isAtEnd()) {
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
const char = this.peek();
|
|
753
|
-
if (char === "\n") {
|
|
754
|
-
return this.consumeNewline();
|
|
755
|
-
}
|
|
756
|
-
if (char === "#") {
|
|
757
|
-
this.skipToEndOfLine();
|
|
758
|
-
return this.nextToken();
|
|
759
|
-
}
|
|
760
|
-
if (char === '"' || char === "'") {
|
|
761
|
-
return this.consumeString(char);
|
|
762
|
-
}
|
|
763
|
-
if (this.isDigit(char)) {
|
|
764
|
-
return this.consumeNumber();
|
|
765
|
-
}
|
|
766
|
-
switch (char) {
|
|
767
|
-
case ":":
|
|
768
|
-
return this.consumeChar("COLON" /* COLON */);
|
|
769
|
-
case ",":
|
|
770
|
-
return this.consumeChar("COMMA" /* COMMA */);
|
|
771
|
-
case "|":
|
|
772
|
-
return this.consumeChar("PIPE" /* PIPE */);
|
|
773
|
-
case ".":
|
|
774
|
-
return this.consumeChar("DOT" /* DOT */);
|
|
775
|
-
case "-":
|
|
776
|
-
return this.consumeChar("DASH" /* DASH */);
|
|
777
|
-
case "[":
|
|
778
|
-
return this.consumeChar("LBRACKET" /* LBRACKET */);
|
|
779
|
-
case "]":
|
|
780
|
-
return this.consumeChar("RBRACKET" /* RBRACKET */);
|
|
781
|
-
case "(":
|
|
782
|
-
return this.consumeChar("LPAREN" /* LPAREN */);
|
|
783
|
-
case ")":
|
|
784
|
-
return this.consumeChar("RPAREN" /* RPAREN */);
|
|
785
|
-
case ">":
|
|
786
|
-
if (this.peekNext() === "=") {
|
|
787
|
-
return this.consumeChars(2, "GREATER_EQUAL" /* GREATER_EQUAL */);
|
|
788
|
-
}
|
|
789
|
-
return this.consumeChar("GREATER_THAN" /* GREATER_THAN */);
|
|
790
|
-
case "<":
|
|
791
|
-
if (this.peekNext() === "=") {
|
|
792
|
-
return this.consumeChars(2, "LESS_EQUAL" /* LESS_EQUAL */);
|
|
793
|
-
}
|
|
794
|
-
return this.consumeChar("LESS_THAN" /* LESS_THAN */);
|
|
795
|
-
case "=":
|
|
796
|
-
if (this.peekNext() === "=") {
|
|
797
|
-
return this.consumeChars(2, "EQUALS" /* EQUALS */);
|
|
798
|
-
}
|
|
799
|
-
break;
|
|
800
|
-
case "!":
|
|
801
|
-
if (this.peekNext() === "=") {
|
|
802
|
-
return this.consumeChars(2, "NOT_EQUALS" /* NOT_EQUALS */);
|
|
803
|
-
}
|
|
804
|
-
break;
|
|
805
|
-
}
|
|
806
|
-
if (char === "{") {
|
|
807
|
-
return this.consumeTemplateVar();
|
|
808
|
-
}
|
|
809
|
-
if (this.isAlpha(char)) {
|
|
810
|
-
return this.consumeIdentifier();
|
|
811
|
-
}
|
|
812
|
-
this.advance();
|
|
813
|
-
return this.nextToken();
|
|
814
|
-
}
|
|
815
|
-
consumeTemplateVar() {
|
|
816
|
-
const start = this.pos;
|
|
817
|
-
this.advance();
|
|
818
|
-
let braceDepth = 1;
|
|
819
|
-
while (!this.isAtEnd() && braceDepth > 0 && this.peek() !== "\n") {
|
|
820
|
-
const ch = this.peek();
|
|
821
|
-
if (ch === "{") {
|
|
822
|
-
braceDepth++;
|
|
823
|
-
} else if (ch === "}") {
|
|
824
|
-
braceDepth--;
|
|
825
|
-
if (braceDepth === 0) {
|
|
826
|
-
this.advance();
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
this.advance();
|
|
831
|
-
}
|
|
832
|
-
const value = this.input.substring(start, this.pos);
|
|
833
|
-
return this.makeToken("TEMPLATE_VAR" /* TEMPLATE_VAR */, value);
|
|
834
|
-
}
|
|
835
|
-
handleIndentation() {
|
|
836
|
-
if (this.isAtEnd() || this.peek() === "\n") {
|
|
837
|
-
return null;
|
|
838
|
-
}
|
|
839
|
-
let indent = 0;
|
|
840
|
-
while (this.peek() === " " || this.peek() === " ") {
|
|
841
|
-
indent += this.peek() === " " ? 4 : 1;
|
|
842
|
-
this.advance();
|
|
843
|
-
}
|
|
844
|
-
if (this.peek() === "\n" || this.peek() === "#") {
|
|
845
|
-
return null;
|
|
846
|
-
}
|
|
847
|
-
const currentIndent = this.indentStack[this.indentStack.length - 1];
|
|
848
|
-
if (indent > currentIndent) {
|
|
849
|
-
this.indentStack.push(indent);
|
|
850
|
-
return this.makeToken("INDENT" /* INDENT */, "");
|
|
851
|
-
} else if (indent < currentIndent) {
|
|
852
|
-
while (this.indentStack.length > 1 && this.indentStack[this.indentStack.length - 1] > indent) {
|
|
853
|
-
this.indentStack.pop();
|
|
854
|
-
}
|
|
855
|
-
return this.makeToken("DEDENT" /* DEDENT */, "");
|
|
856
|
-
}
|
|
857
|
-
return null;
|
|
858
|
-
}
|
|
859
|
-
consumeNewline() {
|
|
860
|
-
const token = this.makeToken("NEWLINE" /* NEWLINE */, "\n");
|
|
861
|
-
this.advance();
|
|
862
|
-
this.line++;
|
|
863
|
-
this.column = 1;
|
|
864
|
-
return token;
|
|
865
|
-
}
|
|
866
|
-
consumeString(quote) {
|
|
867
|
-
const startLine = this.line;
|
|
868
|
-
const startColumn = this.column;
|
|
869
|
-
const startOffset = this.pos;
|
|
870
|
-
this.advance();
|
|
871
|
-
let value = "";
|
|
872
|
-
while (!this.isAtEnd() && this.peek() !== quote) {
|
|
873
|
-
if (this.peek() === "\\" && this.peekNext() === quote) {
|
|
874
|
-
this.advance();
|
|
875
|
-
value += quote;
|
|
876
|
-
} else if (this.peek() === "\n") {
|
|
877
|
-
break;
|
|
878
|
-
} else {
|
|
879
|
-
value += this.peek();
|
|
880
|
-
}
|
|
881
|
-
this.advance();
|
|
882
|
-
}
|
|
883
|
-
if (this.peek() === quote) {
|
|
884
|
-
this.advance();
|
|
885
|
-
}
|
|
886
|
-
return {
|
|
887
|
-
type: "STRING" /* STRING */,
|
|
888
|
-
value,
|
|
889
|
-
line: startLine,
|
|
890
|
-
column: startColumn,
|
|
891
|
-
offset: startOffset
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
consumeNumber() {
|
|
895
|
-
const startLine = this.line;
|
|
896
|
-
const startColumn = this.column;
|
|
897
|
-
const startOffset = this.pos;
|
|
898
|
-
let value = "";
|
|
899
|
-
while (this.isDigit(this.peek())) {
|
|
900
|
-
value += this.advance();
|
|
901
|
-
}
|
|
902
|
-
if (this.peek() === "." && this.isDigit(this.peekNext())) {
|
|
903
|
-
value += this.advance();
|
|
904
|
-
while (this.isDigit(this.peek())) {
|
|
905
|
-
value += this.advance();
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
return {
|
|
909
|
-
type: "NUMBER_LITERAL" /* NUMBER_LITERAL */,
|
|
910
|
-
value,
|
|
911
|
-
line: startLine,
|
|
912
|
-
column: startColumn,
|
|
913
|
-
offset: startOffset
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
consumeIdentifier() {
|
|
917
|
-
const startLine = this.line;
|
|
918
|
-
const startColumn = this.column;
|
|
919
|
-
const startOffset = this.pos;
|
|
920
|
-
let value = "";
|
|
921
|
-
while (this.isAlphaNumeric(this.peek()) || this.peek() === "_" || this.peek() === "/") {
|
|
922
|
-
value += this.advance();
|
|
923
|
-
}
|
|
924
|
-
const lowerValue = value.toLowerCase();
|
|
925
|
-
const keywordType = KEYWORDS[lowerValue];
|
|
926
|
-
return {
|
|
927
|
-
type: keywordType || "IDENTIFIER" /* IDENTIFIER */,
|
|
928
|
-
value,
|
|
929
|
-
line: startLine,
|
|
930
|
-
column: startColumn,
|
|
931
|
-
offset: startOffset
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
consumeChar(type) {
|
|
935
|
-
const token = this.makeToken(type, this.peek());
|
|
936
|
-
this.advance();
|
|
937
|
-
return token;
|
|
938
|
-
}
|
|
939
|
-
consumeChars(count, type) {
|
|
940
|
-
let value = "";
|
|
941
|
-
for (let i = 0; i < count; i++) {
|
|
942
|
-
value += this.advance();
|
|
943
|
-
}
|
|
944
|
-
return {
|
|
945
|
-
type,
|
|
946
|
-
value,
|
|
947
|
-
line: this.line,
|
|
948
|
-
column: this.column - count,
|
|
949
|
-
offset: this.pos - count
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
skipWhitespace() {
|
|
953
|
-
while (!this.isAtEnd() && (this.peek() === " " || this.peek() === " ")) {
|
|
954
|
-
this.advance();
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
skipToEndOfLine() {
|
|
958
|
-
while (!this.isAtEnd() && this.peek() !== "\n") {
|
|
959
|
-
this.advance();
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
makeToken(type, value) {
|
|
963
|
-
return {
|
|
964
|
-
type,
|
|
965
|
-
value,
|
|
966
|
-
line: this.line,
|
|
967
|
-
column: this.column,
|
|
968
|
-
offset: this.pos
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
peek() {
|
|
972
|
-
return this.input[this.pos] || "\0";
|
|
973
|
-
}
|
|
974
|
-
peekNext() {
|
|
975
|
-
return this.input[this.pos + 1] || "\0";
|
|
976
|
-
}
|
|
977
|
-
advance() {
|
|
978
|
-
const char = this.peek();
|
|
979
|
-
this.pos++;
|
|
980
|
-
this.column++;
|
|
981
|
-
return char;
|
|
982
|
-
}
|
|
983
|
-
isAtEnd() {
|
|
984
|
-
return this.pos >= this.input.length;
|
|
985
|
-
}
|
|
986
|
-
isDigit(char) {
|
|
987
|
-
return char >= "0" && char <= "9";
|
|
988
|
-
}
|
|
989
|
-
isAlpha(char) {
|
|
990
|
-
return char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char === "_";
|
|
991
|
-
}
|
|
992
|
-
isAlphaNumeric(char) {
|
|
993
|
-
return this.isAlpha(char) || this.isDigit(char);
|
|
994
|
-
}
|
|
995
|
-
};
|
|
996
|
-
|
|
997
|
-
// src/orbitals/domain-language/parsers/entity-parser.ts
|
|
998
|
-
function parseEntity(text) {
|
|
999
|
-
const ctx = { errors: [], warnings: [] };
|
|
1000
|
-
const lexer = new Lexer(text);
|
|
1001
|
-
const tokens = lexer.tokenize();
|
|
1002
|
-
let pos = 0;
|
|
1003
|
-
const current = () => tokens[pos] || { type: "EOF" /* EOF */, value: "", line: 0, column: 0, offset: 0 };
|
|
1004
|
-
const advance = () => tokens[pos++];
|
|
1005
|
-
const isAtEnd = () => current().type === "EOF" /* EOF */;
|
|
1006
|
-
const skip = (type) => {
|
|
1007
|
-
while (current().type === type) advance();
|
|
1008
|
-
};
|
|
1009
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1010
|
-
const entityHeader = parseEntityHeader();
|
|
1011
|
-
if (!entityHeader) {
|
|
1012
|
-
return {
|
|
1013
|
-
success: false,
|
|
1014
|
-
errors: ctx.errors.length > 0 ? ctx.errors : [{
|
|
1015
|
-
message: 'Expected entity definition starting with "A [Name] is..."'
|
|
1016
|
-
}],
|
|
1017
|
-
warnings: []
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
const entity = {
|
|
1021
|
-
type: "entity",
|
|
1022
|
-
name: entityHeader.name,
|
|
1023
|
-
description: entityHeader.description,
|
|
1024
|
-
fields: [],
|
|
1025
|
-
relationships: []
|
|
1026
|
-
};
|
|
1027
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1028
|
-
while (!isAtEnd()) {
|
|
1029
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1030
|
-
if (isAtEnd()) break;
|
|
1031
|
-
const sectionResult = parseSection(entity);
|
|
1032
|
-
if (!sectionResult) {
|
|
1033
|
-
advance();
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
return {
|
|
1037
|
-
success: true,
|
|
1038
|
-
data: entity,
|
|
1039
|
-
errors: ctx.errors,
|
|
1040
|
-
warnings: ctx.warnings
|
|
1041
|
-
};
|
|
1042
|
-
function isEntityName(token) {
|
|
1043
|
-
if (token.type === "IDENTIFIER" /* IDENTIFIER */) return true;
|
|
1044
|
-
const keywordEntityNames = [
|
|
1045
|
-
"USER" /* USER */,
|
|
1046
|
-
// "User"
|
|
1047
|
-
"TEXT" /* TEXT */,
|
|
1048
|
-
// "Text" (unlikely but possible)
|
|
1049
|
-
"NUMBER" /* NUMBER */,
|
|
1050
|
-
// "Number" (unlikely but possible)
|
|
1051
|
-
"DATE" /* DATE */
|
|
1052
|
-
// "Date" (unlikely but possible)
|
|
1053
|
-
];
|
|
1054
|
-
return keywordEntityNames.includes(token.type);
|
|
1055
|
-
}
|
|
1056
|
-
function parseEntityHeader() {
|
|
1057
|
-
if (current().type !== "A" /* A */ && current().type !== "AN" /* AN */) {
|
|
1058
|
-
ctx.errors.push({ message: 'Expected "A" or "An" at start of entity definition' });
|
|
1059
|
-
return null;
|
|
1060
|
-
}
|
|
1061
|
-
advance();
|
|
1062
|
-
if (!isEntityName(current())) {
|
|
1063
|
-
ctx.errors.push({ message: 'Expected entity name after "A"' });
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
const name = current().value;
|
|
1067
|
-
advance();
|
|
1068
|
-
if (current().type !== "IS" /* IS */) {
|
|
1069
|
-
ctx.errors.push({ message: `Expected "is" after entity name "${name}"` });
|
|
1070
|
-
return null;
|
|
1071
|
-
}
|
|
1072
|
-
advance();
|
|
1073
|
-
const descParts = [];
|
|
1074
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
1075
|
-
descParts.push(current().value);
|
|
1076
|
-
advance();
|
|
1077
|
-
}
|
|
1078
|
-
return {
|
|
1079
|
-
name,
|
|
1080
|
-
description: descParts.join(" ").trim()
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
function parseSection(entity2) {
|
|
1084
|
-
const token = current();
|
|
1085
|
-
if (token.type === "INDENT" /* INDENT */) {
|
|
1086
|
-
advance();
|
|
1087
|
-
parseIndentedFields(entity2);
|
|
1088
|
-
return true;
|
|
1089
|
-
}
|
|
1090
|
-
if (token.type === "IT" /* IT */) {
|
|
1091
|
-
advance();
|
|
1092
|
-
if (current().type === "HAS" /* HAS */) {
|
|
1093
|
-
advance();
|
|
1094
|
-
skip("COLON" /* COLON */);
|
|
1095
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1096
|
-
parseFieldsSection(entity2);
|
|
1097
|
-
return true;
|
|
1098
|
-
}
|
|
1099
|
-
if (current().type === "BELONGS" /* BELONGS */) {
|
|
1100
|
-
advance();
|
|
1101
|
-
if (current().type === "TO" /* TO */) {
|
|
1102
|
-
advance();
|
|
1103
|
-
parseRelationship(entity2, "belongs_to");
|
|
1104
|
-
return true;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
if (current().type === "CAN" /* CAN */) {
|
|
1108
|
-
advance();
|
|
1109
|
-
if (current().type === "BE" /* BE */) {
|
|
1110
|
-
advance();
|
|
1111
|
-
skip("COLON" /* COLON */);
|
|
1112
|
-
parseStates(entity2);
|
|
1113
|
-
return true;
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
if (current().type === "STARTS" /* STARTS */) {
|
|
1117
|
-
advance();
|
|
1118
|
-
if (current().type === "AS" /* AS */) {
|
|
1119
|
-
advance();
|
|
1120
|
-
parseInitialState(entity2);
|
|
1121
|
-
return true;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
if (token.type === "HAS" /* HAS */) {
|
|
1126
|
-
advance();
|
|
1127
|
-
if (current().type === "MANY" /* MANY */) {
|
|
1128
|
-
advance();
|
|
1129
|
-
parseRelationship(entity2, "has_many");
|
|
1130
|
-
return true;
|
|
1131
|
-
}
|
|
1132
|
-
if (current().type === "ONE" /* ONE */) {
|
|
1133
|
-
advance();
|
|
1134
|
-
parseRelationship(entity2, "has_one");
|
|
1135
|
-
return true;
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
return false;
|
|
1139
|
-
}
|
|
1140
|
-
function parseIndentedFields(entity2) {
|
|
1141
|
-
while (!isAtEnd() && current().type !== "DEDENT" /* DEDENT */) {
|
|
1142
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1143
|
-
if (current().type === "DEDENT" /* DEDENT */) break;
|
|
1144
|
-
if (current().type === "DASH" /* DASH */) {
|
|
1145
|
-
advance();
|
|
1146
|
-
}
|
|
1147
|
-
if (current().type === "HAS" /* HAS */) {
|
|
1148
|
-
advance();
|
|
1149
|
-
if (current().type === "MANY" /* MANY */) {
|
|
1150
|
-
advance();
|
|
1151
|
-
parseRelationship(entity2, "has_many");
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
const field = parseHasFieldLine();
|
|
1155
|
-
if (field) {
|
|
1156
|
-
entity2.fields.push(field);
|
|
1157
|
-
}
|
|
1158
|
-
continue;
|
|
1159
|
-
}
|
|
1160
|
-
if (current().type === "BELONGS" /* BELONGS */) {
|
|
1161
|
-
advance();
|
|
1162
|
-
if (current().type === "TO" /* TO */) {
|
|
1163
|
-
advance();
|
|
1164
|
-
parseRelationship(entity2, "belongs_to");
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */ && current().type !== "DEDENT" /* DEDENT */) {
|
|
1169
|
-
advance();
|
|
1170
|
-
}
|
|
1171
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1172
|
-
}
|
|
1173
|
-
if (current().type === "DEDENT" /* DEDENT */) {
|
|
1174
|
-
advance();
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
function parseHasFieldLine() {
|
|
1178
|
-
if (current().type !== "IDENTIFIER" /* IDENTIFIER */) {
|
|
1179
|
-
return null;
|
|
1180
|
-
}
|
|
1181
|
-
const fieldName = toCamelCase(current().value);
|
|
1182
|
-
advance();
|
|
1183
|
-
if (current().type !== "AS" /* AS */) {
|
|
1184
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */ && current().type !== "DEDENT" /* DEDENT */) {
|
|
1185
|
-
advance();
|
|
1186
|
-
}
|
|
1187
|
-
return null;
|
|
1188
|
-
}
|
|
1189
|
-
advance();
|
|
1190
|
-
const field = {
|
|
1191
|
-
type: "field",
|
|
1192
|
-
name: fieldName,
|
|
1193
|
-
fieldType: "text",
|
|
1194
|
-
required: false,
|
|
1195
|
-
unique: false,
|
|
1196
|
-
auto: false
|
|
1197
|
-
};
|
|
1198
|
-
const typeParts = [];
|
|
1199
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */ && current().type !== "DEDENT" /* DEDENT */ && current().type !== "LPAREN" /* LPAREN */ && current().value !== "with") {
|
|
1200
|
-
typeParts.push(current().value);
|
|
1201
|
-
advance();
|
|
1202
|
-
}
|
|
1203
|
-
const typeText = typeParts.join(" ").trim().toLowerCase();
|
|
1204
|
-
if (typeText.includes("long text")) {
|
|
1205
|
-
field.fieldType = "long text";
|
|
1206
|
-
} else if (typeText === "text" || typeText === "string") {
|
|
1207
|
-
field.fieldType = "text";
|
|
1208
|
-
} else if (typeText === "number" || typeText === "integer") {
|
|
1209
|
-
field.fieldType = "number";
|
|
1210
|
-
} else if (typeText === "currency") {
|
|
1211
|
-
field.fieldType = "currency";
|
|
1212
|
-
} else if (typeText === "yes/no" || typeText === "boolean") {
|
|
1213
|
-
field.fieldType = "yes/no";
|
|
1214
|
-
} else if (typeText === "date") {
|
|
1215
|
-
field.fieldType = "date";
|
|
1216
|
-
} else if (typeText === "timestamp" || typeText === "datetime") {
|
|
1217
|
-
field.fieldType = "timestamp";
|
|
1218
|
-
} else if (typeText.startsWith("enum")) {
|
|
1219
|
-
field.fieldType = "enum";
|
|
1220
|
-
const enumMatch = typeText.match(/enum\s*\[([^\]]+)\]/);
|
|
1221
|
-
if (enumMatch) {
|
|
1222
|
-
field.enumValues = enumMatch[1].split(",").map((v) => v.trim());
|
|
1223
|
-
}
|
|
1224
|
-
} else if (typeText === "list" || typeText === "array") {
|
|
1225
|
-
field.fieldType = "list";
|
|
1226
|
-
} else if (typeText === "object") {
|
|
1227
|
-
field.fieldType = "object";
|
|
1228
|
-
} else {
|
|
1229
|
-
field.fieldType = "text";
|
|
1230
|
-
}
|
|
1231
|
-
if (current().type === "LPAREN" /* LPAREN */) {
|
|
1232
|
-
advance();
|
|
1233
|
-
while (!isAtEnd() && current().type !== "RPAREN" /* RPAREN */ && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
1234
|
-
const constraint = current().value.toLowerCase();
|
|
1235
|
-
if (constraint === "required") {
|
|
1236
|
-
field.required = true;
|
|
1237
|
-
} else if (constraint === "optional") {
|
|
1238
|
-
field.required = false;
|
|
1239
|
-
} else if (constraint === "unique") {
|
|
1240
|
-
field.unique = true;
|
|
1241
|
-
}
|
|
1242
|
-
advance();
|
|
1243
|
-
}
|
|
1244
|
-
if (current().type === "RPAREN" /* RPAREN */) {
|
|
1245
|
-
advance();
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */ && current().value.toLowerCase() === "with") {
|
|
1249
|
-
advance();
|
|
1250
|
-
if (current().type === "DEFAULT" /* DEFAULT */) {
|
|
1251
|
-
advance();
|
|
1252
|
-
if (current().type === "STRING" /* STRING */) {
|
|
1253
|
-
field.default = current().value;
|
|
1254
|
-
advance();
|
|
1255
|
-
} else if (current().type === "NUMBER_LITERAL" /* NUMBER_LITERAL */) {
|
|
1256
|
-
field.default = parseFloat(current().value);
|
|
1257
|
-
advance();
|
|
1258
|
-
} else if (current().type === "BOOLEAN" /* BOOLEAN */) {
|
|
1259
|
-
field.default = current().value === "true";
|
|
1260
|
-
advance();
|
|
1261
|
-
} else if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1262
|
-
field.default = current().value;
|
|
1263
|
-
advance();
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
return field;
|
|
1268
|
-
}
|
|
1269
|
-
function parseFieldsSection(entity2) {
|
|
1270
|
-
if (current().type !== "INDENT" /* INDENT */) {
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
advance();
|
|
1274
|
-
while (!isAtEnd() && current().type !== "DEDENT" /* DEDENT */) {
|
|
1275
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1276
|
-
if (current().type === "DEDENT" /* DEDENT */) break;
|
|
1277
|
-
const field = parseFieldLine();
|
|
1278
|
-
if (field) {
|
|
1279
|
-
entity2.fields.push(field);
|
|
1280
|
-
}
|
|
1281
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1282
|
-
}
|
|
1283
|
-
if (current().type === "DEDENT" /* DEDENT */) {
|
|
1284
|
-
advance();
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
function parseFieldLine() {
|
|
1288
|
-
if (current().type === "DASH" /* DASH */) {
|
|
1289
|
-
advance();
|
|
1290
|
-
}
|
|
1291
|
-
const nameParts = [];
|
|
1292
|
-
while (!isAtEnd() && current().type !== "COLON" /* COLON */ && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
1293
|
-
nameParts.push(current().value);
|
|
1294
|
-
advance();
|
|
1295
|
-
}
|
|
1296
|
-
if (nameParts.length === 0) {
|
|
1297
|
-
return null;
|
|
1298
|
-
}
|
|
1299
|
-
const fieldName = toCamelCase(nameParts.join(" "));
|
|
1300
|
-
if (current().type !== "COLON" /* COLON */) {
|
|
1301
|
-
ctx.errors.push({ message: `Expected ":" after field name "${fieldName}"` });
|
|
1302
|
-
return null;
|
|
1303
|
-
}
|
|
1304
|
-
advance();
|
|
1305
|
-
return parseFieldTypeAndConstraints(fieldName);
|
|
1306
|
-
}
|
|
1307
|
-
function parseFieldTypeAndConstraints(fieldName) {
|
|
1308
|
-
const field = {
|
|
1309
|
-
type: "field",
|
|
1310
|
-
name: fieldName,
|
|
1311
|
-
fieldType: "text",
|
|
1312
|
-
required: false,
|
|
1313
|
-
unique: false,
|
|
1314
|
-
auto: false
|
|
1315
|
-
};
|
|
1316
|
-
const parts = [];
|
|
1317
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
1318
|
-
parts.push(current().value);
|
|
1319
|
-
advance();
|
|
1320
|
-
}
|
|
1321
|
-
const content = parts.join(" ").trim();
|
|
1322
|
-
if (content.includes("|")) {
|
|
1323
|
-
field.fieldType = "enum";
|
|
1324
|
-
field.enumValues = content.split("|").map((v) => v.trim()).filter((v) => v);
|
|
1325
|
-
return field;
|
|
1326
|
-
}
|
|
1327
|
-
const segments = content.split(",").map((s) => s.trim().toLowerCase());
|
|
1328
|
-
for (const segment of segments) {
|
|
1329
|
-
if (segment === "text") field.fieldType = "text";
|
|
1330
|
-
else if (segment === "long text") field.fieldType = "long text";
|
|
1331
|
-
else if (segment === "number") field.fieldType = "number";
|
|
1332
|
-
else if (segment === "currency") field.fieldType = "currency";
|
|
1333
|
-
else if (segment === "date") field.fieldType = "date";
|
|
1334
|
-
else if (segment === "timestamp") field.fieldType = "timestamp";
|
|
1335
|
-
else if (segment === "datetime") field.fieldType = "datetime";
|
|
1336
|
-
else if (segment === "yes/no" || segment === "boolean") field.fieldType = "yes/no";
|
|
1337
|
-
else if (segment === "list") field.fieldType = "list";
|
|
1338
|
-
else if (segment === "object") field.fieldType = "object";
|
|
1339
|
-
else if (segment === "required") field.required = true;
|
|
1340
|
-
else if (segment === "unique") field.unique = true;
|
|
1341
|
-
else if (segment === "auto") field.auto = true;
|
|
1342
|
-
else if (segment.startsWith("default ")) {
|
|
1343
|
-
field.default = parseValue(segment.slice(8));
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
return field;
|
|
1347
|
-
}
|
|
1348
|
-
function parseRelationship(entity2, relType) {
|
|
1349
|
-
if (current().type !== "IDENTIFIER" /* IDENTIFIER */) {
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1352
|
-
const targetEntity = current().value;
|
|
1353
|
-
advance();
|
|
1354
|
-
let alias;
|
|
1355
|
-
if (current().type === "AS" /* AS */) {
|
|
1356
|
-
advance();
|
|
1357
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1358
|
-
alias = current().value;
|
|
1359
|
-
advance();
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
entity2.relationships.push({
|
|
1363
|
-
type: "relationship",
|
|
1364
|
-
relationshipType: relType,
|
|
1365
|
-
targetEntity,
|
|
1366
|
-
alias
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
function parseStates(entity2) {
|
|
1370
|
-
const states = [];
|
|
1371
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
1372
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1373
|
-
states.push(current().value);
|
|
1374
|
-
}
|
|
1375
|
-
advance();
|
|
1376
|
-
}
|
|
1377
|
-
entity2.states = states;
|
|
1378
|
-
}
|
|
1379
|
-
function parseInitialState(entity2) {
|
|
1380
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1381
|
-
entity2.initialState = current().value;
|
|
1382
|
-
advance();
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
function formatEntityToSchema(entity) {
|
|
1387
|
-
const fields = entity.fields.map((field) => ({
|
|
1388
|
-
name: field.name,
|
|
1389
|
-
type: mapFieldTypeToSchema(field.fieldType),
|
|
1390
|
-
required: field.required || void 0,
|
|
1391
|
-
unique: field.unique || void 0,
|
|
1392
|
-
auto: field.auto || void 0,
|
|
1393
|
-
values: field.enumValues,
|
|
1394
|
-
// OrbitalSchema uses 'values' not 'enumValues'
|
|
1395
|
-
default: field.default
|
|
1396
|
-
}));
|
|
1397
|
-
for (const rel of entity.relationships) {
|
|
1398
|
-
if (rel.relationshipType === "belongs_to") {
|
|
1399
|
-
const fieldName = rel.alias ? toCamelCase(rel.alias) + "Id" : toCamelCase(rel.targetEntity) + "Id";
|
|
1400
|
-
fields.push({
|
|
1401
|
-
name: fieldName,
|
|
1402
|
-
type: "relation",
|
|
1403
|
-
required: void 0,
|
|
1404
|
-
unique: void 0,
|
|
1405
|
-
auto: void 0,
|
|
1406
|
-
values: void 0,
|
|
1407
|
-
default: void 0,
|
|
1408
|
-
relation: {
|
|
1409
|
-
entity: rel.targetEntity,
|
|
1410
|
-
type: "many-to-one"
|
|
1411
|
-
}
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
return {
|
|
1416
|
-
name: entity.name,
|
|
1417
|
-
collection: toKebabCase(entity.name) + "s",
|
|
1418
|
-
fields: fields.filter((f) => Object.keys(f).length > 0),
|
|
1419
|
-
states: entity.states,
|
|
1420
|
-
initialState: entity.initialState
|
|
1421
|
-
};
|
|
1422
|
-
}
|
|
1423
|
-
function toCamelCase(text) {
|
|
1424
|
-
return text.toLowerCase().split(/\s+/).map(
|
|
1425
|
-
(word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)
|
|
1426
|
-
).join("");
|
|
1427
|
-
}
|
|
1428
|
-
function toKebabCase(text) {
|
|
1429
|
-
return text.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1430
|
-
}
|
|
1431
|
-
function parseValue(text) {
|
|
1432
|
-
text = text.trim();
|
|
1433
|
-
if (text.toLowerCase() === "true") return true;
|
|
1434
|
-
if (text.toLowerCase() === "false") return false;
|
|
1435
|
-
const num = parseFloat(text);
|
|
1436
|
-
if (!isNaN(num)) return num;
|
|
1437
|
-
return text.replace(/^["']|["']$/g, "");
|
|
1438
|
-
}
|
|
1439
|
-
function mapFieldTypeToSchema(fieldType) {
|
|
1440
|
-
const mapping = {
|
|
1441
|
-
"text": "string",
|
|
1442
|
-
"long text": "string",
|
|
1443
|
-
"number": "number",
|
|
1444
|
-
"currency": "number",
|
|
1445
|
-
"date": "date",
|
|
1446
|
-
"timestamp": "timestamp",
|
|
1447
|
-
"datetime": "datetime",
|
|
1448
|
-
"yes/no": "boolean",
|
|
1449
|
-
"enum": "enum",
|
|
1450
|
-
"list": "array",
|
|
1451
|
-
"object": "object",
|
|
1452
|
-
"relation": "relation"
|
|
1453
|
-
};
|
|
1454
|
-
return mapping[fieldType] || "string";
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
// src/orbitals/domain-language/parsers/page-parser.ts
|
|
1458
|
-
function parsePage(text) {
|
|
1459
|
-
const ctx = { errors: [], warnings: [] };
|
|
1460
|
-
const lexer = new Lexer(text);
|
|
1461
|
-
const tokens = lexer.tokenize();
|
|
1462
|
-
let pos = 0;
|
|
1463
|
-
const current = () => tokens[pos] || { type: "EOF" /* EOF */, value: "", line: 0, column: 0, offset: 0 };
|
|
1464
|
-
const advance = () => tokens[pos++];
|
|
1465
|
-
const isAtEnd = () => current().type === "EOF" /* EOF */;
|
|
1466
|
-
const skip = (type) => {
|
|
1467
|
-
while (current().type === type) advance();
|
|
1468
|
-
};
|
|
1469
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1470
|
-
const pageHeader = parsePageHeader();
|
|
1471
|
-
if (!pageHeader) {
|
|
1472
|
-
return {
|
|
1473
|
-
success: false,
|
|
1474
|
-
errors: ctx.errors.length > 0 ? ctx.errors : [{
|
|
1475
|
-
message: 'Expected page definition starting with "The [PageName] shows..."'
|
|
1476
|
-
}],
|
|
1477
|
-
warnings: []
|
|
1478
|
-
};
|
|
1479
|
-
}
|
|
1480
|
-
const page = {
|
|
1481
|
-
type: "page",
|
|
1482
|
-
name: pageHeader.name,
|
|
1483
|
-
description: pageHeader.description,
|
|
1484
|
-
purpose: "",
|
|
1485
|
-
url: pageHeader.url || "",
|
|
1486
|
-
primaryEntity: pageHeader.primaryEntity,
|
|
1487
|
-
traitName: pageHeader.traitName,
|
|
1488
|
-
sections: [],
|
|
1489
|
-
actions: []
|
|
1490
|
-
};
|
|
1491
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1492
|
-
while (!isAtEnd()) {
|
|
1493
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1494
|
-
if (isAtEnd()) break;
|
|
1495
|
-
const parsed = parsePageSection(page);
|
|
1496
|
-
if (!parsed) {
|
|
1497
|
-
advance();
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
if (!page.url) {
|
|
1501
|
-
page.url = "/" + toKebabCase2(page.name.replace(/Page$/i, ""));
|
|
1502
|
-
}
|
|
1503
|
-
return {
|
|
1504
|
-
success: true,
|
|
1505
|
-
data: page,
|
|
1506
|
-
errors: ctx.errors,
|
|
1507
|
-
warnings: ctx.warnings
|
|
1508
|
-
};
|
|
1509
|
-
function parsePageHeader() {
|
|
1510
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1511
|
-
const firstToken = current().value;
|
|
1512
|
-
advance();
|
|
1513
|
-
const nameParts2 = [firstToken];
|
|
1514
|
-
while (current().type === "IDENTIFIER" /* IDENTIFIER */ && current().value.toLowerCase() !== "at") {
|
|
1515
|
-
nameParts2.push(current().value);
|
|
1516
|
-
advance();
|
|
1517
|
-
}
|
|
1518
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */ && current().value.toLowerCase() === "at") {
|
|
1519
|
-
advance();
|
|
1520
|
-
const pathParts = [];
|
|
1521
|
-
while (!isAtEnd() && current().type !== "COLON" /* COLON */ && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
1522
|
-
pathParts.push(current().value);
|
|
1523
|
-
advance();
|
|
1524
|
-
}
|
|
1525
|
-
const url = pathParts.join("");
|
|
1526
|
-
if (current().type === "COLON" /* COLON */) {
|
|
1527
|
-
advance();
|
|
1528
|
-
}
|
|
1529
|
-
let name2 = nameParts2.map((p) => toPascalCase(p)).join("");
|
|
1530
|
-
if (!name2.toLowerCase().endsWith("page")) {
|
|
1531
|
-
name2 += "Page";
|
|
1532
|
-
}
|
|
1533
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1534
|
-
let primaryEntity;
|
|
1535
|
-
let traitName;
|
|
1536
|
-
if (current().type === "INDENT" /* INDENT */) {
|
|
1537
|
-
advance();
|
|
1538
|
-
while (!isAtEnd() && current().type !== "DEDENT" /* DEDENT */) {
|
|
1539
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1540
|
-
if (current().type === "DEDENT" /* DEDENT */) break;
|
|
1541
|
-
if (current().type === "DASH" /* DASH */) {
|
|
1542
|
-
advance();
|
|
1543
|
-
}
|
|
1544
|
-
if (current().type === "SHOWS" /* SHOWS */) {
|
|
1545
|
-
advance();
|
|
1546
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1547
|
-
primaryEntity = current().value;
|
|
1548
|
-
advance();
|
|
1549
|
-
}
|
|
1550
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */ && current().value.toLowerCase() === "using") {
|
|
1551
|
-
advance();
|
|
1552
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1553
|
-
traitName = current().value;
|
|
1554
|
-
advance();
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */ && current().type !== "DEDENT" /* DEDENT */) {
|
|
1559
|
-
advance();
|
|
1560
|
-
}
|
|
1561
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1562
|
-
}
|
|
1563
|
-
if (current().type === "DEDENT" /* DEDENT */) {
|
|
1564
|
-
advance();
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
return {
|
|
1568
|
-
name: name2,
|
|
1569
|
-
description: `Page at ${url}`,
|
|
1570
|
-
url,
|
|
1571
|
-
primaryEntity,
|
|
1572
|
-
traitName
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
if (current().type !== "THE" /* THE */) {
|
|
1577
|
-
ctx.errors.push({ message: 'Expected "The" at start of page definition' });
|
|
1578
|
-
return null;
|
|
1579
|
-
}
|
|
1580
|
-
advance();
|
|
1581
|
-
const nameParts = [];
|
|
1582
|
-
while (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1583
|
-
nameParts.push(current().value);
|
|
1584
|
-
advance();
|
|
1585
|
-
}
|
|
1586
|
-
if (nameParts.length === 0) {
|
|
1587
|
-
ctx.errors.push({ message: 'Expected page name after "The"' });
|
|
1588
|
-
return null;
|
|
1589
|
-
}
|
|
1590
|
-
let name = nameParts.map((p) => toPascalCase(p)).join("");
|
|
1591
|
-
if (!name.toLowerCase().endsWith("page")) {
|
|
1592
|
-
name += "Page";
|
|
1593
|
-
}
|
|
1594
|
-
if (current().type !== "SHOWS" /* SHOWS */) {
|
|
1595
|
-
ctx.errors.push({ message: `Expected "shows" after page name "${name}"` });
|
|
1596
|
-
return null;
|
|
1597
|
-
}
|
|
1598
|
-
advance();
|
|
1599
|
-
const descParts = [];
|
|
1600
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
1601
|
-
descParts.push(current().value);
|
|
1602
|
-
advance();
|
|
1603
|
-
}
|
|
1604
|
-
return {
|
|
1605
|
-
name,
|
|
1606
|
-
description: descParts.join(" ").trim()
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
function parsePageSection(page2) {
|
|
1610
|
-
const token = current();
|
|
1611
|
-
if (token.type === "ENTITY" /* ENTITY */) {
|
|
1612
|
-
advance();
|
|
1613
|
-
skip("COLON" /* COLON */);
|
|
1614
|
-
page2.primaryEntity = collectUntilNewline();
|
|
1615
|
-
return true;
|
|
1616
|
-
}
|
|
1617
|
-
if (token.type === "PURPOSE" /* PURPOSE */) {
|
|
1618
|
-
advance();
|
|
1619
|
-
skip("COLON" /* COLON */);
|
|
1620
|
-
page2.purpose = collectUntilNewline();
|
|
1621
|
-
return true;
|
|
1622
|
-
}
|
|
1623
|
-
if (token.type === "URL" /* URL */) {
|
|
1624
|
-
advance();
|
|
1625
|
-
skip("COLON" /* COLON */);
|
|
1626
|
-
page2.url = collectUntilNewline();
|
|
1627
|
-
return true;
|
|
1628
|
-
}
|
|
1629
|
-
if (token.type === "IT" /* IT */) {
|
|
1630
|
-
advance();
|
|
1631
|
-
if (current().type === "DISPLAYS" /* DISPLAYS */) {
|
|
1632
|
-
advance();
|
|
1633
|
-
skip("COLON" /* COLON */);
|
|
1634
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1635
|
-
parseDisplaysSection(page2);
|
|
1636
|
-
return true;
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
if (token.type === "USERS" /* USERS */) {
|
|
1640
|
-
advance();
|
|
1641
|
-
if (current().type === "CAN" /* CAN */) {
|
|
1642
|
-
advance();
|
|
1643
|
-
skip("COLON" /* COLON */);
|
|
1644
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1645
|
-
parseActionsSection(page2);
|
|
1646
|
-
return true;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
if (token.type === "WHEN" /* WHEN */) {
|
|
1650
|
-
advance();
|
|
1651
|
-
if (current().type === "ACCESSED" /* ACCESSED */) {
|
|
1652
|
-
advance();
|
|
1653
|
-
skip("COLON" /* COLON */);
|
|
1654
|
-
page2.onAccess = collectUntilNewline();
|
|
1655
|
-
return true;
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
return false;
|
|
1659
|
-
}
|
|
1660
|
-
function parseDisplaysSection(page2) {
|
|
1661
|
-
if (current().type !== "INDENT" /* INDENT */) {
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
advance();
|
|
1665
|
-
while (!isAtEnd() && current().type !== "DEDENT" /* DEDENT */) {
|
|
1666
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1667
|
-
if (current().type === "DEDENT" /* DEDENT */) break;
|
|
1668
|
-
const section = parseSectionLine();
|
|
1669
|
-
if (section) {
|
|
1670
|
-
page2.sections.push(section);
|
|
1671
|
-
}
|
|
1672
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1673
|
-
}
|
|
1674
|
-
if (current().type === "DEDENT" /* DEDENT */) {
|
|
1675
|
-
advance();
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
function parseSectionLine() {
|
|
1679
|
-
if (current().type === "DASH" /* DASH */) {
|
|
1680
|
-
advance();
|
|
1681
|
-
}
|
|
1682
|
-
const description = collectUntilNewline();
|
|
1683
|
-
if (!description) {
|
|
1684
|
-
return null;
|
|
1685
|
-
}
|
|
1686
|
-
return {
|
|
1687
|
-
type: "page_section",
|
|
1688
|
-
description
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
|
-
function parseActionsSection(page2) {
|
|
1692
|
-
if (current().type !== "INDENT" /* INDENT */) {
|
|
1693
|
-
return;
|
|
1694
|
-
}
|
|
1695
|
-
advance();
|
|
1696
|
-
while (!isAtEnd() && current().type !== "DEDENT" /* DEDENT */) {
|
|
1697
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1698
|
-
if (current().type === "DEDENT" /* DEDENT */) break;
|
|
1699
|
-
const action = parseActionLine();
|
|
1700
|
-
if (action) {
|
|
1701
|
-
page2.actions.push(action);
|
|
1702
|
-
}
|
|
1703
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
1704
|
-
}
|
|
1705
|
-
if (current().type === "DEDENT" /* DEDENT */) {
|
|
1706
|
-
advance();
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
function parseActionLine() {
|
|
1710
|
-
if (current().type === "DASH" /* DASH */) {
|
|
1711
|
-
advance();
|
|
1712
|
-
}
|
|
1713
|
-
const fullText = collectUntilNewline();
|
|
1714
|
-
if (!fullText) {
|
|
1715
|
-
return null;
|
|
1716
|
-
}
|
|
1717
|
-
const toIndex = fullText.toLowerCase().indexOf(" to ");
|
|
1718
|
-
if (toIndex === -1) {
|
|
1719
|
-
return {
|
|
1720
|
-
type: "page_action",
|
|
1721
|
-
trigger: fullText,
|
|
1722
|
-
action: ""
|
|
1723
|
-
};
|
|
1724
|
-
}
|
|
1725
|
-
return {
|
|
1726
|
-
type: "page_action",
|
|
1727
|
-
trigger: fullText.slice(0, toIndex).trim(),
|
|
1728
|
-
action: fullText.slice(toIndex + 4).trim()
|
|
1729
|
-
};
|
|
1730
|
-
}
|
|
1731
|
-
function collectUntilNewline() {
|
|
1732
|
-
const parts = [];
|
|
1733
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
1734
|
-
parts.push(current().value);
|
|
1735
|
-
advance();
|
|
1736
|
-
}
|
|
1737
|
-
return parts.join(" ").trim();
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
function formatPageToSchema(page) {
|
|
1741
|
-
const viewType = inferViewTypeFromPageName(page.name);
|
|
1742
|
-
const primaryEntity = page.primaryEntity;
|
|
1743
|
-
return {
|
|
1744
|
-
name: page.name,
|
|
1745
|
-
path: page.url.startsWith("/") ? page.url : `/${page.url}`,
|
|
1746
|
-
purpose: page.purpose || page.description,
|
|
1747
|
-
sections: page.sections.map((section, index) => {
|
|
1748
|
-
const pattern = inferPatternFromDescription(section.description);
|
|
1749
|
-
const patternConfig = inferPatternConfigFromDescription(section.description, page.primaryEntity);
|
|
1750
|
-
return {
|
|
1751
|
-
id: `section-${index}`,
|
|
1752
|
-
purpose: section.description,
|
|
1753
|
-
order: index,
|
|
1754
|
-
estimatedSize: "medium",
|
|
1755
|
-
pattern: {
|
|
1756
|
-
type: pattern,
|
|
1757
|
-
...patternConfig
|
|
1758
|
-
}
|
|
1759
|
-
};
|
|
1760
|
-
}),
|
|
1761
|
-
traits: [],
|
|
1762
|
-
primaryEntity,
|
|
1763
|
-
viewType,
|
|
1764
|
-
isInitial: page.url === "/" || page.url === "/dashboard"
|
|
1765
|
-
};
|
|
1766
|
-
}
|
|
1767
|
-
function inferViewTypeFromPageName(name) {
|
|
1768
|
-
const lower = name.toLowerCase();
|
|
1769
|
-
if (lower.includes("list") || lower.includes("index")) {
|
|
1770
|
-
return "list";
|
|
1771
|
-
}
|
|
1772
|
-
if (lower.includes("detail") || lower.includes("view")) {
|
|
1773
|
-
return "detail";
|
|
1774
|
-
}
|
|
1775
|
-
if (lower.includes("create") || lower.includes("new")) {
|
|
1776
|
-
return "create";
|
|
1777
|
-
}
|
|
1778
|
-
if (lower.includes("edit")) {
|
|
1779
|
-
return "edit";
|
|
1780
|
-
}
|
|
1781
|
-
if (lower.includes("dashboard")) {
|
|
1782
|
-
return "dashboard";
|
|
1783
|
-
}
|
|
1784
|
-
return "list";
|
|
1785
|
-
}
|
|
1786
|
-
function inferPatternConfigFromDescription(description, primaryEntity) {
|
|
1787
|
-
const lower = description.toLowerCase();
|
|
1788
|
-
const entityName = primaryEntity || "Item";
|
|
1789
|
-
if (lower.includes("header") || lower.includes("title")) {
|
|
1790
|
-
const titleMatch = description.match(/title\s*["']?([^"']+)["']?/i) || description.match(/["']([^"']+)["']/);
|
|
1791
|
-
return {
|
|
1792
|
-
title: titleMatch ? titleMatch[1] : "Page Title"
|
|
1793
|
-
};
|
|
1794
|
-
}
|
|
1795
|
-
if (lower.includes("list") || lower.includes("table")) {
|
|
1796
|
-
return {
|
|
1797
|
-
entity: entityName,
|
|
1798
|
-
fieldNames: ["id", "name", "createdAt"]
|
|
1799
|
-
};
|
|
1800
|
-
}
|
|
1801
|
-
if (lower.includes("detail")) {
|
|
1802
|
-
return {
|
|
1803
|
-
entity: entityName,
|
|
1804
|
-
fieldNames: ["id", "name", "description", "createdAt"]
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1807
|
-
if (lower.includes("form")) {
|
|
1808
|
-
return {
|
|
1809
|
-
entity: entityName,
|
|
1810
|
-
fields: [
|
|
1811
|
-
{ field: "name", label: "Name", type: "text", required: true },
|
|
1812
|
-
{ field: "description", label: "Description", type: "textarea" }
|
|
1813
|
-
]
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
if (lower.includes("stats") || lower.includes("statistics") || lower.includes("summary") || lower.includes("overview")) {
|
|
1817
|
-
return {
|
|
1818
|
-
entity: entityName,
|
|
1819
|
-
fieldNames: ["id", "name", "value"]
|
|
1820
|
-
};
|
|
1821
|
-
}
|
|
1822
|
-
return {
|
|
1823
|
-
entity: entityName,
|
|
1824
|
-
fieldNames: ["id", "name", "createdAt"]
|
|
1825
|
-
};
|
|
1826
|
-
}
|
|
1827
|
-
function toPascalCase(text) {
|
|
1828
|
-
if (/^[A-Z][a-zA-Z]*$/.test(text)) {
|
|
1829
|
-
return text;
|
|
1830
|
-
}
|
|
1831
|
-
if (text.includes(" ")) {
|
|
1832
|
-
return text.split(" ").map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
1833
|
-
}
|
|
1834
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
1835
|
-
}
|
|
1836
|
-
function toKebabCase2(text) {
|
|
1837
|
-
return text.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/\s+/g, "-").toLowerCase();
|
|
1838
|
-
}
|
|
1839
|
-
function inferPatternFromDescription(description) {
|
|
1840
|
-
const lower = description.toLowerCase();
|
|
1841
|
-
if (lower.includes("header")) {
|
|
1842
|
-
return "page-header";
|
|
1843
|
-
}
|
|
1844
|
-
if (lower.includes("list") || lower.includes("table")) {
|
|
1845
|
-
return "entity-list";
|
|
1846
|
-
}
|
|
1847
|
-
if (lower.includes("statistics") || lower.includes("stats") || lower.includes("summary") || lower.includes("overview")) {
|
|
1848
|
-
return "entity-list";
|
|
1849
|
-
}
|
|
1850
|
-
if (lower.includes("form") || lower.includes("input") || lower.includes("edit")) {
|
|
1851
|
-
return "form-section";
|
|
1852
|
-
}
|
|
1853
|
-
if (lower.includes("detail") || lower.includes("view")) {
|
|
1854
|
-
return "entity-detail";
|
|
1855
|
-
}
|
|
1856
|
-
if (lower.includes("chart") || lower.includes("graph")) {
|
|
1857
|
-
return "entity-list";
|
|
1858
|
-
}
|
|
1859
|
-
if (lower.includes("timeline") || lower.includes("history") || lower.includes("activity")) {
|
|
1860
|
-
return "entity-list";
|
|
1861
|
-
}
|
|
1862
|
-
if (lower.includes("title")) {
|
|
1863
|
-
return "page-header";
|
|
1864
|
-
}
|
|
1865
|
-
return "entity-list";
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
// src/orbitals/domain-language/parsers/guard-parser.ts
|
|
1869
|
-
function parseGuard(text, entityName) {
|
|
1870
|
-
const ctx = { entityName, errors: [] };
|
|
1871
|
-
let expression = text.trim();
|
|
1872
|
-
if (expression.toLowerCase().startsWith("if ")) {
|
|
1873
|
-
expression = expression.slice(3).trim();
|
|
1874
|
-
}
|
|
1875
|
-
const condition = parseCondition(expression, ctx);
|
|
1876
|
-
if (!condition) {
|
|
1877
|
-
return {
|
|
1878
|
-
success: false,
|
|
1879
|
-
errors: ctx.errors.length > 0 ? ctx.errors : [{
|
|
1880
|
-
message: `Failed to parse guard expression: "${text}"`
|
|
1881
|
-
}],
|
|
1882
|
-
warnings: []
|
|
1883
|
-
};
|
|
1884
|
-
}
|
|
1885
|
-
return {
|
|
1886
|
-
success: true,
|
|
1887
|
-
data: {
|
|
1888
|
-
type: "guard",
|
|
1889
|
-
condition,
|
|
1890
|
-
raw: text
|
|
1891
|
-
},
|
|
1892
|
-
errors: [],
|
|
1893
|
-
warnings: []
|
|
1894
|
-
};
|
|
1895
|
-
}
|
|
1896
|
-
function parseCondition(text, ctx) {
|
|
1897
|
-
text = text.trim();
|
|
1898
|
-
const andMatch = splitAtTopLevel(text, " AND ");
|
|
1899
|
-
if (andMatch) {
|
|
1900
|
-
const left = parseCondition(andMatch.left, ctx);
|
|
1901
|
-
const right = parseCondition(andMatch.right, ctx);
|
|
1902
|
-
if (left && right) {
|
|
1903
|
-
return {
|
|
1904
|
-
type: "logical",
|
|
1905
|
-
operator: "AND",
|
|
1906
|
-
left,
|
|
1907
|
-
right
|
|
1908
|
-
};
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
const orMatch = splitAtTopLevel(text, " OR ");
|
|
1912
|
-
if (orMatch) {
|
|
1913
|
-
const left = parseCondition(orMatch.left, ctx);
|
|
1914
|
-
const right = parseCondition(orMatch.right, ctx);
|
|
1915
|
-
if (left && right) {
|
|
1916
|
-
return {
|
|
1917
|
-
type: "logical",
|
|
1918
|
-
operator: "OR",
|
|
1919
|
-
left,
|
|
1920
|
-
right
|
|
1921
|
-
};
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
return parseUserCheck(text) || parseFieldCheck(text, ctx) || parseComparison(text, ctx);
|
|
1925
|
-
}
|
|
1926
|
-
function parseUserCheck(text, ctx) {
|
|
1927
|
-
const lowerText = text.toLowerCase();
|
|
1928
|
-
const roleMatch = lowerText.match(/^user\s+is\s+(\w+)$/);
|
|
1929
|
-
if (roleMatch) {
|
|
1930
|
-
return {
|
|
1931
|
-
type: "user_check",
|
|
1932
|
-
check: "is_role",
|
|
1933
|
-
role: roleMatch[1]
|
|
1934
|
-
};
|
|
1935
|
-
}
|
|
1936
|
-
if (lowerText === "user owns this") {
|
|
1937
|
-
return {
|
|
1938
|
-
type: "user_check",
|
|
1939
|
-
check: "owns_this",
|
|
1940
|
-
ownerField: "ownerId"
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
return null;
|
|
1944
|
-
}
|
|
1945
|
-
function parseFieldCheck(text, ctx) {
|
|
1946
|
-
text.toLowerCase();
|
|
1947
|
-
const providedMatch = text.match(/^(.+?)\s+is\s+provided$/i);
|
|
1948
|
-
if (providedMatch) {
|
|
1949
|
-
const field = parseFieldReference(providedMatch[1], ctx);
|
|
1950
|
-
if (field) {
|
|
1951
|
-
return {
|
|
1952
|
-
type: "field_check",
|
|
1953
|
-
field,
|
|
1954
|
-
check: "provided"
|
|
1955
|
-
};
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
const emptyMatch = text.match(/^(.+?)\s+is\s+empty$/i);
|
|
1959
|
-
if (emptyMatch) {
|
|
1960
|
-
const field = parseFieldReference(emptyMatch[1], ctx);
|
|
1961
|
-
if (field) {
|
|
1962
|
-
return {
|
|
1963
|
-
type: "field_check",
|
|
1964
|
-
field,
|
|
1965
|
-
check: "empty"
|
|
1966
|
-
};
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
const notEqualsMatch = text.match(/^(.+?)\s+is\s+not\s+(.+)$/i);
|
|
1970
|
-
if (notEqualsMatch) {
|
|
1971
|
-
return null;
|
|
1972
|
-
}
|
|
1973
|
-
const equalsMatch = text.match(/^(.+?)\s+is\s+(.+)$/i);
|
|
1974
|
-
if (equalsMatch) {
|
|
1975
|
-
const fieldName = equalsMatch[1].trim();
|
|
1976
|
-
const value = equalsMatch[2].trim();
|
|
1977
|
-
if (["provided", "empty", "not"].includes(value.toLowerCase())) {
|
|
1978
|
-
return null;
|
|
1979
|
-
}
|
|
1980
|
-
const field = parseFieldReference(fieldName, ctx);
|
|
1981
|
-
if (field) {
|
|
1982
|
-
return {
|
|
1983
|
-
type: "field_check",
|
|
1984
|
-
field,
|
|
1985
|
-
check: "equals",
|
|
1986
|
-
value: parseValue2(value)
|
|
1987
|
-
};
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
return null;
|
|
1991
|
-
}
|
|
1992
|
-
function parseComparison(text, ctx) {
|
|
1993
|
-
const operators = [
|
|
1994
|
-
{ pattern: /^(.+?)\s*>=\s*(.+)$/, operator: ">=" },
|
|
1995
|
-
{ pattern: /^(.+?)\s*<=\s*(.+)$/, operator: "<=" },
|
|
1996
|
-
{ pattern: /^(.+?)\s*!=\s*(.+)$/, operator: "!=" },
|
|
1997
|
-
{ pattern: /^(.+?)\s*==\s*(.+)$/, operator: "==" },
|
|
1998
|
-
{ pattern: /^(.+?)\s*>\s*(.+)$/, operator: ">" },
|
|
1999
|
-
{ pattern: /^(.+?)\s*<\s*(.+)$/, operator: "<" }
|
|
2000
|
-
];
|
|
2001
|
-
const isNotMatch = text.match(/^(.+?)\s+is\s+not\s+(.+)$/i);
|
|
2002
|
-
if (isNotMatch) {
|
|
2003
|
-
const field = parseFieldReference(isNotMatch[1], ctx);
|
|
2004
|
-
if (field) {
|
|
2005
|
-
return {
|
|
2006
|
-
type: "comparison",
|
|
2007
|
-
field,
|
|
2008
|
-
operator: "!=",
|
|
2009
|
-
value: parseValue2(isNotMatch[2])
|
|
2010
|
-
};
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
for (const { pattern, operator } of operators) {
|
|
2014
|
-
const match = text.match(pattern);
|
|
2015
|
-
if (match) {
|
|
2016
|
-
const field = parseFieldReference(match[1], ctx);
|
|
2017
|
-
if (field) {
|
|
2018
|
-
return {
|
|
2019
|
-
type: "comparison",
|
|
2020
|
-
field,
|
|
2021
|
-
operator,
|
|
2022
|
-
value: parseValue2(match[2])
|
|
2023
|
-
};
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
return null;
|
|
2028
|
-
}
|
|
2029
|
-
function parseFieldReference(text, ctx) {
|
|
2030
|
-
text = text.trim();
|
|
2031
|
-
const dotMatch = text.match(/^(\w+)\.(\w+)$/);
|
|
2032
|
-
if (dotMatch) {
|
|
2033
|
-
return {
|
|
2034
|
-
type: "field_reference",
|
|
2035
|
-
entityName: dotMatch[1],
|
|
2036
|
-
fieldName: dotMatch[2]
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
const fieldName = toCamelCase2(text);
|
|
2040
|
-
return {
|
|
2041
|
-
type: "field_reference",
|
|
2042
|
-
entityName: ctx.entityName,
|
|
2043
|
-
fieldName
|
|
2044
|
-
};
|
|
2045
|
-
}
|
|
2046
|
-
function parseValue2(text) {
|
|
2047
|
-
text = text.trim();
|
|
2048
|
-
if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
|
|
2049
|
-
return text.slice(1, -1);
|
|
2050
|
-
}
|
|
2051
|
-
if (text.toLowerCase() === "true") return true;
|
|
2052
|
-
if (text.toLowerCase() === "false") return false;
|
|
2053
|
-
const num = parseFloat(text);
|
|
2054
|
-
if (!isNaN(num)) return num;
|
|
2055
|
-
return text;
|
|
2056
|
-
}
|
|
2057
|
-
function toCamelCase2(text) {
|
|
2058
|
-
return text.toLowerCase().split(/\s+/).map(
|
|
2059
|
-
(word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)
|
|
2060
|
-
).join("");
|
|
2061
|
-
}
|
|
2062
|
-
function splitAtTopLevel(text, separator) {
|
|
2063
|
-
const separatorLower = separator.toLowerCase();
|
|
2064
|
-
const textLower = text.toLowerCase();
|
|
2065
|
-
let depth = 0;
|
|
2066
|
-
for (let i = 0; i <= textLower.length - separatorLower.length; i++) {
|
|
2067
|
-
const char = text[i];
|
|
2068
|
-
if (char === "(") depth++;
|
|
2069
|
-
else if (char === ")") depth--;
|
|
2070
|
-
else if (depth === 0 && textLower.slice(i, i + separatorLower.length) === separatorLower) {
|
|
2071
|
-
return {
|
|
2072
|
-
left: text.slice(0, i),
|
|
2073
|
-
right: text.slice(i + separator.length)
|
|
2074
|
-
};
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
return null;
|
|
2078
|
-
}
|
|
2079
|
-
function isKnownPattern(pattern) {
|
|
2080
|
-
return getPatternDefinition(pattern) !== null;
|
|
2081
|
-
}
|
|
2082
|
-
function parseDomainGuard(text, entityName) {
|
|
2083
|
-
let expr = text.trim();
|
|
2084
|
-
if (expr.toLowerCase().startsWith("if ")) {
|
|
2085
|
-
expr = expr.slice(3).trim();
|
|
2086
|
-
}
|
|
2087
|
-
return parseExpression(expr);
|
|
2088
|
-
}
|
|
2089
|
-
function parseDomainEffect(text, entityName) {
|
|
2090
|
-
let expr = text.trim();
|
|
2091
|
-
if (expr.toLowerCase().startsWith("then ")) {
|
|
2092
|
-
expr = expr.slice(5).trim();
|
|
2093
|
-
}
|
|
2094
|
-
return parseEffect(expr);
|
|
2095
|
-
}
|
|
2096
|
-
function isInlineSExpr(text) {
|
|
2097
|
-
const trimmed = text.trim();
|
|
2098
|
-
return trimmed.startsWith("[") || trimmed.startsWith("{") || trimmed.startsWith("(");
|
|
2099
|
-
}
|
|
2100
|
-
function parseInlineSExpr(text) {
|
|
2101
|
-
const trimmed = text.trim();
|
|
2102
|
-
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
2103
|
-
try {
|
|
2104
|
-
return JSON.parse(trimmed);
|
|
2105
|
-
} catch {
|
|
2106
|
-
return parseRelaxedJson(trimmed);
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
if (trimmed.startsWith("(")) {
|
|
2110
|
-
return parseLispSExpr(trimmed);
|
|
2111
|
-
}
|
|
2112
|
-
throw new Error(`Invalid inline S-Expression: ${text}`);
|
|
2113
|
-
}
|
|
2114
|
-
function parseRelaxedJson(text) {
|
|
2115
|
-
let normalized = text;
|
|
2116
|
-
normalized = normalized.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
|
|
2117
|
-
normalized = normalized.replace(/(\{|,)\s*(\w+)\s*:/g, '$1"$2":');
|
|
2118
|
-
try {
|
|
2119
|
-
return JSON.parse(normalized);
|
|
2120
|
-
} catch (e) {
|
|
2121
|
-
throw new Error(`Failed to parse relaxed JSON: ${text}`);
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
function parseLispSExpr(text) {
|
|
2125
|
-
const trimmed = text.trim();
|
|
2126
|
-
if (!trimmed.startsWith("(") || !trimmed.endsWith(")")) {
|
|
2127
|
-
throw new Error(`Invalid Lisp S-Expression: ${text}`);
|
|
2128
|
-
}
|
|
2129
|
-
const inner = trimmed.slice(1, -1).trim();
|
|
2130
|
-
const tokens = tokenizeLisp(inner);
|
|
2131
|
-
if (tokens.length === 0) {
|
|
2132
|
-
return [];
|
|
2133
|
-
}
|
|
2134
|
-
return tokens.map(parseLispToken);
|
|
2135
|
-
}
|
|
2136
|
-
function tokenizeLisp(text) {
|
|
2137
|
-
const tokens = [];
|
|
2138
|
-
let current = "";
|
|
2139
|
-
let depth = 0;
|
|
2140
|
-
let inString = false;
|
|
2141
|
-
let stringChar = "";
|
|
2142
|
-
for (let i = 0; i < text.length; i++) {
|
|
2143
|
-
const char = text[i];
|
|
2144
|
-
if (inString) {
|
|
2145
|
-
current += char;
|
|
2146
|
-
if (char === stringChar && text[i - 1] !== "\\") {
|
|
2147
|
-
inString = false;
|
|
2148
|
-
}
|
|
2149
|
-
continue;
|
|
2150
|
-
}
|
|
2151
|
-
if (char === '"' || char === "'") {
|
|
2152
|
-
inString = true;
|
|
2153
|
-
stringChar = char;
|
|
2154
|
-
current += char;
|
|
2155
|
-
continue;
|
|
2156
|
-
}
|
|
2157
|
-
if (char === "(" || char === "[" || char === "{") {
|
|
2158
|
-
depth++;
|
|
2159
|
-
current += char;
|
|
2160
|
-
continue;
|
|
2161
|
-
}
|
|
2162
|
-
if (char === ")" || char === "]" || char === "}") {
|
|
2163
|
-
depth--;
|
|
2164
|
-
current += char;
|
|
2165
|
-
continue;
|
|
2166
|
-
}
|
|
2167
|
-
if (char === " " || char === " " || char === "\n") {
|
|
2168
|
-
if (depth === 0 && current.trim()) {
|
|
2169
|
-
tokens.push(current.trim());
|
|
2170
|
-
current = "";
|
|
2171
|
-
} else if (depth > 0) {
|
|
2172
|
-
current += char;
|
|
2173
|
-
}
|
|
2174
|
-
continue;
|
|
2175
|
-
}
|
|
2176
|
-
current += char;
|
|
2177
|
-
}
|
|
2178
|
-
if (current.trim()) {
|
|
2179
|
-
tokens.push(current.trim());
|
|
2180
|
-
}
|
|
2181
|
-
return tokens;
|
|
2182
|
-
}
|
|
2183
|
-
function parseLispToken(token) {
|
|
2184
|
-
const trimmed = token.trim();
|
|
2185
|
-
if (trimmed.startsWith("(")) {
|
|
2186
|
-
return parseLispSExpr(trimmed);
|
|
2187
|
-
}
|
|
2188
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
2189
|
-
return parseRelaxedJson(trimmed);
|
|
2190
|
-
}
|
|
2191
|
-
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
2192
|
-
return trimmed.slice(1, -1);
|
|
2193
|
-
}
|
|
2194
|
-
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
2195
|
-
return parseFloat(trimmed);
|
|
2196
|
-
}
|
|
2197
|
-
if (trimmed === "true") return true;
|
|
2198
|
-
if (trimmed === "false") return false;
|
|
2199
|
-
if (trimmed === "null") return null;
|
|
2200
|
-
if (trimmed.startsWith("@")) {
|
|
2201
|
-
return trimmed;
|
|
2202
|
-
}
|
|
2203
|
-
return trimmed;
|
|
2204
|
-
}
|
|
2205
|
-
function parseExpression(text, ctx) {
|
|
2206
|
-
text = text.trim();
|
|
2207
|
-
if (isInlineSExpr(text)) {
|
|
2208
|
-
return parseInlineSExpr(text);
|
|
2209
|
-
}
|
|
2210
|
-
const andMatch = splitAtTopLevel2(text, " and ");
|
|
2211
|
-
if (andMatch) {
|
|
2212
|
-
const left = parseExpression(andMatch.left);
|
|
2213
|
-
const right = parseExpression(andMatch.right);
|
|
2214
|
-
return ["and", left, right];
|
|
2215
|
-
}
|
|
2216
|
-
const orMatch = splitAtTopLevel2(text, " or ");
|
|
2217
|
-
if (orMatch) {
|
|
2218
|
-
const left = parseExpression(orMatch.left);
|
|
2219
|
-
const right = parseExpression(orMatch.right);
|
|
2220
|
-
return ["or", left, right];
|
|
2221
|
-
}
|
|
2222
|
-
if (text.toLowerCase().startsWith("not ")) {
|
|
2223
|
-
const inner = parseExpression(text.slice(4).trim());
|
|
2224
|
-
return ["not", inner];
|
|
2225
|
-
}
|
|
2226
|
-
if (text.startsWith("(") && text.endsWith(")")) {
|
|
2227
|
-
return parseExpression(text.slice(1, -1));
|
|
2228
|
-
}
|
|
2229
|
-
return parseComparison2(text);
|
|
2230
|
-
}
|
|
2231
|
-
function parseComparison2(text, ctx) {
|
|
2232
|
-
const atLeastMatch = text.match(/^(.+?)\s+is\s+at\s+least\s+(.+)$/i);
|
|
2233
|
-
if (atLeastMatch) {
|
|
2234
|
-
const field = parseFieldRef(atLeastMatch[1]);
|
|
2235
|
-
const value = parseValue3(atLeastMatch[2]);
|
|
2236
|
-
return [">=", field, value];
|
|
2237
|
-
}
|
|
2238
|
-
const atMostMatch = text.match(/^(.+?)\s+is\s+at\s+most\s+(.+)$/i);
|
|
2239
|
-
if (atMostMatch) {
|
|
2240
|
-
const field = parseFieldRef(atMostMatch[1]);
|
|
2241
|
-
const value = parseValue3(atMostMatch[2]);
|
|
2242
|
-
return ["<=", field, value];
|
|
2243
|
-
}
|
|
2244
|
-
const greaterThanMatch = text.match(/^(.+?)\s+is\s+greater\s+than\s+(.+)$/i);
|
|
2245
|
-
if (greaterThanMatch) {
|
|
2246
|
-
const field = parseFieldRef(greaterThanMatch[1]);
|
|
2247
|
-
const value = parseValue3(greaterThanMatch[2]);
|
|
2248
|
-
return [">", field, value];
|
|
2249
|
-
}
|
|
2250
|
-
const lessThanMatch = text.match(/^(.+?)\s+is\s+less\s+than\s+(.+)$/i);
|
|
2251
|
-
if (lessThanMatch) {
|
|
2252
|
-
const field = parseFieldRef(lessThanMatch[1]);
|
|
2253
|
-
const value = parseValue3(lessThanMatch[2]);
|
|
2254
|
-
return ["<", field, value];
|
|
2255
|
-
}
|
|
2256
|
-
const isNotMatch = text.match(/^(.+?)\s+is\s+not\s+(.+)$/i);
|
|
2257
|
-
if (isNotMatch) {
|
|
2258
|
-
const field = parseFieldRef(isNotMatch[1]);
|
|
2259
|
-
const value = parseValue3(isNotMatch[2]);
|
|
2260
|
-
return ["!=", field, value];
|
|
2261
|
-
}
|
|
2262
|
-
const isMatch = text.match(/^(.+?)\s+is\s+(.+)$/i);
|
|
2263
|
-
if (isMatch) {
|
|
2264
|
-
const field = parseFieldRef(isMatch[1]);
|
|
2265
|
-
const value = parseValue3(isMatch[2]);
|
|
2266
|
-
return ["=", field, value];
|
|
2267
|
-
}
|
|
2268
|
-
const opPatterns = [
|
|
2269
|
-
{ pattern: /^(.+?)\s*>=\s*(.+)$/, op: ">=" },
|
|
2270
|
-
{ pattern: /^(.+?)\s*<=\s*(.+)$/, op: "<=" },
|
|
2271
|
-
{ pattern: /^(.+?)\s*!=\s*(.+)$/, op: "!=" },
|
|
2272
|
-
{ pattern: /^(.+?)\s*==\s*(.+)$/, op: "=" },
|
|
2273
|
-
{ pattern: /^(.+?)\s*>\s*(.+)$/, op: ">" },
|
|
2274
|
-
{ pattern: /^(.+?)\s*<\s*(.+)$/, op: "<" }
|
|
2275
|
-
];
|
|
2276
|
-
for (const { pattern, op } of opPatterns) {
|
|
2277
|
-
const match = text.match(pattern);
|
|
2278
|
-
if (match) {
|
|
2279
|
-
const left = parseFieldRef(match[1]);
|
|
2280
|
-
const right = parseValue3(match[2]);
|
|
2281
|
-
return [op, left, right];
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
return parseFieldRef(text);
|
|
2285
|
-
}
|
|
2286
|
-
function parseEffect(text, ctx) {
|
|
2287
|
-
text = text.trim();
|
|
2288
|
-
if (isInlineSExpr(text)) {
|
|
2289
|
-
return parseInlineSExpr(text);
|
|
2290
|
-
}
|
|
2291
|
-
const updateMatch = text.match(/^update\s+(.+?)\s+to\s+(.+)$/i);
|
|
2292
|
-
if (updateMatch) {
|
|
2293
|
-
const field = parseFieldRef(updateMatch[1]);
|
|
2294
|
-
const value = parseEffectValue(updateMatch[2]);
|
|
2295
|
-
return ["set", field, value];
|
|
2296
|
-
}
|
|
2297
|
-
const emitWithMatch = text.match(/^emit\s+(\S+)\s+with\s+(.+)$/i);
|
|
2298
|
-
if (emitWithMatch) {
|
|
2299
|
-
const event = emitWithMatch[1];
|
|
2300
|
-
const payload = parseEffectValue(emitWithMatch[2]);
|
|
2301
|
-
return ["emit", event, payload];
|
|
2302
|
-
}
|
|
2303
|
-
const emitMatch = text.match(/^emit\s+(\S+)$/i);
|
|
2304
|
-
if (emitMatch) {
|
|
2305
|
-
return ["emit", emitMatch[1]];
|
|
2306
|
-
}
|
|
2307
|
-
const renderNullMatch = text.match(/^render\s+null\s+to\s+(\S+)$/i);
|
|
2308
|
-
if (renderNullMatch) {
|
|
2309
|
-
return ["render-ui", renderNullMatch[1], null];
|
|
2310
|
-
}
|
|
2311
|
-
const renderFullMatch = text.match(/^render\s+(\S+)\s+to\s+(\S+)\s+for\s+(\S+)\s+with\s+(.+)$/i);
|
|
2312
|
-
if (renderFullMatch) {
|
|
2313
|
-
const pattern = renderFullMatch[1];
|
|
2314
|
-
const slot = renderFullMatch[2];
|
|
2315
|
-
const entity = renderFullMatch[3];
|
|
2316
|
-
const propsText = renderFullMatch[4];
|
|
2317
|
-
const props = parseRenderProps(propsText);
|
|
2318
|
-
validatePatternType(pattern);
|
|
2319
|
-
return ["render-ui", slot, { type: pattern, entity, ...props }];
|
|
2320
|
-
}
|
|
2321
|
-
const renderEntityMatch = text.match(/^render\s+(\S+)\s+to\s+(\S+)\s+for\s+(\S+)$/i);
|
|
2322
|
-
if (renderEntityMatch) {
|
|
2323
|
-
const pattern = renderEntityMatch[1];
|
|
2324
|
-
const slot = renderEntityMatch[2];
|
|
2325
|
-
const entity = renderEntityMatch[3];
|
|
2326
|
-
validatePatternType(pattern);
|
|
2327
|
-
return ["render-ui", slot, { type: pattern, entity }];
|
|
2328
|
-
}
|
|
2329
|
-
const renderPropsMatch = text.match(/^render\s+(\S+)\s+to\s+(\S+)\s+with\s+(.+)$/i);
|
|
2330
|
-
if (renderPropsMatch) {
|
|
2331
|
-
const pattern = renderPropsMatch[1];
|
|
2332
|
-
const slot = renderPropsMatch[2];
|
|
2333
|
-
const propsText = renderPropsMatch[3];
|
|
2334
|
-
const props = parseRenderProps(propsText);
|
|
2335
|
-
validatePatternType(pattern);
|
|
2336
|
-
return ["render-ui", slot, { type: pattern, ...props }];
|
|
2337
|
-
}
|
|
2338
|
-
const renderMatch = text.match(/^render\s+(\S+)\s+to\s+(\S+)$/i);
|
|
2339
|
-
if (renderMatch) {
|
|
2340
|
-
const pattern = renderMatch[1];
|
|
2341
|
-
const slot = renderMatch[2];
|
|
2342
|
-
validatePatternType(pattern);
|
|
2343
|
-
return ["render-ui", slot, { type: pattern }];
|
|
2344
|
-
}
|
|
2345
|
-
const renderSlotMatch = text.match(/^render\s+to\s+(\S+)$/i);
|
|
2346
|
-
if (renderSlotMatch) {
|
|
2347
|
-
return ["render-ui", renderSlotMatch[1]];
|
|
2348
|
-
}
|
|
2349
|
-
const navWithMatch = text.match(/^navigate\s+to\s+(.+?)\s+with\s+(.+)$/i);
|
|
2350
|
-
if (navWithMatch) {
|
|
2351
|
-
const path6 = navWithMatch[1];
|
|
2352
|
-
const params = parseEffectValue(navWithMatch[2]);
|
|
2353
|
-
return ["navigate", path6, params];
|
|
2354
|
-
}
|
|
2355
|
-
const navMatch = text.match(/^navigate\s+to\s+(.+)$/i);
|
|
2356
|
-
if (navMatch) {
|
|
2357
|
-
return ["navigate", navMatch[1]];
|
|
2358
|
-
}
|
|
2359
|
-
const showNotifyMatch = text.match(/^show\s+(\w+)\s+notification\s+"(.+)"$/i);
|
|
2360
|
-
if (showNotifyMatch) {
|
|
2361
|
-
return ["notify", showNotifyMatch[2], showNotifyMatch[1]];
|
|
2362
|
-
}
|
|
2363
|
-
const notifyMatch = text.match(/^notify\s+"(.+)"$/i);
|
|
2364
|
-
if (notifyMatch) {
|
|
2365
|
-
return ["notify", notifyMatch[1]];
|
|
2366
|
-
}
|
|
2367
|
-
const persistWithMatch = text.match(/^persist\s+(\w+)\s+(.+)$/i);
|
|
2368
|
-
if (persistWithMatch) {
|
|
2369
|
-
const action = persistWithMatch[1];
|
|
2370
|
-
const data = parseFieldRef(persistWithMatch[2]);
|
|
2371
|
-
return ["persist", action, data];
|
|
2372
|
-
}
|
|
2373
|
-
const persistMatch = text.match(/^persist\s+(\w+)$/i);
|
|
2374
|
-
if (persistMatch) {
|
|
2375
|
-
return ["persist", persistMatch[1]];
|
|
2376
|
-
}
|
|
2377
|
-
const spawnWithMatch = text.match(/^spawn\s+(\S+)\s+with\s+(.+)$/i);
|
|
2378
|
-
if (spawnWithMatch) {
|
|
2379
|
-
const entityType = spawnWithMatch[1];
|
|
2380
|
-
const props = parseEffectValue(spawnWithMatch[2]);
|
|
2381
|
-
return ["spawn", entityType, props];
|
|
2382
|
-
}
|
|
2383
|
-
const spawnMatch = text.match(/^spawn\s+(\S+)$/i);
|
|
2384
|
-
if (spawnMatch) {
|
|
2385
|
-
return ["spawn", spawnMatch[1]];
|
|
2386
|
-
}
|
|
2387
|
-
const despawnIdMatch = text.match(/^despawn\s+(.+)$/i);
|
|
2388
|
-
if (despawnIdMatch && despawnIdMatch[1] !== "this") {
|
|
2389
|
-
return ["despawn", parseFieldRef(despawnIdMatch[1])];
|
|
2390
|
-
}
|
|
2391
|
-
if (text.toLowerCase() === "despawn" || text.toLowerCase() === "despawn this") {
|
|
2392
|
-
return ["despawn"];
|
|
2393
|
-
}
|
|
2394
|
-
const callServiceMatch = text.match(/^call\s+(\w+)\.(\w+)$/i);
|
|
2395
|
-
if (callServiceMatch) {
|
|
2396
|
-
return ["call-service", callServiceMatch[1], callServiceMatch[2]];
|
|
2397
|
-
}
|
|
2398
|
-
const callMatch = text.match(/^call\s+(\S+)$/i);
|
|
2399
|
-
if (callMatch) {
|
|
2400
|
-
return ["call-service", callMatch[1]];
|
|
2401
|
-
}
|
|
2402
|
-
return text;
|
|
2403
|
-
}
|
|
2404
|
-
function parseFieldRef(text, ctx) {
|
|
2405
|
-
text = text.trim();
|
|
2406
|
-
if (text.startsWith("@")) {
|
|
2407
|
-
return text;
|
|
2408
|
-
}
|
|
2409
|
-
if (text.toLowerCase() === "current state") {
|
|
2410
|
-
return "@state";
|
|
2411
|
-
}
|
|
2412
|
-
if (text.toLowerCase() === "current time") {
|
|
2413
|
-
return "@now";
|
|
2414
|
-
}
|
|
2415
|
-
if (text.toLowerCase() === "entity") {
|
|
2416
|
-
return "@entity";
|
|
2417
|
-
}
|
|
2418
|
-
if (text.toLowerCase() === "payload") {
|
|
2419
|
-
return "@payload";
|
|
2420
|
-
}
|
|
2421
|
-
if (text.toLowerCase().startsWith("incoming ")) {
|
|
2422
|
-
const field = toCamelCase3(text.slice(9).trim());
|
|
2423
|
-
return `@payload.${field}`;
|
|
2424
|
-
}
|
|
2425
|
-
const possessiveMatch = text.match(/^(\w+)'s\s+(.+)$/);
|
|
2426
|
-
if (possessiveMatch) {
|
|
2427
|
-
const entity = possessiveMatch[1];
|
|
2428
|
-
const field = toCamelCase3(possessiveMatch[2]);
|
|
2429
|
-
return `@${entity}.${field}`;
|
|
2430
|
-
}
|
|
2431
|
-
if (text.includes(".")) {
|
|
2432
|
-
const parts = text.split(".");
|
|
2433
|
-
if (parts[0] && /^[A-Z]/.test(parts[0])) {
|
|
2434
|
-
return `@${text}`;
|
|
2435
|
-
}
|
|
2436
|
-
return `@entity.${text}`;
|
|
2437
|
-
}
|
|
2438
|
-
const fieldName = toCamelCase3(text);
|
|
2439
|
-
return `@entity.${fieldName}`;
|
|
2440
|
-
}
|
|
2441
|
-
function parseValue3(text) {
|
|
2442
|
-
text = text.trim();
|
|
2443
|
-
if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
|
|
2444
|
-
return text.slice(1, -1);
|
|
2445
|
-
}
|
|
2446
|
-
if (text.toLowerCase() === "nothing" || text.toLowerCase() === "null") {
|
|
2447
|
-
return null;
|
|
2448
|
-
}
|
|
2449
|
-
if (text.toLowerCase() === "true") return true;
|
|
2450
|
-
if (text.toLowerCase() === "false") return false;
|
|
2451
|
-
const num = parseFloat(text);
|
|
2452
|
-
if (!isNaN(num) && text.match(/^-?\d+(\.\d+)?$/)) {
|
|
2453
|
-
return num;
|
|
2454
|
-
}
|
|
2455
|
-
if (text.startsWith("@")) {
|
|
2456
|
-
return text;
|
|
2457
|
-
}
|
|
2458
|
-
if (/^[a-zA-Z_]\w*$/.test(text)) {
|
|
2459
|
-
return text;
|
|
2460
|
-
}
|
|
2461
|
-
return text;
|
|
2462
|
-
}
|
|
2463
|
-
function parseEffectValue(text, ctx) {
|
|
2464
|
-
text = text.trim();
|
|
2465
|
-
if (text.startsWith("{") && text.endsWith("}")) {
|
|
2466
|
-
try {
|
|
2467
|
-
const processed = text.replace(/@[\w.]+/g, (match) => `"${match}"`);
|
|
2468
|
-
const obj = JSON.parse(processed);
|
|
2469
|
-
return processBindingsInObject(obj);
|
|
2470
|
-
} catch {
|
|
2471
|
-
return text;
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
const arithmeticMatch = text.match(/^\((.+?)\s+(plus|minus|times|divided by)\s+(.+)\)$/i);
|
|
2475
|
-
if (arithmeticMatch) {
|
|
2476
|
-
const left = parseEffectValue(arithmeticMatch[1]);
|
|
2477
|
-
const right = parseEffectValue(arithmeticMatch[3]);
|
|
2478
|
-
const opMap = {
|
|
2479
|
-
"plus": "+",
|
|
2480
|
-
"minus": "-",
|
|
2481
|
-
"times": "*",
|
|
2482
|
-
"divided by": "/"
|
|
2483
|
-
};
|
|
2484
|
-
return [opMap[arithmeticMatch[2].toLowerCase()] || arithmeticMatch[2], left, right];
|
|
2485
|
-
}
|
|
2486
|
-
if (text.includes(".") || text.toLowerCase().startsWith("incoming ") || text.match(/^\w+'s\s+/) || /^[a-z]/.test(text)) {
|
|
2487
|
-
return parseFieldRef(text);
|
|
2488
|
-
}
|
|
2489
|
-
return parseValue3(text);
|
|
2490
|
-
}
|
|
2491
|
-
function processBindingsInObject(obj) {
|
|
2492
|
-
if (obj === null || obj === void 0) {
|
|
2493
|
-
return null;
|
|
2494
|
-
}
|
|
2495
|
-
if (typeof obj === "string") {
|
|
2496
|
-
if (obj.startsWith("@")) {
|
|
2497
|
-
return obj;
|
|
2498
|
-
}
|
|
2499
|
-
return obj;
|
|
2500
|
-
}
|
|
2501
|
-
if (typeof obj !== "object") {
|
|
2502
|
-
return obj;
|
|
2503
|
-
}
|
|
2504
|
-
if (Array.isArray(obj)) {
|
|
2505
|
-
return obj.map(processBindingsInObject);
|
|
2506
|
-
}
|
|
2507
|
-
const result = {};
|
|
2508
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
2509
|
-
result[key] = processBindingsInObject(value);
|
|
2510
|
-
}
|
|
2511
|
-
return result;
|
|
2512
|
-
}
|
|
2513
|
-
function splitAtTopLevel2(text, separator) {
|
|
2514
|
-
const separatorLower = separator.toLowerCase();
|
|
2515
|
-
const textLower = text.toLowerCase();
|
|
2516
|
-
let depth = 0;
|
|
2517
|
-
for (let i = 0; i <= textLower.length - separatorLower.length; i++) {
|
|
2518
|
-
const char = text[i];
|
|
2519
|
-
if (char === "(" || char === "[" || char === "{") depth++;
|
|
2520
|
-
else if (char === ")" || char === "]" || char === "}") depth--;
|
|
2521
|
-
else if (depth === 0 && textLower.slice(i, i + separatorLower.length) === separatorLower) {
|
|
2522
|
-
return {
|
|
2523
|
-
left: text.slice(0, i).trim(),
|
|
2524
|
-
right: text.slice(i + separator.length).trim()
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
return null;
|
|
2529
|
-
}
|
|
2530
|
-
function toCamelCase3(text) {
|
|
2531
|
-
const words = text.split(/\s+/);
|
|
2532
|
-
if (words.length === 1) {
|
|
2533
|
-
return words[0];
|
|
2534
|
-
}
|
|
2535
|
-
return words.map(
|
|
2536
|
-
(word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
2537
|
-
).join("");
|
|
2538
|
-
}
|
|
2539
|
-
function splitArrayItems(content) {
|
|
2540
|
-
const items = [];
|
|
2541
|
-
let current = "";
|
|
2542
|
-
let inQuote = false;
|
|
2543
|
-
let quoteChar = "";
|
|
2544
|
-
for (let i = 0; i < content.length; i++) {
|
|
2545
|
-
const char = content[i];
|
|
2546
|
-
if ((char === "'" || char === '"') && (i === 0 || content[i - 1] !== "\\")) {
|
|
2547
|
-
if (!inQuote) {
|
|
2548
|
-
inQuote = true;
|
|
2549
|
-
quoteChar = char;
|
|
2550
|
-
} else if (char === quoteChar) {
|
|
2551
|
-
inQuote = false;
|
|
2552
|
-
}
|
|
2553
|
-
current += char;
|
|
2554
|
-
} else if (char === "," && !inQuote) {
|
|
2555
|
-
if (current.trim()) {
|
|
2556
|
-
items.push(current.trim());
|
|
2557
|
-
}
|
|
2558
|
-
current = "";
|
|
2559
|
-
} else {
|
|
2560
|
-
current += char;
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
if (current.trim()) {
|
|
2564
|
-
items.push(current.trim());
|
|
2565
|
-
}
|
|
2566
|
-
return items;
|
|
2567
|
-
}
|
|
2568
|
-
function unquote(value) {
|
|
2569
|
-
const trimmed = value.trim();
|
|
2570
|
-
if (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
2571
|
-
return trimmed.slice(1, -1);
|
|
2572
|
-
}
|
|
2573
|
-
return trimmed;
|
|
2574
|
-
}
|
|
2575
|
-
function parseArrayItem(item) {
|
|
2576
|
-
const trimmed = item.trim();
|
|
2577
|
-
const tabMatch = trimmed.match(/^(\w+):\s*['"]([^'"]+)['"]\s*->\s*(\w+)$/);
|
|
2578
|
-
if (tabMatch) {
|
|
2579
|
-
return { id: tabMatch[1], label: tabMatch[2], event: tabMatch[3] };
|
|
2580
|
-
}
|
|
2581
|
-
const actionVariantMatch = trimmed.match(/^([^->]+)\s*->\s*(\w+):(\w+)$/);
|
|
2582
|
-
if (actionVariantMatch) {
|
|
2583
|
-
return {
|
|
2584
|
-
label: actionVariantMatch[1].trim(),
|
|
2585
|
-
event: actionVariantMatch[2],
|
|
2586
|
-
variant: actionVariantMatch[3]
|
|
2587
|
-
};
|
|
2588
|
-
}
|
|
2589
|
-
const actionMatch = trimmed.match(/^(['"]?)([^'"->]+)\1\s*->\s*(\w+)$/);
|
|
2590
|
-
if (actionMatch) {
|
|
2591
|
-
return { label: actionMatch[2].trim(), event: actionMatch[3] };
|
|
2592
|
-
}
|
|
2593
|
-
const cellSpanMatch = trimmed.match(/^(\w+)\s*@\s*(\d+)x(\d+)$/);
|
|
2594
|
-
if (cellSpanMatch) {
|
|
2595
|
-
return {
|
|
2596
|
-
section: cellSpanMatch[1],
|
|
2597
|
-
colSpan: parseInt(cellSpanMatch[2], 10),
|
|
2598
|
-
rowSpan: parseInt(cellSpanMatch[3], 10)
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
|
-
const cellMatch = trimmed.match(/^(\w+)\s*@\s*(\d+)$/);
|
|
2602
|
-
if (cellMatch) {
|
|
2603
|
-
return {
|
|
2604
|
-
section: cellMatch[1],
|
|
2605
|
-
colSpan: parseInt(cellMatch[2], 10)
|
|
2606
|
-
};
|
|
2607
|
-
}
|
|
2608
|
-
const asMatch = trimmed.match(/^(\w+)\s+as\s+['"]([^'"]+)['"]$/);
|
|
2609
|
-
if (asMatch) {
|
|
2610
|
-
return { field: asMatch[1], label: asMatch[2] };
|
|
2611
|
-
}
|
|
2612
|
-
return unquote(trimmed);
|
|
2613
|
-
}
|
|
2614
|
-
function parseArrayProp(content) {
|
|
2615
|
-
const items = splitArrayItems(content);
|
|
2616
|
-
return items.map(parseArrayItem);
|
|
2617
|
-
}
|
|
2618
|
-
function hasComplexSyntax(content) {
|
|
2619
|
-
return /\s+as\s+['"]/.test(content) || // "as" pattern
|
|
2620
|
-
/->\s*\w+/.test(content) || // "->" pattern
|
|
2621
|
-
/\w+\s*@\s*\d+/.test(content);
|
|
2622
|
-
}
|
|
2623
|
-
function parseRenderProps(text) {
|
|
2624
|
-
const props = {};
|
|
2625
|
-
const propsRegex = /(\w+)\s+(?:'([^']+)'|"([^"]+)"|\[([^\]]+)\]|(\S+))/g;
|
|
2626
|
-
let match;
|
|
2627
|
-
while ((match = propsRegex.exec(text)) !== null) {
|
|
2628
|
-
const key = match[1];
|
|
2629
|
-
const singleQuoted = match[2];
|
|
2630
|
-
const doubleQuoted = match[3];
|
|
2631
|
-
const arrayContent = match[4];
|
|
2632
|
-
const bareValue = match[5];
|
|
2633
|
-
if (singleQuoted !== void 0) {
|
|
2634
|
-
props[key] = singleQuoted;
|
|
2635
|
-
} else if (doubleQuoted !== void 0) {
|
|
2636
|
-
props[key] = doubleQuoted;
|
|
2637
|
-
} else if (arrayContent !== void 0) {
|
|
2638
|
-
if (hasComplexSyntax(arrayContent)) {
|
|
2639
|
-
props[key] = parseArrayProp(arrayContent);
|
|
2640
|
-
} else {
|
|
2641
|
-
props[key] = arrayContent.split(/\s*,\s*/).map((s) => s.trim());
|
|
2642
|
-
}
|
|
2643
|
-
} else if (bareValue !== void 0) {
|
|
2644
|
-
if (bareValue === "true") props[key] = true;
|
|
2645
|
-
else if (bareValue === "false") props[key] = false;
|
|
2646
|
-
else if (/^\d+(\.\d+)?$/.test(bareValue)) props[key] = parseFloat(bareValue);
|
|
2647
|
-
else props[key] = bareValue;
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
return props;
|
|
2651
|
-
}
|
|
2652
|
-
function validatePatternType(pattern) {
|
|
2653
|
-
if (!isKnownPattern(pattern)) ;
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
// src/orbitals/domain-language/parsers/behavior-parser.ts
|
|
2657
|
-
function parseBehavior(text, entityName) {
|
|
2658
|
-
const ctx = { entityName, errors: [], warnings: [] };
|
|
2659
|
-
const lexer = new Lexer(text);
|
|
2660
|
-
const tokens = lexer.tokenize();
|
|
2661
|
-
let pos = 0;
|
|
2662
|
-
const current = () => tokens[pos] || { type: "EOF" /* EOF */, value: "", line: 0, column: 0, offset: 0 };
|
|
2663
|
-
const advance = () => tokens[pos++];
|
|
2664
|
-
const isAtEnd = () => current().type === "EOF" /* EOF */;
|
|
2665
|
-
const skip = (type) => {
|
|
2666
|
-
while (current().type === type) advance();
|
|
2667
|
-
};
|
|
2668
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2669
|
-
const behaviorName = parseBehaviorName();
|
|
2670
|
-
if (!behaviorName) {
|
|
2671
|
-
return {
|
|
2672
|
-
success: false,
|
|
2673
|
-
errors: ctx.errors.length > 0 ? ctx.errors : [{
|
|
2674
|
-
message: "Expected behavior name at start of definition"
|
|
2675
|
-
}],
|
|
2676
|
-
warnings: []
|
|
2677
|
-
};
|
|
2678
|
-
}
|
|
2679
|
-
const behavior = {
|
|
2680
|
-
type: "behavior",
|
|
2681
|
-
name: behaviorName,
|
|
2682
|
-
entityName,
|
|
2683
|
-
states: [],
|
|
2684
|
-
initialState: "",
|
|
2685
|
-
transitions: [],
|
|
2686
|
-
ticks: [],
|
|
2687
|
-
rules: []
|
|
2688
|
-
};
|
|
2689
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2690
|
-
while (!isAtEnd()) {
|
|
2691
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2692
|
-
if (isAtEnd()) break;
|
|
2693
|
-
const parsed = parseBehaviorSection(behavior);
|
|
2694
|
-
if (!parsed) {
|
|
2695
|
-
advance();
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
if (!behavior.initialState && behavior.states.length > 0) {
|
|
2699
|
-
behavior.initialState = behavior.states[0];
|
|
2700
|
-
}
|
|
2701
|
-
return {
|
|
2702
|
-
success: true,
|
|
2703
|
-
data: behavior,
|
|
2704
|
-
errors: ctx.errors,
|
|
2705
|
-
warnings: ctx.warnings
|
|
2706
|
-
};
|
|
2707
|
-
function parseBehaviorName() {
|
|
2708
|
-
const nameParts = [];
|
|
2709
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
2710
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */ && current().value.toLowerCase() === "entity") {
|
|
2711
|
-
advance();
|
|
2712
|
-
if (current().type === "COLON" /* COLON */) {
|
|
2713
|
-
advance();
|
|
2714
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
2715
|
-
ctx.entityName = current().value;
|
|
2716
|
-
advance();
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
continue;
|
|
2720
|
-
}
|
|
2721
|
-
if (current().type === "LPAREN" /* LPAREN */ || current().type === "RPAREN" /* RPAREN */) {
|
|
2722
|
-
advance();
|
|
2723
|
-
continue;
|
|
2724
|
-
}
|
|
2725
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */ || current().type === "LIFECYCLE" /* LIFECYCLE */ || current().type === "AUTO" /* AUTO */) {
|
|
2726
|
-
nameParts.push(current().value);
|
|
2727
|
-
}
|
|
2728
|
-
advance();
|
|
2729
|
-
}
|
|
2730
|
-
if (nameParts.length === 0) {
|
|
2731
|
-
return null;
|
|
2732
|
-
}
|
|
2733
|
-
return nameParts.join(" ");
|
|
2734
|
-
}
|
|
2735
|
-
function parseBehaviorSection(behavior2) {
|
|
2736
|
-
const token = current();
|
|
2737
|
-
if (token.type === "IDENTIFIER" /* IDENTIFIER */ && token.value.toLowerCase() === "entity") {
|
|
2738
|
-
advance();
|
|
2739
|
-
skip("COLON" /* COLON */);
|
|
2740
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
2741
|
-
ctx.entityName = current().value;
|
|
2742
|
-
behavior2.entityName = current().value;
|
|
2743
|
-
advance();
|
|
2744
|
-
}
|
|
2745
|
-
return true;
|
|
2746
|
-
}
|
|
2747
|
-
if (token.type === "STATES" /* STATES */) {
|
|
2748
|
-
advance();
|
|
2749
|
-
skip("COLON" /* COLON */);
|
|
2750
|
-
parseStatesLine(behavior2);
|
|
2751
|
-
return true;
|
|
2752
|
-
}
|
|
2753
|
-
if (token.type === "INITIAL" /* INITIAL */) {
|
|
2754
|
-
advance();
|
|
2755
|
-
skip("COLON" /* COLON */);
|
|
2756
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
2757
|
-
behavior2.initialState = current().value;
|
|
2758
|
-
advance();
|
|
2759
|
-
}
|
|
2760
|
-
return true;
|
|
2761
|
-
}
|
|
2762
|
-
if (token.type === "TRANSITIONS" /* TRANSITIONS */) {
|
|
2763
|
-
advance();
|
|
2764
|
-
skip("COLON" /* COLON */);
|
|
2765
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2766
|
-
parseTransitionsSection(behavior2);
|
|
2767
|
-
return true;
|
|
2768
|
-
}
|
|
2769
|
-
if (token.type === "RULES" /* RULES */) {
|
|
2770
|
-
advance();
|
|
2771
|
-
skip("COLON" /* COLON */);
|
|
2772
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2773
|
-
parseRulesSection(behavior2);
|
|
2774
|
-
return true;
|
|
2775
|
-
}
|
|
2776
|
-
if (token.type === "EVERY" /* EVERY */) {
|
|
2777
|
-
advance();
|
|
2778
|
-
parseTick(behavior2);
|
|
2779
|
-
return true;
|
|
2780
|
-
}
|
|
2781
|
-
if (token.type === "LIFECYCLE" /* LIFECYCLE */) {
|
|
2782
|
-
advance();
|
|
2783
|
-
return true;
|
|
2784
|
-
}
|
|
2785
|
-
return false;
|
|
2786
|
-
}
|
|
2787
|
-
function parseStatesLine(behavior2) {
|
|
2788
|
-
const states = [];
|
|
2789
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
2790
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
2791
|
-
states.push(current().value);
|
|
2792
|
-
}
|
|
2793
|
-
advance();
|
|
2794
|
-
}
|
|
2795
|
-
behavior2.states = states;
|
|
2796
|
-
}
|
|
2797
|
-
function parseTransitionsSection(behavior2) {
|
|
2798
|
-
if (current().type === "INDENT" /* INDENT */) {
|
|
2799
|
-
advance();
|
|
2800
|
-
while (!isAtEnd() && current().type !== "DEDENT" /* DEDENT */) {
|
|
2801
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2802
|
-
if (current().type === "DEDENT" /* DEDENT */) break;
|
|
2803
|
-
if (current().type === "DASH" /* DASH */ || current().type === "FROM" /* FROM */) {
|
|
2804
|
-
const transition = parseTransitionLine();
|
|
2805
|
-
if (transition) {
|
|
2806
|
-
behavior2.transitions.push(transition);
|
|
2807
|
-
}
|
|
2808
|
-
} else {
|
|
2809
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */ && current().type !== "DEDENT" /* DEDENT */) {
|
|
2810
|
-
advance();
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
if (current().type === "DEDENT" /* DEDENT */) {
|
|
2815
|
-
advance();
|
|
2816
|
-
}
|
|
2817
|
-
} else {
|
|
2818
|
-
while (!isAtEnd() && current().type !== "RULES" /* RULES */ && current().type !== "EVERY" /* EVERY */) {
|
|
2819
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2820
|
-
if (current().type === "RULES" /* RULES */ || current().type === "EVERY" /* EVERY */ || isAtEnd()) break;
|
|
2821
|
-
if (current().type === "DASH" /* DASH */ || current().type === "FROM" /* FROM */) {
|
|
2822
|
-
const transition = parseTransitionLine();
|
|
2823
|
-
if (transition) {
|
|
2824
|
-
behavior2.transitions.push(transition);
|
|
2825
|
-
}
|
|
2826
|
-
} else {
|
|
2827
|
-
advance();
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
function parseTransitionLine() {
|
|
2833
|
-
if (current().type === "DASH" /* DASH */) {
|
|
2834
|
-
advance();
|
|
2835
|
-
}
|
|
2836
|
-
if (current().type !== "FROM" /* FROM */) {
|
|
2837
|
-
return null;
|
|
2838
|
-
}
|
|
2839
|
-
advance();
|
|
2840
|
-
let fromState = "";
|
|
2841
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
2842
|
-
fromState = current().value;
|
|
2843
|
-
advance();
|
|
2844
|
-
}
|
|
2845
|
-
if (fromState.toLowerCase() === "any") {
|
|
2846
|
-
fromState = "*";
|
|
2847
|
-
}
|
|
2848
|
-
if (current().type !== "TO" /* TO */) {
|
|
2849
|
-
ctx.errors.push({ message: `Expected "to" in transition from "${fromState}"` });
|
|
2850
|
-
return null;
|
|
2851
|
-
}
|
|
2852
|
-
advance();
|
|
2853
|
-
let toState = "";
|
|
2854
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
2855
|
-
toState = current().value;
|
|
2856
|
-
advance();
|
|
2857
|
-
}
|
|
2858
|
-
let event = "";
|
|
2859
|
-
if (current().type === "WHEN" /* WHEN */) {
|
|
2860
|
-
advance();
|
|
2861
|
-
if (current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
2862
|
-
event = current().value;
|
|
2863
|
-
advance();
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
const transition = {
|
|
2867
|
-
type: "transition",
|
|
2868
|
-
fromState,
|
|
2869
|
-
toState,
|
|
2870
|
-
event: event.toUpperCase(),
|
|
2871
|
-
guards: [],
|
|
2872
|
-
effects: []
|
|
2873
|
-
};
|
|
2874
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2875
|
-
if (current().type === "INDENT" /* INDENT */) {
|
|
2876
|
-
advance();
|
|
2877
|
-
while (!isAtEnd() && current().type !== "DEDENT" /* DEDENT */) {
|
|
2878
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2879
|
-
if (current().type === "DEDENT" /* DEDENT */) break;
|
|
2880
|
-
if (current().type === "IF" /* IF */) {
|
|
2881
|
-
const guardText = collectLine();
|
|
2882
|
-
const guardResult = parseGuard(guardText, entityName);
|
|
2883
|
-
if (guardResult.success && guardResult.data) {
|
|
2884
|
-
transition.guards.push(guardResult.data);
|
|
2885
|
-
}
|
|
2886
|
-
continue;
|
|
2887
|
-
}
|
|
2888
|
-
if (current().type === "THEN" /* THEN */) {
|
|
2889
|
-
advance();
|
|
2890
|
-
const effectText = collectUntilNewline();
|
|
2891
|
-
const effect = parseEffectText(effectText);
|
|
2892
|
-
if (effect) {
|
|
2893
|
-
transition.effects.push(effect);
|
|
2894
|
-
}
|
|
2895
|
-
continue;
|
|
2896
|
-
}
|
|
2897
|
-
if (current().type !== "NEWLINE" /* NEWLINE */ && current().type !== "DEDENT" /* DEDENT */) {
|
|
2898
|
-
advance();
|
|
2899
|
-
}
|
|
2900
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2901
|
-
}
|
|
2902
|
-
if (current().type === "DEDENT" /* DEDENT */) {
|
|
2903
|
-
advance();
|
|
2904
|
-
}
|
|
2905
|
-
} else {
|
|
2906
|
-
if (current().type === "IF" /* IF */) {
|
|
2907
|
-
const guardText = collectLine();
|
|
2908
|
-
const guardResult = parseGuard(guardText, entityName);
|
|
2909
|
-
if (guardResult.success && guardResult.data) {
|
|
2910
|
-
transition.guards.push(guardResult.data);
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
return transition;
|
|
2915
|
-
}
|
|
2916
|
-
function parseRulesSection(behavior2) {
|
|
2917
|
-
if (current().type === "INDENT" /* INDENT */) {
|
|
2918
|
-
advance();
|
|
2919
|
-
while (!isAtEnd() && current().type !== "DEDENT" /* DEDENT */) {
|
|
2920
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2921
|
-
if (current().type === "DEDENT" /* DEDENT */) break;
|
|
2922
|
-
if (current().type === "DASH" /* DASH */) {
|
|
2923
|
-
advance();
|
|
2924
|
-
}
|
|
2925
|
-
const rule = collectUntilNewline();
|
|
2926
|
-
if (rule) {
|
|
2927
|
-
behavior2.rules.push(rule);
|
|
2928
|
-
}
|
|
2929
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2930
|
-
}
|
|
2931
|
-
if (current().type === "DEDENT" /* DEDENT */) {
|
|
2932
|
-
advance();
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
function parseTick(behavior2) {
|
|
2937
|
-
const intervalParts = [];
|
|
2938
|
-
while (!isAtEnd() && current().type !== "COLON" /* COLON */ && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
2939
|
-
intervalParts.push(current().value);
|
|
2940
|
-
advance();
|
|
2941
|
-
}
|
|
2942
|
-
const interval = intervalParts.join(" ");
|
|
2943
|
-
skip("COLON" /* COLON */);
|
|
2944
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2945
|
-
const tick = {
|
|
2946
|
-
type: "tick",
|
|
2947
|
-
name: `tick_${interval.replace(/\s+/g, "_").toLowerCase()}`,
|
|
2948
|
-
interval,
|
|
2949
|
-
intervalMs: parseInterval(interval),
|
|
2950
|
-
effects: []
|
|
2951
|
-
};
|
|
2952
|
-
if (current().type === "INDENT" /* INDENT */) {
|
|
2953
|
-
advance();
|
|
2954
|
-
while (!isAtEnd() && current().type !== "DEDENT" /* DEDENT */) {
|
|
2955
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2956
|
-
if (current().type === "DEDENT" /* DEDENT */) break;
|
|
2957
|
-
if (current().type === "IF" /* IF */) {
|
|
2958
|
-
const guardText = collectLine();
|
|
2959
|
-
const guardResult = parseGuard(guardText, entityName);
|
|
2960
|
-
if (guardResult.success && guardResult.data) {
|
|
2961
|
-
tick.guard = guardResult.data;
|
|
2962
|
-
}
|
|
2963
|
-
}
|
|
2964
|
-
if (current().type === "THEN" /* THEN */) {
|
|
2965
|
-
advance();
|
|
2966
|
-
}
|
|
2967
|
-
if (current().type === "DASH" /* DASH */) {
|
|
2968
|
-
advance();
|
|
2969
|
-
}
|
|
2970
|
-
const effectText = collectUntilNewline();
|
|
2971
|
-
if (effectText && current().type !== "IF" /* IF */) {
|
|
2972
|
-
const effect = parseEffectText(effectText);
|
|
2973
|
-
if (effect) {
|
|
2974
|
-
tick.effects.push(effect);
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
skip("NEWLINE" /* NEWLINE */);
|
|
2978
|
-
}
|
|
2979
|
-
if (current().type === "DEDENT" /* DEDENT */) {
|
|
2980
|
-
advance();
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
behavior2.ticks.push(tick);
|
|
2984
|
-
}
|
|
2985
|
-
function collectLine() {
|
|
2986
|
-
return collectTokensSmartJoin();
|
|
2987
|
-
}
|
|
2988
|
-
function collectUntilNewline() {
|
|
2989
|
-
return collectTokensSmartJoin();
|
|
2990
|
-
}
|
|
2991
|
-
function collectTokensSmartJoin() {
|
|
2992
|
-
const parts = [];
|
|
2993
|
-
while (!isAtEnd() && current().type !== "NEWLINE" /* NEWLINE */) {
|
|
2994
|
-
parts.push({ value: current().value, type: current().type });
|
|
2995
|
-
advance();
|
|
2996
|
-
}
|
|
2997
|
-
const startsWithBracket = parts.length > 0 && parts[0].type === "LBRACKET" /* LBRACKET */;
|
|
2998
|
-
let result = "";
|
|
2999
|
-
for (let i = 0; i < parts.length; i++) {
|
|
3000
|
-
const part = parts[i];
|
|
3001
|
-
const prev = parts[i - 1];
|
|
3002
|
-
const noSpaceBefore = part.type === "DASH" /* DASH */ || part.type === "DOT" /* DOT */ || part.type === "COMMA" /* COMMA */ || part.type === "COLON" /* COLON */ || part.type === "RBRACKET" /* RBRACKET */ || part.type === "RPAREN" /* RPAREN */;
|
|
3003
|
-
const prevNoSpaceAfter = prev && (prev.type === "DASH" /* DASH */ || prev.type === "DOT" /* DOT */ || prev.type === "LBRACKET" /* LBRACKET */ || prev.type === "LPAREN" /* LPAREN */);
|
|
3004
|
-
if (i > 0 && !noSpaceBefore && !prevNoSpaceAfter) {
|
|
3005
|
-
result += " ";
|
|
3006
|
-
}
|
|
3007
|
-
if (startsWithBracket && part.type === "STRING" /* STRING */) {
|
|
3008
|
-
result += `"${part.value}"`;
|
|
3009
|
-
} else {
|
|
3010
|
-
result += part.value;
|
|
3011
|
-
}
|
|
3012
|
-
}
|
|
3013
|
-
return result.trim();
|
|
3014
|
-
}
|
|
3015
|
-
function parseEffectText(text2) {
|
|
3016
|
-
const trimmed = text2.trim();
|
|
3017
|
-
const lower = trimmed.toLowerCase();
|
|
3018
|
-
if (trimmed.startsWith("[")) {
|
|
3019
|
-
try {
|
|
3020
|
-
const parsed = JSON.parse(trimmed);
|
|
3021
|
-
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
3022
|
-
const effectOp = String(parsed[0]);
|
|
3023
|
-
return {
|
|
3024
|
-
type: "effect",
|
|
3025
|
-
effectType: effectOp,
|
|
3026
|
-
description: trimmed,
|
|
3027
|
-
config: { _rawSExpr: parsed }
|
|
3028
|
-
// Store parsed S-expression
|
|
3029
|
-
};
|
|
3030
|
-
}
|
|
3031
|
-
} catch {
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
const persistMatch = text2.match(/^persist\s+(\w+)(?:\s+(\w+))?$/i);
|
|
3035
|
-
if (persistMatch) {
|
|
3036
|
-
return {
|
|
3037
|
-
type: "effect",
|
|
3038
|
-
effectType: "persist",
|
|
3039
|
-
description: text2,
|
|
3040
|
-
config: {
|
|
3041
|
-
dataAction: persistMatch[1].toLowerCase(),
|
|
3042
|
-
entity: persistMatch[2] || void 0
|
|
3043
|
-
}
|
|
3044
|
-
};
|
|
3045
|
-
}
|
|
3046
|
-
const sendInAppMatch = text2.match(/^send_in_app\s+to\s+(.+?)\s+title\s+"([^"]+)"\s+message\s+"([^"]+)"(?:\s+type\s+(\w+))?$/i);
|
|
3047
|
-
if (sendInAppMatch) {
|
|
3048
|
-
return {
|
|
3049
|
-
type: "effect",
|
|
3050
|
-
effectType: "notify",
|
|
3051
|
-
description: text2,
|
|
3052
|
-
config: {
|
|
3053
|
-
userId: sendInAppMatch[1],
|
|
3054
|
-
title: sendInAppMatch[2],
|
|
3055
|
-
message: sendInAppMatch[3],
|
|
3056
|
-
type: sendInAppMatch[4] || "info"
|
|
3057
|
-
}
|
|
3058
|
-
};
|
|
3059
|
-
}
|
|
3060
|
-
const jsonMatch = text2.match(/^(\w+):(\{.+\})$/);
|
|
3061
|
-
if (jsonMatch) {
|
|
3062
|
-
try {
|
|
3063
|
-
const config = JSON.parse(jsonMatch[2]);
|
|
3064
|
-
return {
|
|
3065
|
-
type: "effect",
|
|
3066
|
-
effectType: jsonMatch[1],
|
|
3067
|
-
description: text2,
|
|
3068
|
-
config
|
|
3069
|
-
};
|
|
3070
|
-
} catch {
|
|
3071
|
-
}
|
|
3072
|
-
}
|
|
3073
|
-
const notifyWithRecipientMatch = text2.match(/^notify\s+(\S+)\s+"([^"]+)"$/i);
|
|
3074
|
-
if (notifyWithRecipientMatch) {
|
|
3075
|
-
return {
|
|
3076
|
-
type: "effect",
|
|
3077
|
-
effectType: "notify",
|
|
3078
|
-
description: text2,
|
|
3079
|
-
config: {
|
|
3080
|
-
recipient: notifyWithRecipientMatch[1],
|
|
3081
|
-
message: notifyWithRecipientMatch[2]
|
|
3082
|
-
}
|
|
3083
|
-
};
|
|
3084
|
-
}
|
|
3085
|
-
if (lower.startsWith("notify")) {
|
|
3086
|
-
const remaining = text2.slice(6).trim();
|
|
3087
|
-
const quotedMatch = remaining.match(/^"([^"]+)"$/);
|
|
3088
|
-
return {
|
|
3089
|
-
type: "effect",
|
|
3090
|
-
effectType: "notify",
|
|
3091
|
-
description: text2,
|
|
3092
|
-
config: {
|
|
3093
|
-
message: quotedMatch ? quotedMatch[1] : remaining
|
|
3094
|
-
}
|
|
3095
|
-
};
|
|
3096
|
-
}
|
|
3097
|
-
const updateMatch = text2.match(/^update\s+(.+?)\s+to\s+(.+)$/i);
|
|
3098
|
-
if (updateMatch) {
|
|
3099
|
-
return {
|
|
3100
|
-
type: "effect",
|
|
3101
|
-
effectType: "set",
|
|
3102
|
-
description: text2,
|
|
3103
|
-
config: {
|
|
3104
|
-
field: preserveTemplateVars(updateMatch[1]),
|
|
3105
|
-
value: updateMatch[2]
|
|
3106
|
-
}
|
|
3107
|
-
};
|
|
3108
|
-
}
|
|
3109
|
-
const navMatch = text2.match(/^navigate\s+to\s+(.+)$/i);
|
|
3110
|
-
if (navMatch) {
|
|
3111
|
-
return {
|
|
3112
|
-
type: "effect",
|
|
3113
|
-
effectType: "navigate",
|
|
3114
|
-
description: text2,
|
|
3115
|
-
config: {
|
|
3116
|
-
path: navMatch[1]
|
|
3117
|
-
}
|
|
3118
|
-
};
|
|
3119
|
-
}
|
|
3120
|
-
const emitMatch = text2.match(/^emit\s+(.+)$/i);
|
|
3121
|
-
if (emitMatch) {
|
|
3122
|
-
return {
|
|
3123
|
-
type: "effect",
|
|
3124
|
-
effectType: "emit",
|
|
3125
|
-
description: text2,
|
|
3126
|
-
config: {
|
|
3127
|
-
eventKey: emitMatch[1].toUpperCase().replace(/\s+/g, "_")
|
|
3128
|
-
}
|
|
3129
|
-
};
|
|
3130
|
-
}
|
|
3131
|
-
const callMatch = text2.match(/^call\s+(.+)$/i);
|
|
3132
|
-
if (callMatch) {
|
|
3133
|
-
return {
|
|
3134
|
-
type: "effect",
|
|
3135
|
-
effectType: "call-service",
|
|
3136
|
-
description: text2,
|
|
3137
|
-
config: {
|
|
3138
|
-
service: callMatch[1]
|
|
3139
|
-
}
|
|
3140
|
-
};
|
|
3141
|
-
}
|
|
3142
|
-
if (lower.startsWith("render ")) {
|
|
3143
|
-
try {
|
|
3144
|
-
const result = parseDomainEffect(text2);
|
|
3145
|
-
if (Array.isArray(result) && result[0] === "render-ui") {
|
|
3146
|
-
return {
|
|
3147
|
-
type: "effect",
|
|
3148
|
-
effectType: "render-ui",
|
|
3149
|
-
description: text2,
|
|
3150
|
-
config: {
|
|
3151
|
-
slot: result[1],
|
|
3152
|
-
pattern: result[2]
|
|
3153
|
-
}
|
|
3154
|
-
};
|
|
3155
|
-
}
|
|
3156
|
-
} catch {
|
|
3157
|
-
}
|
|
3158
|
-
}
|
|
3159
|
-
ctx.warnings.push({
|
|
3160
|
-
message: `Unknown effect syntax: "${text2}". Effect will be ignored. Use a valid effect format like "update X to Y", "emit EVENT", "navigate to /path", "notify 'message'", or "render pattern to slot".`
|
|
3161
|
-
});
|
|
3162
|
-
return null;
|
|
3163
|
-
}
|
|
3164
|
-
function parseInterval(text2) {
|
|
3165
|
-
const lower = text2.toLowerCase();
|
|
3166
|
-
const match = lower.match(/(\d+)\s*(second|minute|hour|day|week)s?/);
|
|
3167
|
-
if (match) {
|
|
3168
|
-
const value = parseInt(match[1], 10);
|
|
3169
|
-
const unit = match[2];
|
|
3170
|
-
switch (unit) {
|
|
3171
|
-
case "second":
|
|
3172
|
-
return value * 1e3;
|
|
3173
|
-
case "minute":
|
|
3174
|
-
return value * 60 * 1e3;
|
|
3175
|
-
case "hour":
|
|
3176
|
-
return value * 60 * 60 * 1e3;
|
|
3177
|
-
case "day":
|
|
3178
|
-
return value * 24 * 60 * 60 * 1e3;
|
|
3179
|
-
case "week":
|
|
3180
|
-
return value * 7 * 24 * 60 * 60 * 1e3;
|
|
3181
|
-
}
|
|
3182
|
-
}
|
|
3183
|
-
return 60 * 60 * 1e3;
|
|
3184
|
-
}
|
|
3185
|
-
}
|
|
3186
|
-
function formatBehaviorToSchema(behavior) {
|
|
3187
|
-
const trait = {
|
|
3188
|
-
// Just remove spaces, preserve the casing from the source
|
|
3189
|
-
// Trust the LLM/author to use consistent PascalCase naming
|
|
3190
|
-
name: behavior.name.replace(/\s+/g, ""),
|
|
3191
|
-
description: behavior.name
|
|
3192
|
-
};
|
|
3193
|
-
if (behavior.states.length > 0) {
|
|
3194
|
-
trait.stateMachine = {
|
|
3195
|
-
states: behavior.states.map((state, index) => ({
|
|
3196
|
-
name: state,
|
|
3197
|
-
isInitial: state === behavior.initialState || index === 0 && !behavior.initialState
|
|
3198
|
-
})),
|
|
3199
|
-
events: extractEventsFromTransitions(behavior.transitions),
|
|
3200
|
-
transitions: behavior.transitions.map((t) => {
|
|
3201
|
-
const transition = {
|
|
3202
|
-
from: t.fromState,
|
|
3203
|
-
to: t.toState,
|
|
3204
|
-
event: t.event
|
|
3205
|
-
};
|
|
3206
|
-
if (t.guards.length > 0) {
|
|
3207
|
-
const guardExprs = t.guards.map((g) => {
|
|
3208
|
-
if (g.raw) {
|
|
3209
|
-
return parseDomainGuard(g.raw, behavior.entityName);
|
|
3210
|
-
}
|
|
3211
|
-
return parseDomainGuard(formatGuardToCondition(g), behavior.entityName);
|
|
3212
|
-
});
|
|
3213
|
-
transition.guard = guardExprs.length === 1 ? guardExprs[0] : ["and", ...guardExprs];
|
|
3214
|
-
}
|
|
3215
|
-
if (t.effects.length > 0) {
|
|
3216
|
-
transition.effects = t.effects.map((e) => {
|
|
3217
|
-
if (e.config && "_rawSExpr" in e.config && Array.isArray(e.config._rawSExpr)) {
|
|
3218
|
-
return e.config._rawSExpr;
|
|
3219
|
-
}
|
|
3220
|
-
return parseDomainEffect(e.description, behavior.entityName);
|
|
3221
|
-
});
|
|
3222
|
-
}
|
|
3223
|
-
return transition;
|
|
3224
|
-
})
|
|
3225
|
-
};
|
|
3226
|
-
}
|
|
3227
|
-
if (behavior.ticks.length > 0) {
|
|
3228
|
-
trait.ticks = behavior.ticks.map((t) => ({
|
|
3229
|
-
name: toPascalCase2(t.name.replace(/\s+/g, "")),
|
|
3230
|
-
interval: t.intervalMs,
|
|
3231
|
-
// Direct number in ms
|
|
3232
|
-
guard: t.guard ? parseDomainGuard(t.guard.raw || formatGuardToCondition(t.guard), behavior.entityName) : void 0,
|
|
3233
|
-
effects: t.effects.map((e) => {
|
|
3234
|
-
if (e.config && "_rawSExpr" in e.config && Array.isArray(e.config._rawSExpr)) {
|
|
3235
|
-
return e.config._rawSExpr;
|
|
3236
|
-
}
|
|
3237
|
-
return parseDomainEffect(e.description, behavior.entityName);
|
|
3238
|
-
})
|
|
3239
|
-
}));
|
|
3240
|
-
}
|
|
3241
|
-
return trait;
|
|
3242
|
-
}
|
|
3243
|
-
function toCamelCase4(text) {
|
|
3244
|
-
return text.toLowerCase().split(/\s+/).map(
|
|
3245
|
-
(word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)
|
|
3246
|
-
).join("");
|
|
3247
|
-
}
|
|
3248
|
-
function toPascalCase2(text) {
|
|
3249
|
-
if (!text.includes(" ")) {
|
|
3250
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
3251
|
-
}
|
|
3252
|
-
return text.split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
3253
|
-
}
|
|
3254
|
-
function extractEventsFromTransitions(transitions) {
|
|
3255
|
-
const events = /* @__PURE__ */ new Map();
|
|
3256
|
-
for (const t of transitions) {
|
|
3257
|
-
if (t.event && !events.has(t.event)) {
|
|
3258
|
-
events.set(t.event, toTitleCase(t.event.replace(/_/g, " ")));
|
|
3259
|
-
}
|
|
3260
|
-
}
|
|
3261
|
-
return Array.from(events.entries()).map(([key, name]) => ({ key, name }));
|
|
3262
|
-
}
|
|
3263
|
-
function toTitleCase(text) {
|
|
3264
|
-
return text.toLowerCase().split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
3265
|
-
}
|
|
3266
|
-
function formatGuardToCondition(guard) {
|
|
3267
|
-
if (guard.raw) {
|
|
3268
|
-
return guard.raw.replace(/^if\s+/i, "");
|
|
3269
|
-
}
|
|
3270
|
-
return "true";
|
|
3271
|
-
}
|
|
3272
|
-
function preserveTemplateVars(text) {
|
|
3273
|
-
if (text.includes("{") && text.includes("}")) {
|
|
3274
|
-
return text.replace(/\s*\.\s*/g, ".");
|
|
3275
|
-
}
|
|
3276
|
-
const cleanedText = text.replace(/\s*\.\s*/g, ".");
|
|
3277
|
-
if (cleanedText.includes(".")) {
|
|
3278
|
-
return cleanedText;
|
|
3279
|
-
}
|
|
3280
|
-
return toCamelCase4(text);
|
|
3281
|
-
}
|
|
3282
|
-
function getPageName(page) {
|
|
3283
|
-
if (isPageReferenceString(page)) {
|
|
3284
|
-
const parts = page.split(".");
|
|
3285
|
-
return parts[parts.length - 1];
|
|
3286
|
-
}
|
|
3287
|
-
if (isPageReferenceObject(page)) {
|
|
3288
|
-
const parts = page.ref.split(".");
|
|
3289
|
-
return parts[parts.length - 1];
|
|
3290
|
-
}
|
|
3291
|
-
return page.name;
|
|
3292
|
-
}
|
|
3293
|
-
function convertDomainToSchema(domainText, baseSchema) {
|
|
3294
|
-
const errors = [];
|
|
3295
|
-
const warnings = [];
|
|
3296
|
-
const mappings = [];
|
|
3297
|
-
const schema = baseSchema ? {
|
|
3298
|
-
...baseSchema,
|
|
3299
|
-
orbitals: [...baseSchema.orbitals || []]
|
|
3300
|
-
} : {
|
|
3301
|
-
name: "Application",
|
|
3302
|
-
orbitals: []
|
|
3303
|
-
};
|
|
3304
|
-
const sections = splitDomainDocument(domainText);
|
|
3305
|
-
const parsedEntities = [];
|
|
3306
|
-
const parsedPages = [];
|
|
3307
|
-
const parsedTraits = [];
|
|
3308
|
-
for (const text of sections.entities) {
|
|
3309
|
-
const result = parseEntity(text);
|
|
3310
|
-
if (result.success && result.data) {
|
|
3311
|
-
const entityRecord = formatEntityToSchema(result.data);
|
|
3312
|
-
const entity = {
|
|
3313
|
-
name: entityRecord.name,
|
|
3314
|
-
fields: entityRecord.fields || [],
|
|
3315
|
-
persistence: "persistent"
|
|
3316
|
-
};
|
|
3317
|
-
parsedEntities.push({
|
|
3318
|
-
name: result.data.name,
|
|
3319
|
-
entity,
|
|
3320
|
-
text
|
|
3321
|
-
});
|
|
3322
|
-
} else {
|
|
3323
|
-
errors.push(...result.errors);
|
|
3324
|
-
}
|
|
3325
|
-
warnings.push(...result.warnings);
|
|
3326
|
-
}
|
|
3327
|
-
for (const text of sections.pages) {
|
|
3328
|
-
const result = parsePage(text);
|
|
3329
|
-
if (result.success && result.data) {
|
|
3330
|
-
const pageRecord = formatPageToSchema(result.data);
|
|
3331
|
-
const page = {
|
|
3332
|
-
name: pageRecord.name,
|
|
3333
|
-
path: pageRecord.path || "/",
|
|
3334
|
-
primaryEntity: pageRecord.primaryEntity
|
|
3335
|
-
};
|
|
3336
|
-
if (result.data.traitName) {
|
|
3337
|
-
page.traits = [{
|
|
3338
|
-
ref: result.data.traitName,
|
|
3339
|
-
linkedEntity: result.data.primaryEntity
|
|
3340
|
-
}];
|
|
3341
|
-
}
|
|
3342
|
-
const forEntity = result.data.primaryEntity || page.primaryEntity;
|
|
3343
|
-
parsedPages.push({
|
|
3344
|
-
name: result.data.name,
|
|
3345
|
-
page,
|
|
3346
|
-
forEntity,
|
|
3347
|
-
text
|
|
3348
|
-
});
|
|
3349
|
-
} else {
|
|
3350
|
-
errors.push(...result.errors);
|
|
3351
|
-
}
|
|
3352
|
-
warnings.push(...result.warnings);
|
|
3353
|
-
}
|
|
3354
|
-
for (const text of sections.behaviors) {
|
|
3355
|
-
if (text.trim().toLowerCase().startsWith("every")) {
|
|
3356
|
-
parseTickFromDomain(text);
|
|
3357
|
-
continue;
|
|
3358
|
-
}
|
|
3359
|
-
const result = parseBehavior(text, "");
|
|
3360
|
-
if (result.success && result.data) {
|
|
3361
|
-
const traitRecord = formatBehaviorToSchema(result.data);
|
|
3362
|
-
const trait = {
|
|
3363
|
-
name: traitRecord.name,
|
|
3364
|
-
stateMachine: traitRecord.stateMachine
|
|
3365
|
-
};
|
|
3366
|
-
const forEntity = result.data.entityName || void 0;
|
|
3367
|
-
parsedTraits.push({
|
|
3368
|
-
name: result.data.name,
|
|
3369
|
-
trait,
|
|
3370
|
-
forEntity,
|
|
3371
|
-
text
|
|
3372
|
-
});
|
|
3373
|
-
} else {
|
|
3374
|
-
errors.push(...result.errors);
|
|
3375
|
-
}
|
|
3376
|
-
warnings.push(...result.warnings);
|
|
3377
|
-
}
|
|
3378
|
-
console.log(`[DomainToSchema] Parsed: ${parsedEntities.length} entities, ${parsedPages.length} pages, ${parsedTraits.length} traits`);
|
|
3379
|
-
if (parsedEntities.length > 0) {
|
|
3380
|
-
console.log(`[DomainToSchema] Entities: ${parsedEntities.map((e) => e.name).join(", ")}`);
|
|
3381
|
-
}
|
|
3382
|
-
if (parsedTraits.length > 0) {
|
|
3383
|
-
console.log(`[DomainToSchema] Traits: ${parsedTraits.map((t) => t.name).join(", ")}`);
|
|
3384
|
-
}
|
|
3385
|
-
const traitToEntityMap = /* @__PURE__ */ new Map();
|
|
3386
|
-
for (const { page } of parsedPages) {
|
|
3387
|
-
if (page.traits && page.primaryEntity) {
|
|
3388
|
-
for (const traitRef of page.traits) {
|
|
3389
|
-
if (traitRef.ref) {
|
|
3390
|
-
const linkedEntity = traitRef.linkedEntity || page.primaryEntity;
|
|
3391
|
-
if (linkedEntity) {
|
|
3392
|
-
traitToEntityMap.set(traitRef.ref.toLowerCase(), linkedEntity);
|
|
3393
|
-
}
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
}
|
|
3397
|
-
}
|
|
3398
|
-
if (traitToEntityMap.size > 0) {
|
|
3399
|
-
console.log(`[DomainToSchema] Page trait references: ${Array.from(traitToEntityMap.entries()).map(([t, e]) => `${t}\u2192${e}`).join(", ")}`);
|
|
3400
|
-
} else {
|
|
3401
|
-
console.log(`[DomainToSchema] No page trait references found (pages may be missing primaryEntity or traits)`);
|
|
3402
|
-
}
|
|
3403
|
-
const orbitals = [];
|
|
3404
|
-
for (let i = 0; i < parsedEntities.length; i++) {
|
|
3405
|
-
const { name, entity, text } = parsedEntities[i];
|
|
3406
|
-
const entityPages = parsedPages.filter((p) => p.forEntity === name || p.page.primaryEntity === name).map((p) => p.page);
|
|
3407
|
-
const entityTraits = parsedTraits.filter((t) => {
|
|
3408
|
-
const traitNameLower = t.name.toLowerCase();
|
|
3409
|
-
if (t.forEntity === name) {
|
|
3410
|
-
console.log(`[DomainToSchema] \u2713 Trait "${t.name}" \u2192 Entity "${name}" (explicit Entity: syntax)`);
|
|
3411
|
-
return true;
|
|
3412
|
-
}
|
|
3413
|
-
const linkedEntity = traitToEntityMap.get(traitNameLower);
|
|
3414
|
-
if (linkedEntity === name) {
|
|
3415
|
-
console.log(`[DomainToSchema] \u2713 Trait "${t.name}" \u2192 Entity "${name}" (page trait reference)`);
|
|
3416
|
-
return true;
|
|
3417
|
-
}
|
|
3418
|
-
return false;
|
|
3419
|
-
}).map((t) => t.trait.name);
|
|
3420
|
-
const orbital = {
|
|
3421
|
-
name,
|
|
3422
|
-
entity,
|
|
3423
|
-
traits: entityTraits,
|
|
3424
|
-
pages: entityPages
|
|
3425
|
-
};
|
|
3426
|
-
orbitals.push(orbital);
|
|
3427
|
-
mappings.push({
|
|
3428
|
-
sectionId: `entity_${name}`,
|
|
3429
|
-
sectionType: "entity",
|
|
3430
|
-
schemaPath: `orbitals[${i}].entity`,
|
|
3431
|
-
domainText: text
|
|
3432
|
-
});
|
|
3433
|
-
}
|
|
3434
|
-
for (const { name, forEntity, text } of parsedPages) {
|
|
3435
|
-
const orbitalIndex = parsedEntities.findIndex((e) => e.name === forEntity);
|
|
3436
|
-
const pageIndex = orbitalIndex >= 0 ? orbitals[orbitalIndex].pages.findIndex((p) => getPageName(p) === name) : -1;
|
|
3437
|
-
mappings.push({
|
|
3438
|
-
sectionId: `page_${name}`,
|
|
3439
|
-
sectionType: "page",
|
|
3440
|
-
schemaPath: orbitalIndex >= 0 && pageIndex >= 0 ? `orbitals[${orbitalIndex}].pages[${pageIndex}]` : `orphanedPages`,
|
|
3441
|
-
domainText: text
|
|
3442
|
-
});
|
|
3443
|
-
}
|
|
3444
|
-
for (const { name, forEntity, text } of parsedTraits) {
|
|
3445
|
-
const orbitalIndex = parsedEntities.findIndex((e) => e.name === forEntity);
|
|
3446
|
-
const traitIndex = orbitalIndex >= 0 ? orbitals[orbitalIndex].traits.findIndex((t) => getTraitName(t) === name) : -1;
|
|
3447
|
-
mappings.push({
|
|
3448
|
-
sectionId: `behavior_${name}`,
|
|
3449
|
-
sectionType: "behavior",
|
|
3450
|
-
schemaPath: orbitalIndex >= 0 && traitIndex >= 0 ? `orbitals[${orbitalIndex}].traits[${traitIndex}]` : `traits`,
|
|
3451
|
-
domainText: text
|
|
3452
|
-
});
|
|
3453
|
-
}
|
|
3454
|
-
const assignedTraitNames = /* @__PURE__ */ new Set();
|
|
3455
|
-
for (const orbital of orbitals) {
|
|
3456
|
-
for (const traitRef of orbital.traits) {
|
|
3457
|
-
const traitName = getTraitName(traitRef);
|
|
3458
|
-
assignedTraitNames.add(traitName.toLowerCase());
|
|
3459
|
-
}
|
|
3460
|
-
}
|
|
3461
|
-
console.log(`[DomainToSchema] ${parsedTraits.length} traits parsed, ${assignedTraitNames.size} assigned to orbitals`);
|
|
3462
|
-
for (const orbital of orbitals) {
|
|
3463
|
-
if (orbital.traits.length > 0) {
|
|
3464
|
-
console.log(`[DomainToSchema] Orbital ${orbital.name}: ${orbital.traits.length} traits - ${orbital.traits.join(", ")}`);
|
|
3465
|
-
}
|
|
3466
|
-
}
|
|
3467
|
-
schema.orbitals = orbitals;
|
|
3468
|
-
return {
|
|
3469
|
-
success: errors.length === 0,
|
|
3470
|
-
schema,
|
|
3471
|
-
errors,
|
|
3472
|
-
warnings,
|
|
3473
|
-
mappings
|
|
3474
|
-
};
|
|
3475
|
-
}
|
|
3476
|
-
function splitDomainDocument(text) {
|
|
3477
|
-
const result = {
|
|
3478
|
-
entities: [],
|
|
3479
|
-
pages: [],
|
|
3480
|
-
behaviors: []
|
|
3481
|
-
};
|
|
3482
|
-
const normalizedText = text.replace(/\r\n/g, "\n");
|
|
3483
|
-
let currentSection = null;
|
|
3484
|
-
let currentContent = [];
|
|
3485
|
-
const lines = normalizedText.split("\n");
|
|
3486
|
-
for (const line of lines) {
|
|
3487
|
-
const trimmed = line.trim().toLowerCase();
|
|
3488
|
-
if (trimmed === "# entities" || trimmed === "#entities") {
|
|
3489
|
-
flushSection();
|
|
3490
|
-
currentSection = "entities";
|
|
3491
|
-
continue;
|
|
3492
|
-
}
|
|
3493
|
-
if (trimmed === "# pages" || trimmed === "#pages") {
|
|
3494
|
-
flushSection();
|
|
3495
|
-
currentSection = "pages";
|
|
3496
|
-
continue;
|
|
3497
|
-
}
|
|
3498
|
-
if (trimmed === "# behaviors" || trimmed === "#behaviors" || trimmed === "# traits" || trimmed === "#traits") {
|
|
3499
|
-
flushSection();
|
|
3500
|
-
currentSection = "behaviors";
|
|
3501
|
-
continue;
|
|
3502
|
-
}
|
|
3503
|
-
currentContent.push(line);
|
|
3504
|
-
}
|
|
3505
|
-
flushSection();
|
|
3506
|
-
if (result.entities.length === 0 && result.pages.length === 0 && result.behaviors.length === 0) {
|
|
3507
|
-
return autoDetectSections(normalizedText);
|
|
3508
|
-
}
|
|
3509
|
-
return result;
|
|
3510
|
-
function flushSection() {
|
|
3511
|
-
if (!currentSection || currentContent.length === 0) {
|
|
3512
|
-
currentContent = [];
|
|
3513
|
-
return;
|
|
3514
|
-
}
|
|
3515
|
-
const content = currentContent.join("\n").trim();
|
|
3516
|
-
if (content) {
|
|
3517
|
-
let items = content.split(/\n---\n/).map((s) => s.trim()).filter((s) => s);
|
|
3518
|
-
if (currentSection === "entities") {
|
|
3519
|
-
items = splitEntitiesByPattern(items);
|
|
3520
|
-
} else if (currentSection === "pages") {
|
|
3521
|
-
items = splitPagesByPattern(items);
|
|
3522
|
-
} else if (currentSection === "behaviors") {
|
|
3523
|
-
items = splitBehaviorsByPattern(items);
|
|
3524
|
-
}
|
|
3525
|
-
result[currentSection].push(...items);
|
|
3526
|
-
}
|
|
3527
|
-
currentContent = [];
|
|
3528
|
-
}
|
|
3529
|
-
}
|
|
3530
|
-
function splitEntitiesByPattern(items) {
|
|
3531
|
-
const result = [];
|
|
3532
|
-
for (const item of items) {
|
|
3533
|
-
const lines = item.split("\n");
|
|
3534
|
-
let currentEntity = [];
|
|
3535
|
-
for (const line of lines) {
|
|
3536
|
-
const trimmed = line.trim();
|
|
3537
|
-
if (/^an?\s+\w+\s+is\s/i.test(trimmed) && currentEntity.length > 0) {
|
|
3538
|
-
result.push(currentEntity.join("\n").trim());
|
|
3539
|
-
currentEntity = [line];
|
|
3540
|
-
} else {
|
|
3541
|
-
currentEntity.push(line);
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
if (currentEntity.length > 0) {
|
|
3545
|
-
result.push(currentEntity.join("\n").trim());
|
|
3546
|
-
}
|
|
3547
|
-
}
|
|
3548
|
-
return result.filter((s) => s);
|
|
3549
|
-
}
|
|
3550
|
-
function splitPagesByPattern(items) {
|
|
3551
|
-
const result = [];
|
|
3552
|
-
for (const item of items) {
|
|
3553
|
-
const lines = item.split("\n");
|
|
3554
|
-
let currentPage = [];
|
|
3555
|
-
for (const line of lines) {
|
|
3556
|
-
const trimmed = line.trim();
|
|
3557
|
-
if (/^\w+(?:page)?\s+at\s+\//i.test(trimmed) && currentPage.length > 0) {
|
|
3558
|
-
result.push(currentPage.join("\n").trim());
|
|
3559
|
-
currentPage = [line];
|
|
3560
|
-
} else {
|
|
3561
|
-
currentPage.push(line);
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
|
-
if (currentPage.length > 0) {
|
|
3565
|
-
result.push(currentPage.join("\n").trim());
|
|
3566
|
-
}
|
|
3567
|
-
}
|
|
3568
|
-
return result.filter((s) => s);
|
|
3569
|
-
}
|
|
3570
|
-
function splitBehaviorsByPattern(items) {
|
|
3571
|
-
const result = [];
|
|
3572
|
-
for (const item of items) {
|
|
3573
|
-
const lines = item.split("\n");
|
|
3574
|
-
let currentBehavior = [];
|
|
3575
|
-
for (const line of lines) {
|
|
3576
|
-
const trimmed = line.trim();
|
|
3577
|
-
if ((/^\w+\s+behavior:?$/i.test(trimmed) || /^every\s+/i.test(trimmed)) && currentBehavior.length > 0) {
|
|
3578
|
-
result.push(currentBehavior.join("\n").trim());
|
|
3579
|
-
currentBehavior = [line];
|
|
3580
|
-
} else {
|
|
3581
|
-
currentBehavior.push(line);
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
if (currentBehavior.length > 0) {
|
|
3585
|
-
result.push(currentBehavior.join("\n").trim());
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
return result.filter((s) => s);
|
|
3589
|
-
}
|
|
3590
|
-
function autoDetectSections(text) {
|
|
3591
|
-
const result = {
|
|
3592
|
-
entities: [],
|
|
3593
|
-
pages: [],
|
|
3594
|
-
behaviors: []
|
|
3595
|
-
};
|
|
3596
|
-
const sections = text.split(/\n---\n/).map((s) => s.trim()).filter((s) => s);
|
|
3597
|
-
for (const section of sections) {
|
|
3598
|
-
const firstLine = section.split("\n")[0].toLowerCase();
|
|
3599
|
-
if (firstLine.match(/^an?\s+\w+\s+is/)) {
|
|
3600
|
-
result.entities.push(section);
|
|
3601
|
-
} else if (firstLine.match(/^the\s+\w+.*shows/)) {
|
|
3602
|
-
result.pages.push(section);
|
|
3603
|
-
} else if (firstLine.match(/^every\s+/)) {
|
|
3604
|
-
result.behaviors.push(section);
|
|
3605
|
-
} else if (section.toLowerCase().includes("states:") || section.toLowerCase().includes("transitions:")) {
|
|
3606
|
-
result.behaviors.push(section);
|
|
3607
|
-
} else if (section.toLowerCase().includes("it has:")) {
|
|
3608
|
-
result.entities.push(section);
|
|
3609
|
-
} else if (section.toLowerCase().includes("it displays:")) {
|
|
3610
|
-
result.pages.push(section);
|
|
3611
|
-
} else {
|
|
3612
|
-
result.entities.push(section);
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
return result;
|
|
3616
|
-
}
|
|
3617
|
-
function parseTickFromDomain(text) {
|
|
3618
|
-
const lines = text.trim().split("\n");
|
|
3619
|
-
if (lines.length === 0) return null;
|
|
3620
|
-
const firstLine = lines[0].trim();
|
|
3621
|
-
const intervalMatch = firstLine.match(/^every\s+(.+?):?$/i);
|
|
3622
|
-
if (!intervalMatch) return null;
|
|
3623
|
-
const intervalStr = intervalMatch[1];
|
|
3624
|
-
const intervalMs = parseIntervalToMs(intervalStr);
|
|
3625
|
-
const tickName = intervalStr.split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
3626
|
-
const tick = {
|
|
3627
|
-
name: tickName,
|
|
3628
|
-
interval: intervalMs,
|
|
3629
|
-
effects: []
|
|
3630
|
-
// S-Expression arrays
|
|
3631
|
-
};
|
|
3632
|
-
for (let i = 1; i < lines.length; i++) {
|
|
3633
|
-
const line = lines[i].trim();
|
|
3634
|
-
if (line.toLowerCase().startsWith("if ")) {
|
|
3635
|
-
tick.guard = line.slice(3).trim();
|
|
3636
|
-
} else if (line.startsWith("-")) {
|
|
3637
|
-
const effectText = line.slice(1).trim();
|
|
3638
|
-
const effect = parseEffectFromText(effectText);
|
|
3639
|
-
if (effect) {
|
|
3640
|
-
tick.effects.push(effect);
|
|
3641
|
-
}
|
|
3642
|
-
}
|
|
3643
|
-
}
|
|
3644
|
-
return tick;
|
|
3645
|
-
}
|
|
3646
|
-
function parseIntervalToMs(text) {
|
|
3647
|
-
const lower = text.toLowerCase();
|
|
3648
|
-
const match = lower.match(/(\d+)\s*(second|minute|hour|day|week)s?/);
|
|
3649
|
-
if (match) {
|
|
3650
|
-
const value = parseInt(match[1], 10);
|
|
3651
|
-
const unit = match[2];
|
|
3652
|
-
switch (unit) {
|
|
3653
|
-
case "second":
|
|
3654
|
-
return value * 1e3;
|
|
3655
|
-
case "minute":
|
|
3656
|
-
return value * 60 * 1e3;
|
|
3657
|
-
case "hour":
|
|
3658
|
-
return value * 60 * 60 * 1e3;
|
|
3659
|
-
case "day":
|
|
3660
|
-
return value * 24 * 60 * 60 * 1e3;
|
|
3661
|
-
case "week":
|
|
3662
|
-
return value * 7 * 24 * 60 * 60 * 1e3;
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3665
|
-
return 60 * 60 * 1e3;
|
|
3666
|
-
}
|
|
3667
|
-
function parseEffectFromText(text) {
|
|
3668
|
-
try {
|
|
3669
|
-
const result = parseDomainEffect(text);
|
|
3670
|
-
if (Array.isArray(result)) {
|
|
3671
|
-
return result;
|
|
3672
|
-
}
|
|
3673
|
-
return null;
|
|
3674
|
-
} catch {
|
|
3675
|
-
return null;
|
|
660
|
+
schema: orbitalSchema,
|
|
661
|
+
validation,
|
|
662
|
+
stats
|
|
663
|
+
};
|
|
664
|
+
} catch (error) {
|
|
665
|
+
return {
|
|
666
|
+
success: false,
|
|
667
|
+
error: error instanceof Error ? error.message : String(error),
|
|
668
|
+
stats
|
|
669
|
+
};
|
|
3676
670
|
}
|
|
3677
671
|
}
|
|
3678
672
|
|
|
673
|
+
// src/orbitals/domain-language/index.ts
|
|
674
|
+
var domain_language_exports = {};
|
|
675
|
+
__export(domain_language_exports, {
|
|
676
|
+
ODL_EXAMPLES: () => ODL_EXAMPLES,
|
|
677
|
+
ODL_PATTERNS: () => ODL_PATTERNS,
|
|
678
|
+
ODL_SYNTAX_REFERENCE: () => ODL_SYNTAX_REFERENCE,
|
|
679
|
+
ODL_TO_SCHEMA_MAPPING: () => ODL_TO_SCHEMA_MAPPING
|
|
680
|
+
});
|
|
681
|
+
__reExport(domain_language_exports, domain_language_star);
|
|
682
|
+
|
|
683
|
+
// src/orbitals/domain-language/prompts/odl-syntax.ts
|
|
684
|
+
var ODL_SYNTAX_REFERENCE = `
|
|
685
|
+
## Domain Language Syntax
|
|
686
|
+
|
|
687
|
+
### Entity Definition
|
|
688
|
+
|
|
689
|
+
\`\`\`
|
|
690
|
+
A [EntityName] is a [persistence] entity that:
|
|
691
|
+
- has [fieldName] as [type] (required|optional)
|
|
692
|
+
- has [fieldName] as [type] with default [value]
|
|
693
|
+
- belongs to [RelatedEntity]
|
|
694
|
+
- belongs to [RelatedEntity] as [alias]
|
|
695
|
+
- has many [RelatedEntity]s
|
|
696
|
+
\`\`\`
|
|
697
|
+
|
|
698
|
+
**Persistence types:**
|
|
699
|
+
- \`persistent\` - Stored in database (default)
|
|
700
|
+
- \`runtime\` - Memory only, for games/UI state
|
|
701
|
+
- \`singleton\` - Single instance, for config
|
|
702
|
+
|
|
703
|
+
**Field types:**
|
|
704
|
+
- \`text\` / \`long text\` - String fields
|
|
705
|
+
- \`number\` / \`currency\` - Numeric fields
|
|
706
|
+
- \`yes/no\` - Boolean fields
|
|
707
|
+
- \`date\` / \`timestamp\` / \`datetime\` - Date fields
|
|
708
|
+
- \`enum [val1, val2]\` - Enumeration
|
|
709
|
+
- \`list\` / \`object\` - Complex types
|
|
710
|
+
|
|
711
|
+
### Page Definition
|
|
712
|
+
|
|
713
|
+
\`\`\`
|
|
714
|
+
[PageName] at /[path]:
|
|
715
|
+
- shows [Entity] using [TraitName]
|
|
716
|
+
- view type: [list|detail|create|edit|dashboard]
|
|
717
|
+
- is initial page
|
|
718
|
+
\`\`\`
|
|
719
|
+
|
|
720
|
+
### Behavior Definition
|
|
721
|
+
|
|
722
|
+
\`\`\`
|
|
723
|
+
[BehaviorName] behavior:
|
|
724
|
+
States: [State1], [State2], [State3]
|
|
725
|
+
Initial: [State1]
|
|
726
|
+
|
|
727
|
+
Transitions:
|
|
728
|
+
- From [State1] to [State2] on [EVENT_NAME]
|
|
729
|
+
if [guard condition]
|
|
730
|
+
then [effect1]
|
|
731
|
+
then [effect2]
|
|
732
|
+
\`\`\`
|
|
733
|
+
|
|
734
|
+
### Section Headers
|
|
735
|
+
|
|
736
|
+
Domain Language text is organized into sections:
|
|
737
|
+
|
|
738
|
+
\`\`\`
|
|
739
|
+
# Entities
|
|
740
|
+
[entity definitions]
|
|
741
|
+
|
|
742
|
+
# Pages
|
|
743
|
+
[page definitions]
|
|
744
|
+
|
|
745
|
+
# Behaviors
|
|
746
|
+
[behavior/trait definitions]
|
|
747
|
+
\`\`\`
|
|
748
|
+
`;
|
|
749
|
+
|
|
750
|
+
// src/orbitals/domain-language/prompts/odl-examples.ts
|
|
751
|
+
var ODL_EXAMPLES = `
|
|
752
|
+
## Complete Example
|
|
753
|
+
|
|
754
|
+
### Entity
|
|
755
|
+
|
|
756
|
+
\`\`\`
|
|
757
|
+
A Task is a persistent entity that:
|
|
758
|
+
- has title as text (required)
|
|
759
|
+
- has description as long text
|
|
760
|
+
- has status as enum [todo, in_progress, done] with default "todo"
|
|
761
|
+
- has dueDate as date
|
|
762
|
+
- belongs to User as assignee
|
|
763
|
+
- belongs to Project
|
|
764
|
+
\`\`\`
|
|
765
|
+
|
|
766
|
+
### Page
|
|
767
|
+
|
|
768
|
+
\`\`\`
|
|
769
|
+
TasksPage at /tasks:
|
|
770
|
+
- shows Task using TaskManagement
|
|
771
|
+
- view type: list
|
|
772
|
+
- is initial page
|
|
773
|
+
\`\`\`
|
|
774
|
+
|
|
775
|
+
### Behavior
|
|
776
|
+
|
|
777
|
+
\`\`\`
|
|
778
|
+
TaskManagement behavior:
|
|
779
|
+
States: Viewing, Editing, Creating
|
|
780
|
+
Initial: Viewing
|
|
781
|
+
|
|
782
|
+
Transitions:
|
|
783
|
+
- From Viewing to Viewing on INIT
|
|
784
|
+
then render page-header to main with title 'Tasks'
|
|
785
|
+
then render entity-table to center for Task
|
|
786
|
+
|
|
787
|
+
- From Viewing to Editing on EDIT_TASK
|
|
788
|
+
then render form-section to modal for Task
|
|
789
|
+
|
|
790
|
+
- From Editing to Viewing on SAVE_TASK
|
|
791
|
+
then persist update Task
|
|
792
|
+
then render null to modal
|
|
793
|
+
then emit TASK_UPDATED
|
|
794
|
+
\`\`\`
|
|
795
|
+
|
|
796
|
+
## Multi-Entity Example
|
|
797
|
+
|
|
798
|
+
\`\`\`
|
|
799
|
+
# Entities
|
|
800
|
+
|
|
801
|
+
A User is a persistent entity that:
|
|
802
|
+
- has name as text (required)
|
|
803
|
+
- has email as text (required)
|
|
804
|
+
- has role as enum [admin, user, guest] with default "user"
|
|
805
|
+
|
|
806
|
+
A Project is a persistent entity that:
|
|
807
|
+
- has name as text (required)
|
|
808
|
+
- has description as long text
|
|
809
|
+
- has status as enum [active, archived] with default "active"
|
|
810
|
+
- belongs to User as owner
|
|
811
|
+
|
|
812
|
+
A Task is a persistent entity that:
|
|
813
|
+
- has title as text (required)
|
|
814
|
+
- has status as enum [todo, in_progress, done] with default "todo"
|
|
815
|
+
- has priority as number with default 0
|
|
816
|
+
- belongs to Project
|
|
817
|
+
- belongs to User as assignee
|
|
818
|
+
|
|
819
|
+
# Pages
|
|
820
|
+
|
|
821
|
+
ProjectsPage at /projects:
|
|
822
|
+
- shows Project using ProjectManagement
|
|
823
|
+
- view type: list
|
|
824
|
+
- is initial page
|
|
825
|
+
|
|
826
|
+
TasksPage at /projects/:projectId/tasks:
|
|
827
|
+
- shows Task using TaskManagement
|
|
828
|
+
- view type: list
|
|
829
|
+
|
|
830
|
+
# Behaviors
|
|
831
|
+
|
|
832
|
+
ProjectManagement behavior:
|
|
833
|
+
States: List, Detail
|
|
834
|
+
Initial: List
|
|
835
|
+
|
|
836
|
+
Transitions:
|
|
837
|
+
- From List to List on INIT
|
|
838
|
+
then render entity-table to main for Project
|
|
839
|
+
|
|
840
|
+
- From List to Detail on VIEW_PROJECT
|
|
841
|
+
then render entity-detail to drawer for Project
|
|
842
|
+
|
|
843
|
+
TaskManagement behavior:
|
|
844
|
+
States: Viewing, Editing
|
|
845
|
+
Initial: Viewing
|
|
846
|
+
|
|
847
|
+
Transitions:
|
|
848
|
+
- From Viewing to Viewing on INIT
|
|
849
|
+
then ["render-ui", "main", {"type": "entity-table", "entity": "Task", "itemActions": [{"label": "Edit", "event": "EDIT_TASK"}, {"label": "Delete", "event": "DELETE_TASK", "variant": "danger"}]}]
|
|
850
|
+
|
|
851
|
+
- From Viewing to Editing on EDIT_TASK
|
|
852
|
+
then render form-section to modal for Task
|
|
853
|
+
|
|
854
|
+
- From Editing to Viewing on SAVE_TASK
|
|
855
|
+
then persist update Task
|
|
856
|
+
then render null to modal
|
|
857
|
+
\`\`\`
|
|
858
|
+
|
|
859
|
+
## Dashboard Example (Complex Patterns)
|
|
860
|
+
|
|
861
|
+
\`\`\`
|
|
862
|
+
Dashboard behavior:
|
|
863
|
+
States: Viewing
|
|
864
|
+
Initial: Viewing
|
|
865
|
+
|
|
866
|
+
Transitions:
|
|
867
|
+
- From Viewing to Viewing on INIT
|
|
868
|
+
then render page-header to main with title 'Dashboard'
|
|
869
|
+
then ["render-ui", "center", {"type": "stats", "metrics": [{"field": "totalTasks", "label": "Total Tasks"}, {"field": "completedTasks", "label": "Completed"}, {"field": "overdueTasks", "label": "Overdue"}]}]
|
|
870
|
+
then ["render-ui", "bottom", {"type": "tabs", "tabs": [{"id": "overview", "label": "Overview", "event": "VIEW_OVERVIEW"}, {"id": "recent", "label": "Recent Activity", "event": "VIEW_RECENT"}]}]
|
|
871
|
+
\`\`\`
|
|
872
|
+
|
|
873
|
+
**Note**: Use inline JSON for \`render-ui\` effects with complex props (metrics, tabs, itemActions, cells).
|
|
874
|
+
`;
|
|
875
|
+
|
|
876
|
+
// src/orbitals/domain-language/prompts/odl-patterns.ts
|
|
877
|
+
var ODL_PATTERNS = `
|
|
878
|
+
## Common Patterns
|
|
879
|
+
|
|
880
|
+
### CRUD Pattern
|
|
881
|
+
|
|
882
|
+
\`\`\`
|
|
883
|
+
[Entity]Management behavior:
|
|
884
|
+
States: List, Detail, Create, Edit
|
|
885
|
+
Initial: List
|
|
886
|
+
|
|
887
|
+
Transitions:
|
|
888
|
+
- From List to List on INIT
|
|
889
|
+
then render entity-table to main for [Entity]
|
|
890
|
+
|
|
891
|
+
- From List to Create on CREATE_[ENTITY]
|
|
892
|
+
then render form-section to modal for [Entity]
|
|
893
|
+
|
|
894
|
+
- From Create to List on SUBMIT_CREATE
|
|
895
|
+
then persist create [Entity]
|
|
896
|
+
then render null to modal
|
|
897
|
+
|
|
898
|
+
- From List to Detail on VIEW_[ENTITY]
|
|
899
|
+
then render entity-detail to drawer for [Entity]
|
|
900
|
+
|
|
901
|
+
- From Detail to Edit on EDIT_[ENTITY]
|
|
902
|
+
then render form-section to modal for [Entity]
|
|
903
|
+
|
|
904
|
+
- From Edit to Detail on SUBMIT_EDIT
|
|
905
|
+
then persist update [Entity]
|
|
906
|
+
then render null to modal
|
|
907
|
+
|
|
908
|
+
- From Detail to List on DELETE_[ENTITY]
|
|
909
|
+
then persist delete [Entity]
|
|
910
|
+
then render null to drawer
|
|
911
|
+
\`\`\`
|
|
912
|
+
|
|
913
|
+
### Game Health Pattern
|
|
914
|
+
|
|
915
|
+
\`\`\`
|
|
916
|
+
Health behavior:
|
|
917
|
+
States: Healthy, Wounded, Critical, Dead
|
|
918
|
+
Initial: Healthy
|
|
919
|
+
|
|
920
|
+
Transitions:
|
|
921
|
+
- From Healthy to Wounded on TAKE_DAMAGE
|
|
922
|
+
if health >= 30 and health < 70
|
|
923
|
+
then update status to 'wounded'
|
|
924
|
+
|
|
925
|
+
- From Wounded to Critical on TAKE_DAMAGE
|
|
926
|
+
if health < 30
|
|
927
|
+
then update status to 'critical'
|
|
928
|
+
|
|
929
|
+
- From Critical to Dead on TAKE_DAMAGE
|
|
930
|
+
if health <= 0
|
|
931
|
+
then update isAlive to false
|
|
932
|
+
then emit ENTITY_DIED
|
|
933
|
+
\`\`\`
|
|
934
|
+
|
|
935
|
+
### Approval Workflow Pattern
|
|
936
|
+
|
|
937
|
+
\`\`\`
|
|
938
|
+
ApprovalWorkflow behavior:
|
|
939
|
+
States: Draft, Pending, Approved, Rejected
|
|
940
|
+
Initial: Draft
|
|
941
|
+
|
|
942
|
+
Transitions:
|
|
943
|
+
- From Draft to Pending on SUBMIT
|
|
944
|
+
then update status to 'pending'
|
|
945
|
+
then emit APPROVAL_REQUESTED
|
|
946
|
+
|
|
947
|
+
- From Pending to Approved on APPROVE
|
|
948
|
+
then update status to 'approved'
|
|
949
|
+
then update approvedAt to now
|
|
950
|
+
then emit APPROVAL_GRANTED
|
|
951
|
+
|
|
952
|
+
- From Pending to Rejected on REJECT
|
|
953
|
+
then update status to 'rejected'
|
|
954
|
+
then emit APPROVAL_DENIED
|
|
955
|
+
|
|
956
|
+
- From Rejected to Draft on REVISE
|
|
957
|
+
then update status to 'draft'
|
|
958
|
+
\`\`\`
|
|
959
|
+
|
|
960
|
+
### Form Validation Pattern
|
|
961
|
+
|
|
962
|
+
\`\`\`
|
|
963
|
+
FormValidation behavior:
|
|
964
|
+
States: Idle, Validating, Valid, Invalid
|
|
965
|
+
Initial: Idle
|
|
966
|
+
|
|
967
|
+
Transitions:
|
|
968
|
+
- From Idle to Validating on VALIDATE
|
|
969
|
+
then update isValidating to true
|
|
970
|
+
|
|
971
|
+
- From Validating to Valid on VALIDATION_SUCCESS
|
|
972
|
+
then update isValidating to false
|
|
973
|
+
then update errors to null
|
|
974
|
+
|
|
975
|
+
- From Validating to Invalid on VALIDATION_FAILED
|
|
976
|
+
then update isValidating to false
|
|
977
|
+
|
|
978
|
+
- From Invalid to Validating on VALIDATE
|
|
979
|
+
then update isValidating to true
|
|
980
|
+
|
|
981
|
+
- From Valid to Idle on RESET
|
|
982
|
+
then update errors to null
|
|
983
|
+
\`\`\`
|
|
984
|
+
|
|
985
|
+
### Dashboard Pattern
|
|
986
|
+
|
|
987
|
+
\`\`\`
|
|
988
|
+
DashboardView behavior:
|
|
989
|
+
States: Loading, Ready
|
|
990
|
+
Initial: Loading
|
|
991
|
+
|
|
992
|
+
Transitions:
|
|
993
|
+
- From Loading to Ready on DATA_LOADED
|
|
994
|
+
then render stats to main
|
|
995
|
+
then render entity-cards to center
|
|
996
|
+
|
|
997
|
+
- From Ready to Loading on REFRESH
|
|
998
|
+
then emit FETCH_DASHBOARD_DATA
|
|
999
|
+
\`\`\`
|
|
1000
|
+
`;
|
|
1001
|
+
|
|
1002
|
+
// src/orbitals/domain-language/prompts/odl-to-schema.ts
|
|
1003
|
+
var ODL_TO_SCHEMA_MAPPING = `
|
|
1004
|
+
## Domain Language -> OrbitalSchema Mapping
|
|
1005
|
+
|
|
1006
|
+
| Domain Language | OrbitalSchema |
|
|
1007
|
+
|-----------------|---------------|
|
|
1008
|
+
| "A Task is..." | \`{ "entity": { "name": "Task", ... } }\` |
|
|
1009
|
+
| "persistent entity" | \`"persistence": "persistent"\` |
|
|
1010
|
+
| "runtime entity" | \`"persistence": "runtime"\` |
|
|
1011
|
+
| "singleton entity" | \`"persistence": "singleton"\` |
|
|
1012
|
+
| "has X as text" | \`{ "name": "X", "type": "string" }\` |
|
|
1013
|
+
| "has X as number" | \`{ "name": "X", "type": "number" }\` |
|
|
1014
|
+
| "has X as yes/no" | \`{ "name": "X", "type": "boolean" }\` |
|
|
1015
|
+
| "has X as enum [a, b]" | \`{ "name": "X", "type": "enum", "values": ["a", "b"] }\` |
|
|
1016
|
+
| "belongs to Y" | \`{ "type": "relation", "relation": { "entity": "Y" } }\` |
|
|
1017
|
+
| "belongs to Y as alias" | \`{ "name": "alias", "type": "relation", "relation": { "entity": "Y" } }\` |
|
|
1018
|
+
| "States: A, B" | \`"states": [{ "name": "A" }, { "name": "B" }]\` |
|
|
1019
|
+
| "Initial: A" | \`{ "name": "A", "isInitial": true }\` |
|
|
1020
|
+
| "From X to Y on Z" | \`{ "from": "X", "to": "Y", "event": "Z" }\` |
|
|
1021
|
+
| "if health >= 0" | \`"guard": [">=", "@entity.health", 0]\` |
|
|
1022
|
+
| "then update status to 'done'" | \`"effects": [["set", "@entity.status", "done"]]\` |
|
|
1023
|
+
| "then emit EVENT" | \`"effects": [["emit", "EVENT"]]\` |
|
|
1024
|
+
| "then render X to slot" | \`"effects": [["render-ui", "slot", { "type": "X" }]]\` |
|
|
1025
|
+
| "then persist create Task" | \`"effects": [["persist", "create", "Task"]]\` |
|
|
1026
|
+
| "then navigate to /path" | \`"effects": [["navigate", "/path"]]\` |
|
|
1027
|
+
|
|
1028
|
+
## Conversion Process
|
|
1029
|
+
|
|
1030
|
+
1. **Parse Domain Language** - Split into sections (Entities, Pages, Behaviors)
|
|
1031
|
+
2. **Extract entities** - Parse "A X is..." definitions
|
|
1032
|
+
3. **Extract pages** - Parse "[Name] at /path" definitions
|
|
1033
|
+
4. **Extract behaviors** - Parse "[Name] behavior:" definitions
|
|
1034
|
+
5. **Parse guards** - Convert human-readable conditions to S-expressions
|
|
1035
|
+
6. **Parse effects** - Convert human-readable effects to S-expressions
|
|
1036
|
+
7. **Assemble schema** - Build OrbitalSchema JSON structure
|
|
1037
|
+
|
|
1038
|
+
## CLI Commands
|
|
1039
|
+
|
|
1040
|
+
\`\`\`bash
|
|
1041
|
+
# Convert Domain Language to OrbitalSchema
|
|
1042
|
+
npx kflow domain:to-schema input.txt -o output.orb
|
|
1043
|
+
|
|
1044
|
+
# Convert OrbitalSchema to Domain Language
|
|
1045
|
+
npx kflow domain:to-text input.orb -o output.txt
|
|
1046
|
+
|
|
1047
|
+
# Validate round-trip conversion
|
|
1048
|
+
npx kflow domain:validate input.orb --verbose
|
|
1049
|
+
\`\`\`
|
|
1050
|
+
`;
|
|
1051
|
+
|
|
3679
1052
|
// src/tools/finish-task.ts
|
|
3680
1053
|
var execAsync2 = promisify(exec);
|
|
3681
1054
|
async function collectOrbitalsFromDir(workDir) {
|
|
@@ -3752,7 +1125,7 @@ function createFinishTaskTool(workDir) {
|
|
|
3752
1125
|
if (domainText.trim()) {
|
|
3753
1126
|
source = "domain";
|
|
3754
1127
|
const appName = input.summary?.match(/Generated \d+ orbitals?:?\s*(.+)/)?.[1] || "Application";
|
|
3755
|
-
const domainResult = convertDomainToSchema(domainText, {
|
|
1128
|
+
const domainResult = (0, domain_language_exports.convertDomainToSchema)(domainText, {
|
|
3756
1129
|
name: appName,
|
|
3757
1130
|
orbitals: []
|
|
3758
1131
|
});
|
|
@@ -4207,7 +1580,7 @@ function createConstructCombinedDomainTool(options = {}) {
|
|
|
4207
1580
|
role: "assistant",
|
|
4208
1581
|
isComplete: false
|
|
4209
1582
|
});
|
|
4210
|
-
const schemaResult = convertDomainToSchema(domainText, {
|
|
1583
|
+
const schemaResult = (0, domain_language_exports.convertDomainToSchema)(domainText, {
|
|
4211
1584
|
name: appName,
|
|
4212
1585
|
orbitals: []
|
|
4213
1586
|
});
|
|
@@ -4955,6 +2328,273 @@ function createSchemaChunkingTools(workDir) {
|
|
|
4955
2328
|
applyChunk: createApplyChunkTool(workDir)
|
|
4956
2329
|
};
|
|
4957
2330
|
}
|
|
2331
|
+
var designCache = /* @__PURE__ */ new Map();
|
|
2332
|
+
var CACHE_TTL_MS2 = 24 * 60 * 60 * 1e3;
|
|
2333
|
+
var CACHE_VERSION2 = 1;
|
|
2334
|
+
function generateFingerprint2(input) {
|
|
2335
|
+
const normalized = JSON.stringify({
|
|
2336
|
+
version: CACHE_VERSION2,
|
|
2337
|
+
...input
|
|
2338
|
+
});
|
|
2339
|
+
return crypto.createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
2340
|
+
}
|
|
2341
|
+
function getCached2(fingerprint) {
|
|
2342
|
+
const entry = designCache.get(fingerprint);
|
|
2343
|
+
if (!entry) return null;
|
|
2344
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS2) {
|
|
2345
|
+
designCache.delete(fingerprint);
|
|
2346
|
+
return null;
|
|
2347
|
+
}
|
|
2348
|
+
return entry;
|
|
2349
|
+
}
|
|
2350
|
+
var STATIC_DESIGN_PROMPT = null;
|
|
2351
|
+
function getDesignSystemPrompt() {
|
|
2352
|
+
if (!STATIC_DESIGN_PROMPT) {
|
|
2353
|
+
const skill = generateKflowDesignSkill();
|
|
2354
|
+
STATIC_DESIGN_PROMPT = skill.content;
|
|
2355
|
+
}
|
|
2356
|
+
return STATIC_DESIGN_PROMPT;
|
|
2357
|
+
}
|
|
2358
|
+
function getPatternRecommendations(input) {
|
|
2359
|
+
const recContext = buildRecommendationContext({
|
|
2360
|
+
state: input.from,
|
|
2361
|
+
event: input.event,
|
|
2362
|
+
slot: input.slot,
|
|
2363
|
+
domainCategory: input.domainCategory,
|
|
2364
|
+
entityFields: input.entityFields
|
|
2365
|
+
});
|
|
2366
|
+
return recommendPatterns(recContext, 8);
|
|
2367
|
+
}
|
|
2368
|
+
function buildDesignUserPrompt(input) {
|
|
2369
|
+
const fieldList = input.entityFields.map((f) => {
|
|
2370
|
+
let desc = ` - ${f.name}: ${f.type}`;
|
|
2371
|
+
if (f.values) desc += ` (values: ${f.values.join(", ")})`;
|
|
2372
|
+
return desc;
|
|
2373
|
+
}).join("\n");
|
|
2374
|
+
const hints = [];
|
|
2375
|
+
if (input.designStyle) hints.push(`Style: ${input.designStyle}`);
|
|
2376
|
+
if (input.flowPattern) hints.push(`Flow: ${input.flowPattern}`);
|
|
2377
|
+
if (input.listPattern) hints.push(`List: ${input.listPattern}`);
|
|
2378
|
+
if (input.formPattern) hints.push(`Form: ${input.formPattern}`);
|
|
2379
|
+
if (input.detailPattern) hints.push(`Detail: ${input.detailPattern}`);
|
|
2380
|
+
const vocab = input.vocabulary ? Object.entries(input.vocabulary).map(([k, v]) => ` ${k} \u2192 "${v}"`).join("\n") : "";
|
|
2381
|
+
return `Design render-ui effects for this transition:
|
|
2382
|
+
|
|
2383
|
+
## Transition
|
|
2384
|
+
- **From**: ${input.from}
|
|
2385
|
+
- **To**: ${input.to}
|
|
2386
|
+
- **Event**: ${input.event}
|
|
2387
|
+
- **Slot**: ${input.slot}
|
|
2388
|
+
|
|
2389
|
+
## Entity: ${input.entityName}
|
|
2390
|
+
${fieldList}
|
|
2391
|
+
|
|
2392
|
+
## Domain
|
|
2393
|
+
- **Category**: ${input.domainCategory || "business"}
|
|
2394
|
+
${vocab ? `- **Vocabulary**:
|
|
2395
|
+
${vocab}` : ""}
|
|
2396
|
+
${hints.length > 0 ? `- **Design Hints**: ${hints.join(", ")}` : ""}
|
|
2397
|
+
|
|
2398
|
+
${input.recommendationsSection || ""}
|
|
2399
|
+
|
|
2400
|
+
${input.existingEffects ? `## Existing Effects (enhance these)
|
|
2401
|
+
\`\`\`json
|
|
2402
|
+
${JSON.stringify(input.existingEffects, null, 2)}
|
|
2403
|
+
\`\`\`` : ""}
|
|
2404
|
+
|
|
2405
|
+
Return ONLY the JSON array of render-ui effect tuples.`;
|
|
2406
|
+
}
|
|
2407
|
+
var DesignTransitionSchema = z.object({
|
|
2408
|
+
from: z.string().describe('Source state name (e.g., "Browsing")'),
|
|
2409
|
+
to: z.string().describe('Target state name (e.g., "Browsing" for self-loop, "Creating" for transition)'),
|
|
2410
|
+
event: z.string().describe('Event name (e.g., "INIT", "CREATE", "VIEW", "EDIT", "DELETE", "SAVE", "CANCEL")'),
|
|
2411
|
+
slot: z.string().describe('UI slot to render into: "main", "modal", "drawer", "sidebar", "overlay"'),
|
|
2412
|
+
entityName: z.string().describe('Entity name (e.g., "Task", "Order")'),
|
|
2413
|
+
entityFields: z.array(z.object({
|
|
2414
|
+
name: z.string(),
|
|
2415
|
+
type: z.string(),
|
|
2416
|
+
values: z.array(z.string()).optional()
|
|
2417
|
+
})).describe("Entity fields with types and optional enum values"),
|
|
2418
|
+
domainCategory: z.enum(["business", "game", "dashboard", "form", "content", "social", "e-commerce", "workflow"]).optional().describe("Domain category for pattern selection"),
|
|
2419
|
+
vocabulary: z.record(z.string(), z.string()).optional().describe('Domain vocabulary mapping (e.g., { "create": "Place Order", "item": "Order" })'),
|
|
2420
|
+
designStyle: z.enum(["minimal", "modern", "playful", "data-driven", "immersive"]).optional().describe("Visual style hint"),
|
|
2421
|
+
flowPattern: z.enum(["hub-spoke", "master-detail", "crud-cycle", "linear", "role-based"]).optional().describe("Application flow pattern"),
|
|
2422
|
+
listPattern: z.enum(["entity-table", "entity-cards", "entity-list"]).optional().describe("Preferred list pattern"),
|
|
2423
|
+
formPattern: z.enum(["modal", "drawer", "page"]).optional().describe("Preferred form pattern"),
|
|
2424
|
+
detailPattern: z.enum(["drawer", "page", "split"]).optional().describe("Preferred detail view pattern"),
|
|
2425
|
+
existingEffects: z.array(z.any()).optional().describe("Existing render-ui effects to enhance (for refinement passes)")
|
|
2426
|
+
});
|
|
2427
|
+
function createDesignTransitionTool(options = {}) {
|
|
2428
|
+
let eventCallback = options.onEvent;
|
|
2429
|
+
const setEventCallback = (callback) => {
|
|
2430
|
+
eventCallback = callback;
|
|
2431
|
+
};
|
|
2432
|
+
const emitEvent = (transitionId, type, data) => {
|
|
2433
|
+
if (eventCallback) {
|
|
2434
|
+
eventCallback(transitionId, {
|
|
2435
|
+
type,
|
|
2436
|
+
data,
|
|
2437
|
+
timestamp: Date.now()
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
const designTransitionTool = tool(
|
|
2442
|
+
async (input) => {
|
|
2443
|
+
const transitionId = `${input.entityName}:${input.from}->${input.to}:${input.event}`;
|
|
2444
|
+
const fingerprint = generateFingerprint2({
|
|
2445
|
+
from: input.from,
|
|
2446
|
+
to: input.to,
|
|
2447
|
+
event: input.event,
|
|
2448
|
+
slot: input.slot,
|
|
2449
|
+
entityName: input.entityName,
|
|
2450
|
+
entityFields: input.entityFields,
|
|
2451
|
+
domainCategory: input.domainCategory,
|
|
2452
|
+
designStyle: input.designStyle,
|
|
2453
|
+
flowPattern: input.flowPattern,
|
|
2454
|
+
listPattern: input.listPattern
|
|
2455
|
+
});
|
|
2456
|
+
try {
|
|
2457
|
+
emitEvent(transitionId, "message", {
|
|
2458
|
+
content: `Designing UI for ${transitionId}`,
|
|
2459
|
+
role: "assistant",
|
|
2460
|
+
isComplete: false
|
|
2461
|
+
});
|
|
2462
|
+
const cached = getCached2(fingerprint);
|
|
2463
|
+
if (cached) {
|
|
2464
|
+
emitEvent(transitionId, "generation_log", {
|
|
2465
|
+
level: "info",
|
|
2466
|
+
message: `Design cache HIT for ${transitionId}`,
|
|
2467
|
+
data: { fingerprint }
|
|
2468
|
+
});
|
|
2469
|
+
return JSON.stringify({
|
|
2470
|
+
success: true,
|
|
2471
|
+
transitionId,
|
|
2472
|
+
effects: cached.effects,
|
|
2473
|
+
cached: true,
|
|
2474
|
+
usage: cached.usage
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
const recommendations = getPatternRecommendations(input);
|
|
2478
|
+
const recommendationsSection = formatRecommendationsForPrompt(recommendations);
|
|
2479
|
+
const systemPrompt = getDesignSystemPrompt();
|
|
2480
|
+
const userPrompt = buildDesignUserPrompt({
|
|
2481
|
+
...input,
|
|
2482
|
+
recommendationsSection
|
|
2483
|
+
});
|
|
2484
|
+
emitEvent(transitionId, "tool_call", {
|
|
2485
|
+
tool: "llm_design_transition",
|
|
2486
|
+
args: {
|
|
2487
|
+
transition: transitionId,
|
|
2488
|
+
slot: input.slot,
|
|
2489
|
+
domain: input.domainCategory
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
const client = new LLMClient({
|
|
2493
|
+
provider: "anthropic",
|
|
2494
|
+
model: "claude-sonnet-4-20250514",
|
|
2495
|
+
temperature: 0.1
|
|
2496
|
+
// Slight creativity for design
|
|
2497
|
+
});
|
|
2498
|
+
const response = await client.callWithCache({
|
|
2499
|
+
systemPrompt: "",
|
|
2500
|
+
systemBlocks: [{
|
|
2501
|
+
type: "text",
|
|
2502
|
+
text: systemPrompt,
|
|
2503
|
+
cache_control: { type: "ephemeral" }
|
|
2504
|
+
}],
|
|
2505
|
+
userPrompt,
|
|
2506
|
+
maxTokens: 4096,
|
|
2507
|
+
rawText: true
|
|
2508
|
+
});
|
|
2509
|
+
const rawText = (response.raw || String(response.data) || "").trim();
|
|
2510
|
+
let effects;
|
|
2511
|
+
try {
|
|
2512
|
+
const jsonText = rawText.replace(/^```(?:json)?\n?/m, "").replace(/\n?```$/m, "").trim();
|
|
2513
|
+
effects = JSON.parse(jsonText);
|
|
2514
|
+
if (!Array.isArray(effects)) {
|
|
2515
|
+
effects = [effects];
|
|
2516
|
+
}
|
|
2517
|
+
} catch {
|
|
2518
|
+
return JSON.stringify({
|
|
2519
|
+
success: false,
|
|
2520
|
+
transitionId,
|
|
2521
|
+
error: "Failed to parse design output as JSON",
|
|
2522
|
+
rawOutput: rawText
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
const usage = {
|
|
2526
|
+
inputTokens: response.usage?.promptTokens || 0,
|
|
2527
|
+
outputTokens: response.usage?.completionTokens || 0,
|
|
2528
|
+
totalTokens: response.usage?.totalTokens || 0
|
|
2529
|
+
};
|
|
2530
|
+
designCache.set(fingerprint, {
|
|
2531
|
+
effects,
|
|
2532
|
+
timestamp: Date.now(),
|
|
2533
|
+
usage
|
|
2534
|
+
});
|
|
2535
|
+
emitEvent(transitionId, "tool_result", {
|
|
2536
|
+
tool: "llm_design_transition",
|
|
2537
|
+
result: { fingerprint, effectCount: effects.length, usage },
|
|
2538
|
+
success: true
|
|
2539
|
+
});
|
|
2540
|
+
emitEvent(transitionId, "message", {
|
|
2541
|
+
content: `Designed ${effects.length} effect(s) for ${transitionId} (${usage.totalTokens} tokens)`,
|
|
2542
|
+
role: "assistant",
|
|
2543
|
+
isComplete: true
|
|
2544
|
+
});
|
|
2545
|
+
return JSON.stringify({
|
|
2546
|
+
success: true,
|
|
2547
|
+
transitionId,
|
|
2548
|
+
effects,
|
|
2549
|
+
cached: false,
|
|
2550
|
+
usage,
|
|
2551
|
+
recommendedPatterns: recommendations.map((r) => r.pattern)
|
|
2552
|
+
});
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2555
|
+
emitEvent(transitionId, "error", {
|
|
2556
|
+
error: errorMessage,
|
|
2557
|
+
code: "DESIGN_TRANSITION_ERROR"
|
|
2558
|
+
});
|
|
2559
|
+
return JSON.stringify({
|
|
2560
|
+
success: false,
|
|
2561
|
+
transitionId,
|
|
2562
|
+
error: errorMessage
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
},
|
|
2566
|
+
{
|
|
2567
|
+
name: "design_transition",
|
|
2568
|
+
description: `Design rich render-ui effects for a single orbital transition.
|
|
2569
|
+
|
|
2570
|
+
Takes the transition context (from/to state, event, entity, domain) and produces
|
|
2571
|
+
polished render-ui effects using the full pattern catalog.
|
|
2572
|
+
|
|
2573
|
+
USE THIS TOOL WHEN:
|
|
2574
|
+
- Generating INIT transitions (always compose header + stats + content)
|
|
2575
|
+
- Generating CREATE/EDIT transitions (form with proper fields)
|
|
2576
|
+
- Generating VIEW transitions (detail with tabs for related entities)
|
|
2577
|
+
- Enhancing existing render-ui effects that are too basic
|
|
2578
|
+
|
|
2579
|
+
The tool uses a specialized design skill with pattern catalog, layout composition,
|
|
2580
|
+
and domain-aware pattern selection to produce rich UI.
|
|
2581
|
+
|
|
2582
|
+
RETURNS: { success, effects: [["render-ui", slot, config], ...], transitionId, usage }
|
|
2583
|
+
|
|
2584
|
+
The effects array contains ONLY render-ui tuples. Non-UI effects (persist, emit, set)
|
|
2585
|
+
are NOT included \u2014 you must preserve those from the original transition.
|
|
2586
|
+
|
|
2587
|
+
INTEGRATION: After calling this tool, use extract_chunk to get the orbital,
|
|
2588
|
+
replace the render-ui effects in the target transition (keep persist/emit/set effects),
|
|
2589
|
+
then apply_chunk to merge back into schema.json.`,
|
|
2590
|
+
schema: DesignTransitionSchema
|
|
2591
|
+
}
|
|
2592
|
+
);
|
|
2593
|
+
return {
|
|
2594
|
+
tool: designTransitionTool,
|
|
2595
|
+
setEventCallback
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
4958
2598
|
function createGitHubTools(config) {
|
|
4959
2599
|
const { token, owner = "", repo = "", workDir } = config;
|
|
4960
2600
|
const integrationConfig = {
|
|
@@ -5926,6 +3566,11 @@ ${skillContents}`;
|
|
|
5926
3566
|
console.log(`[SkillAgent] Domain orbital tools enabled for ${primarySkill.name} skill`);
|
|
5927
3567
|
}
|
|
5928
3568
|
}
|
|
3569
|
+
const needsDesignTool = isDesignSkill || ORBITAL_SKILLS.includes(primarySkill.name);
|
|
3570
|
+
const designTool = needsDesignTool ? createDesignTransitionTool() : null;
|
|
3571
|
+
if (designTool && verbose) {
|
|
3572
|
+
console.log(`[SkillAgent] Design transition tool enabled for ${primarySkill.name} skill`);
|
|
3573
|
+
}
|
|
5929
3574
|
const githubTools = options.githubConfig ? createGitHubToolsArray({
|
|
5930
3575
|
token: options.githubConfig.token,
|
|
5931
3576
|
owner: options.githubConfig.owner,
|
|
@@ -5949,6 +3594,7 @@ ${skillContents}`;
|
|
|
5949
3594
|
chunkingTools.extractChunk,
|
|
5950
3595
|
chunkingTools.applyChunk
|
|
5951
3596
|
] : [],
|
|
3597
|
+
...designTool ? [designTool.tool] : [],
|
|
5952
3598
|
...githubTools || []
|
|
5953
3599
|
];
|
|
5954
3600
|
const checkpointer = sessions.getCheckpointer(threadId);
|