Skip to content

feat(salesforce): read metadata XML, not just Apex - #3117

Open
rsoesemann wants to merge 3 commits into
Graphify-Labs:v8from
rsoesemann:feat/salesforce-metadata-xml
Open

feat(salesforce): read metadata XML, not just Apex#3117
rsoesemann wants to merge 3 commits into
Graphify-Labs:v8from
rsoesemann:feat/salesforce-metadata-xml

Conversation

@rsoesemann

Copy link
Copy Markdown

In a Salesforce codebase, .cls and .trigger are a minority of the files. Objects, fields, layouts, list views and permission sets hold a lot of the wiring, and none of it was in the graph — those files aren't classified at all today. On my-org-butler that's 34 files invisible out of 186 tracked.

Salesforce metadata is plain XML, so this is one generic element walk with the stdlib parser and no new dependency — no logic per component type. What a file declares comes from its filename. What it references comes from leaf element text, which is how Salesforce spells cross-component links:

<classAccesses>
    <apexClass>NotifyUser</apexClass>   <- a reference
    <enabled>true</enabled>             <- a value
</classAccesses>

References are emitted as sourceless placeholders, so the corpus rewire binds them to the real definition. The permission set above ends up pointing at NotifyUser.cls without this extractor knowing what a permission set is.

Telling references from values is the only real judgement here. Two rules, both derived from the element names that actually carry identifiers in real Salesforce codebases: anything with a Salesforce suffix (__c, __mdt, …) is always an API name, and for standard components there's a flat set of about twenty element names (apexClass, object, field, …). Everything else — true, Checkbox, Large, ALL-CAPS layout tokens like TASK.SUBJECT — is dropped. A 134-element object file yields one node.

Dispatch is by filename, following the existing .blade.php precedent, because the component kind sits in a compound suffix. Plain .xml stays unclaimed on purpose: claiming it would pull every pom.xml and web.xml in every repository into every graph, which is a separate decision. That does mean collect_files and classify_file each needed a filename check, since both gate on extension.

On my-org-butler, 186 tracked files: files in the graph 70 → 104, nodes 616 → 692, edges 618 → 702, of which 68 nodes come from metadata. explain Content__c returned nothing before — the field had no node. Now it returns the owning object plus the permission set, page layout and list view that use it.

Tests: 15, in tests/test_salesforce_meta_xml.py. Since the feature is new, "fails without the change" proves nothing, so I checked them by mutation instead — twelve plausible wrong implementations (stamping source_file on references, claiming all .xml, dropping the value filter, losing the owning-object lookup, swallowing parse errors, …), each caught by at least one test. That found a real bug on the first pass: <name> was being discarded as display text, which loses a genuine reference in custom-metadata records. Three negative controls: values must not become edges, ALL-CAPS layout tokens must not become components, and web.xml must not be claimed.

Known limitations. Flows contribute the objects and fields they touch but not their logic, and formula fields and validation rules hold expressions rather than plain names, so their field references aren't picked up. Salesforce names an object's tab after the object, so Memory__c exists as the object, the tab, and the shared name Apex points at; merging by name would be right sometimes and wrong sometimes, so nothing is merged — every edge is still reachable. .cls-meta.xml sidecars are skipped since the Apex extractor already owns those classes.

Independent of #3106; either can land first.

In a Salesforce codebase, .cls and .trigger are a minority of the files.
Objects, fields, layouts, list views and permission sets hold a lot of the
wiring, and none of it reached the graph — those files are not classified at
all today.

Salesforce metadata is plain XML, so this is one generic element walk with the
stdlib parser and no new dependency: no logic per component type. What a file
declares comes from its filename; what it references comes from leaf element
text, emitted as sourceless placeholders so the corpus rewire binds them to the
real definition. A permission set ends up pointing at NotifyUser.cls without
this extractor knowing what a permission set is.

Dispatch is by filename, following the .blade.php precedent, because the
component kind sits in a compound suffix. Plain .xml stays unclaimed: claiming
it would pull every pom.xml and web.xml in every repository into every graph,
which is a separate decision.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds a Salesforce *-meta.xml extractor and wires it into detection, dispatch, and file collection so source-format metadata is treated as code without claiming plain .xml. is_salesforce_meta_xml_path gates ownership by the compound -meta.xml suffix while excluding sidecars (.cls/.trigger/.js) that another extractor already owns; extract_salesforce_meta_xml walks the XML generically, declaring the component from its filename and emitting cross-component references (Apex classes, custom __c names, standard components) as sourceless stubs for the corpus-level rewire to bind. Rejects oversized files and any XML carrying a DOCTYPE/ENTITY declaration to avoid billion-laughs expansion, returning an empty node/edge set with an error in those cases.

Worth a look

  • Reference node id salt differs from definition node id, breaking rewiregraphify/extractors/salesforce_meta_xml.py:178 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Direct Salesforce metadata file targets are still rejected by collect_filesgraphify/extract.py:7135 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • DOCTYPE/ENTITY screen is bypassable with non-UTF-8 XML encodingsgraphify/extractors/salesforce_meta_xml.py:33 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • DOCTYPE/ENTITY screen bypassed by encoding/whitespace variantsgraphify/extractors/salesforce_meta_xml.py:37 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • _is_api_name rejects all-uppercase API namesgraphify/extractors/salesforce_meta_xml.py:78 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2215 functions depend on the 377 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 500 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 40 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: extract_js() — 83 callers, 3 callees
  • …and 41 more — each is listed as a finding

