diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md
index 525b62110..43d91eca5 100644
--- a/graph/bundle/SCHEMA.md
+++ b/graph/bundle/SCHEMA.md
@@ -507,6 +507,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h
| `TAGGED_TEMPLATE_CALL` | javascript | tag`…`. |
| `DYNAMIC_CODE_CALL` | javascript | `eval(…)` / `new Function(…)` — unknowable by construction. |
| `DYNAMIC_IMPORT_CALL` | javascript | `import(…)` — a module load that is also a site. |
+| `JSX_ELEMENT` | javascript | `` — the renderer runs the component (a function component, or a class component's constructor and `render`) with the element's attributes as its props. Engine-authored: no written call; the site is the JSX element expression. An intrinsic tag (`
`) is no site. |
| `SIMPLE_CALL` | python | `f(…)` — a bare name. |
| `METHOD_CALL` | python | `obj.m(…)`. |
| `CHAINED_CALL` | python | `a.b().c(…)` — the receiver is itself a call. |
diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts
index dad9c147e..f94f8cd50 100644
--- a/graph/bundle/schema.ts
+++ b/graph/bundle/schema.ts
@@ -640,6 +640,7 @@ export const VOCAB: readonly VocabSpec[] = [
{ table: 'call_edges', column: 'kind', value: 'TAGGED_TEMPLATE_CALL', languages: S, meaning: 'tag`…`.' },
{ table: 'call_edges', column: 'kind', value: 'DYNAMIC_CODE_CALL', languages: S, meaning: '`eval(…)` / `new Function(…)` — unknowable by construction.' },
{ table: 'call_edges', column: 'kind', value: 'DYNAMIC_IMPORT_CALL', languages: S, meaning: '`import(…)` — a module load that is also a site.' },
+ { table: 'call_edges', column: 'kind', value: 'JSX_ELEMENT', languages: S, meaning: '`` — the renderer runs the component (a function component, or a class component\'s constructor and `render`) with the element\'s attributes as its props. Engine-authored: no written call; the site is the JSX element expression. An intrinsic tag (``) is no site.' },
// — Python (the parser's callKind, plus engine-authored decorator/metaclass forms)
{ table: 'call_edges', column: 'kind', value: 'SIMPLE_CALL', languages: P, meaning: '`f(…)` — a bare name.' },
{ table: 'call_edges', column: 'kind', value: 'METHOD_CALL', languages: P, meaning: '`obj.m(…)`.' },
diff --git a/graph/javascript/README.md b/graph/javascript/README.md
index b3595b612..6a26b2abb 100644
--- a/graph/javascript/README.md
+++ b/graph/javascript/README.md
@@ -191,8 +191,10 @@ constructor), `TYPE_ONLY_TARGET` (the compiler named a JSDoc function type),
## What is not built
-* **JSX component calls do not exist in the IR.** The JavaScript schema has no JSX call
- kind, so `` is neither a site nor a miss; the oracle counts and excludes them.
+* **JSX components reached through a wrapper are not followed.** `` is an edge
+ (kind `JSX_ELEMENT`, call-edge-generation/jsx.dl) and its attributes are the props the
+ component receives; a tag whose value is `memo(C)`, `connect(…)(C)` or another unstaged
+ library's return stays `ambiguous_unknown`. The oracle still excludes JSX sites.
* **Primitive results are not typed.** `this.greet().toUpperCase()` is
`ambiguous_unknown`, not `ambient_terminal`: nothing here knows `greet` returns a
string.
diff --git a/graph/javascript/engine/call-edge-generation/jsx.dl b/graph/javascript/engine/call-edge-generation/jsx.dl
new file mode 100644
index 000000000..9a63833e7
--- /dev/null
+++ b/graph/javascript/engine/call-edge-generation/jsx.dl
@@ -0,0 +1,84 @@
+// ============================================================================
+// CALL-EDGE-GEN · JSX ELEMENTS (`` renders Component)
+//
+// A JSX element whose tag is a reference — ``, `` — is a call
+// of that component by the renderer: a function component runs with the element's
+// props as its first argument; a class component is constructed with them and its
+// `render` runs. The parser emits the element as a JSX_ELEMENT expression with the
+// tag under JSX_TAG_NAME and each attribute as a JSX_ATTRIBUTE_VALUE row (role
+// PROPERTY_KEY, name = the prop) whose PROPERTY_VALUE child is the value. No
+// js_call_site row exists for it, so before this file the element was neither an
+// edge nor a miss: `impact TodoList` answered "nothing uses it".
+//
+// An INTRINSIC tag (``, ``) is JSX_INTRINSIC_ELEMENT and has no
+// tag reference: it names no project code and gets no row here.
+//
+// Two effects, both through the ordinary value layer:
+// 1. an edge per element: call_chain_edge(Element, Caller, -, Component, …,
+// "JSX_ELEMENT"), tiered like a call (one target known_edge, several
+// multi_inferred); a tag with no value is ambiguous_unknown — a declared miss,
+// never silence.
+// 2. the element's attributes are an object value keyed by the element, and it is
+// what the component receives: `` makes
+// `onToggle(todo)` inside `TodoItem({ onToggle })` resolve to handleToggle.
+// Brace children are the `children` prop (a render prop is a function).
+//
+// Not modelled: a component reached only through a library wrapper
+// (`memo(C)`, `connect(…)(C)`, `forwardRef(fn)`) whose library is not staged — the
+// tag's value is the wrapper's return, which is unknown, and the element stays an
+// ambiguous_unknown row.
+// ============================================================================
+
+// ── the element and what its tag holds ──────────────────────────────────────
+jsx_element_tag(je, tag) :- expr_kind(_, "JSX_ELEMENT", _, je), expr_child(_, je, "JSX_TAG_NAME", _, tag).
+jsx_tag_value(je, k, i) :- jsx_element_tag(je, tag), expr_value(tag, k, i).
+// A CommonJS module value renders through its default export, as a call of it does.
+jsx_tag_value(je, k, i) :- jsx_element_tag(je, tag), expr_value(tag, "module", mod),
+ module_export_value(mod, "default", k, i), k != "module".
+
+// ── jsx_renders(Element, Component) — the callables the element runs ───────
+// A function component (a constructor FUNCTION is callable_value's "ctor" half).
+jsx_renders(je, m) :- jsx_tag_value(je, k, i), callable_value(k, i, m).
+// A class component: constructed, then rendered.
+jsx_renders(je, m) :- jsx_tag_value(je, "ctor", t), type_construct_target(t, m).
+jsx_renders(je, m) :- jsx_tag_value(je, "ctor", t), type_member(t, "render", m).
+
+// ── the props object ────────────────────────────────────────────────────────
+// Keyed by the element, as an object literal is keyed by the literal.
+prop_value("obj", je, n, k, i) :- expr_kind(_, "JSX_ELEMENT", _, je),
+ expr_child(_, je, "PROPERTY_KEY", _, a), expr_kind(_, "JSX_ATTRIBUTE_VALUE", _, a), expr_name(_, n, a),
+ expr_child(_, a, "PROPERTY_VALUE", _, v), expr_value(v, k, i).
+// `` hands over every property of `rest`.
+prop_value("obj", je, n, k, i) :- expr_kind(_, "JSX_ELEMENT", _, je),
+ expr_child(_, je, "SPREAD_OPERAND", _, o), expr_value(o, k0, i0), prop_value(k0, i0, n, k, i).
+// `{(x) => …}` — a brace child is `props.children`.
+prop_value("obj", je, "children", k, i) :- expr_kind(_, "JSX_ELEMENT", _, je),
+ expr_child(_, je, "OPERAND", _, c), expr_value(c, k, i).
+
+// A function component's first parameter receives the props — client functions only,
+// and not a fan-capped one, exactly as argument flow at a call.
+param_value(p, "obj", je) :- jsx_tag_value(je, k, i), k != "ctor", callable_value(k, i, m),
+ method_decl("client", _, _, _, _, _, _, m), !method_is_hot(m, _),
+ param_decl(_, _, "0", m, p), !param_is_rest(_, p).
+// A class component: its constructor receives them, and `this.props` holds them.
+param_value(p, "obj", je) :- jsx_tag_value(je, "ctor", t), type_construct_target(t, m),
+ method_decl("client", _, _, _, _, _, _, m), !method_is_hot(m, _),
+ param_decl(_, _, "0", m, p), !param_is_rest(_, p).
+prop_value("inst", t, "props", "obj", je) :- jsx_tag_value(je, "ctor", t), type_decl("client", _, _, _, _, t).
+
+// ── the rows ────────────────────────────────────────────────────────────────
+// The caller is the callable whose body holds the markup; top-level markup
+// (`root.render()`) belongs to the module initializer, as a top-level call does.
+jsx_caller(je, m) :- jsx_element_tag(je, _), expr_owner(_, m, _, je), m != "".
+jsx_caller(je, init) :- jsx_element_tag(je, _), expr_owner(_, "", mod, je), module_init(_, init, mod).
+jsx_target_count(je, n) :- jsx_element_tag(je, _), n = count : { jsx_renders(je, _) }.
+jsx_class(je, "known_edge") :- jsx_target_count(je, 1).
+jsx_class(je, "multi_inferred") :- jsx_target_count(je, n), n >= 2.
+
+call_chain_edge(je, caller, "-", m, "client", cls, "JSX_ELEMENT") :- jsx_renders(je, m),
+ method_prov(m, "client"), jsx_caller(je, caller), jsx_class(je, cls).
+call_chain_edge(je, caller, "-", m, "lib", "boundary_lib", "JSX_ELEMENT") :- jsx_renders(je, m),
+ method_prov(m, "lib"), jsx_caller(je, caller).
+// A tag that names something the value layer could not follow: a declared unknown.
+call_chain_edge(je, caller, "-", "-", "-", "ambiguous_unknown", "JSX_ELEMENT") :-
+ jsx_target_count(je, 0), jsx_caller(je, caller).
diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl
index c4ad3ef53..6a91bfde3 100644
--- a/graph/javascript/souffle/decls_all.dl
+++ b/graph/javascript/souffle/decls_all.dl
@@ -42,6 +42,14 @@
.decl accessor_target_count(c0:symbol, c1:symbol, c2:number)
.decl accessor_class(c0:symbol, c1:symbol, c2:symbol)
+// ── call-edge-generation/jsx.dl ──
+.decl jsx_element_tag(c0:symbol, c1:symbol)
+.decl jsx_tag_value(c0:symbol, c1:symbol, c2:symbol)
+.decl jsx_renders(c0:symbol, c1:symbol)
+.decl jsx_caller(c0:symbol, c1:symbol)
+.decl jsx_target_count(c0:symbol, c1:number)
+.decl jsx_class(c0:symbol, c1:symbol)
+
// ── call-edge-generation/calls.dl ──
.decl method_prov(c0:symbol, c1:symbol)
.decl client_calls_client(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
diff --git a/graph/javascript/souffle/export_manifest.tsv b/graph/javascript/souffle/export_manifest.tsv
index b4dbc3496..b90de230f 100644
--- a/graph/javascript/souffle/export_manifest.tsv
+++ b/graph/javascript/souffle/export_manifest.tsv
@@ -40,3 +40,4 @@ site_in_partial_module site-in-partial-module.csv
package_entry package-entry.csv
import_staged_package_unreached import-staged-package-unreached.csv
member_write_refused member-write-refused.csv
+jsx_renders jsx-renders.csv
diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/App.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/App.jsx
new file mode 100644
index 000000000..538db49ad
--- /dev/null
+++ b/graph/test/javascript/cases/62-jsx-component-render/src/App.jsx
@@ -0,0 +1,26 @@
+import { TodoList } from './components/TodoList.jsx';
+import Panel from './components/Panel.jsx';
+import * as ui from './components/ui.jsx';
+
+// CONTROL: a project function spelled like an intrinsic tag. `` is intrinsic and
+// must not become an edge to it.
+function div() { return 'not a component'; }
+
+// CONTROL: named only in a string, never rendered — no edge.
+function Unused() { return null; }
+const label = 'Unused';
+
+export default function App({ title }) {
+ const extra = { onClose: closeAll };
+ return (
+
+
+
+
+
{(item) => format(item)}
+
+ );
+}
+
+function closeAll() { return div(); }
+function format(item) { return String(item); }
diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/components/Panel.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/components/Panel.jsx
new file mode 100644
index 000000000..b3033b90a
--- /dev/null
+++ b/graph/test/javascript/cases/62-jsx-component-render/src/components/Panel.jsx
@@ -0,0 +1,7 @@
+// A class component with no constructor, as most are: rendered, and `this.props`
+// holds the attributes.
+export default class Panel {
+ render() {
+ return this.props.onClose()} />;
+ }
+}
diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoItem.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoItem.jsx
new file mode 100644
index 000000000..b74829bcd
--- /dev/null
+++ b/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoItem.jsx
@@ -0,0 +1,4 @@
+// Props arrive destructured; the onToggle prop is whatever the element passed.
+export default function TodoItem({ todo, onToggle }) {
+ return onToggle(todo)}>{todo.text};
+}
diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoList.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoList.jsx
new file mode 100644
index 000000000..c09e7390a
--- /dev/null
+++ b/graph/test/javascript/cases/62-jsx-component-render/src/components/TodoList.jsx
@@ -0,0 +1,9 @@
+import TodoItem from './TodoItem.jsx';
+
+export function TodoList({ heading }) {
+ function handleToggle(todo) { return save(todo); }
+ const todos = [{ text: heading }];
+ return ;
+}
+
+function save(todo) { return todo; }
diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/components/ui.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/components/ui.jsx
new file mode 100644
index 000000000..743dfa1a9
--- /dev/null
+++ b/graph/test/javascript/cases/62-jsx-component-render/src/components/ui.jsx
@@ -0,0 +1,3 @@
+export function Badge(props) { return {props.count}; }
+// A render prop: the brace child is props.children.
+export function List(props) { return {[1, 2].map((x) => props.children(x))}
; }
diff --git a/graph/test/javascript/cases/62-jsx-component-render/src/main.jsx b/graph/test/javascript/cases/62-jsx-component-render/src/main.jsx
new file mode 100644
index 000000000..cc5033bb0
--- /dev/null
+++ b/graph/test/javascript/cases/62-jsx-component-render/src/main.jsx
@@ -0,0 +1,4 @@
+import App from './App.jsx';
+// Top-level markup belongs to the module initializer.
+mount();
+function mount(el) { return el; }
diff --git a/graph/test/javascript/expected/62-jsx-component-render.diag b/graph/test/javascript/expected/62-jsx-component-render.diag
new file mode 100644
index 000000000..6073284d3
--- /dev/null
+++ b/graph/test/javascript/expected/62-jsx-component-render.diag
@@ -0,0 +1,4 @@
+package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
+unresolved App.jsx:26:32 FUNCTION_CALL String no_target
+unresolved components/TodoList.jsx:6:15 METHOD_CALL map no_target
+unresolved components/ui.jsx:3:43 METHOD_CALL map no_target
diff --git a/graph/test/javascript/expected/62-jsx-component-render.edges b/graph/test/javascript/expected/62-jsx-component-render.edges
new file mode 100644
index 000000000..05290ffbf
--- /dev/null
+++ b/graph/test/javascript/expected/62-jsx-component-render.edges
@@ -0,0 +1,18 @@
+App.jsx:17:7 JSX_ELEMENT -> known_edge components/TodoList.jsx:3:1 TodoList
+App.jsx:18:7 JSX_ELEMENT -> known_edge components/Panel.jsx:4:3 render
+App.jsx:19:7 JSX_ELEMENT -> known_edge components/ui.jsx:1:1 Badge
+App.jsx:20:27 FUNCTION_CALL format -> known_edge App.jsx:26:1 format
+App.jsx:20:7 JSX_ELEMENT {(item) => format(item)} -> known_edge components/ui.jsx:3:1 List
+App.jsx:25:30 FUNCTION_CALL div -> known_edge App.jsx:7:1 div
+App.jsx:26:32 FUNCTION_CALL String -> ambient_terminal -
+components/Panel.jsx:5:36 METHOD_CALL this.props.onClose -> known_edge App.jsx:25:1 closeAll
+components/TodoItem.jsx:3:29 FUNCTION_CALL onToggle -> known_edge components/TodoList.jsx:4:3 handleToggle
+components/TodoList.jsx:4:40 FUNCTION_CALL save -> known_edge components/TodoList.jsx:9:1 save
+components/TodoList.jsx:6:15 METHOD_CALL todos.map -> ambient_terminal -
+components/TodoList.jsx:6:15 METHOD_CALL todos.map -> callback_registered components/TodoList.jsx:6:25
+components/TodoList.jsx:6:32 JSX_ELEMENT -> known_edge components/TodoItem.jsx:2:1 TodoItem
+components/ui.jsx:3:43 METHOD_CALL [1, 2].map -> ambient_terminal -
+components/ui.jsx:3:43 METHOD_CALL [1, 2].map -> callback_registered components/ui.jsx:3:54
+components/ui.jsx:3:61 METHOD_CALL props.children -> known_edge App.jsx:20:17
+main.jsx:3:1 FUNCTION_CALL mount -> known_edge main.jsx:4:1 mount
+main.jsx:3:7 JSX_ELEMENT -> known_edge App.jsx:13:1 App
diff --git a/graph/test/javascript/expected/62-jsx-component-render.oracle b/graph/test/javascript/expected/62-jsx-component-render.oracle
new file mode 100644
index 000000000..b79c5b4f1
--- /dev/null
+++ b/graph/test/javascript/expected/62-jsx-component-render.oracle
@@ -0,0 +1,8 @@
+App.jsx:20:27 FUNCTION_CALL format EXACT App.jsx:26:1
+App.jsx:25:30 FUNCTION_CALL div EXACT App.jsx:7:1
+App.jsx:26:32 FUNCTION_CALL String LIB_AMBIENT_OK
+components/TodoList.jsx:4:40 FUNCTION_CALL save EXACT components/TodoList.jsx:9:1
+components/TodoList.jsx:6:15 METHOD_CALL map LIB_AMBIENT_OK
+components/ui.jsx:3:43 METHOD_CALL map LIB_AMBIENT_OK
+main.jsx:3:1 FUNCTION_CALL mount EXACT main.jsx:4:1
+# defects: 0
diff --git a/graph/test/javascript/tools/normalize_edges.py b/graph/test/javascript/tools/normalize_edges.py
index c219389d2..8c515c322 100644
--- a/graph/test/javascript/tools/normalize_edges.py
+++ b/graph/test/javascript/tools/normalize_edges.py
@@ -51,7 +51,8 @@ def methods(d, tag):
if h:
i = {c: h.index(c) for c in ('jsExpressionUniqueHash', 'ownerModuleLinkHash', 'startLine', 'startColumn', 'expressionKind', 'text')}
for r in exprs:
- if r[i['expressionKind']] in ('PROPERTY_ACCESS', 'OPTIONAL_ACCESS', 'ELEMENT_ACCESS'):
+ # A JSX element edge is keyed on the JSX_ELEMENT expression, likewise no call site.
+ if r[i['expressionKind']] in ('PROPERTY_ACCESS', 'OPTIONAL_ACCESS', 'ELEMENT_ACCESS', 'JSX_ELEMENT'):
access[r[i['jsExpressionUniqueHash']]] = '%s:%s:%s %%s %s' % (
modfile.get(r[i['ownerModuleLinkHash']], '?'), r[i['startLine']], r[i['startColumn']],
r[i['text']].replace('\n', ' ')[:60])