Skip to content

feat(dynamic-calls): phase 6 — C#/Swift/Elixir/Lua long-tail dynamic dispatch#1657

Merged
carlos-alm merged 6 commits into
mainfrom
feat/dynamic-call-phase6
Jun 21, 2026
Merged

feat(dynamic-calls): phase 6 — C#/Swift/Elixir/Lua long-tail dynamic dispatch#1657
carlos-alm merged 6 commits into
mainfrom
feat/dynamic-call-phase6

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

Stacked on PRs #1629-#1655. Phase 6 completes the language-family detection sweep for the long-tail languages.

Language Pattern Kind Result
C# type.GetMethod("name") reflection Detects; 0% recall (class-qualified DB names, #1650)
C# method.Invoke(target, args) unresolved-dynamic Sink edge
Swift NSSelectorFromString("greet") reflection ✅ 100% P/R (top-level fns unqualified)
Swift performSelector unresolved-dynamic Sink edge
Elixir apply(module, :function, args) reflection Detects; 0% recall (module-qualified, #1650)
Elixir apply(module, variable, args) computed-key Sink edge
Lua load(code) / loadstring eval Sink edge
Lua t["greet"]() resolved ✅ 100% P/R (string literal key)
Lua t[k]() computed-key Sink edge

Rust parity gap filed as issue #1656 — Rust extractors for C#, Swift, Elixir, Lua not yet updated (WASM only).

Benchmark results

  • dynamic-csharp: 0%/0% recall (class-qualified method names → RES-3)
  • dynamic-swift: 100%/100% — top-level Swift fns unqualified, same as Kotlin/Python/Ruby/Go
  • dynamic-elixir: 0%/0% recall (module-qualified function names → RES-3)
  • dynamic-lua: 100%/100% — string-literal bracket calls resolve; load(code) flagged

Pattern summary across all phases

100% recall languages (unqualified DB names): JS, TS, Python, Ruby, PHP, Go, C, Kotlin, Swift, Lua top-level
0% recall languages (class/module-qualified DB names): Java, Scala, Groovy, C#, Elixir → need RES-3 type-aware lookup

Issues filed

…dispatch

C# (csharp.ts):
- type.GetMethod("name") / GetRuntimeMethod / GetDeclaredMethod → reflection, keyExpr captured
- method.Invoke(target, args) → unresolved-dynamic sink edge

Swift (swift.ts):
- NSSelectorFromString("greet") → reflection kind, 100% P/R (top-level fns unqualified)
- performSelector → unresolved-dynamic sink edge

Elixir (elixir.ts):
- apply(module, :function, args) with atom literal → reflection kind
- apply(module, variable, args) → computed-key sink edge

Lua (lua.ts):
- load(code) / loadstring(code) / dofile(code) → eval kind, sink edge
- t["key"]() string-literal bracket call → resolved (computed-literal, 100% P/R)
- t[k]() variable bracket call → computed-key sink edge

Fixtures:
- dynamic-csharp: 0%/0% (class-qualified names, tracked for RES-3 in #1650)
- dynamic-swift: 100%/100% (NSSelectorFromString → top-level fn resolves)
- dynamic-elixir: 0%/0% (module-qualified names, tracked for RES-3)
- dynamic-lua: 100%/100% (t["greet"]() → local greet() resolves)

Rust parity gap filed as #1656 (C#, Swift, Elixir, Lua Rust extractors not updated).
docs check acknowledged

Impact: 21 functions changed, 11 affected
@greptile-apps

greptile-apps Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Phase 6 extends dynamic-dispatch detection to C#, Swift, Elixir, and Lua by adding extractors for GetMethod/Invoke reflection, NSSelectorFromString/performSelector, apply/3, and load/bracket-index patterns. It also carries forward fixes from prior review rounds (Python no-op ternary, Swift BFS false-positive, roles.ts confidence filter, types.ts comment accuracy).

  • C#: Detects type.GetMethod("name") as reflection and method.Invoke() as unresolved-dynamic; 0%/0% recall thresholds are set explicitly because class-qualified DB names prevent resolution until RES-3.
  • Swift: NSSelectorFromString with a string literal resolves at 100% P/R; performSelector emits a sink edge. The prior BFS false-positive is fixed by inspecting only direct call arguments.
  • Elixir: apply(module, :atom, args) emits a reflection edge and apply(module, variable, args) emits computed-key; however, the 2-arity apply(fun, args) form is mishandled — the argument list is read as the function name, producing spurious computed-key edges with incorrect keyExpr for any apply/2 call site.
  • Lua: load/loadstring/dofile are flagged as eval; string-literal bracket calls (t["greet"]()) resolve correctly; variable bracket calls emit computed-key.

Confidence Score: 4/5

Safe to merge with one known defect: the Elixir extractor misidentifies 2-arity apply/2 calls, producing incorrect computed-key edges for anonymous function applications in any Elixir codebase.

The Elixir apply handler reads the second argument unconditionally as a function name selector. For the 3-arity apply(module, :atom, args) form this is correct, but for the 2-arity apply(fun, args) form the second argument is an argument list — not a function name. Every apply/2 call site emits a spurious computed-key edge with keyExpr set to the argument list text, which is incorrect graph data for any Elixir project using anonymous function application.

src/extractors/elixir.ts — the apply case block needs arity detection to avoid mishandling the 2-arity form.

Important Files Changed

Filename Overview
src/extractors/elixir.ts Adds apply/3 dynamic dispatch detection; the 2-arity apply/2 form is mishandled — its argument list is treated as a function name selector, emitting spurious computed-key edges.
src/extractors/csharp.ts Adds GetMethod/Invoke dynamic detection; Invoke matches any method of that name (delegates, custom classes), but this is conservative over-detection acknowledged in the PR.
src/extractors/lua.ts Adds load/loadstring/dofile eval detection and bracket_index_expression dynamic dispatch handling; logic is correct.
src/extractors/swift.ts Adds NSSelectorFromString and performSelector handling; replaces the prior BFS subtree search with direct argument inspection, fixing the previously flagged false-positive issue.
tests/benchmarks/resolution/resolution-benchmark.test.ts Adds Phase 6 thresholds out of sequence (before Phase 5); precision: 0.0 for C# and Elixir leaves no CI gate for emission regressions in those extractors.
src/extractors/python.ts Fixes the no-op ternary: else branch now returns undefined for non-identifier receivers, preventing spurious lookup mismatches.
src/domain/analysis/roles.ts Adds AND confidence = 0 to dynamicCallsData query, aligning it with the benchmark's extractFlaggedDynamicCalls filter.
src/types.ts Updates reflection DynamicKind comment to accurately describe the silent-drop behavior (RES-3), replacing the misleading 'flagged if target unknown'.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Extractor sees call node] --> B{Language}

    B --> CS[C#]
    CS --> CS1{member_access_expression method name?}
    CS1 -->|Invoke| CS2[unresolved-dynamic sink edge]
    CS1 -->|GetMethod / GetRuntimeMethod / GetDeclaredMethod| CS3{string literal arg?}
    CS3 -->|yes| CS4[reflection edge — name = literal]
    CS3 -->|no| CS5[computed-key sink edge]
    CS1 -->|other| CS6[normal call edge]

    B --> SW[Swift]
    SW --> SW1{call.name?}
    SW1 -->|performSelector| SW2[unresolved-dynamic sink edge]
    SW1 -->|NSSelectorFromString| SW3{direct first arg is string literal?}
    SW3 -->|yes| SW4[reflection edge — name = literal]
    SW3 -->|no| SW5[unresolved-dynamic sink edge]
    SW1 -->|other| SW6[normal call edge]

    B --> EX[Elixir]
    EX --> EX1{keyword = apply?}
    EX1 -->|3-arity: apply M F args| EX2{2nd arg type?}
    EX2 -->|atom / atom_literal| EX3[reflection edge — name = stripped atom]
    EX2 -->|identifier / other| EX4[computed-key sink edge]
    EX1 -->|2-arity: apply fun args| EX5[computed-key sink edge — keyExpr = args list WRONG]
    EX1 -->|no| EX6[normal call edge]

    B --> LU[Lua]
    LU --> LU1{nameNode type?}
    LU1 -->|identifier: load / loadstring / dofile| LU2[eval sink edge]
    LU1 -->|bracket_index_expression| LU3{key type?}
    LU3 -->|string / string_literal| LU4[normal call — name = stripped literal]
    LU3 -->|other| LU5[computed-key sink edge]
    LU1 -->|other| LU6[normal call edge]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Extractor sees call node] --> B{Language}

    B --> CS[C#]
    CS --> CS1{member_access_expression method name?}
    CS1 -->|Invoke| CS2[unresolved-dynamic sink edge]
    CS1 -->|GetMethod / GetRuntimeMethod / GetDeclaredMethod| CS3{string literal arg?}
    CS3 -->|yes| CS4[reflection edge — name = literal]
    CS3 -->|no| CS5[computed-key sink edge]
    CS1 -->|other| CS6[normal call edge]

    B --> SW[Swift]
    SW --> SW1{call.name?}
    SW1 -->|performSelector| SW2[unresolved-dynamic sink edge]
    SW1 -->|NSSelectorFromString| SW3{direct first arg is string literal?}
    SW3 -->|yes| SW4[reflection edge — name = literal]
    SW3 -->|no| SW5[unresolved-dynamic sink edge]
    SW1 -->|other| SW6[normal call edge]

    B --> EX[Elixir]
    EX --> EX1{keyword = apply?}
    EX1 -->|3-arity: apply M F args| EX2{2nd arg type?}
    EX2 -->|atom / atom_literal| EX3[reflection edge — name = stripped atom]
    EX2 -->|identifier / other| EX4[computed-key sink edge]
    EX1 -->|2-arity: apply fun args| EX5[computed-key sink edge — keyExpr = args list WRONG]
    EX1 -->|no| EX6[normal call edge]

    B --> LU[Lua]
    LU --> LU1{nameNode type?}
    LU1 -->|identifier: load / loadstring / dofile| LU2[eval sink edge]
    LU1 -->|bracket_index_expression| LU3{key type?}
    LU3 -->|string / string_literal| LU4[normal call — name = stripped literal]
    LU3 -->|other| LU5[computed-key sink edge]
    LU1 -->|other| LU6[normal call edge]
Loading

Reviews (7): Last reviewed commit: "fix: resolve merge conflicts with main" | Re-trigger Greptile

Comment thread src/extractors/swift.ts
Comment on lines +297 to 332
if (call.name === 'NSSelectorFromString') {
// Search the call_expression subtree for a string literal (BFS)
const queue: TreeSitterNode[] = [node];
let literal: string | null = null;
while (queue.length > 0 && !literal) {
const curr = queue.shift()!;
if (curr.type === 'line_string_literal' || curr.type === 'string_literal') {
// Look for inner string_content node
for (let i = 0; i < curr.childCount; i++) {
const ch = curr.child(i);
if (ch?.type === 'string_content') {
literal = ch.text;
break;
}
}
if (!literal) literal = curr.text.replace(/^["']|["']$/g, '');
break;
}
for (let i = 0; i < curr.childCount; i++) {
const ch = curr.child(i);
if (ch) queue.push(ch);
}
}
ctx.calls.push({
name: literal ?? '<dynamic:unresolved>',
line: call.line,
dynamic: true,
dynamicKind: literal ? 'reflection' : 'unresolved-dynamic',
keyExpr: literal ?? undefined,
});
return;
}

ctx.calls.push(call);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 NSSelectorFromString BFS matches nested string literals

The BFS starts from the entire call_expression node and accepts the first line_string_literal or string_literal it finds anywhere in the subtree. For code like NSSelectorFromString(generateSelector("greet")) or NSSelectorFromString(dict["greet"]), the BFS descends into the nested call/subscript and extracts "greet" as if it were the directly-passed selector. This creates a false-positive reflection edge pointing to greet when the actual runtime selector is not statically knowable. The fix is to look only at the direct arguments of the call rather than BFS-ing the entire subtree.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — replaced the BFS over the entire call_expression subtree with a direct argument inspection. The new implementation walks only the direct children of the argument_list (or call_suffix/value_arguments), stopping at the first real argument. Nested string literals inside sub-expressions (e.g. NSSelectorFromString(generateSelector("greet"))) are no longer extracted.

Comment thread src/extractors/python.ts Outdated
const argIter = iterPyArgs(node);
const firstArg = argIter.next().value as TreeSitterNode | undefined;
const secondArg = argIter.next().value as TreeSitterNode | undefined;
const receiver = firstArg?.type === 'identifier' ? firstArg.text : firstArg?.text;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No-op ternary — receiver is always firstArg?.text

The condition firstArg?.type === 'identifier' ? firstArg.text : firstArg?.text evaluates to firstArg?.text in both branches. If the intent was to restrict receiver to simple identifiers (e.g. avoid setting a complex expression like obj.attr as the receiver), the else branch should be undefined; as written, complex expressions are silently accepted as receiver strings, which can cause spurious lookup mismatches downstream.

Suggested change
const receiver = firstArg?.type === 'identifier' ? firstArg.text : firstArg?.text;
const receiver = firstArg?.type === 'identifier' ? firstArg.text : undefined;

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — changed the else branch from firstArg?.text to undefined. Now only simple identifiers are used as the receiver string; complex expressions (e.g. obj.attr, self.registry) are excluded, avoiding spurious lookup mismatches downstream.

if targets.is_empty() {
if let Some(ref dk) = call.dynamic_kind {
if dk == "eval" || dk == "computed-key" || dk == "unresolved-dynamic" {
let sink_key = (caller_id, fc.file_node_id, dk.as_bytes()[0]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 First-byte dedup key is fragile for future dynamic_kind values

dk.as_bytes()[0] discriminates the three current kinds by their first ASCII byte (e/c/u). Any future kind whose name starts with the same letter as an existing one (e.g. "computed-call" vs "computed-key") would silently be treated as a duplicate and the second sink edge would be suppressed. Using the full string as the discriminator is cheap (all values are short) and eliminates the hazard.

Suggested change
let sink_key = (caller_id, fc.file_node_id, dk.as_bytes()[0]);
let sink_key = (caller_id, fc.file_node_id, dk.as_str());

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in the merge conflict resolution — the seen_sink_edges type was changed from HashSet<(u32, u32, u8)> (first byte) to HashSet<(u32, u32, String)> (full kind string), and the key construction was updated to use dk.clone(). The full kind string discriminator eliminates the first-byte collision hazard you identified.

Comment thread src/types.ts Outdated
export type DynamicKind =
| 'computed-literal' // obj["foo"]() — resolvable; already emitted as normal edge
| 'computed-key' // obj[k]() — potentially resolvable via pts; else flagged
| 'reflection' // .call/.apply/.bind — usually resolved; flagged if target unknown

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading comment on reflection — it is never flagged when unresolved

The inline comment says "flagged if target unknown" but reflection is intentionally excluded from FLAG_ONLY_KINDS in build-edges.ts; an unresolved reflection call is silently dropped, not emitted as a sink edge. The comment should say "resolved if target reachable; silently dropped if not (tracked by RES-3)".

Suggested change
| 'reflection' // .call/.apply/.bind — usually resolved; flagged if target unknown
| 'reflection' // .call/.apply/.bind — resolved if target reachable; silently dropped if not (RES-3)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated — the comment now reads: 'resolved if target reachable; silently dropped if not (RES-3)', matching the actual behavior where reflection calls are excluded from FLAG_ONLY_KINDS and silently dropped when unresolved.

Comment on lines +17 to +27
try {
return db
.prepare(
`SELECT dynamic_kind, COUNT(*) AS count
FROM edges
WHERE dynamic_kind IS NOT NULL
GROUP BY dynamic_kind
ORDER BY count DESC`,
)
.all() as DynamicCallCount[];
} finally {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 dynamicCallsData does not filter by confidence = 0

The query counts all edges where dynamic_kind IS NOT NULL, while the mirror query in resolution-benchmark.test.ts (extractFlaggedDynamicCalls) also requires confidence = 0. Today these are equivalent because dynamic_kind is only written to sink edges (confidence=0), but the invariant is implicit and not enforced here. If a future change sets dynamic_kind on a resolved edge the two counters will silently diverge. Adding AND confidence = 0 makes the intent explicit and future-proof.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added AND confidence = 0 to the WHERE clause in dynamicCallsData. This makes the intent explicit and ensures the query stays aligned with the benchmark's extractFlaggedDynamicCalls if dynamic_kind is ever written to a non-sink edge in the future.

Impact: 29 functions changed, 165 affected
…, roles query (#1657)

Impact: 3 functions changed, 6 affected
carlos-alm added a commit that referenced this pull request Jun 21, 2026
…, roles query (#1657)

Impact: 3 functions changed, 6 affected
@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

26 functions changed16 callers affected across 8 files

  • dynamicCallsData in src/domain/analysis/roles.ts:15 (1 transitive callers)
  • getCsFirstStringArg in src/extractors/csharp.ts:230 (3 transitive callers)
  • getCsFirstStringArg in src/extractors/csharp.ts:231 (3 transitive callers)
  • handleCsInvocationExpr in src/extractors/csharp.ts:248 (2 transitive callers)
  • handleCsInvocationExpr in src/extractors/csharp.ts:249 (2 transitive callers)
  • handleElixirCall in src/extractors/elixir.ts:53 (2 transitive callers)
  • handleLuaFunctionCall in src/extractors/lua.ts:131 (2 transitive callers)
  • handlePhpFuncCall in src/extractors/php.ts:321 (2 transitive callers)
  • handlePyCall in src/extractors/python.ts:170 (2 transitive callers)
  • handleSwiftCallExpression in src/extractors/swift.ts:249 (2 transitive callers)
  • Reflection in tests/benchmarks/resolution/fixtures/dynamic-csharp/Reflection.cs:6 (0 transitive callers)
  • Reflection.Greet in tests/benchmarks/resolution/fixtures/dynamic-csharp/Reflection.cs:7 (0 transitive callers)
  • Reflection.RunGetMethod in tests/benchmarks/resolution/fixtures/dynamic-csharp/Reflection.cs:10 (0 transitive callers)
  • Reflection.RunInvoke in tests/benchmarks/resolution/fixtures/dynamic-csharp/Reflection.cs:15 (0 transitive callers)
  • DynamicDispatch.greet in tests/benchmarks/resolution/fixtures/dynamic-elixir/dispatch.ex:6 (0 transitive callers)
  • DynamicDispatch.farewell in tests/benchmarks/resolution/fixtures/dynamic-elixir/dispatch.ex:7 (0 transitive callers)
  • DynamicDispatch.run_apply_atom in tests/benchmarks/resolution/fixtures/dynamic-elixir/dispatch.ex:10 (0 transitive callers)
  • DynamicDispatch.run_apply_variable in tests/benchmarks/resolution/fixtures/dynamic-elixir/dispatch.ex:15 (0 transitive callers)
  • greet in tests/benchmarks/resolution/fixtures/dynamic-lua/dispatch.lua:6 (1 transitive callers)
  • run_literal_dispatch in tests/benchmarks/resolution/fixtures/dynamic-lua/dispatch.lua:11 (0 transitive callers)

…icate UPDATE (#1657)

- swift.ts: fix NSSelectorFromString argument parsing — resolve through
  call_suffix/value_arguments/value_argument tree, look for line_str_text
  (not string_content). Fixes dynamic-swift benchmark: was 0/0 recall.

- lua.ts: replace fragile positional child(childCount-2) with an id-based
  scan, eliminating the Greptile-flagged off-by-one hazard.

- build-edges.ts: remove duplicate dynamic_kind UPDATE block that lacked
  the confidence=0.0 AND dynamic=1 guards.

- c.ts, cpp.ts, csharp.ts: remove unused Call import.

Impact: 3 functions changed, 8 affected
@carlos-alm carlos-alm force-pushed the feat/dynamic-call-phase6 branch from 1692faa to e8f8df5 Compare June 21, 2026 05:02
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Addressed all Greptile feedback and fixed CI failures:

CI fixes:

  • dynamic-swift 0% precision/recall → Fixed: NSSelectorFromString argument extraction was using old BFS code path in dist; updated src to traverse call_suffix→value_arguments→value_argument→line_str_text (Swift uses line_str_text, not string_content). Both benchmark and unit tests now pass 100%/100%.
  • Validate commits failure → Fixed: The uppercase 'Phase 1' commit (subject-case violation) fell out of the commitlint range after phases 0–3 were merged to main. Force-pushed with rewritten history.

Greptile feedback addressed (from latest review):

  • build-edges.ts duplicate UPDATE block → Removed the second back-fill UPDATE block that lacked confidence=0.0 AND dynamic=1 guards. Only the guarded block remains.
  • lua.ts fragile child(childCount-2) → Replaced with an id-based scan: skips '[', ']', and the table node by id, robust to future AST shape changes.

Inline comments addressed (in previous commit):

  • swift.ts: BFS false-positive → direct arg inspection (already replied)
  • python.ts: no-op ternary receiver → undefined for non-identifiers (already replied)
  • build_edges.rs: first-byte dedup → full kind string (already replied)
  • types.ts: misleading reflection comment → updated (already replied)
  • roles.ts: dynamicCallsData confidence guard → AND confidence = 0 added (already replied)

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

Resolves conflicts after phase 3 merged to main as #1653:
- python.ts: keep identifier-only receiver fix
- overview.ts: incorporate dead sub-role metadata from main
- types.ts, resolution-benchmark.test.ts, dynamic-call-ffi.test.ts:
  auto-resolved to retain our phase 4/5/6 additions
docs check acknowledged

Impact: 4 functions changed, 7 affected
@carlos-alm carlos-alm merged commit 77c1309 into main Jun 21, 2026
29 checks passed
@carlos-alm carlos-alm deleted the feat/dynamic-call-phase6 branch June 21, 2026 07:28
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant