Skip to content

Make the Subject label optional - #1309

Draft
alistair3149 wants to merge 7 commits into
masterfrom
1283-optional-subject-label
Draft

Make the Subject label optional#1309
alistair3149 wants to merge 7 commits into
masterfrom
1283-optional-subject-label

Conversation

@alistair3149

Copy link
Copy Markdown
Member

Fixes #1283

A Subject may now have no label. The slot omits the key, and a blank label is read as absence, so the two cannot drift
apart. Display falls back at the point of use — a Main Subject to its page name, every other Subject to its Schema
name — held in one rule, SubjectDisplayName, mirrored for the creator by placeholderSubjectLabel.ts.

The requirement was enforced in three independent layers, all dropped together: the label-required violation, hard
guards in the create and replace Actions, and PARAM_REQUIRED on four endpoints. Relaxing fewer would have been
invisible from the API.

Decisions worth a look:

  • The graph materializes the fallback for Main Subjects only. The Schema name there would make every unnamed
    Subject of a Schema indistinguishable in query results, and the Schema is already the node's second label. That
    makes an absent name no longer the mark of a stub, so the orphan sweep now identifies one by its node labels —
    the old oracle would have deleted real Subjects.
  • rdfs:label always falls back; a Mapping's labelPredicate does not. That term states what the thing is
    called in the target ontology, and a Schema name under foaf:name would assert a type as a name, which
    reconciliation keys on.
  • Lua diverges from REST on purpose. subject.label is the display name and subject.storedLabel the nullable
    stored one, because an absent key is nil in Lua and concatenating it replaces the rendered page with a script
    error. REST splits them the other way, with a nullable label and a non-null displayName.
  • PUT is a full replacement, so an omitted label clears a stored one.

NeoWiki:ClearDefaultSubjectLabels clears pre-existing stored defaults once. It is load-bearing rather than polish:
the stale-label fix only reaches Subjects that have no stored label, so without it existing Subjects keep the bug.

Considered, omitted:

  • Making the display name configurable. #1140 is
    titled "configurable / extendable", but its body asks for an outcome — stop showing the page name, show the Schema
    name — which this delivers. The consumer with the strongest claim renders its own subject column from the REST API
    and now receives label, displayName and schema, so it needs no server-side hook. If configurability does
    arrive it is most likely Schema-authored data, for the identical-siblings case, which wants more arguments to a
    pure function rather than dependency injection.
  • Discriminating several label-less Child Subjects of one Schema. They still display identically. The fallback
    can gain discriminators later without touching stored data, which stored defaults could not.

Known gaps, not fixed here: ?oldid= views resolve main-ness from the live page, so a historical label-less Subject
can be named from today's state; and GET /subject-labels returns a computed page name in its label field for a
label-less Main Subject without saying so.

Manual Browser Check

  1. On a page with no Subjects, open "Create subject" and pick a schema: the label field shows the page name greyed as
    a placeholder, and the field itself is empty. On a page that already has a Main Subject, it shows the schema
    name instead.
  2. Save without typing a label. The new Subject appears under that name, and action=raw&slot=neo shows its entry
    with no label key.
  3. Open that Subject in the editor: the title and the rename field show the same fallback as a placeholder. Edit only
    a statement and save — a labelled Subject must keep its label (the field round-trips the stored value, not the
    placeholder).
  4. GET /rest.php/neowiki/v0/subject/<id> returns "label": null beside a non-null "displayName".
  5. Relation autocomplete still finds Subjects by name; a label-less Child Subject is absent from it by design.

AI-authored — Claude Code, Opus 5 (xhigh); design authored by @alistair3149 in #1283 and followed as written, with his redirections on ADR non-modification, doc trimming, and the YAGNI call above; implementation and review fanned out across subagents, then a code review, a design review and a blind text review, of which 18 of 34 findings survived adversarial verification; human review in progress. Verified: phpcs, phpstan, 1904 unit tests, every @group Database class run individually, 1369 vitest, lint and build; browser-verified on a dev wiki including the creator placeholder, a blank save storing no label, the graph name policy, EDM foaf:name skipped without a stored label, the migration clearing 77 real labels, and a page move renaming a label-less Subject.

alistair3149 and others added 7 commits August 19, 2026 13:30
A Subject is about to be allowed no label, so every surface that shows one
needs a value to fall back on, and they all need the same one. Mirrors the
frontend's defaultSubjectLabel, which applies the same rule to a Subject that
does not exist yet.

Also exports defaultSubjectLabel from the frontend public API, so extensions
displaying Subjects can reach it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #1283

A stored default cannot be told apart from a chosen label, which is what keeps
a renamed page from renaming the Subjects that took their name from it, and
what would make an importer's synthesized names look curated. The label becomes
optional and every surface computes what to show instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
For #1283

