Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions agent/scripts/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ def load_value_lists_index(solution_dir):
def load_custom_functions_index(solution_dir):
return _parse_index(
solution_dir / "custom_functions.index",
["name", "id", "parameters", "access", "display", "category"],
["name", "id", "parameters", "access", "display", "category",
"folder_path"],
)


Expand Down Expand Up @@ -1429,13 +1430,25 @@ def analyze_custom_functions(solution_name):

stub_dir = XML_PARSED_DIR / "custom_function_stubs" / solution_name

cf_files = sorted(cf_dir.glob("*.txt"))
# Recurse into folder subdirectories — CFs organised into FileMaker
# folders are nested in "{FolderName} - ID N/" subdirs, and a flat
# glob would silently skip them.
cf_files = sorted(cf_dir.rglob("*.txt"))
functions = {}
all_cf_names = set()

# First pass: collect names and read all content
cf_data = [] # (name, id, text, param_count) — single pass I/O
cf_data = [] # (name, id, text, param_count, folder_path) — single pass I/O
for cf_path in cf_files:
# Folder/separator pseudo-files (e.g. "ACCOUNTS - ID 39.txt",
# "-- - ID 37.txt") are emitted at the flat level by fmparse.sh
# alongside the per-folder subdirs. They have no backing stub XML
# and are not real custom functions — skip them.
rel_parent = cf_path.parent.relative_to(cf_dir)
stub_path = stub_dir / rel_parent / f"{cf_path.stem}.xml"
if not stub_path.exists():
continue

name = cf_path.stem.rsplit(" - ID ", 1)[0]
all_cf_names.add(name)
cf_id = None
Expand All @@ -1448,19 +1461,21 @@ def analyze_custom_functions(solution_name):
except (OSError, UnicodeDecodeError):
continue

# Read param count from stub XML (ObjectList/@membercount)
# Derive the folder path (parent dirs between the solution root and
# the file), stripping " - ID N" suffixes to match scripts/layouts.
folder_path = re.sub(r' - ID \d+', '', str(rel_parent)) if str(rel_parent) != '.' else ''

# Read param count from stub XML (ObjectList/@membercount).
param_count = 0
stub_path = stub_dir / f"{cf_path.stem}.xml"
if stub_path.exists():
try:
stub_text = stub_path.read_text(encoding="utf-8")
mc_match = re.search(r'membercount="(\d+)"', stub_text)
if mc_match:
param_count = int(mc_match.group(1))
except (OSError, UnicodeDecodeError):
pass
try:
stub_text = stub_path.read_text(encoding="utf-8")
mc_match = re.search(r'membercount="(\d+)"', stub_text)
if mc_match:
param_count = int(mc_match.group(1))
except (OSError, UnicodeDecodeError):
pass

cf_data.append((name, cf_id, text, param_count))
cf_data.append((name, cf_id, text, param_count, folder_path))

# Patterns for classification
_FIELD_REF_RE = re.compile(r'[A-Za-z_][A-Za-z0-9_ ]*::[A-Za-z_]')
Expand All @@ -1473,7 +1488,7 @@ def analyze_custom_functions(solution_name):
re.IGNORECASE,
)

for name, cf_id, text, param_count in cf_data:
for name, cf_id, text, param_count, folder_path in cf_data:

# Find references to other custom functions (substring match)
deps = sorted(
Expand All @@ -1487,6 +1502,7 @@ def analyze_custom_functions(solution_name):
"id": cf_id,
"param_count": param_count,
"line_count": line_count,
"folder_path": folder_path,
"dependencies": deps,
"text": text,
}
Expand Down
13 changes: 6 additions & 7 deletions agent/scripts/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,11 @@ def build_cf_names(solution_name):
cfs = []
if not cf_dir.exists():
return cfs
for f in cf_dir.iterdir():
if f.suffix == ".txt":
# Parse "FuncName - ID NNN.txt"
m = re.match(r'^(.+?)\s*-\s*ID\s+(\d+)\.txt$', f.name)
if m:
cfs.append({"name": m.group(1), "id": m.group(2), "path": f})
for f in cf_dir.rglob("*.txt"):
# Parse "FuncName - ID NNN.txt"
m = re.match(r'^(.+?)\s*-\s*ID\s+(\d+)\.txt$', f.name)
if m:
cfs.append({"name": m.group(1), "id": m.group(2), "path": f})
return cfs


Expand Down Expand Up @@ -383,7 +382,7 @@ def parse_scripts(solution_name, scripts_index, to_map, cf_names):
))

# --- Custom function references in expressions ---
if "[" in line: # Only scan lines with parameters
if "[" in line or "]" in line: # Catch both opening and continuation lines
for cf in cf_name_set:
# Match CF name with parens (function call) or standalone (zero-param)
pattern = re.compile(
Expand Down
20 changes: 15 additions & 5 deletions fmcontext.sh
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ mkdir -p "$CONTEXT_DIR"
# Discover solutions to process
# ---------------------------------------------------------------------------
if [[ -z "$SOLUTION_NAME" ]]; then
mapfile -t SOLUTIONS < <(
# Avoid `mapfile` — it requires bash 4+, and macOS ships bash 3.2.
SOLUTIONS=()
while IFS= read -r _solution_line; do
SOLUTIONS+=("$_solution_line")
done < <(
find "$XML_PARSED_DIR" -mindepth 2 -maxdepth 2 -type d \
| sed 's|.*/||' | sort -u
)
Expand Down Expand Up @@ -392,7 +396,7 @@ for SOLUTION in "${SOLUTIONS[@]}"; do
# functional – everything else
# ---------------------------------------------------------------------------
{
echo "# FunctionName|FunctionID|Parameters|Access|Display|Category"
echo "# FunctionName|FunctionID|Parameters|Access|Display|Category|FolderPath"

STUB_DIR="$XML_PARSED_DIR/custom_function_stubs/$SOLUTION"
SANITIZED_DIR="$XML_PARSED_DIR/custom_functions_sanitized/$SOLUTION"
Expand All @@ -404,6 +408,8 @@ for SOLUTION in "${SOLUTIONS[@]}"; do
cf_access=$(xval 'string(/CustomFunction/@access)' "$file")
cf_display=$(xval 'string(/CustomFunction/Display)' "$file")

folder_path=$(get_folder_path "$file" "$XML_PARSED_DIR/custom_function_stubs")

# Extract parameter names from stub
param_count=$(xval 'string(/CustomFunction/ObjectList/@membercount)' "$file")
params=""
Expand All @@ -418,9 +424,13 @@ for SOLUTION in "${SOLUTIONS[@]}"; do
done
fi

# Classify using the sanitized body
# Classify using the sanitized body — derive the body path from the
# stub path so nested-in-folder CFs resolve correctly (flat lookup
# silently misses them and leaves every folder-nested CF mis-
# classified as "functional").
category="functional"
txt_file="$SANITIZED_DIR/${cf_name} - ID ${cf_id}.txt"
stub_rel="${file#"$STUB_DIR/"}"
txt_file="$SANITIZED_DIR/${stub_rel%.xml}.txt"

if [[ -f "$txt_file" ]]; then
body=$(<"$txt_file")
Expand Down Expand Up @@ -451,7 +461,7 @@ for SOLUTION in "${SOLUTIONS[@]}"; do
fi
fi

echo "${cf_name}|${cf_id}|${params}|${cf_access}|${cf_display}|${category}"
echo "${cf_name}|${cf_id}|${params}|${cf_access}|${cf_display}|${category}|${folder_path}"
done
fi
} > "$SOLUTION_CONTEXT_DIR/custom_functions.index"
Expand Down