Skip to content

Resolve schema references through their Source#1044

Draft
malberts wants to merge 11 commits into
source-registry-and-local-sourcefrom
schema-source-references
Draft

Resolve schema references through their Source#1044
malberts wants to merge 11 commits into
source-registry-and-local-sourcefrom
schema-source-references

Conversation

@malberts

Copy link
Copy Markdown
Contributor

For #993 (work order T3). Builds on the T2 Source
contract PR and targets its branch, so the diff shows only this track.

Schema references become (source, name), resolved through the schema's own Source and degrading
gracefully when unresolvable. Reference plumbing only; no non-local schema source exists yet.

Design calls recorded here:

  • A distinct SchemaReference value object with SchemaName intact underneath, per the work
    order's own lean: locally the name IS the page title in the Schema namespace, and a
    source-qualified reference is not a valid title.
  • Foreign references serialize as a JSON object { "source": ..., "name": ... }, not a
    source:Name string.
    Schema names are page titles and may legitimately contain colons, so a
    string-concatenation grammar is ambiguous; subject ids avoid that only because the bare nanoid
    alphabet excludes the separator. Local references stay bare name strings, byte-identical to
    before (regression-pinned on both persisted spots).
  • Canonicalization: on the subject schema field, an object naming the local Source normalizes
    to the bare form at parse time (mirroring SubjectIdParser), so one Schema is never referenced
    under two identities. A relation property's targetSchema is deliberately not at full parity
    yet: schema save validation still accepts only the bare string form, so the object form has no
    ingress there, and normalization is omitted where it can never run. Widening the schema-save
    contract and canonicalizing that path belongs with sourced-schema authoring; flip it in review if
    you prefer parity now.
  • Resolution routes through the T2 registry via SourceRoutingSchemaReferenceResolver,
    mirroring the subject routing semantics exactly: unknown source keys degrade to null plus one
    logged warning, and unresolvable references land in the pre-existing schema-not-found /
    null-schema handling in all five converted consumers (validators, replace action, RDF projector,
    Neo4j updater), never fatally. Name-parameterized paths (REST schema APIs, new-subject creation)
    stay name-based.
  • The Neo4j schema-name node label stays local-only (work-order constraint), and the frontend
    reduces string-or-object references to the bare name: sourced rendering is deferred by ADR 23, so
    the TS domain keeps plain names end to end.

Docs: docs/api/subject-format.md gains the Schema References section, docs/api/schema-format.md
documents the targetSchema forms and their current acceptance honestly, and the glossary Schema
entry notes the reference form.

AI-authored — Claude Code, Fable 5 (max); standing directive from @malberts to complete the #993 tracks without blocking on inter-track human review, design calls delegated; diff not yet human-reviewed; TDD with watched failures per behavior, two adversarial AI review lenses with live probes (hook-ordering re-trace across five accessor orders, byte-identity on seeded content, ingress enumeration for the canonicalization asymmetry), make ci and make ts-ci green locally, branch CI pending at posting time.

Production notes

Design and review adjudication by Fable 5 (max); implementation by an Opus 4.8 (max) subagent;
two Opus 4.8 (max) review lenses (runtime correctness with container probes, tests/conventions/
docs) stood in for the usual pre-PR human review at @malberts' direction. The review confirmed the
targetSchema asymmetry is unreachable through every current ingress and found only docs overclaims,
which the final commit scopes to shipped behavior.

malberts and others added 11 commits July 15, 2026 10:43
A SchemaReference carries an optional source (null means local) and a
SchemaName, resolved through the Schema's own Source (ADR 23). The
serialized form is a bare name string for a local reference and a
{ source, name } object for a foreign one: Schema names are page titles
and may contain colons, so a string-concatenation grammar would be
ambiguous. The parser canonicalizes a reference that names the local
source to the bare local form, mirroring SubjectIdParser; source keys
compare byte-for-byte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Subject now holds a SchemaReference internally instead of a bare
SchemaName. getSchemaName() keeps returning the reference's name so
existing consumers stay source-blind, and getSchemaReference() exposes
the full reference. The createNew() and newSubject() factories still take
a SchemaName and wrap it as a local reference. Serialization is
unchanged: this only threads the type through the construction sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The subject-slot `schema` field now serializes a SchemaReference: a
local reference stays the bare name string, byte-identical to before,
while a foreign reference becomes a { source, name } object. The
deserializer accepts either form through SchemaReferenceParser, so a
reference naming the local wiki canonicalizes to the bare local form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A RelationProperty's targetSchema is now a SchemaReference. It
serializes as the bare name string for a local target, byte-identical
to before, and as a { source, name } object for a foreign one, matching
the Subject slot format. getTargetSchema() keeps returning the name so
existing consumers stay source-blind; getTargetSchemaReference() exposes
the source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SchemaReferenceResolver resolves a SchemaReference to its Schema through
the reference's own Source, independent of any subject's source (ADR 23).
The source-routing implementation mirrors SourceRoutingSubjectLookup: a
local reference routes through the local Source and behaves exactly as a
name-keyed lookup, an unregistered source key returns null plus one
logged warning, and a registered Source without the schema returns null
without logging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The query, validation, and projection paths that resolve a Subject's
schema (ProposedSubjectValidator, ValidateSubjectUpdateQuery,
ReplaceSubjectAction, RdfPageProjector, and the Neo4j subject updater)
now resolve the Subject's SchemaReference through SchemaReferenceResolver
instead of looking the name up directly. Local references route through
the local Source unchanged; an unresolvable reference degrades to the
existing schema-not-found handling. The name-parameterized paths
(CreateSubjectAction, ValidateSubjectQuery, GetSchema, the frontend
schema map) stay name-based.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The subject `schema` field and the relation `targetSchema` field now
document both reference forms: a bare name string for a local Schema and
a { source, name } object for a Schema from another Source, with the
local-source canonicalization rule and the colon-safety rationale. The
glossary Schema entry notes the (source, name) reference form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frontend gains a SchemaReference type mirroring the backend: a bare
name string is a local reference, an object { source, name } references
a Schema from another Source. SubjectDeserializer and the relation
property deserializer now accept either form and reduce it to the
schema name, so a local reference stays a plain string end to end while
a foreign one no longer leaks its object shape into a name field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The object targetSchema form is defined by the reference format, but
schema save validation still accepts only the bare string, and
local-source normalization runs on the subject schema field only. The
docs claimed both for targetSchema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A foreign SchemaReference accepted any string as its source key, including
empty and whitespace, while a SubjectId source key must match
[A-Za-z0-9+_-]+. Apply that same grammar on the foreign-source construction
path so the two agree byte-for-byte; the local bare form and the name stay
unaffected. The check also covers deserialization, matching how a SubjectId
rejects a malformed persisted id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Graceful degradation of an unresolvable schema reference holds only on the
resolution, validation, and projection paths, which return null with one
logged warning and skip the Subject. The read and View path does not degrade
the same way: a Subject whose schema reference is unresolvable is not
presentable yet, though the surrounding page still renders. Scope the claim
in the subject and schema format docs and the glossary so they no longer
imply graceful presentation, and tie the gap to the deferred sourced-Schema
rendering work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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