Widen SubjectId to a source-qualified pair#1005
Draft
malberts wants to merge 7 commits into
Draft
Conversation
A Subject id is now a (source, localId) pair per ADR 23: the bare nanoid form stays valid everywhere and means the local source, and a qualified source:localId form parses and serializes alongside it. The new SubjectIdParser canonicalizes ids that explicitly name the local source to the bare form, so one Subject never has two textual identities. Shared test vectors in tests/subject-id-vectors.json are consumed by both the PHPUnit and vitest suites, so PHP/TS grammar parity is asserted rather than assumed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every place that turns an untrusted or persisted id string into a SubjectId now goes through the parser wired in NeoWikiExtension, so an explicitly-local-qualified id canonicalizes to bare at every boundary: the REST handlers, the queries and actions taking payload ids, StatementListBuilder relation targets, SubjectResolver, and the slot deserializers. Ids read back from the local graph keep the plain constructor, since those are canonical by construction. Test-only construction sites thread TestData::newSubjectIdParser(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The {{#view:}} parser function injects arbitrary wikitext strings into
data-mw-neowiki-subject-id without server-side validation, and the
subject store is keyed by canonical id text, so placeholder hydration
now parses ids with SubjectIdParser (local key from wgWikiID): an
explicitly-local-qualified id resolves to the same store key as its
bare form instead of producing a broken view, and invalid input keeps
degrading to a skipped view. The hydration logic moves out of
NeoWikiApp.vue into components/viewData.ts so it is testable.
RestSubjectRepository now percent-encodes subject ids in URL paths,
as the schema and layout repositories already do; this is a
byte-for-byte no-op for bare nanoid ids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The round-trip pin runs a full local-only page through the parser-threaded deserializer and asserts the re-serialized slot JSON is byte-identical. The serializer fixture's 'string' writer's-schema type becomes 'text' so the fixture is deserializable; the serializer writes the type verbatim either way. Docs now describe both id forms: subject-format.md carries the grammar, rest-api.md and the REST param descriptions say either form is accepted, graph-model.md notes stored ids stay bare, and the glossary's always-15-characters claim is scoped to local ids. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keys differing only in case denote different Sources: dbnames on Linux MySQL/MariaDB (lower_case_table_names=0) can differ only in case, and the graph's wiki_id stores the wiki ID verbatim, so case-folding is not an option. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PCRE's $ anchor matches before a final newline, unlike the JS anchor, so the PHP grammar accepted ids like "s...\n" that TS rejects, and a qualified id kept the newline in its text while getLocalId() lost it. Anchoring with \z makes end-of-string strict and the two implementations identical; the shared vectors now pin both newline cases as invalid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SetSubjectsOrderingAction compared the canonical stored id texts against the raw request strings, so a request carrying the qualified-local alias of the current ordering skipped the no-change check, saved a semantically identical ordering, and reported it as changed. Ids are now parsed once up front, so the no-change comparison and the write agree on canonical text; syntactically invalid ids keep reporting an invalid ordering, and the permission check still runs first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
malberts
force-pushed
the
subject-id-source-pair
branch
from
July 14, 2026 23:23
0b557af to
45ddf06
Compare
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.
For #993 (work order T1).
SubjectId(PHP and TS) is now a(source, localId)pair per the Identity section ofADR 23:
the bare nanoid form stays valid everywhere and means the local source, and a qualified
source:localIdform can be carried across every boundary. This is a type-and-wire-format changeonly: nothing can be done with a sourced id yet, no path creates sourced Subjects, and local
Subjects stay stored bare (slot JSON, Neo4j
idproperties, and API output unchanged, per ADR 22).Owner design calls made in this PR, within the ADR's boundaries:
source:localId, splitting on the first:only, so future URI-shapedlocalIds stay expressible.
:is absent from both the nanoid alphabet and the key grammar, sothe two forms cannot collide.
[A-Za-z0-9+_-]+, compared case-sensitively. The authority for what awiki's key can look like is the installer, not
WikiMap(which concatenates verbatim): MySQLdbnames validate as
/^[a-z0-9+_-]+$/iand SQLite dbnames are unchecked. Case-sensitivity isload-bearing rather than style: with
lower_case_table_names=0(the MySQL/MariaDB default onLinux) two dbnames differing only in case are distinct databases, so two wikis' keys can differ
only in case too, and the graph's
wiki_idproperty stores the wiki ID verbatim, whichforecloses case-folding. Because hand-set dbnames are unconstrained, a configured local key
outside the grammar never throws; it is compared verbatim, and such keys simply cannot appear in
qualified input strings.
time, so one Subject never has two textual identities (
equals()and the text-keyed collectionsstay sound). Parsing happens in the new
SubjectIdParser(PHP and TS), constructed with thelocal source key (
WikiMap::getCurrentWikiId()server-side,wgWikiIDclient-side, which isthe same value by construction) and threaded to the boundary parse sites from the composition
root. Ids read back from the local graph keep the plain constructor, being canonical by
construction.
URL-path-segment-safe characters (RFC 3986 pchar minus percent-encoding). Looser rules cannot
round-trip: MediaWiki REST rawurldecodes path parameters after routing, so for example
%2Fwould reach the handler as
/.RestSubjectRepositorynow also percent-encodes ids in URLs,as the schema and layout repositories already do (a byte-for-byte no-op for bare ids).
{{#view:}}injects arbitrary wikitext strings into the placeholderattribute, and the subject store is keyed by canonical id text, so hydration now parses ids
instead of constructing them raw: an explicitly-local-qualified id resolves to the same store
key as its bare form, and invalid input keeps degrading to a skipped view. The hydration logic
moved to
components/viewData.tsto be testable.PHP/TS grammar parity is asserted rather than assumed: both suites consume the shared vector
fixture
tests/subject-id-vectors.json(a new convention, per the work-order checklist item).Object-level semantics stay in explicit per-language tests, and
testLocalOnlyPageSlotJsonRoundTripsByteIdenticallypins that a local-only page's slot JSONserialization is byte-identical through the new parse path.