Summary
build_from_json() canonicalizes some legacy field aliases before validation, but not others. Nodes and edges persisted in a graph.json that use the unhandled aliases fail validation on every subsequent build, and because graphify update merges from graph.json, the errors are permanent — no amount of re-running fixes them.
Worse than the warning noise: affected nodes end up with norm_label == "", which makes them unmatchable by query/explain.
Version
Reproduced on 0.8.13. The gap is still present in current main (0.9.26) — verified by reading graphify/build.py and graphify/validate.py from this repo, not by re-running, so the reproduction numbers below are from 0.8.13.
What happens
A real graph (Java/Spring backend, 9,019 nodes / 17,394 edges) emitted this on every build:
[graphify] Extraction warning (1911 issues): Node 8537 (id='repo:PIFAnnexAIVerdictRepository') missing required field 'label'
Breaking the 1,911 errors down:
| count |
error |
| 864 |
missing source_file (496 nodes + 368 edges) |
| 370 |
node missing label |
| 368 |
edge missing confidence |
| 309 |
edge missing relation |
Every one of the 370 label-less nodes had file_type: "concept" and carried the data under a different key:
validate.py requires |
what was persisted |
node.label |
node.name (345 nodes; the other 25 only had id) |
node.source_file |
node.path (149 nodes) |
edge.relation |
edge.type (309 edges — all 309 had type) |
edge.confidence |
edge.confidence_score (float; all 368 had it) |
Where the gap is
build.py's canonicalization block runs before validate_extraction() and already handles several aliases:
It does not handle name→label, path→source_file, type→relation, or confidence_score→confidence. Since REQUIRED_NODE_FIELDS = {"id", "label", "file_type", "source_file"}, those nodes can never validate.
Why it matters
- Self-perpetuating.
graphify update rebuilds AST nodes but merges concept nodes straight through from graph.json. Once a semantic pass writes these aliases, every future run re-reports the same errors forever. The only escape today is hand-editing graph.json or a full (paid) semantic rebuild.
- Silent functional loss, not just noise.
export.py computes norm_label = _strip_diacritics(node.get("label", "")).lower(). With no label that is "", and serve.py's matching falls back to norm_label — so all 370 nodes (Repositories, Entities, Services) were invisible to query and explain. The graph looked healthy; the nodes were simply unreachable.
- The one-line warning shows only
real_errors[0], so the scale (1,911 errors spanning four distinct causes) isn't visible without inspecting graph.json manually.
Suggested fix
Extend the existing canonicalization block to fold these aliases the same way source→source_file is already folded:
if "label" not in node and node.get("name"):
node["label"] = node["name"]
if "source_file" not in node and node.get("path"):
node["source_file"] = node["path"]
and for edges, type → relation.
confidence_score → confidence is the one that needs a project decision rather than a mechanical mapping: turning a float into the EXTRACTED/INFERRED/AMBIGUOUS audit enum is a claim about provenance. Given the honesty rules in the skill doc, defaulting alias-recovered edges to INFERRED (never EXTRACTED) seems the defensible choice, keeping the original float alongside.
It would also help if the warning reported a per-cause breakdown rather than only the first error, so a 4-cause / 1,911-error graph is diagnosable from the console output.
Workaround
For anyone hitting this: the aliases can be folded onto the canonical names directly in graph.json — 1,911 → 0 errors, with node/edge counts unchanged — after which norm_label repopulates and the nodes become queryable again.
Related
Summary
build_from_json()canonicalizes some legacy field aliases before validation, but not others. Nodes and edges persisted in agraph.jsonthat use the unhandled aliases fail validation on every subsequent build, and becausegraphify updatemerges fromgraph.json, the errors are permanent — no amount of re-running fixes them.Worse than the warning noise: affected nodes end up with
norm_label == "", which makes them unmatchable byquery/explain.Version
Reproduced on 0.8.13. The gap is still present in current
main(0.9.26) — verified by readinggraphify/build.pyandgraphify/validate.pyfrom this repo, not by re-running, so the reproduction numbers below are from 0.8.13.What happens
A real graph (Java/Spring backend, 9,019 nodes / 17,394 edges) emitted this on every build:
Breaking the 1,911 errors down:
source_file(496 nodes + 368 edges)labelconfidencerelationEvery one of the 370 label-less nodes had
file_type: "concept"and carried the data under a different key:validate.pyrequiresnode.labelnode.name(345 nodes; the other 25 only hadid)node.source_filenode.path(149 nodes)edge.relationedge.type(309 edges — all 309 hadtype)edge.confidenceedge.confidence_score(float; all 368 had it)Where the gap is
build.py's canonicalization block runs beforevalidate_extraction()and already handles several aliases:node.source→node.source_file(with a warning, Prevent partial-chunk overwrite in --update: pre-build node-count assertion + build_merge() helper #479)node.file_typeempty/unknown →"concept"/_FILE_TYPE_SYNONYMS(Extraction warning: "None" file_type not in VALID_FILE_TYPES #660)edge.from/edge.to→edge.source/edge.targetmembers/node_ids→nodes(Hyperedgemembers/node_idsalias keys silently dropped (onlynodesis read) #1561)It does not handle
name→label,path→source_file,type→relation, orconfidence_score→confidence. SinceREQUIRED_NODE_FIELDS = {"id", "label", "file_type", "source_file"}, those nodes can never validate.Why it matters
graphify updaterebuilds AST nodes but merges concept nodes straight through fromgraph.json. Once a semantic pass writes these aliases, every future run re-reports the same errors forever. The only escape today is hand-editinggraph.jsonor a full (paid) semantic rebuild.export.pycomputesnorm_label = _strip_diacritics(node.get("label", "")).lower(). With nolabelthat is"", andserve.py's matching falls back tonorm_label— so all 370 nodes (Repositories, Entities, Services) were invisible toqueryandexplain. The graph looked healthy; the nodes were simply unreachable.real_errors[0], so the scale (1,911 errors spanning four distinct causes) isn't visible without inspectinggraph.jsonmanually.Suggested fix
Extend the existing canonicalization block to fold these aliases the same way
source→source_fileis already folded:and for edges,
type→relation.confidence_score→confidenceis the one that needs a project decision rather than a mechanical mapping: turning a float into theEXTRACTED/INFERRED/AMBIGUOUSaudit enum is a claim about provenance. Given the honesty rules in the skill doc, defaulting alias-recovered edges toINFERRED(neverEXTRACTED) seems the defensible choice, keeping the original float alongside.It would also help if the warning reported a per-cause breakdown rather than only the first error, so a 4-cause / 1,911-error graph is diagnosable from the console output.
Workaround
For anyone hitting this: the aliases can be folded onto the canonical names directly in
graph.json— 1,911 → 0 errors, with node/edge counts unchanged — after whichnorm_labelrepopulates and the nodes become queryable again.Related
source_file#1279 — fixed the extractor emitting edges withoutsource_file; this issue is the consumer-side counterpart (already-persisted graphs can't self-heal).members/node_idsalias keys silently dropped (onlynodesis read) #1561 — prior alias-canonicalization fixes in the same block.