Skip to content

Commit 57e40bf

Browse files
committed
create morph array in polynode and apply transition from frontend to the selected morph
1 parent dc56951 commit 57e40bf

8 files changed

Lines changed: 523 additions & 20 deletions

File tree

CNL_Examples/morphs-test.cnl

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Morphs and Transitions Test
2+
3+
This CNL file demonstrates the morphs functionality in NodeBook, showing how nodes can transition between different states without creating new nodes.
4+
5+
# Oxygen [Element]
6+
has atomic number: 8;
7+
has number of protons: 8;
8+
has number of neutrons: 8;
9+
has number of electrons: 8;
10+
has state: "gas";
11+
12+
## Oxide ion
13+
has atomic number: 8;
14+
has number of protons: 8;
15+
has number of neutrons: 8;
16+
has number of electrons: 10;
17+
has charge: -2;
18+
has state: "ion";
19+
<part of> Water;
20+
21+
## Ozone
22+
has atomic number: 8;
23+
has number of protons: 8;
24+
has number of neutrons: 8;
25+
has number of electrons: 8;
26+
has molecular formula: "O3";
27+
has state: "gas";
28+
29+
# Hydrogen [Element]
30+
has atomic number: 1;
31+
has number of protons: 1;
32+
has number of neutrons: 0;
33+
has number of electrons: 1;
34+
has state: "gas";
35+
36+
## Hydrogen ion
37+
has atomic number: 1;
38+
has number of protons: 1;
39+
has number of neutrons: 0;
40+
has number of electrons: 0;
41+
has charge: +1;
42+
has state: "ion";
43+
<part of> Water;
44+
45+
# Water [Molecule]
46+
has molecular formula: "H2O";
47+
has state: "liquid";
48+
<is a type of> Compound;
49+
50+
## Ice
51+
has molecular formula: "H2O";
52+
has state: "solid";
53+
has temperature: 0 *Celsius*;
54+
55+
## Steam
56+
has molecular formula: "H2O";
57+
has state: "gas";
58+
has temperature: 100 *Celsius*;
59+
60+
# Oxidation [Transition]
61+
<has prior_state> Oxygen;
62+
<has prior_state> Hydrogen;
63+
<has post_state> Oxide ion;
64+
<has post_state> Hydrogen ion;
65+
66+
# Reduction [Transition]
67+
<has prior_state> Oxide ion;
68+
<has prior_state> Hydrogen ion;
69+
<has post_state> Oxygen;
70+
<has post_state> Hydrogen;
71+
72+
# Phase Change [Transition]
73+
<has prior_state> Water;
74+
<has prior_state> Heat;
75+
<has post_state> Steam;
76+
77+
# Freezing [Transition]
78+
<has prior_state> Water;
79+
<has prior_state> Cold;
80+
<has post_state> Ice;
81+
82+
# Heat [Energy]
83+
# Cold [Energy]

nodebook-base/cnl-parser.js

Lines changed: 200 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,34 @@ function getOperationsFromCnl(cnlText, mode = 'richgraph') {
3737
const { id: nodeId, payload: nodePayload } = processNodeHeading(nodeBlock.heading);
3838
operations.push({ type: 'addNode', payload: nodePayload, id: nodeId });
3939

40+
// Process main node content
4041
const neighborhoodOps = processNeighborhood(nodeId, nodeBlock.content);
4142
operations.push(...neighborhoodOps);
43+
44+
// Process morphs
45+
for (const morph of nodeBlock.morphs || []) {
46+
const morphId = `${nodeId}_morph_${morph.name.toLowerCase().replace(/\s+/g, '_')}_${Date.now()}`;
47+
48+
// Add morph operation
49+
operations.push({
50+
type: 'addMorph',
51+
payload: {
52+
nodeId: nodeId,
53+
morph: {
54+
morph_id: morphId,
55+
node_id: nodeId,
56+
name: morph.name,
57+
relationNode_ids: [],
58+
attributeNode_ids: []
59+
}
60+
},
61+
id: `${nodeId}_morph_${morph.name}`
62+
});
63+
64+
// Process morph content
65+
const morphOps = processMorphNeighborhood(nodeId, morphId, morph.content);
66+
operations.push(...morphOps);
67+
}
4268
}
4369