Verification — 2215 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1988 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify classify\_file.

The verifier did not have enough to check classify\_file, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify collect\_files.

The verifier did not have enough to check collect\_files, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `target` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_get\_extractor.

The verifier did not have enough to check \_get\_extractor, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 1 grounded finding(s) anchored inline below; 48 more finding(s) on lines outside this diff (see the check run).

return None


def extract_salesforce_meta_xml(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionextract_salesforce_meta_xml()

fans out to 10 callees (efferent coupling); 15 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

collect_files has two walks and only the plain one got the filename
exception, so with follow_symlinks=True every *-meta.xml was silently
dropped. Both now share one predicate rather than repeating the condition.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds a Salesforce *-meta.xml extractor that treats the metadata as generic XML: the declared component name/kind come from the filename, cross-component links are read from leaf element text and emitted as sourceless placeholders for corpus-level rewiring, and nested fields resolve their owning object by walking ancestor directories. Routes these files as FileType.CODE and dispatches them by filename in both classify_file and _get_extractor, while deliberately leaving plain .xml unclaimed to avoid sweeping every pom.xml/web.xml into the graph. Skips sidecars (Foo.cls-meta.xml, triggers, .js) whose companion already has an extractor, refuses files over 2 MB or carrying a DOCTYPE/ENTITY declaration, and shares a single _claimed check so the symlink-following file walk no longer misses the filename-dispatched files.

Worth a look

  • add_stub uses different id scheme than add_node, so references never bind to definitionsgraphify/extractors/salesforce_meta_xml.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • UTF-16 XML bypasses DOCTYPE/ENTITY rejectiongraphify/extractors/salesforce_meta_xml.py:31 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • _is_api_name rejects all-uppercase identifiers, dropping valid API namesgraphify/extractors/salesforce_meta_xml.py:96 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Reference id built from qualified part collides with component ids using different aritygraphify/extractors/salesforce_meta_xml.py:200 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2218 functions depend on the 380 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 500 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 40 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: extract_js() — 83 callers, 3 callees
  • …and 41 more — each is listed as a finding

Verification — 2218 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1991 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify classify\_file.

The verifier did not have enough to check classify\_file, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify collect\_files.

The verifier did not have enough to check collect\_files, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `target` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_get\_extractor.

The verifier did not have enough to check \_get\_extractor, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 1 grounded finding(s) anchored inline below; 48 more finding(s) on lines outside this diff (see the check run).

return None


def extract_salesforce_meta_xml(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionextract_salesforce_meta_xml()

fans out to 10 callees (efferent coupling); 15 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

The DOCTYPE/ENTITY screen matches ASCII bytes, so a UTF-16 encoded declaration
walked straight past it while ElementTree honoured the encoding and expanded
the entity anyway — the billion-laughs hole the screen exists to close. A NUL
byte is the reliable tell for UTF-16/32, and Salesforce source format is always
UTF-8, so refusing those closes it without costing anything real.

Reported by the review bot on Graphify-Labs#3117.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds a Salesforce *-meta.xml extractor and wires it into detection, dispatch, and file collection. classify_file and _get_extractor now route these files by filename (via is_salesforce_meta_xml_path) rather than extension, deliberately leaving plain .xml unclaimed so pom.xml/web.xml don't get swept in; collect_files shares a _claimed helper across both walks so the symlink-following path no longer misses them. The extractor derives what a file declares from its filename and emits leaf-element references as sourceless placeholders for corpus-level rewiring, skips .cls/.trigger/.js sidecars another extractor owns, caps input at 2 MB, and refuses DOCTYPE/ENTITY or NUL-containing (UTF-16/32) files to avoid entity-expansion attacks.

Worth a look

  • Reference stub id uses _make_id(part) but component nodes use _make_id(stem, name), so references never rewire to definitionsgraphify/extractors/salesforce_meta_xml.py:210 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • _is_api_name rejects all-uppercase identifiers, dropping valid Apex/object referencesgraphify/extractors/salesforce_meta_xml.py · Escalate · medium · 2 independent checks
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • owner stub id _make_id(owner) does not match owner object's own component id _make_id(stem, name)graphify/extractors/salesforce_meta_xml.py:190 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Layout fullName references with spaces or hyphens are skippedgraphify/extractors/salesforce_meta_xml.py:198 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2223 functions depend on the 385 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 500 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_manifest() — 40 callers, 11 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: extract_js() — 83 callers, 3 callees
  • …and 41 more — each is listed as a finding

Verification — 2223 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1996 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify classify\_file.

The verifier did not have enough to check classify\_file, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify collect\_files.

The verifier did not have enough to check collect\_files, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `target` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_get\_extractor.

The verifier did not have enough to check \_get\_extractor, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 1 grounded finding(s) anchored inline below; 48 more finding(s) on lines outside this diff (see the check run).

return None


def extract_salesforce_meta_xml(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionextract_salesforce_meta_xml()

fans out to 10 callees (efferent coupling); 17 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant