Implement the Subject Sources foundation - #1265
Open
JeroenDeDauw wants to merge 12 commits into
Open
Conversation
This was referenced Aug 7, 2026
A Subject's id becomes the Source that produced it paired with that Source's own id for it (ADR 23). A bare `s` + 14 nanoid characters keeps meaning a local Subject, so nothing stored changes; everything else serializes as `sourceKey:localId`, split at the first colon. Canonicalizing an id that names the local wiki explicitly needs to know which Source key is local, which the value object deliberately does not, so that is SubjectIdParser's job. Both suites run the same parse vectors, so PHP and TypeScript are asserted equal rather than assumed so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every place an id arrives as text - a REST path, a revision slot, a relation target, a Lua call - now goes through SubjectIdParser rather than constructing a SubjectId directly, so an id naming this wiki explicitly resolves to the same identity as its bare form. Mechanical apart from two points: a caller-supplied id on creation must be a local one, since no path creates a Subject in another Source, and the REST id parameter descriptions now describe both id forms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Subject now comes from a Source (ADR 23): the object that knows how to fetch it, what its ids look like, which Schemas come with it, and whether it may be edited. The local revision slot becomes one such Source, registered under the MediaWiki Wiki ID, which is what a bare Subject id resolves to. Behaviour-preserving: with only the local Source registered, resolving through the registry reaches exactly what the repository did. An id naming a Source this wiki does not have resolves to no Subject and logs a warning, rather than breaking the page that names it. Sources are registered up front but built on first use, because building the local one reaches the graph projection, which a wiki may not have configured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Schema is now referenced as (Source, name) rather than by name alone (ADR 23),
in the two places a reference is stored: a Subject's `schema` field and a
relation property's `targetSchema`. Schema identity stays the name (ADR 17), so
SchemaName is untouched; the reference wraps it.
A Schema of this wiki keeps being written bare, which is also its page title, so
stored Schemas and Subjects are unchanged. Resolution routes through the Source
the reference names, which need not be the Source of the Subject using it; a
reference this wiki cannot resolve reports schema-not-found, the degraded state
the editor already renders, rather than failing.
A local Schema name may itself contain a colon (`ISO:9001`), so a stored string
is never split: it is always one local name. A Schema from elsewhere is stored
as a `{source, name}` object instead, which no local name can be mistaken for.
The qualified `source:name` spelling survives only as a one-way rendering for
people.
Resolution reaches the projection boundary too: the Neo4j and RDF projectors now
resolve a Subject's Schema through its own Source. A relation to a Subject of
another Source gets no Neo4j edge, since the stub node would carry this wiki's
wiki_id, and its RDF triple is named under that Source's own base URI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR 23's v1 guard: a Relation whose target names a Source this wiki has not registered is refused on the write and validate paths, since nothing can resolve it. Cross-Source relations open up once resolution for them exists. Refused whatever $wgNeoWikiEnforceValidation is set to, because enforcement is about how strictly a wiki holds data to its Schemas and this is not a Schema question. Only a violation the edit introduces blocks it, so a Subject that already carries such a target stays editable, and no read of persisted data is ever rejected. For the same reason the check is not Schema-scoped, unlike every other one here: it runs over every relation value on the proposed Subject, including a Statement the Schema does not declare and a Subject whose Schema cannot be loaded at all. Such a target would be unreadable from the moment it is written, which no later Schema change fixes. The server is the only validator (ADR 25); the editor renders the violation through the field-error path it already has. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JeroenDeDauw
force-pushed
the
subject-sources-foundation
branch
from
August 7, 2026 19:05
3dbeeab to
68c40df
Compare
Meaning-preserving pass over the prose this branch adds. The ADR's amendment note pointed at an open question the same change removed; three sentences that restated a Consequence, a rationale carried twice, and two garbled clauses are gone or fixed. The graph-model and REST-API cross-references lose their center-embedded parentheticals, the glossary entry drops storage mechanism a first-time reader cannot use, and ADR references match the style of the file they sit in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JeroenDeDauw
marked this pull request as ready for review
August 9, 2026 21:23
The splitSubjectId helper moves below the class it serves (callers before callees). The $localSourceKey doc comment now says what the key is before why deserialization needs it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop prose defending against forms the final design never had, and clauses restating facts whose home is the linked entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also align the planning doc's Source-interface summary with the contract ADR 23 froze: no query role, no write capability. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Member
Author
|
I did not fully review this yet |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #993
The ADR 23 foundation. With
only the local Source registered, every path resolves what it did before: nothing user-visible changes and nothing
stored changes.
What lands
T1 —
SubjectIdis a(source, localId)pair. A bares+ 14 nanoid characters still means a local Subject andis still stored as that bare string; anything else is
sourceKey:localId. Every place an id arrives as text — a RESTpath, a revision slot, a relation target, a Lua call — goes through
SubjectIdParser, which is handed the local Sourcekey and resolves nothing. Local slot JSON, Neo4j
idand API output are unchanged, pinned by a byte-identical slottest.
T2 — the Source contract, its registry, and
LocalSource. One interface, one registry keyed by Source key, thelocal revision slot refactored into the default registered Source under the MediaWiki Wiki ID. Extensions contribute
Sources through
NeoWikiRegistrar::addSource(), alongside the Property Type and Page Property Provider registries.ADR 23's "Source interface contract" open question is resolved in the ADR.
T3 — a Schema reference carries its Source.
SchemaReferencepairs a Source with the unchangedSchemaName, inthe two places a reference is persisted: a Subject's
schemaand a relation property'stargetSchema. Resolutionroutes through the Source the reference names, which need not be the Subject's.
T4 — relation targets naming an unregistered Source are refused, on the write and validate paths and never on read
of persisted data. The refusal stands whatever
$wgNeoWikiEnforceValidationis set to: enforcement governs howstrictly a wiki holds data to its Schemas, and a reference nothing can resolve is not a Schema question. Only
violations the edit introduces block, so a Subject already carrying such a target stays editable.
Design calls
sourceKey:localId, split at the first colon. Source key[A-Za-z][A-Za-z0-9_-]{0,63}; aforeign localId is RFC 3986
pchar, minus percent-encoding and minus&and', capped at 256 characters. EveryPHP grammar is
\z-anchored, so a trailing newline cannot pass.still resolves its own bare ids; only the explicit
<wikiId>:<localId>spelling is unusable there.has one identity — which the id-keyed maps rely on.
{source, name}. A bare string is always a local name, so a Schema titlecontaining a colon (
ISO:9001) stays valid.already has.
subject-to-page index, which lives in the graph projection.
isEditable()and foreign-sourceisValidLocalId()are contract surface with no consumer until sourced-subjectrendering lands. They are frozen now because a Source author must answer them from the start.
project. The projection skip covers every foreign target, resolvable or not; T4 refuses only the unresolvable ones.
RelationIdsharedSubjectId's$-anchor newline flaw, fixed here. Pre-existing, one line.Considered, omitted
Source. Write-back brings its own method when it is built.SubjectIdParser. Only canonical ids reach the frontend, so there is nothing to canonicalize there.reinterpret a Schema name that already contains a colon.
wiki_id, and one without it scopesno query correctly.
Follow-ups filed
(pre-existing).
ViolationDiffidentity ignoresargs.Reviewing
The commits stage T1→T4, mechanical changes separated from behavioural ones. PHP and TypeScript assert the id grammars
against one shared fixture,
tests/vectors/subject-ids.json.Production notes
Design and orchestration by
Fable 5 (max); implementation and the post-review fix batch byOpus 5 (max)subagents.Four independent AI review passes — code, security, tests, and a diff scan against a superseded branch — with every
blocking finding fixed before this description was written.