@vizualmodel/vmblu-cli 0.3.1 → 0.3.2

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.
@@ -160,7 +160,7 @@ async function initProject(opts) {
160
160
 
161
161
  const absTarget = path.resolve(targetDir);
162
162
  const modelFile = path.join(absTarget, `${projectName}.vmblu`);
163
- const docFile = path.join(absTarget, `${projectName}-doc.json`);
163
+ const docFile = path.join(absTarget, `${projectName}.prf.json`);
164
164
 
165
165
  const llmDir = path.join(absTarget, 'llm');
166
166
  const sessionDir = path.join(llmDir, 'session');
@@ -2855,23 +2855,23 @@ async getFolderContent(){
2855
2855
  const sourceMapHandling = {
2856
2856
 
2857
2857
  // reads the source doc file and parses it into documentation
2858
- async handleSourceDoc() {
2858
+ async handleSourceMap() {
2859
2859
 
2860
2860
  // read the source doc file
2861
- const rawSourceDoc = await this.readSourceDoc();
2861
+ const rawSourceMap = await this.readSourceMap();
2862
2862
 
2863
2863
  // check
2864
- if (! rawSourceDoc) return;
2864
+ if (! rawSourceMap) return;
2865
2865
 
2866
2866
  // parse to extract the juicy bits
2867
- this.sourceMap = this.parseSourceDoc(rawSourceDoc);
2867
+ this.sourceMap = this.parseSourceMap(rawSourceMap);
2868
2868
 
2869
2869
  // ok
2870
- // console.log('** SourceDoc **', this.sourceMap)
2870
+ // console.log('** SourceMap **', this.sourceMap)
2871
2871
  },
2872
2872
 
2873
2873
  // Reads the sourceMap of the model
2874
- async readSourceDoc() {
2874
+ async readSourceMap() {
2875
2875
 
2876
2876
  // get the full path
2877
2877
  const fullPath = this.arl?.getFullPath();
@@ -2880,7 +2880,7 @@ async readSourceDoc() {
2880
2880
  if (!fullPath) return null
2881
2881
 
2882
2882
  // make an arl
2883
- const sourceMapArl = this.arl.resolve(removeExt(fullPath) + '-doc.json');
2883
+ const sourceMapArl = this.arl.resolve(removeExt(fullPath) + '.prf.json');
2884
2884
 
2885
2885
  // get the file
2886
2886
  return await sourceMapArl.get('json')
@@ -2893,7 +2893,7 @@ async readSourceDoc() {
2893
2893
  * @param {Array<{node: string, handlers: Array}>} docEntries
2894
2894
  * @returns {Map<string, Map<string, object>>} Map of nodeName -> Map of pinName -> handler metadata
2895
2895
  */
2896
- parseSourceDoc(raw) {
2896
+ parseSourceMap(raw) {
2897
2897
 
2898
2898
  // check
2899
2899
  if (!raw.entries) return null;
@@ -2944,7 +2944,7 @@ parseSourceDoc(raw) {
2944
2944
  /**
2945
2945
  * Optional helper to flatten the nested map into a plain array (useful for UI).
2946
2946
  */
2947
- flattenSourceDoc(nodeMap) {
2947
+ flattenSourceMap(nodeMap) {
2948
2948
  const flatList = [];
2949
2949
  for (const [node, pins] of nodeMap.entries()) {
2950
2950
  for (const [pin, meta] of pins.entries()) {
@@ -3047,7 +3047,7 @@ makeMcpToolString(root) {
3047
3047
  * Generate MCP-compatible tool specs in an LLM-neutral format.
3048
3048
  * Only handlers with `mcp: true` will be included.
3049
3049
  *
3050
- * @param {Map<string, Map<string, object>>} nodeMap - Output from parseSourceDoc
3050
+ * @param {Map<string, Map<string, object>>} nodeMap - Output from parseSourceMap
3051
3051
  * @returns {Array<object>} - Abstract tool specs
3052
3052
  */
3053
3053
  generateToolSpecs() {
@@ -12010,6 +12010,9 @@ changeFactory: {
12010
12010
 
12011
12011
  };
12012
12012
 
12013
+ /**
12014
+ * @node editor editor
12015
+ */
12013
12016
  const redoxWidget = {
12014
12017
 
12015
12018
  newPin: {
@@ -22656,136 +22659,144 @@ async getFolderContent(){
22656
22659
  }
22657
22660
  };
22658
22661
 
22659
- // extractHandlersFromFile.js
22660
-
22661
-
22662
- let currentNode = null;
22663
- let topLevelClass = null;
22664
- let nodeMap = null;
22665
- let filePath = null;
22666
-
22667
- function findHandlers(sourceFile, _filePath, _nodeMap) {
22668
-
22669
- // Reset any node context carried over from previous files.
22670
- currentNode = null;
22671
-
22672
- // The fallback name is the top-level class
22673
- topLevelClass = sourceFile.getClasses()[0]?.getName?.() || null;
22674
- nodeMap = _nodeMap;
22675
- filePath = _filePath;
22676
-
22677
- // Check all the functions in the sourcefile - typically generator functions
22678
- sourceFile.getFunctions().forEach(fn => {
22679
-
22680
- // Capture node annotations on generator-style functions and harvest handlers returned from them.
22681
- const jsdoc = getFullJsDoc(fn);
22682
- updateNodeFromJsdoc(jsdoc);
22683
-
22684
- const name = fn.getName();
22685
-
22686
- if (isHandler(name)) {
22687
-
22688
- const line = fn.getNameNode()?.getStartLineNumber() ?? fn.getStartLineNumber();
22689
- const docTags = getParamDocs(fn);
22690
- const params = fn.getParameters().flatMap(p => describeParam(p, docTags));
22691
-
22692
- collect(name, params, line, jsdoc);
22693
- }
22694
-
22695
- collectHandlersFromFunctionReturns(fn);
22696
- });
22697
-
22698
- // Check the variable declarations in the sourcefile
22699
- sourceFile.getVariableDeclarations().forEach(decl => {
22700
-
22701
- // check the name
22702
- const name = decl.getName();
22703
- const init = decl.getInitializer();
22704
- const line = decl.getStartLineNumber();
22705
- const jsdoc = getFullJsDoc(decl);
22706
- updateNodeFromJsdoc(jsdoc);
22707
-
22708
- // check if the name is a handler and initialised with a function
22709
- if (isHandler(name) && init && init.getKindName().includes('Function')) {
22710
-
22711
- const docTags = getParamDocs(decl);
22712
- const params = init.getParameters().flatMap(p => describeParam(p, docTags));
22713
-
22714
- collect(name, params, line, jsdoc);
22715
- }
22716
-
22662
+ // extractHandlersFromFile.js
22663
+
22664
+
22665
+ let currentNode = null;
22666
+ let topLevelClass = null;
22667
+ let nodeMap = null;
22668
+ let filePath = null;
22669
+ let nodeAliases = new Map();
22670
+
22671
+ let knownIdentifiers = new Set();
22672
+
22673
+ function findHandlers(sourceFile, _filePath, _nodeMap) {
22674
+
22675
+ // Reset any node context carried over from previous files.
22676
+ currentNode = null;
22677
+
22678
+ // The fallback name is the top-level class
22679
+ topLevelClass = sourceFile.getClasses()[0]?.getName?.() || null;
22680
+ nodeMap = _nodeMap;
22681
+ filePath = _filePath;
22682
+ nodeAliases = new Map();
22683
+ knownIdentifiers = collectKnownIdentifiers(sourceFile);
22684
+
22685
+ // Check all the functions in the sourcefile - typically generator functions
22686
+ sourceFile.getFunctions().forEach(fn => {
22687
+
22688
+ // Capture node annotations on generator-style functions and harvest handlers returned from them.
22689
+ const jsdoc = getFullJsDoc(fn);
22690
+ updateNodeFromJsdoc(jsdoc);
22691
+
22692
+ const name = fn.getName();
22693
+
22694
+ if (isHandler(name)) {
22695
+
22696
+ const line = fn.getNameNode()?.getStartLineNumber() ?? fn.getStartLineNumber();
22697
+ const docTags = getParamDocs(fn);
22698
+ const params = fn.getParameters().flatMap(p => describeParam(p, docTags));
22699
+
22700
+ collect(name, params, line, jsdoc);
22701
+ }
22702
+
22703
+ collectHandlersFromFunctionReturns(fn);
22704
+ });
22705
+
22706
+ // Check the variable declarations in the sourcefile
22707
+ sourceFile.getVariableDeclarations().forEach(decl => {
22708
+
22709
+ // check the name
22710
+ const name = decl.getName();
22711
+ const init = decl.getInitializer();
22712
+ const line = decl.getStartLineNumber();
22713
+ const declJsdoc = getFullJsDoc(decl);
22714
+ const statement = decl.getFirstAncestorByKind?.(SyntaxKind.VariableStatement);
22715
+ const statementJsdoc = statement ? getFullJsDoc(statement) : null;
22716
+ const jsdoc = hasDocMetadata(declJsdoc) ? declJsdoc : statementJsdoc ?? declJsdoc;
22717
+ updateNodeFromJsdoc(jsdoc);
22718
+
22719
+ // check if the name is a handler and initialised with a function
22720
+ if (isHandler(name) && init && init.getKindName().includes('Function')) {
22721
+
22722
+ const docTags = getParamDocs(decl);
22723
+ const params = init.getParameters().flatMap(p => describeParam(p, docTags));
22724
+
22725
+ collect(name, params, line, jsdoc);
22726
+ }
22727
+
22717
22728
  const objectLiteral = resolveObjectLiteralExpression(init);
22718
22729
  if (objectLiteral) {
22719
22730
  collectObjectLiteralHandlers(objectLiteral);
22720
22731
  }
22721
- });
22722
-
22723
- // check all the classes in the file
22724
- sourceFile.getClasses().forEach(cls => {
22725
-
22726
- // get the name of the node
22727
- cls.getName?.() || topLevelClass;
22728
-
22729
- // check all the methods
22730
- cls.getMethods().forEach(method => {
22731
-
22732
- // check the name
22733
- const name = method.getName();
22734
- if (!isHandler(name)) return;
22735
-
22736
- // extract
22737
- const line = method.getNameNode()?.getStartLineNumber() ?? method.getStartLineNumber();
22738
- const jsdoc = getFullJsDoc(method);
22739
- const docTags = getParamDocs(method);
22740
- const params = method.getParameters().flatMap(p => describeParam(p, docTags));
22741
-
22742
- // and collect
22743
- collect(name, params, line, jsdoc);
22744
- });
22745
- });
22746
-
22747
- // check all the statements
22748
- sourceFile.getStatements().forEach(stmt => {
22749
-
22750
- // only binary expressions
22751
- if (!stmt.isKind(ts.SyntaxKind.ExpressionStatement)) return;
22752
- const expr = stmt.getExpression();
22753
- if (!expr.isKind(ts.SyntaxKind.BinaryExpression)) return;
22754
-
22755
- // get the two parts of the statement
22756
- const left = expr.getLeft().getText();
22757
- const right = expr.getRight();
22758
-
22759
- // check for protype
22760
- if (left.includes('.prototype.') && right.isKind(ts.SyntaxKind.FunctionExpression)) {
22761
-
22762
- // get the name and check
22763
- const parts = left.split('.');
22764
- const name = parts[parts.length - 1];
22765
- if (!isHandler(name)) return;
22766
-
22767
- // extract
22768
- const line = expr.getStartLineNumber();
22769
- const params = right.getParameters().flatMap(p => describeParam(p));
22770
- const jsdoc = getFullJsDoc(expr);
22771
-
22772
- // and save in nodemap
22773
- collect(name, params, line, jsdoc);
22774
- }
22775
-
22732
+ });
22733
+
22734
+ // check all the classes in the file
22735
+ sourceFile.getClasses().forEach(cls => {
22736
+
22737
+ // get the name of the node
22738
+ const nodeName = cls.getName?.() || topLevelClass;
22739
+
22740
+ // check all the methods
22741
+ cls.getMethods().forEach(method => {
22742
+
22743
+ // check the name
22744
+ const name = method.getName();
22745
+ if (!isHandler(name)) return;
22746
+
22747
+ // extract
22748
+ const line = method.getNameNode()?.getStartLineNumber() ?? method.getStartLineNumber();
22749
+ const jsdoc = getFullJsDoc(method);
22750
+ const docTags = getParamDocs(method);
22751
+ const params = method.getParameters().flatMap(p => describeParam(p, docTags));
22752
+
22753
+ // and collect
22754
+ collect(name, params, line, jsdoc, nodeName);
22755
+ });
22756
+ });
22757
+
22758
+ // check all the statements
22759
+ sourceFile.getStatements().forEach(stmt => {
22760
+
22761
+ // only binary expressions
22762
+ if (!stmt.isKind(SyntaxKind.ExpressionStatement)) return;
22763
+ const expr = stmt.getExpression();
22764
+ if (!expr.isKind(SyntaxKind.BinaryExpression)) return;
22765
+
22766
+ // get the two parts of the statement
22767
+ const left = expr.getLeft().getText();
22768
+ const right = expr.getRight();
22769
+
22770
+ // check for protype
22771
+ if (left.includes('.prototype.') && right.isKind(SyntaxKind.FunctionExpression)) {
22772
+
22773
+ // get the name and check
22774
+ const parts = left.split('.');
22775
+ const name = parts[parts.length - 1];
22776
+ if (!isHandler(name)) return;
22777
+
22778
+ // extract
22779
+ const line = expr.getStartLineNumber();
22780
+ const params = right.getParameters().flatMap(p => describeParam(p));
22781
+ const jsdoc = getFullJsDoc(expr);
22782
+
22783
+ // and save in nodemap
22784
+ collect(name, params, line, jsdoc);
22785
+ }
22786
+
22776
22787
  const objectLiteral = resolveObjectLiteralExpression(right);
22777
22788
  if (left.endsWith('.prototype') && objectLiteral) {
22778
22789
  collectObjectLiteralHandlers(objectLiteral);
22779
22790
  }
22780
- });
22781
- }
22782
-
22783
-
22784
- function collectHandlersFromFunctionReturns(fn) {
22785
-
22786
- // Look for factory-style returns that expose handlers via object literals.
22787
- fn.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement).forEach(ret => {
22788
- const expr = ret.getExpression();
22791
+ });
22792
+ }
22793
+
22794
+
22795
+ function collectHandlersFromFunctionReturns(fn) {
22796
+
22797
+ // Look for factory-style returns that expose handlers via object literals.
22798
+ fn.getDescendantsOfKind(SyntaxKind.ReturnStatement).forEach(ret => {
22799
+ const expr = ret.getExpression();
22789
22800
  const objectLiteral = resolveObjectLiteralExpression(expr);
22790
22801
  if (!objectLiteral) return;
22791
22802
 
@@ -22802,16 +22813,16 @@ function resolveObjectLiteralExpression(expression) {
22802
22813
  return expression;
22803
22814
  }
22804
22815
 
22805
- if (expression.isKind?.(ts.SyntaxKind.ParenthesizedExpression)) {
22816
+ if (expression.isKind?.(SyntaxKind.ParenthesizedExpression)) {
22806
22817
  return resolveObjectLiteralExpression(expression.getExpression());
22807
22818
  }
22808
22819
 
22809
- if (expression.isKind?.(ts.SyntaxKind.AsExpression)
22810
- || expression.isKind?.(ts.SyntaxKind.TypeAssertionExpression)
22811
- || expression.isKind?.(ts.SyntaxKind.SatisfiesExpression)
22812
- || expression.isKind?.(ts.SyntaxKind.NonNullExpression)
22820
+ if (expression.isKind?.(SyntaxKind.AsExpression)
22821
+ || expression.isKind?.(SyntaxKind.TypeAssertionExpression)
22822
+ || expression.isKind?.(SyntaxKind.SatisfiesExpression)
22823
+ || expression.isKind?.(SyntaxKind.NonNullExpression)
22813
22824
  ) {
22814
- return resolveObjectLiteralExpression(expression.getExpression());
22825
+ return resolveObjectLiteralExpression(expression.getExpression?.());
22815
22826
  }
22816
22827
 
22817
22828
  return null;
@@ -22826,227 +22837,385 @@ function collectObjectLiteralHandlers(objectLiteral) {
22826
22837
 
22827
22838
  const propName = prop.getName?.();
22828
22839
  if (!isHandler(propName)) return;
22829
-
22830
- let params = [];
22831
- if (prop.getKind() === ts.SyntaxKind.MethodDeclaration) {
22832
- const docTags = getParamDocs(prop);
22833
- params = prop.getParameters().flatMap(p => describeParam(p, docTags));
22834
- } else if (prop.getKind() === ts.SyntaxKind.PropertyAssignment) {
22835
- const fn = prop.getInitializerIfKind(ts.SyntaxKind.FunctionExpression) || prop.getInitializerIfKind(ts.SyntaxKind.ArrowFunction);
22836
- if (fn) {
22837
- const docTags = getParamDocs(fn);
22838
- params = fn.getParameters().flatMap(p => describeParam(p, docTags));
22839
- }
22840
- }
22841
-
22842
- const jsdoc = getFullJsDoc(prop);
22843
- const line = prop.getStartLineNumber();
22844
-
22845
- collect(propName, params, line, jsdoc);
22846
- });
22847
- }
22848
-
22849
- function updateNodeFromJsdoc(jsdoc = {}) {
22850
-
22851
- const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
22852
- if (nodeTag) currentNode = nodeTag.trim();
22853
- }
22854
-
22855
- function collect(rawName, params, line, jsdoc = {}) {
22856
-
22857
- //if (!isHandler(rawName)) return;
22858
- const cleanHandler = rawName.replace(/^['"]|['"]$/g, '');
22859
-
22860
- let pin = null;
22861
- let node = null;
22862
-
22863
- const pinTag = jsdoc.tags?.find(t => t.tagName === 'pin')?.comment;
22864
- const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
22865
- const mcpTag = jsdoc.tags?.find(t => t.tagName === 'mcp')?.comment ?? null;
22866
-
22867
- // if there is a node tag, change the name of the current node
22868
- if (nodeTag) currentNode = nodeTag.trim();
22869
-
22870
- // check the pin tag to get a pin name and node name
22871
- if (pinTag) {
22872
-
22873
- if (pinTag.includes('@')) {
22874
- const [p, n] = pinTag.split('@').map(s => s.trim());
22875
- pin = p;
22876
- node = n;
22877
- }
22878
- else pin = pinTag.trim();
22879
-
22880
- // Use the current context when the pin tag does not specify a node.
22881
- if (!node) node = currentNode || topLevelClass || null;
22882
- }
22883
-
22884
- // check the pin tag to get a pin name and node name
22885
- // if (pinTag && pinTag.includes('@')) {
22886
- // const [p, n] = pinTag.split('@').map(s => s.trim());
22887
- // pin = p;
22888
- // node = n;
22889
- // }
22890
- else {
22891
-
22892
- // no explicit tag - try these...
22893
- node = currentNode || topLevelClass || null;
22894
-
22895
- // deduct the pin name from the handler name
22896
- if (cleanHandler.startsWith('on')) {
22897
- pin = cleanHandler.slice(2).replace(/([A-Z])/g, ' $1').trim().toLowerCase();
22898
- } else if (cleanHandler.startsWith('->')) {
22899
- pin = cleanHandler.slice(2).trim();
22900
- }
22901
- }
22902
-
22903
- // if there is no node we just don't save the data
22904
- if (!node) return
22905
-
22906
- // check if we have an entry for the node
22907
- if (!nodeMap.has(node)) nodeMap.set(node, { handles: [], transmits: [] });
22908
-
22909
- // The handler data to save
22910
- const handlerData = {
22911
- pin,
22912
- handler: cleanHandler,
22913
- file: filePath,
22914
- line,
22915
- summary: jsdoc.summary || '',
22916
- returns: jsdoc.returns || '',
22917
- examples: jsdoc.examples || [],
22918
- params
22919
- };
22920
-
22921
- // extract the data from an mcp tag if present
22922
- if (mcpTag !== null) {
22923
- handlerData.mcp = true;
22924
- if (mcpTag.includes('name:') || mcpTag.includes('description:')) {
22925
- const nameMatch = /name:\s*\"?([^\"]+)\"?/.exec(mcpTag);
22926
- const descMatch = /description:\s*\"?([^\"]+)\"?/.exec(mcpTag);
22927
- if (nameMatch) handlerData.mcpName = nameMatch[1];
22928
- if (descMatch) handlerData.mcpDescription = descMatch[1];
22929
- }
22930
- }
22931
-
22932
- // and put it in the nodemap
22933
- nodeMap.get(node).handles.push(handlerData);
22934
- }
22935
- // determines if a name is the name for a handler
22936
- function isHandler(name) {
22937
- // must be a string
22938
- if (typeof name !== 'string') return false;
22939
-
22940
- // get rid of " and '
22941
- const clean = name.replace(/^['"]|['"]$/g, '');
22942
-
22943
- // check that it starts with the right symbols...
22944
- return clean.startsWith('on') || clean.startsWith('->');
22945
- }
22946
-
22947
- // Get the parameter description from the function or method
22948
- function getParamDocs(fnOrMethod) {
22949
-
22950
- // extract
22951
- const docs = fnOrMethod.getJsDocs?.() ?? [];
22952
- const tags = docs.flatMap(d => d.getTags());
22953
- const paramDocs = {};
22954
-
22955
- // check the tags
22956
- for (const tag of tags) {
22957
- if (tag.getTagName() === 'param') {
22958
- const name = tag.getNameNode()?.getText?.() || tag.getName();
22959
- const desc = tag.getComment() ?? '';
22960
- const type = tag.getTypeNode?.()?.getText?.() || tag.getTypeExpression()?.getTypeNode()?.getText();
22961
- paramDocs[name] = { description: desc, type };
22962
- }
22963
- }
22964
- return paramDocs;
22965
- }
22966
-
22967
- // Get the jsdoc
22968
- function getFullJsDoc(node) {
22969
-
22970
- const docs = node.getJsDocs?.() ?? [];
22971
- const summary = docs.map(d => d.getComment()).filter(Boolean).join('\n');
22972
- const tags = docs.flatMap(d => d.getTags()).map(t => ({
22973
- tagName: t.getTagName(),
22974
- comment: t.getComment() || ''
22975
- }));
22976
-
22977
- const returns = tags.find(t => t.tagName === 'returns')?.comment || '';
22978
- const examples = tags.filter(t => t.tagName === 'example').map(t => t.comment);
22979
-
22980
- return { summary, returns, examples, tags };
22981
- }
22982
-
22983
- // make a parameter description
22984
- function describeParam(p, docTags = {}) {
22985
-
22986
- const nameNode = p.getNameNode();
22987
-
22988
- // const func = p.getParent();
22989
- // const funcName = func.getName?.() || '<anonymous>';
22990
- // console.log(funcName)
22991
-
22992
- if (nameNode.getKindName() === 'ObjectBindingPattern') {
22993
-
22994
- const objType = p.getType();
22995
- const properties = objType.getProperties();
22996
- const isTSFallback = objType.getText() === 'any' || objType.getText() === 'string' || properties.length === 0;
22997
-
22998
- return nameNode.getElements().map(el => {
22999
-
23000
- const subName = el.getName();
23001
- const doc = docTags[subName] ?? {};
23002
- let tsType = null;
23003
-
23004
- if (!isTSFallback) {
23005
- const symbol = objType.getProperty(subName);
23006
- if (symbol) {
23007
- const resolvedType = symbol.getTypeAtLocation(el);
23008
- const text = resolvedType.getText();
23009
- if (text && text !== 'any') {
23010
- tsType = text;
23011
- }
23012
- }
23013
- }
23014
-
23015
- const type = tsType || doc.type || 'string';
23016
- const description = doc.description || '';
23017
- return { name: subName, type, description };
23018
- });
23019
- }
23020
-
23021
- const name = p.getName();
23022
- const doc = docTags[name] ?? {};
23023
- const tsType = p.getType().getText();
23024
-
23025
- // const isTSFallback = tsType === 'any' || tsType === 'string';
23026
- // if (isTSFallback && !doc.type) {
23027
- // console.warn(`⚠️ No type info for param "${name}" in function "${funcName}"`);
23028
- // }
23029
-
23030
- return {
23031
- name,
23032
- type: doc.type || tsType || 'string',
23033
- description: doc.description || '',
23034
- };
22840
+
22841
+ let params = [];
22842
+ if (prop.getKind() === SyntaxKind.MethodDeclaration) {
22843
+ const docTags = getParamDocs(prop);
22844
+ params = prop.getParameters().flatMap(p => describeParam(p, docTags));
22845
+ } else if (prop.getKind() === SyntaxKind.PropertyAssignment) {
22846
+ const fn = prop.getInitializerIfKind(SyntaxKind.FunctionExpression) || prop.getInitializerIfKind(SyntaxKind.ArrowFunction);
22847
+ if (fn) {
22848
+ const docTags = getParamDocs(fn);
22849
+ params = fn.getParameters().flatMap(p => describeParam(p, docTags));
22850
+ }
22851
+ }
22852
+
22853
+ const jsdoc = getFullJsDoc(prop);
22854
+ const line = prop.getStartLineNumber();
22855
+
22856
+ collect(propName, params, line, jsdoc);
22857
+ });
23035
22858
  }
23036
22859
 
23037
- /**
23038
- * Finds tx.send or this.tx.send calls and maps them to their node context.
23039
- *
23040
- * @param {import('ts-morph').SourceFile} sourceFile - The source file being analyzed
22860
+ function updateNodeFromJsdoc(jsdoc = {}) {
22861
+
22862
+ const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
22863
+ if (nodeTag) {
22864
+ applyNodeTag(nodeTag);
22865
+ }
22866
+ }
22867
+
22868
+ function collect(rawName, params, line, jsdoc = {}, defaultNode = null) {
22869
+
22870
+ const cleanHandler = rawName.replace(/^['"]|['"]$/g, '');
22871
+
22872
+ let pin = null;
22873
+ let node = defaultNode || null;
22874
+
22875
+ const pinTag = jsdoc.tags?.find(t => t.tagName === 'pin')?.comment;
22876
+ const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
22877
+ const mcpTag = jsdoc.tags?.find(t => t.tagName === 'mcp')?.comment ?? null;
22878
+
22879
+ if (nodeTag) {
22880
+ const context = applyNodeTag(nodeTag);
22881
+ if (context?.nodeName) {
22882
+ node = context.nodeName;
22883
+ }
22884
+ }
22885
+
22886
+ if (pinTag) {
22887
+
22888
+ if (pinTag.includes('@')) {
22889
+ const [p, n] = pinTag.split('@').map(s => s.trim());
22890
+ pin = p;
22891
+ node = n;
22892
+ }
22893
+ else pin = pinTag.trim();
22894
+ }
22895
+ else if (!node) {
22896
+
22897
+ // no explicit tag - try these...
22898
+ node = currentNode || topLevelClass || null;
22899
+
22900
+ // deduct the pin name from the handler name
22901
+ if (cleanHandler.startsWith('on')) {
22902
+ pin = cleanHandler.slice(2).replace(/([A-Z])/g, ' $1').trim().toLowerCase();
22903
+ } else if (cleanHandler.startsWith('->')) {
22904
+ pin = cleanHandler.slice(2).trim();
22905
+ }
22906
+ }
22907
+
22908
+ // if there is no node we just don't save the data
22909
+ if (!node) return;
22910
+
22911
+ // check if we have an entry for the node
22912
+ if (!nodeMap.has(node)) nodeMap.set(node, { handles: [], transmits: [] });
22913
+
22914
+ // The handler data to save
22915
+ const handlerData = {
22916
+ pin,
22917
+ handler: cleanHandler,
22918
+ file: filePath,
22919
+ line,
22920
+ summary: jsdoc.summary || '',
22921
+ returns: jsdoc.returns || '',
22922
+ examples: jsdoc.examples || [],
22923
+ params
22924
+ };
22925
+
22926
+ // extract the data from an mcp tag if present
22927
+ if (mcpTag !== null) {
22928
+ handlerData.mcp = true;
22929
+ if (mcpTag.includes('name:') || mcpTag.includes('description:')) {
22930
+ const nameMatch = /name:\s*\"?([^\"]+)\"?/.exec(mcpTag);
22931
+ const descMatch = /description:\s*\"?([^\"]+)\"?/.exec(mcpTag);
22932
+ if (nameMatch) handlerData.mcpName = nameMatch[1];
22933
+ if (descMatch) handlerData.mcpDescription = descMatch[1];
22934
+ }
22935
+ }
22936
+
22937
+ // and put it in the nodemap
22938
+ nodeMap.get(node).handles.push(handlerData);
22939
+ }
22940
+
22941
+ // determines if a name is the name for a handler
22942
+ function isHandler(name) {
22943
+ if (typeof name !== 'string') return false;
22944
+
22945
+ const clean = name.replace(/^['"]|['"]$/g, '');
22946
+ return clean.startsWith('on') || clean.startsWith('->');
22947
+ }
22948
+
22949
+ // Get the parameter description from the function or method
22950
+ function getParamDocs(fnOrMethod) {
22951
+
22952
+ const docs = fnOrMethod.getJsDocs?.() ?? [];
22953
+ const tags = docs.flatMap(d => d.getTags());
22954
+ const paramDocs = {};
22955
+
22956
+ for (const tag of tags) {
22957
+ if (tag.getTagName() === 'param') {
22958
+ const name = tag.getNameNode()?.getText?.() || tag.getName();
22959
+ const desc = tag.getComment() ?? '';
22960
+ const type = tag.getTypeNode?.()?.getText?.() || tag.getTypeExpression()?.getTypeNode()?.getText();
22961
+ paramDocs[name] = { description: desc, type };
22962
+ }
22963
+ }
22964
+ return paramDocs;
22965
+ }
22966
+
22967
+ // Get the jsdoc
22968
+ function getFullJsDoc(node) {
22969
+
22970
+ const docs = node.getJsDocs?.() ?? [];
22971
+ const summary = docs.map(d => d.getComment()).filter(Boolean).join('\n');
22972
+ const tags = docs.flatMap(d => d.getTags()).map(t => ({
22973
+ tagName: t.getTagName(),
22974
+ comment: t.getComment() || ''
22975
+ }));
22976
+
22977
+ const returns = tags.find(t => t.tagName === 'returns')?.comment || '';
22978
+ const examples = tags.filter(t => t.tagName === 'example').map(t => t.comment);
22979
+
22980
+ return { summary, returns, examples, tags };
22981
+ }
22982
+
22983
+ function hasDocMetadata(jsdoc) {
22984
+ if (!jsdoc) return false;
22985
+ if (jsdoc.summary && jsdoc.summary.trim()) return true;
22986
+ return Array.isArray(jsdoc.tags) && jsdoc.tags.length > 0;
22987
+ }
22988
+
22989
+ // make a parameter description
22990
+ function describeParam(p, docTags = {}) {
22991
+
22992
+ const nameNode = p.getNameNode();
22993
+
22994
+ if (nameNode.getKindName() === 'ObjectBindingPattern') {
22995
+
22996
+ const objType = p.getType();
22997
+ const properties = objType.getProperties();
22998
+ const isTSFallback = objType.getText() === 'any' || objType.getText() === 'string' || properties.length === 0;
22999
+
23000
+ return nameNode.getElements().map(el => {
23001
+
23002
+ const subName = el.getName();
23003
+ const doc = docTags[subName] ?? {};
23004
+ let tsType = null;
23005
+
23006
+ if (!isTSFallback) {
23007
+ const symbol = objType.getProperty(subName);
23008
+ if (symbol) {
23009
+ const resolvedType = symbol.getTypeAtLocation(el);
23010
+ const text = resolvedType.getText();
23011
+ if (text && text !== 'any') {
23012
+ tsType = text;
23013
+ }
23014
+ }
23015
+ }
23016
+
23017
+ const type = tsType || doc.type || 'string';
23018
+ const description = doc.description || '';
23019
+ return { name: subName, type, description };
23020
+ });
23021
+ }
23022
+
23023
+ const name = p.getName();
23024
+ const doc = docTags[name] ?? {};
23025
+ const tsType = p.getType().getText();
23026
+
23027
+ return {
23028
+ name,
23029
+ type: doc.type || tsType || 'string',
23030
+ description: doc.description || '',
23031
+ };
23032
+ }
23033
+
23034
+ function applyNodeTag(rawTag) {
23035
+ const { nodeName, aliases } = parseNodeTag(rawTag);
23036
+ if (!nodeName) return null;
23037
+ registerNodeContext(nodeName, aliases);
23038
+ return { nodeName, aliases };
23039
+ }
23040
+
23041
+ function registerNodeContext(nodeName, aliases = []) {
23042
+ const normalizedNode = nodeName.trim();
23043
+ if (!normalizedNode) return;
23044
+ currentNode = normalizedNode;
23045
+
23046
+ aliases.forEach(alias => registerAlias(alias, normalizedNode));
23047
+
23048
+ const derivedAlias = deriveIdentifierFromNodeName(normalizedNode);
23049
+ if (derivedAlias) registerAlias(derivedAlias, normalizedNode);
23050
+ }
23051
+
23052
+ function registerAlias(alias, nodeName) {
23053
+ const cleaned = alias?.trim();
23054
+ if (!cleaned) return;
23055
+ if (!isValidIdentifier(cleaned)) return;
23056
+ if (!nodeAliases.has(cleaned)) {
23057
+ nodeAliases.set(cleaned, nodeName);
23058
+ }
23059
+ }
23060
+
23061
+ function parseNodeTag(rawTag) {
23062
+ if (!rawTag || typeof rawTag !== 'string') return { nodeName: null, aliases: [] };
23063
+
23064
+ let text = rawTag.trim();
23065
+ if (!text) return { nodeName: null, aliases: [] };
23066
+
23067
+ let nodeName = text;
23068
+ let aliasChunk = '';
23069
+
23070
+ const explicitMatch = text.match(/^(.*?)(?:\s+(?:@|as|=>|->|\||:)\s+)(.+)$/i);
23071
+ if (explicitMatch) {
23072
+ nodeName = explicitMatch[1].trim();
23073
+ aliasChunk = explicitMatch[2].trim();
23074
+ } else {
23075
+ const quotedMatch = text.match(/^["']([^"']+)["']\s+(.+)$/);
23076
+ if (quotedMatch) {
23077
+ nodeName = quotedMatch[1].trim();
23078
+ aliasChunk = quotedMatch[2].trim();
23079
+ }
23080
+ }
23081
+
23082
+ if (!aliasChunk) {
23083
+ const parts = text.split(/\s+/);
23084
+ if (parts.length > 1) {
23085
+ const candidateAlias = parts[parts.length - 1];
23086
+ const candidateNode = parts.slice(0, -1).join(' ');
23087
+ if (isValidIdentifier(candidateAlias) && isKnownIdentifier(candidateAlias)) {
23088
+ aliasChunk = candidateAlias;
23089
+ nodeName = candidateNode.trim();
23090
+ }
23091
+ }
23092
+ }
23093
+
23094
+ const aliases = aliasChunk
23095
+ ? aliasChunk.split(/[,\s]+/).map(a => a.trim()).filter(Boolean)
23096
+ : [];
23097
+
23098
+ return { nodeName, aliases };
23099
+ }
23100
+
23101
+ function deriveIdentifierFromNodeName(name) {
23102
+ const chunks = name.split(/[\s\-]+/).filter(Boolean);
23103
+ if (chunks.length === 0) return null;
23104
+ if (chunks.length === 1) {
23105
+ const single = chunks[0];
23106
+ return isValidIdentifier(single) ? single : null;
23107
+ }
23108
+ const [first, ...rest] = chunks;
23109
+ const camel = first.toLowerCase() + rest.map(capitalize).join('');
23110
+ return isValidIdentifier(camel) ? camel : null;
23111
+ }
23112
+
23113
+ function capitalize(word) {
23114
+ if (!word) return '';
23115
+ return word.charAt(0).toUpperCase() + word.slice(1);
23116
+ }
23117
+
23118
+ function isValidIdentifier(value) {
23119
+ return /^[A-Za-z_$][\w$]*$/.test(value);
23120
+ }
23121
+
23122
+ function isKnownIdentifier(name) {
23123
+ return knownIdentifiers.has(name);
23124
+ }
23125
+
23126
+ function collectKnownIdentifiers(sourceFile) {
23127
+ const identifiers = new Set();
23128
+
23129
+ sourceFile.getVariableDeclarations().forEach(decl => {
23130
+ const name = decl.getName();
23131
+ if (typeof name === 'string' && isValidIdentifier(name)) {
23132
+ identifiers.add(name);
23133
+ }
23134
+ });
23135
+
23136
+ sourceFile.getFunctions().forEach(fn => {
23137
+ const name = fn.getName?.();
23138
+ if (name && isValidIdentifier(name)) identifiers.add(name);
23139
+ });
23140
+
23141
+ sourceFile.getClasses().forEach(cls => {
23142
+ const name = cls.getName?.();
23143
+ if (name && isValidIdentifier(name)) identifiers.add(name);
23144
+ });
23145
+
23146
+ if (typeof sourceFile.getInterfaces === 'function') {
23147
+ sourceFile.getInterfaces().forEach(iface => {
23148
+ const name = iface.getName?.();
23149
+ if (name && isValidIdentifier(name)) identifiers.add(name);
23150
+ });
23151
+ }
23152
+
23153
+ if (typeof sourceFile.getTypeAliases === 'function') {
23154
+ sourceFile.getTypeAliases().forEach(alias => {
23155
+ const name = alias.getName?.();
23156
+ if (name && isValidIdentifier(name)) identifiers.add(name);
23157
+ });
23158
+ }
23159
+
23160
+ if (typeof sourceFile.getEnums === 'function') {
23161
+ sourceFile.getEnums().forEach(enm => {
23162
+ const name = enm.getName?.();
23163
+ if (name && isValidIdentifier(name)) identifiers.add(name);
23164
+ });
23165
+ }
23166
+
23167
+ sourceFile.getImportDeclarations().forEach(decl => {
23168
+ const defaultImport = decl.getDefaultImport();
23169
+ if (defaultImport) {
23170
+ const name = defaultImport.getText();
23171
+ if (isValidIdentifier(name)) identifiers.add(name);
23172
+ }
23173
+
23174
+ const namespaceImport = decl.getNamespaceImport();
23175
+ if (namespaceImport) {
23176
+ const nsName = typeof namespaceImport.getText === 'function'
23177
+ ? namespaceImport.getText()
23178
+ : namespaceImport.getName?.();
23179
+ if (nsName && isValidIdentifier(nsName)) identifiers.add(nsName);
23180
+ }
23181
+
23182
+ decl.getNamedImports().forEach(spec => {
23183
+ const alias = spec.getAliasNode()?.getText();
23184
+ if (alias && isValidIdentifier(alias)) {
23185
+ identifiers.add(alias);
23186
+ } else {
23187
+ const name = spec.getName();
23188
+ if (isValidIdentifier(name)) identifiers.add(name);
23189
+ }
23190
+ });
23191
+ });
23192
+
23193
+ return identifiers;
23194
+ }
23195
+
23196
+ /**
23197
+ * Finds tx.send or this.tx.send calls and maps them to their node context.
23198
+ *
23199
+ * @param {import('ts-morph').SourceFile} sourceFile - The source file being analyzed
23041
23200
  * @param {string} filePath - The (relative) path of the source file
23042
23201
  * @param {Map} nodeMap - Map from node name to metadata
23043
23202
  * @param {string|null} currentNode - Explicitly set node name (takes priority)
23044
23203
  */
23045
- function findTransmissions(sourceFile, filePath, nodeMap) {
23046
-
23047
- // Search all call expressions
23048
- sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach(node => {
23049
- const expr = node.getExpression();
23204
+ function findTransmissions(sourceFile, filePath, nodeMap) {
23205
+
23206
+ // Create a quick lookup from handler name + file to node name to attribute transmissions precisely.
23207
+ const handlerToNode = new Map();
23208
+ for (const [nodeName, meta] of nodeMap.entries()) {
23209
+ for (const handle of meta.handles) {
23210
+ if (!handle?.handler) continue;
23211
+ const key = createHandlerKey(handle.file ?? filePath, handle.handler);
23212
+ if (!handlerToNode.has(key)) handlerToNode.set(key, nodeName);
23213
+ }
23214
+ }
23215
+
23216
+ // Search all call expressions
23217
+ sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach(node => {
23218
+ const expr = node.getExpression();
23050
23219
 
23051
23220
  // check
23052
23221
  if (expr.getKind() !== SyntaxKind.PropertyAccessExpression) return
@@ -23063,15 +23232,24 @@ function findTransmissions(sourceFile, filePath, nodeMap) {
23063
23232
  const pin = args[0].getLiteralText();
23064
23233
 
23065
23234
  // Try to infer the class context of the tx.send call
23066
- const method = node.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
23067
- const classDecl = method?.getFirstAncestorByKind(SyntaxKind.ClassDeclaration) ?? node.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
23068
- const className = classDecl?.getName?.();
23069
-
23070
- // Priority order: currentNode > className > topLevelClass > 'global'
23071
- const nodeName = currentNode || className || topLevelClass || null;
23072
-
23073
- // check
23074
- if (!nodeName) return
23235
+ const method = node.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
23236
+ const classDecl = method?.getFirstAncestorByKind(SyntaxKind.ClassDeclaration) ?? node.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
23237
+ const className = classDecl?.getName?.();
23238
+ const handlerName = getEnclosingHandlerName(node);
23239
+ const handlerKey = handlerName ? createHandlerKey(filePath, handlerName) : null;
23240
+ const aliasCandidate = getAliasCandidate(expr);
23241
+ const aliasNode = aliasCandidate ? nodeAliases.get(aliasCandidate) ?? null : null;
23242
+
23243
+ // Priority order: handler lookup > alias mapping > current context > class fallback
23244
+ const nodeName = (handlerKey ? handlerToNode.get(handlerKey) : null)
23245
+ || aliasNode
23246
+ || currentNode
23247
+ || className
23248
+ || topLevelClass
23249
+ || null;
23250
+
23251
+ // check
23252
+ if (!nodeName) return
23075
23253
 
23076
23254
  // check if there is an entry for the node or create it
23077
23255
  nodeMap.has(nodeName) || nodeMap.set(nodeName, { handles: [], transmits: [] });
@@ -23082,7 +23260,70 @@ function findTransmissions(sourceFile, filePath, nodeMap) {
23082
23260
  file: filePath,
23083
23261
  line: node.getStartLineNumber()
23084
23262
  });
23085
- });
23263
+ });
23264
+ }
23265
+
23266
+ function getAliasCandidate(propertyAccess) {
23267
+ if (!propertyAccess || !propertyAccess.getExpression) return null;
23268
+ const target = propertyAccess.getExpression();
23269
+ const root = resolveRootIdentifier(target);
23270
+ if (!root) return null;
23271
+ if (root === 'tx' || root === 'this') return null;
23272
+ return root;
23273
+ }
23274
+
23275
+ function resolveRootIdentifier(expression) {
23276
+ if (!expression) return null;
23277
+ if (expression.isKind?.(SyntaxKind.Identifier)) {
23278
+ return expression.getText();
23279
+ }
23280
+ if (expression.isKind?.(SyntaxKind.ThisKeyword)) {
23281
+ return 'this';
23282
+ }
23283
+ if (expression.isKind?.(SyntaxKind.PropertyAccessExpression)) {
23284
+ return resolveRootIdentifier(expression.getExpression());
23285
+ }
23286
+ if (expression.isKind?.(SyntaxKind.ElementAccessExpression)) {
23287
+ return resolveRootIdentifier(expression.getExpression());
23288
+ }
23289
+ return null;
23290
+ }
23291
+
23292
+ function createHandlerKey(file, handler) {
23293
+ return `${file}::${handler}`;
23294
+ }
23295
+
23296
+ function getEnclosingHandlerName(callExpression) {
23297
+ const method = callExpression.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
23298
+ if (method?.getName?.()) return method.getName();
23299
+
23300
+ const funcDecl = callExpression.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration);
23301
+ if (funcDecl?.getName?.()) return funcDecl.getName();
23302
+
23303
+ const funcExpr = callExpression.getFirstAncestorByKind(SyntaxKind.FunctionExpression);
23304
+ if (funcExpr) {
23305
+ if (funcExpr.getName?.()) return funcExpr.getName();
23306
+ const prop = funcExpr.getFirstAncestorByKind(SyntaxKind.PropertyAssignment);
23307
+ if (prop?.getName?.()) return prop.getName();
23308
+ const variable = funcExpr.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
23309
+ if (variable) return variable.getName();
23310
+ }
23311
+
23312
+ const arrowFunc = callExpression.getFirstAncestorByKind(SyntaxKind.ArrowFunction);
23313
+ if (arrowFunc) {
23314
+ const prop = arrowFunc.getFirstAncestorByKind(SyntaxKind.PropertyAssignment);
23315
+ if (prop?.getName?.()) return prop.getName();
23316
+ const variable = arrowFunc.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
23317
+ if (variable) return variable.getName();
23318
+ }
23319
+
23320
+ const propAssignment = callExpression.getFirstAncestorByKind(SyntaxKind.PropertyAssignment);
23321
+ if (propAssignment?.getName?.()) return propAssignment.getName();
23322
+
23323
+ const varDecl = callExpression.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
23324
+ if (varDecl?.getName?.()) return varDecl.getName();
23325
+
23326
+ return null;
23086
23327
  }
23087
23328
 
23088
23329
  const SRC_DOC_VERSION = '0.2';
@@ -23109,7 +23350,7 @@ async function profile(argv = process.argv.slice(2)) {
23109
23350
  ? path.resolve(cli.outFile)
23110
23351
  : (() => {
23111
23352
  const { dir, name } = path.parse(absoluteModelPath);
23112
- return path.join(dir, `${name}-doc.json`);
23353
+ return path.join(dir, `${name}.prf.json`);
23113
23354
  })();
23114
23355
 
23115
23356
  if (cli.deltaFile) cli.deltaFile = path.resolve(cli.deltaFile);
@@ -23145,6 +23386,9 @@ async function profile(argv = process.argv.slice(2)) {
23145
23386
  const generatedAt = new Date().toISOString();
23146
23387
  for (const sourceFile of sourceFiles) {
23147
23388
 
23389
+ // display file scanned..
23390
+ // console.log(sourceFile.getFilePath())
23391
+
23148
23392
  // A file reference is always relative to the model file
23149
23393
  const filePath = path.relative(path.dirname(modelPath), sourceFile.getFilePath()).replace(/\\/g, '/');
23150
23394
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizualmodel/vmblu-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vmblu": "bin/vmblu.js"
@@ -24,6 +24,6 @@
24
24
  "scripts": {
25
25
  "vmblu": "vmblu",
26
26
  "local": "node ./bin/vmblu.js profile ../browser/vmblu.vmblu",
27
- "build:profile": "rollup -c ./commands/profile/rollup.config.js"
27
+ "build": "rollup -c ./commands/profile/rollup.config.js"
28
28
  }
29
29
  }