validate_graph
Walk the loaded graph and return a per-class, per-node report of schema drift plus anti-pattern violations from `UPG_ANTI_PATTERNS`. Schema-drift classes: non-canonical entity types, non-canonical edge types, top-level fields outside `UPGBaseNode`, invalid status values, self-referential `source_id`/`source_type`, properties matching `UPG_PROPERTY_MIGRATIONS` rules. Anti-patterns: catalog entries that fired against the live graph, sorted high → medium → low. Each entry carries `suggested_migration` (drift) or `remediation` (anti-pattern). Top-level `valid` is true iff drift is empty AND no violations fired. Read-only; pairs with `migrate_type`, `rename_edge_type`, `get_anti_pattern_violations_for`.
Arguments
scopestringoptionalWhich drift class(es) to include in the response (default "all"). Counts in `summary` are always returned for every class.
limitnumberoptionalMax entries per class (default 100, max 1000)
severitystringoptionalFilter anti-pattern violations to one severity tier.
anti_pattern_idsarrayoptionalRestrict anti-pattern evaluation to a subset of catalog ids (e.g. ["features-without-hypotheses"]).
skip_driftbooleanoptionalSkip the schema-drift block. Only returns anti-pattern violations.
skip_anti_patternsbooleanoptionalSkip anti-pattern evaluation. Only returns schema drift.
configurationobjectoptionalEvaluate ANTI-PATTERNS against one configuration instead of across the family. Maps a configuration_axis (by node id, or by title when unambiguous) to a single one of its values. Configuration drift is NOT narrowed: the declarations it checks are facts about the whole family, so they are always validated on the union, by design. Omit this and anti-patterns are still evaluated per projection automatically: findings true everywhere report unqualified, findings true in only some report annotated with where they hold, and findings true only of the superposed union are suppressed and counted. Rejected with an error alongside skip_anti_patterns: true, since it would then have no effect.
if_changed_sincestringoptionalHash from a previous response. Returns { changed: false } if graph unchanged.
include_polymorphic_upgradesbooleanoptionalWhen true, include a `polymorphic_with_typed_alternative` array listing polymorphic edges (e.g. node_owned_by_person, node_constrains_node) that have a more-specific typed alternative for their actual source/target pair. Opt-in only; omitted by default to avoid cluttering routine validation output. Does not affect `valid`; these are advisory suggestions.
pending_nodesarrayoptionalBatch-4 #18 pre-commit preview: nodes you are ABOUT to create. When supplied (with/without pending_edges), validate_graph evaluates anti-patterns against the CURRENT graph PLUS this delta WITHOUT writing, and returns which violations the delta would newly trigger or resolve. Each item: `{ type, title?, status?, tags?, properties? }`.
pending_edgesarrayoptionalPre-commit preview edges (paired with pending_nodes). Each item: `{ from, to, type? }`, where from/to is an existing node id OR a `$N` index into pending_nodes; type is inferred from endpoints when omitted.
Returns
{ valid, structurally_valid?, summary, entity_drift?, edge_drift?, property_drift?, top_level_drift?, lifecycle_drift?, self_referential?, counts_drift?, integrity_drift?, header_seal_note?, anti_pattern_violations?, notes?, _hash }scope includes that class.limit (default 100).structurally_valid is omitted when skip_drift: true.Examples
Live call against the Notion example graph.
Output
{
"valid": false,
"summary": {
"entity_drift": 0,
"edge_drift": 0,
"top_level_drift": 0,
"lifecycle_drift": 0,
"self_referential": 0,
"property_drift": 164,
"total_nodes": 2054,
"total_edges": 3216,
"spec_version": "0.7.3",
"scope": "all",
"limit": 100,
"anti_pattern_violations_high": 0,
"anti_pattern_violations_medium": 1,
"anti_pattern_violations_low": 0,
"edge_type_pair_drift": 100,
"graph_topology_self_loops": 0,
"property_type_drift": 100
},
"_hash": "c0f701e327319f2e",
"entity_drift": [],
"edge_drift": [],
"top_level_drift": [],
"lifecycle_drift": [],
"self_referential": [],
"property_drift": [
{
"id": "ec3d5479-4947-4bd9-9e77-b5ee01beb851",
"type": "product",
"property": "stage",
"via": "UPG_PROPERTY_MIGRATIONS['0.2.13']"
},
{
"id": "3f1c1804-b33b-4b1f-9094-c91cc8957912",
"type": "assumption",
"property": "validation_status",
"via": "UPG_PROPERTY_MIGRATIONS['0.5.0']"
},
{
"id": "6719fce7-61e7-44df-b6cf-92f7c7312554",
"type": "assumption",
"property": "validation_status",
"via": "UPG_PROPERTY_MIGRATIONS['0.5.0']"
},
{
"id": "7b22f5f3-b344-4e49-8d5a-35cf9087dc6c",
"type": "assumption",
"property": "validation_status",
"via": "UPG_PROPERTY_MIGRATIONS['0.5.0']"
… (truncated)Warnings
`valid` is true ONLY when both drift is empty AND no anti-pattern violations fired — it conflates structure and product-health. For a pure spec-conformance check read `structurally_valid` (or set `skip_anti_patterns: true`, which makes `valid` track structure alone). `skip_drift: true` gives a catalog-only run and omits `structurally_valid`.
`counts_drift` / `integrity_drift` describe the `.upg` file ON DISK, not the in-memory graph — they re-read the file and compare its header to its own body. Unsaved writes therefore never show up as seal drift (the last-written file is still self-consistent), and a seal defect is repaired by `upg fmt`, not by any of the `migrate_*` tools.