4470
console.log(`[CNL Debug] Generated operations:`, operations.map(op => ({ type: op.type, id: op.id })));
@@ -281,12 +307,28 @@ function buildStructuralTree(cnlText) {
281307

282308
for (const line of lines) {
283309
if (!line.trim()) continue;
284-
const headingMatch = line.match(HEADING_REGEX) || line.match(SIMPLE_HEADING_REGEX);
285-
if (headingMatch) {
286-
currentNodeBlock = { heading: line.trim(), content: [] };
310+
311+
// Check if this is a main heading (#) or a morph heading (##)
312+
const mainHeadingMatch = line.match(/^\s*(#)\s+(.+)$/);
313+
const morphHeadingMatch = line.match(/^\s*(##)\s+(.+)$/);
314+
315+
if (mainHeadingMatch) {
316+
// This is a main node heading
317+
currentNodeBlock = { heading: line.trim(), content: [], morphs: [] };
287318
tree.push(currentNodeBlock);
319+
} else if (morphHeadingMatch && currentNodeBlock) {
320+
// This is a morph definition - add it to the current node block
321+
const morphName = morphHeadingMatch[2].trim();
322+
currentNodeBlock.morphs.push({ name: morphName, content: [] });
288323
} else if (currentNodeBlock) {
289-
currentNodeBlock.content.push(line);
324+
// This is content - add it to the current context
325+
if (currentNodeBlock.morphs.length > 0) {
326+
// Add to the last morph
327+
currentNodeBlock.morphs[currentNodeBlock.morphs.length - 1].content.push(line);
328+
} else {
329+
// Add to the main node content
330+
currentNodeBlock.content.push(line);
331+
}
290332
}
291333
}
292334
return tree;
@@ -366,8 +408,6 @@ function processNeighborhood(nodeId, lines) {
366408
const neighborhoodOps = [];
367409
let content = lines.join('\n');
368410

369-
370-
371411
const descriptionMatch = content.match(DESCRIPTION_REGEX);
372412
if (descriptionMatch) {
373413
const description = descriptionMatch[1].trim();
@@ -492,14 +532,17 @@ function processNeighborhood(nodeId, lines) {
492532
id: targetId
493533
});
494534

535+
// Build relation payload
536+
const relationPayload = {
537+
source: nodeId,
538+
target: targetId,
539+
name: relationName.trim()
540+
};
541+
495542
// Create relation to the target node
496543
neighborhoodOps.push({
497544
type: 'addRelation',
498-
payload: {
499-
source: nodeId,
500-
target: targetId,
501-
name: relationName.trim()
502-
},
545+
payload: relationPayload,
503546
id
504547
});
505548
}
@@ -508,6 +551,152 @@ function processNeighborhood(nodeId, lines) {
508551
return neighborhoodOps;
509552
}
510553

554+
function processMorphNeighborhood(nodeId, morphId, lines) {
555+
const neighborhoodOps = [];
556+
let content = lines.join('\n');
557+
558+
const descriptionMatch = content.match(DESCRIPTION_REGEX);
559+
if (descriptionMatch) {
560+
const description = descriptionMatch[1].trim();
561+
const id = `attr_${nodeId}_description_${crypto.createHash('sha1').update(description).digest('hex').slice(0, 6)}`;
562+
neighborhoodOps.push({ type: 'updateNode', payload: { id: nodeId, fields: { description } }, id: `${nodeId}_description` });
563+
content = content.replace(DESCRIPTION_REGEX, '').trim();
564+
}
511565

566+
// Process attributes with priority on unit extraction
567+
const attributeLines = content.split('\n').filter(line => line.trim().startsWith('has '));
568+
569+
for (const line of attributeLines) {
570+
// Simple regex to extract basic parts
571+
const basicMatch = line.match(/^\s*has\s+([^:]+):\s*([^;]+);?/);
572+
if (!basicMatch) continue;
573+
574+
const [, name, fullValue] = basicMatch;
575+
let value = fullValue.trim();
576+
let unit = null;
577+
let adverb = null;
578+
let modality = null;
579+
let quantifier = null;
580+
581+
// Priority 1: Extract units (*unit*)
582+
const unitMatch = value.match(/\*([^*]+)\*/);
583+
if (unitMatch) {
584+
unit = unitMatch[1].trim();
585+
value = value.replace(/\*[^*]+\*/, '').trim();
586+
}
587+
588+
// Priority 2: Extract quantifiers (*quantifier*)
589+
const quantifierMatch = value.match(/\*([^*]+)\*/);
590+
if (quantifierMatch) {
591+
quantifier = quantifierMatch[1].trim();
592+
value = value.replace(/\*[^*]+\*/, '').trim();
593+
}
594+
595+
// Priority 3: Extract adverbs (++adverb++)
596+
const adverbMatch = value.match(/\+\+([^+]+)\+\+/);
597+
if (adverbMatch) {
598+
adverb = adverbMatch[1].trim();
599+
value = value.replace(/\+\+[^+]+\+\+/, '').trim();
600+
}
601+
602+
// Priority 4: Extract modalities [modality]
603+
const modalityMatch = value.match(/\[([^\]]+)\]/);
604+
if (modalityMatch) {
605+
modality = modalityMatch[1].trim();
606+
value = value.replace(/\[[^\]]+\]/, '').trim();
607+
}
608+
609+
// Clean up the final value
610+
value = value.trim();
611+
612+
console.log(`[Morph Attribute Debug] Parsed:`, { name, value, unit, quantifier, adverb, modality });
613+
614+
const valueHash = crypto.createHash('sha1').update(String(value)).digest('hex').slice(0, 6);
615+
const id = `attr_${nodeId}_${name.trim().toLowerCase().replace(/\s+/g, '_')}_${valueHash}`;
616+
617+
// Build enhanced attribute payload with modifiers
618+
const attributePayload = {
619+
source: nodeId,
620+
name: name.trim(),
621+
value: value,
622+
morphId: morphId
623+
};
624+
625+
// Add modifiers in priority order
626+
if (unit) attributePayload.unit = unit;
627+
if (quantifier) attributePayload.quantifier = quantifier;
628+
if (adverb) attributePayload.adverb = adverb;
629+
if (modality) attributePayload.modality = modality;
630+
631+
neighborhoodOps.push({ type: 'addAttribute', payload: attributePayload, id });
632+
}
633+
634+
const functionMatches = [...content.matchAll(FUNCTION_REGEX)];
635+
636+
for (const match of functionMatches) {
637+
const [, name] = match;
638+
639+
const id = `func_${nodeId}_${name.trim().toLowerCase().replace(/\s+/g, '_')}`;
640+
neighborhoodOps.push({ type: 'applyFunction', payload: { source: nodeId, name: name.trim() }, id });
641+
}
642+
643+
const relationMatches = [...content.matchAll(RELATION_REGEX)];
644+
645+
for (const match of relationMatches) {
646+
const [, relationName, targets] = match;
647+
648+
for (const target of targets.split(';').map(t => t.trim()).filter(Boolean)) {
649+
// Parse target for adjectives and base name (similar to node headings)
650+
let targetAdjective = null;
651+
let targetBaseName = target;
652+
let targetDisplayName = target;
653+
654+
// Check if target has adjective formatting (*adjective* baseName)
655+
const adjectiveMatch = target.match(/\*\*?([^*]+)\*\*?\s+(.+)/);
656+
if (adjectiveMatch) {
657+
targetAdjective = adjectiveMatch[1].trim();
658+
targetBaseName = adjectiveMatch[2].trim();
659+
targetDisplayName = target; // Keep the original formatting
660+
}
661+
662+
// Generate clean ID from base name and adjective if present
663+
const cleanTargetBaseName = targetBaseName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '_');
664+
const cleanTargetAdjective = targetAdjective ? targetAdjective.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '_') : null;
665+
const targetId = cleanTargetAdjective ? `${cleanTargetAdjective}_${cleanTargetBaseName}` : cleanTargetBaseName;
666+
const id = `rel_${nodeId}_${relationName.trim().toLowerCase().replace(/\s+/g, '_')}_${targetId}`;
667+
668+
// Create target node if it doesn't exist (for implicit nodes like "Country", "Asia")
669+
neighborhoodOps.push({
670+
type: 'addNode',
671+
payload: {
672+
base_name: targetBaseName,
673+
displayName: targetDisplayName,
674+
role: 'class', // Default role for implicit nodes
675+
options: {
676+
adjective: targetAdjective
677+
}
678+
},
679+
id: targetId
680+
});
681+
682+
// Build relation payload
683+
const relationPayload = {
684+
source: nodeId,
685+
target: targetId,
686+
name: relationName.trim(),
687+
morphId: morphId
688+
};
689+
690+
// Create relation to the target node
691+
neighborhoodOps.push({
692+
type: 'addRelation',
693+
payload: relationPayload,
694+
id
695+
});
696+
}
697+
}
698+
699+
return neighborhoodOps;
700+
}
512701

513702
export { diffCnl, validateOperations, getNodeOrderFromCnl, getOperationsFromCnl };

0 commit comments

Comments
 (0)