Skip to content

SOF-8026: extract schema reference graph (graph.json) and lint the corpus - #416

Open
timurbazhirov wants to merge 1 commit into
epic/SOF-8025from
feature/SOF-8026
Open

SOF-8026: extract schema reference graph (graph.json) and lint the corpus#416
timurbazhirov wants to merge 1 commit into
epic/SOF-8025from
feature/SOF-8026

Conversation

@timurbazhirov

@timurbazhirov timurbazhirov commented Aug 16, 2026

Copy link
Copy Markdown
Member

Ticket: SOF-8026 · Epic: SOF-8025
Plan: plan/review/2026-08-16-entity-graph-foundation.md
Chained onto #415 — review that first; this PR's diff is against epic/SOF-8025.

Extracts the schema reference graph into a single graph.json asset and derives a lint over the corpus from it. Shared foundation for the Entity Map and the concept docs — but the lint stands on its own, and already catches a class of breakage nothing checked before.

What it does

src/js/scripts/buildEntityGraph.ts walks the source schemas (never the resolved copies under dist, which merge allOf and inline $refs — exactly the structure being recorded) and classifies every $ref by its innermost enclosing keyword:

Structural context Relationship
inside an allOf item extends
under properties.<name> or items contains (labelled with the property name)
inside oneOf / anyOf variant

The enclosing property name travels with the reference regardless of kind, so the 42-way union under property/holder's data field is recorded as variants of data rather than as unlabelled edges.

Lint rules

Failures exit non-zero; warnings are reported. Runs on every pull request via npm test, not only at deploy.

# Rule Severity
L1 every $ref resolves to a schema fail
L2 $id matches the setSchemaIds path convention fail
L3 every path classifies to a layer fail
L4 every $ref classifies to an edge kind fail
L5 every JSON-pointer fragment exists in its target fail
L6 every manifest/properties.yaml schemaId resolves fail
L7 no reference cycles fail
L8 isolated-node growth over baseline warn
L9 example coverage warn
L10 graph.json validates against its own ESSE schema fail

L5 and L6 cover breakage that was previously silent: a manifest entry pointing at a schema that does not exist, or a $ref fragment that does not resolve, would have gone unnoticed.

Verified by deliberately breaking each on a scratch branch:

FAILURE: L1 schema/material.json: $ref "does_not_exist.json" does not resolve to a schema
FAILURE: L3 schema/brand_new_dir/thing.json: no layer rule matches this path — add one to classifyLayer
FAILURE: L6 manifest/properties.yaml: "bogus_property" references schemaId "properties-directory/scalar/nonexistent", which does not exist

The layer taxonomy is total

An unclassifiable path is a failure, so adding a top-level directory forces a deliberate decision rather than silently landing in a catch-all. The planning pass had left a 123-schema other bucket; it resolves exactly as predicted — 106 entity-components (keyed by ownerEntity) and 17 application-parsing schemas.

directory category entity-component system reusable primitive application-parsing entity reference abstract in-memory-entity definition
156 152 106 39 31 23 17 11 10 9 7 4

Correction to the recorded baseline

The planning walker counted same-document $refs (#/…) as edges. They are not edges between schemas. The real figures:

planned actual
cross-schema edges 937 917
extends / contains / variant 376 / 384 / 177 372 / 375 / 170
same-document refs 20 (counted separately, pointers checked)
edges carrying a pointer 164 144

The difference is exactly those 20 refs. plan/context/…-schema-graph-measurements.md records the correction; tests/js/entityGraph.tests.ts pins the numbers so they cannot drift silently.

Notes for the reviewer

  • publishedPath is not always the source path. properties_directory/non-scalar/… publishes as non_scalar, because the id round-trip turns dashes into underscores. Both paths are carried on each node — the GitHub link needs one, the Explorer link the other. For the same reason schemaIdToPublishedPath has no pure inverse; the module exports buildPublishedPathIndex(nodes) and the round-trip is tested over every node.
  • meta.generatedAt was dropped in favour of determinism — output is byte-identical across runs, so diffs of the published asset are reviewable. meta instead carries counts worth having.
  • Unrelated churn in five generated pydantic models. datamodel-codegen numbers generated classes globally (Units277Units276), so adding any schema shifts them. No behaviour change.
  • graph.json is 503 KB, larger than the plan's 150 KB estimate — still one fetch, still comfortably client-side.

Testing

tests/js/entityGraph.tests.ts — 15 cases: lint-clean assertion, node/edge/layer baselines, edge-kind partition, same-document refs never becoming self-edges, known relationships (material extends named-defaultable; model contains method; the property/holder union), manifest flags, published-path round-trip, degree consistency, byte-identical determinism, self-schema validation, and pointer/classification unit tests.

25 passing

🤖 Generated with Claude Code


Generated by Claude Code

Adds a build-time extraction of the schema reference graph into a single
graph.json asset, plus a lint over the corpus derived from it. Shared
foundation for the Entity Map and the concept docs; the lint has standalone
value on its own.

- src/js/scripts/buildEntityGraph.ts: walks schema/ sources (never the
  resolved dist copies, which merge allOf and inline $refs) and classifies
  every $ref by its innermost enclosing keyword: allOf extends,
  properties/items contains, oneOf/anyOf variant.
- schema/system/entity_graph.json: the ESSE schema graph.json validates
  against, so the asset describing the schemas is itself described by one.
- build_entity_graph.ts: CLI, emitting to --output and exiting non-zero on
  lint failures.
- Lint rules L1-L10: unresolvable $refs, $id convention, total layer
  classification, edge-kind classification, JSON-pointer existence, manifest
  schemaId resolution and cycles all fail; isolated-node growth and example
  coverage warn.
- Runs on every pull request via npm test, not only at deploy.

The layer taxonomy is total by contract: an unclassifiable path fails the
lint, so a new top-level directory forces a deliberate decision. The former
123-schema 'other' bucket resolves into 106 entity-components and 17
application-parsing schemas.

Corrects the recorded baseline: the planning walker counted same-document
$refs as edges. There are 917 cross-schema edges (372 extends, 375 contains,
170 variant) plus 20 same-document refs, and 144 edges carry a JSON pointer.

The regenerated pydantic models include unrelated class-numbering churn in
five files: datamodel-codegen numbers generated classes globally, so adding
a schema shifts them.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ANArWM4ehvsXKNzQpQgx1
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.

2 participants