An absent label means nobody named this Subject. The slot omits the key, and a
blank label is read as absence so the two cannot drift apart.

Display falls back at the point of use: a Main Subject to its page name, every
other Subject to its Schema name. The editor and the creator show that name as
a placeholder rather than storing it. Read APIs return the stored label, now
nullable, alongside a non-null displayName. Lua diverges deliberately, keeping
subject.label as the name to show and adding subject.storedLabel, because an
absent key is nil there and concatenating it takes the whole page down.

The graph materializes the fallback for Main Subjects only, where the page name
is a true name and a page move already keeps it current. A label-less Child
Subject gets no name property: the Schema name there would make every unnamed
Subject of a Schema indistinguishable in query results, and the Schema is
already on the node as its other label. That makes the absence of a name no
longer the mark of a stub, so the orphan sweep now identifies one by its node
labels; the old test would have let it delete a real Subject.

RDF emits rdfs:label for every Subject from the effective name, since consumers
key on it and the Schema appears there as rdf:type rather than as a label. A
Mapping's labelPredicate is emitted only from a stored label: it states what
the thing is called in the target ontology's own vocabulary, and a Schema name
under foaf:name would assert a type as a name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
For #1283

The stale-label fix only reaches Subjects that have no stored label, so without
this the Subjects that already exist keep the bug. The heuristic is that the
stored label repeats what would be shown anyway; it can only misfire on someone
who deliberately typed that, and loses nothing when it does. --dry-run reports
the page and Subject of every label it would clear, which is the only mitigation
available, since nothing in storage records whether a label was chosen.

Both eras of the default are cleared. Before child Subjects defaulted to their
Schema name, every Subject on a page defaulted to the page name, so matching
only the Schema name would pass over almost every real default. The page name
is matched prefixed and unprefixed, because the editor filled the field from
the unprefixed title while the page node carries the prefixed one.

Every cleared label costs a revision: this codebase has no null-edit path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
For #1283

Code review of the optional-label change. Three defects, the rest documentation.

SubjectResolver named every relation target as though it were unplaced, so a
target that is its page's Main Subject read as its Schema name in wikitext and
Lua while the same Subject read as the page name in every View. Its docblock
blamed the resolver for being unable to place the target; it can, and at no
extra cost, since the Subject lookup was already resolving the page and loading
all its Subjects before dropping the placement.

The subject creator built its placeholder from the unprefixed title, so on a
wiki with an extra content namespace it previewed "Onboarding" for a Subject
that would go on to display "Handbook:Onboarding".

The page-name read was duplicated verbatim between the display-name rule and
the graph projector; it now lives on PageProperties, next to the key and the
caveat about provider chains.

Documentation: the migration renames a Child Subject that carried the page name
and drops it from label search, which neither the ADR nor the operations guide
admitted; clearing costs a revision per page, not per label; the ADR asked for a
rebuild the script makes unnecessary; the payload shape doc never gained
displayName; and the graph model offered a coalesce that cannot be written,
since the Schema is a node label rather than a property.

Tests for the gaps the review found: the subject-page lookup had none, so its
cursor and its latest-revision join were both free to break; the migration's
batch loop never ran past one iteration and neither failure path was covered;
and nothing pinned a page move renaming a label-less Main Subject, which is the
whole reason the graph derives that name rather than storing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
For #1283

A blind text review against the reader map in docs/AGENTS.md.

The relation docs were wrong. They said a target with no label is named
after its Schema, which stopped being true when SubjectResolver started
resolving the target's page: a target that is its page's Main Subject reads
as the page name. The Lua note about nw.getSubject, which really does fall
through to the Schema name, stays.

The Subject shape doc claimed every endpoint returning a Subject serves it;
subject-labels returns id and label pairs and does not.

The ADR blamed the revision-per-page cost on the absence of a null-edit
path, which is not the cause: clearing a label changes slot content, so it
needs a revision either way.

The rest is trimming. The fallback rule was stated in three RDF files; it
now lives in rdf-export.md and the others point there. The graph model
restated the Schema-label line above its own table and explained that Cypher
returns null for an absent property. The upgrade guide carried the one-off
script twice, as fenced blocks, under a trigger no reader can identify in a
project with no release numbers; both are now one sentence pointing at the
task's home. One term, display name, replaces the six the diff had grown.

Also swaps the retired label-required code out of SubjectViolationBanners'
fixtures, which sampled it for banner partitioning and would have asserted
on a violation that can no longer occur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
For #1283

RedHerb is a plain-JS extension linted by its own eslint, whose parser
rejects nullish coalescing. `make ts-lint` covers resources/ext.neowiki
only, so the parse error surfaced first in CI. `||` is equivalent here:
the only falsy label the getter can return is the empty string, which
is what the fallback produces anyway.

Co-Authored-By: Claude Opus 5 (1M context) <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.

Make the subject label optional, with a computed display-name fallback

1 participant