Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ For developers of MediaWiki extensions that build on NeoWiki.
an edit
* [Revision policy](extending/revision-policy.md) — which revision NeoWiki publishes, for approval extensions
* [Edit notices](extending/edit-notices.md) — a message before a user edits a Subject
* [Subject Sources](extending/subject-sources.md) — Subjects and Schemas supplied from outside this wiki's
revision slots
* [Graph Database Backends](extending/graph-database-backends.md) — a store of your own that NeoWiki keeps in sync
* [Using NeoWiki from PHP](extending/php.md) — read Subjects and run Cypher from hooks and special pages
* [Using NeoWiki from JavaScript](extending/javascript.md) — the public JS API, displaying values, mounting Vue
Expand Down
45 changes: 41 additions & 4 deletions docs/adr/023-subject-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Date: 2026-06-22

Status: Accepted (2026-07-06)
Status: Accepted (2026-07-06). The Source contract section was amended 2026-08-07, resolving the "Source interface
contract" open question.

Feedback remains welcome on the **Open questions** section below.

Expand All @@ -25,6 +26,35 @@ store, another NeoWiki, or an external system, can also supply Subjects. A **Sou
Source, which is the authority for its Subjects' capabilities, identity, and schema resolution. A wiki farm is simply
more registered Sources.

#### The Source contract

A Source answers for five things and no more:

- **Fetch by id**, one Subject or a whole list. A list is one call, so a Source that can answer it in one round trip does.
- **Schema resolution by name**, because a Subject's Schema is resolved through the Schema's own Source.
- **Editability**, the one capability the model varies.
- **localId validation**, so the global id-format check can be source-delegated: the nanoid grammar is the local
Source's rule, not a rule about ids. Unconsulted so far — see Identity below.
- **A base URI**, the IRI prefix its Subjects are named under, which is also its RDF prefix entry.

On top of those five it carries one obligation rather than a capability: **a Source vouches for what it returns**.
Everything it hands back is readable by every reader of this wiki, because a sourced Subject has no page here to
authorize against and NeoWiki performs no per-user authorization on it. A Source over data with restrictions of
its own must serve only the unrestricted part — a question it answers without knowing who is asking. The accepted
limitation: sourced data has no per-user granularity — [ADR 27](027-access-control.md) records this as its
source-attested read class; lifting it would be an additive context on this contract, not a redesign.

Two absences are deliberate. There is **no write method**: write-back is end-of-roadmap (below), and a stub for it
would be a contract nobody can implement against, so it arrives with the feature. And **Sources take no part at query
time** — materialisation is the only gate on queryability, so a query never consults a Source.

Editability is likewise stated ahead of its use: nothing reads it until sourced Subjects render. It belongs in the
frozen contract because a Source author must answer it from the start.

Availability is not part of the contract either: a Source that cannot reach its store answers as though the Subject or
Schema is absent. Together with an unregistered source key resolving to nothing, a Subject from a Source this wiki
lacks degrades wherever it is read rather than breaking the page.

### A source decides editability

The distinction:
Expand Down Expand Up @@ -52,8 +82,10 @@ they stay Page Properties.

A Subject's id is a pair `(source, localId)`. The existing `SubjectId` ([ADR 14](014-improved-id-format.md)) is widened
to carry a source, defaulting to local — a bare nanoid still means a local Subject. `localId` is opaque outside its
Source (a nanoid locally; a page id or a remote id / URI elsewhere); each Source owns its grammar, validation, and
minting. The pair is the canonical **reference** form (relation targets, view ids, fetch); IRI and CURIE forms are
Source (a nanoid locally; a page id or a remote id / URI elsewhere), and each Source owns its grammar and minting.
Validation is meant to follow: `Source::isValidLocalId()` is the seam for it, but nothing consults it yet — a
foreign localId is checked only against the shared serialization grammar, so a Source with a narrower rule of its
own is not yet held to it. The pair is the canonical **reference** form (relation targets, view ids, fetch); IRI and CURIE forms are
projections of it, with the source-to-base-URI map doubling as the RDF prefix map. ADR 14's fixed-length and
time-sortable guarantees hold only for local nanoids, not for arbitrary `localId`s.

Expand All @@ -70,6 +102,12 @@ and a schema's source is independent of the subject's source. Rendering a source
schema through *its* Source, which may differ from the subject's. When a schema cannot be resolved — a foreign,
offline, or removed schema — rendering degrades gracefully rather than breaking the page.

Unlike a Subject id, a schema reference is not written as one string. A local schema name is a page title and may
itself contain a colon (`ISO:9001`), so a qualified string would be ambiguous with names that were legal before
Sources existed, and would read them back wrongly. A stored reference is therefore either a plain name, always
local, or an object carrying source and name ([Schema format](../api/schema-format.md#schema-references)). A
qualified `source:name` string survives only as a one-way rendering for people.

## Consequences

- One model spans local data, on-wiki SMW/Wikibase adoption, cross-wiki farm metadata, and external/federated data,
Expand All @@ -89,7 +127,6 @@ Deferred and/or still being designed; consortium feedback is expected here.
and [planning/OntologyMapping.md](../planning/OntologyMapping.md).
- **Editing sourced Subjects (write-back)** — end-of-roadmap. How useful? Things like editing Wikibase Items
via NeoWiki UI, or editing data from a remote NeoWiki
- **The Source interface contract** for by-id and query resolution.

## Alternatives Considered

Expand Down
24 changes: 16 additions & 8 deletions docs/adr/027-access-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Date: 2026-07-22

Status: Accepted (2026-09-02)
Status: Accepted (2026-09-02). Amended 2026-09-11, resolving the "Cross-wiki subject display" open decision with a
fifth read class, source-attested (see Decision).

## Context

Expand Down Expand Up @@ -70,15 +71,19 @@ Constraints the model rests on:
- **Projections and dumps are generated without permission checks.** We may add such support later, enabling a
public projection (a public store with a public query endpoint, holding no restricted content) alongside a private
one that includes restricted content.
- **Source-attested reads are not gated per user.** A Subject or Schema supplied by a registered Source
([ADR 23](023-subject-sources.md)) has no page on this wiki to authorize against. The Source vouches instead:
everything it returns is readable by everyone who may read this wiki at all, so the read surfaces serve it without
a per-row check, and a Source over data with restrictions of its own serves only the unrestricted part — a
question it answers without knowing who is asking. Read surfaces decide by the id's Source, never by whether a
page was found; local Subjects keep the page-attributable gate. Denied and absent are the same thing by
construction: what a Source does not vouch, it does not return. Sourced data therefore has no per-user
granularity; if that is ever demanded, it is an additive optional context on the Source contract, not a redesign.

## Open decisions

These decisions remain open at acceptance; each is deferred to the tracking issue named with it.

- **Cross-wiki subject display.** Rendering a subject from another wiki goes through REST, not Cypher, so query-side
scoping does not cover it. Deferred to [#1341](https://github.com/ProfessionalWiki/NeoWiki/issues/1341): the
check and the degradation behavior when the schema or subject is not accessible. Relates to
[ADR 23](023-subject-sources.md).
- **Default grant of `neowiki-query`.** The right is granted to `*` by default. Deferred to
[#1342](https://github.com/ProfessionalWiki/NeoWiki/issues/1342): whether the default changes, and how deployments
with restricted content are expected to configure it.
Expand All @@ -91,8 +96,9 @@ These decisions remain open at acceptance; each is deferred to the tracking issu
## Consequences

- Every new surface that exposes NeoWiki data must be classified: page-attributable (per-row gate), raw query
(whole-store semantics), projection/dump (no permission checks), or parse-time (parsing user's authority,
output keyed by access class). There is no unclassified option.
(whole-store semantics), projection/dump (no permission checks), parse-time (parsing user's authority, output
keyed by access class), or source-attested (the Source vouches, no per-row gate). There is no unclassified
option.
- A page that reads Subjects or runs queries at parse time holds one parser-cache entry per access class
among its viewers. Current-revision views of other pages are unaffected; old-revision views are keyed per
class wiki-wide, because core's revision-output cache keys on every cache-varying option rather than the
Expand Down Expand Up @@ -134,4 +140,6 @@ These decisions remain open at acceptance; each is deferred to the tracking issu
- [rest-api.md Permissions](../api/rest-api.md), [query-api.md Permissions](../api/query-api.md),
[graph-model](../api/graph-model.md)
- Issues: [#1046](https://github.com/ProfessionalWiki/NeoWiki/issues/1046) (per-page read enforcement),
[#350](https://github.com/ProfessionalWiki/NeoWiki/issues/350) (slot-level access)
[#350](https://github.com/ProfessionalWiki/NeoWiki/issues/350) (slot-level access),
[#1341](https://github.com/ProfessionalWiki/NeoWiki/issues/1341) (cross-wiki subject display, resolved by the
source-attested class)
7 changes: 6 additions & 1 deletion docs/api/graph-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,16 @@ Schema (e.g. `:Subject:Person`, `:Subject:Company`). The Schema label changes if

| Property | Neo4j Type | Description |
|----------|------------|-------------|
| `id` | string | Subject ID, 15 characters starting with `s` (unique) |
| `id` | string | ID of a Subject of this wiki, 15 characters starting with `s` (unique) |
| `name` | string | Subject label, where it has one (see below) |
| `wiki_id` | string | [MediaWiki Wiki ID](https://www.mediawiki.org/wiki/Manual:Wiki_ID) of the wiki that owns the Subject |

Unlike page ids, Subject ids are globally unique nanoids ([ADR 14](../adr/014-improved-id-format.md)), so a Subject's
identity is its `id` alone. The `wiki_id` is carried only for per-wiki query filtering in a shared graph.

Only Subjects of this wiki are projected, so `id` is always the bare form, never the `sourceKey:localId` form of a
Subject from another Source ([ADR 23](../adr/023-subject-sources.md)). See [Subject format](subject-format.md#ids).

A Subject need not have a label ([ADR 31](../adr/031-optional-subject-labels.md)). One with no label carries `name`
only as its page's Main Subject, where the value is the prefixed page title and a page move keeps it current. A query
that must name every Subject can fall back to the Schema label:
Expand Down Expand Up @@ -138,6 +141,8 @@ backtick-escaped.
When a Subject is removed while other Subjects still reference it, its node is kept as a
[stub](#stub-subject-nodes) for as long as those references last.

Relation edges to Subjects from another Source ([ADR 23](../adr/023-subject-sources.md)) are not projected yet.

## Constraints

Two node uniqueness constraints apply: `(wiki_id, id)` on `:Page` nodes
Expand Down
3 changes: 2 additions & 1 deletion docs/api/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ The graph-store endpoints are gated by the `neowiki-admin` right.
### Subjects

Read, change, and validate Subjects. New Subjects are created on a page — see
[Pages and Subjects](#pages-and-subjects). For the body shape, see [Subject format](subject-format.md).
[Pages and Subjects](#pages-and-subjects). For the body shape, see [Subject format](subject-format.md); for the form
of a `{subjectId}`, see [IDs](subject-format.md#ids).

| Endpoint | Description |
|---|---|
Expand Down
21 changes: 20 additions & 1 deletion docs/api/schema-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ Per-type value constraints (`options`, ranges, string formats, `uniqueItems`) ar
| `description` | string | No | Human-readable description of the schema |
| `propertyDefinitions` | object | Yes | Map of property names to property definition objects |

## Schema references

Wherever a Schema is named — a Subject's [`schema`](subject-format.md#subject-object) field, a relation property's
`targetSchema` — the value is a reference.

A string names a Schema of this wiki by its page title in the Schema namespace.

A Schema from another Source ([ADR 023](../adr/023-subject-sources.md)) is an object instead:

```json
{ "source": "otherwiki", "name": "Company" }
```

`source` is a source key, written as for a [Subject ID](subject-format.md#ids). Naming this wiki's own Source is the
same reference as the bare name, and is stored as the bare name.

A reference to a Source this wiki does not have resolves to no Schema, reported as
[`schema-not-found`](validation-codes.md#schema-not-found).

## Property Definition

Every property definition carries the common fields below plus the type-specific fields for its `type`.
Expand Down Expand Up @@ -160,7 +179,7 @@ References to other Subjects.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `relation` | string | Yes | - | The relation type name |
| `targetSchema` | string | Yes | - | Name of the Schema that target Subjects must follow |
| `targetSchema` | string or object | Yes | - | [Reference](#schema-references) to the Schema that target Subjects must follow |
| `multiple` | boolean or object | No | `false` | Allow multiple relations |

```json
Expand Down
13 changes: 12 additions & 1 deletion docs/api/subject-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ the main one.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `label` | string | No | Human-readable label for the Subject. Omitted when the Subject has none. |
| `schema` | string | Yes | Name of the Schema the Subject follows (a page in the Schema namespace). |
| `schema` | string or object | Yes | [Reference](schema-format.md#schema-references) to the Schema the Subject follows. A string for a Schema of this wiki: its page title in the Schema namespace. |
| `statements` | object | No | Map of property name to [Statement object](#statement-object). Omitted when the Subject has none. |

A property mapped to `null` instead of a Statement object is skipped when the JSON is read.
Expand Down Expand Up @@ -122,6 +122,17 @@ Subject and Relation IDs are 15-character nanoid-style strings, lexicographicall
Subject IDs start with `s` (`s1demo5sssssss1`), Relation IDs with `r` (`r1demo5rrrrrrr1`). See
[ADR 014](../adr/014-improved-id-format.md).

A Subject from another Source ([ADR 023](../adr/023-subject-sources.md)) is identified by `sourceKey:localId`, split
at the first colon — `wikibase:Q42`, `otherwiki:s1demo5sssssss1`. The source key is a letter followed by up to 63
letters, digits, underscores or hyphens. The local ID is whatever that Source calls the Subject, up to 256 characters
of `A-Z a-z 0-9` and `. _ ~ - : @ ! $ ( ) * + , ; =` — further colons included. A Subject of this wiki is always
written bare, never `thisWikiId:s1demo5sssssss1`, and the two forms name the same Subject wherever both are accepted.

The keys of a page's `subjects` map are these bare local IDs: a Subject from another Source is fetched from that
Source and is never stored in a page's content. An entry keyed otherwise is skipped when the page is read.

The 15-character grammar, and the creation-time sorting that comes with it, hold for this wiki's Subjects only.

## REST API

### Reading Subjects
Expand Down
27 changes: 24 additions & 3 deletions docs/api/validation-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,10 @@ so the Subject stays saveable.
### `schema-not-found`

The Subject's Schema cannot be loaded — usually deleted or renamed since the Subject
was created, or the Subject was created or imported referencing a Schema that does not (yet) exist.
The write proceeds and reports the violation; creating or renaming the Schema page resolves it.
was created, or the Subject was created or imported referencing a Schema that does not (yet) exist. A
[reference](schema-format.md#schema-references) to a Source this wiki does not have reports the same.
The write proceeds and reports the violation; creating or renaming the Schema page, or registering the
missing Source, resolves it.
Subject-level: `propertyName` is `null`.

Returned by the update dry-run and both write endpoints. The create dry-run
Expand All @@ -186,11 +188,30 @@ legitimately mint the target later.

`args`: `[targetId]`. `valuePartIndex`: the offending target. `severity`: `warning` (fixed).

### `relation-target-unresolvable-source`

On `relation` properties. The relation targets a [Subject ID](subject-format.md#ids) whose source key
names a Source this wiki has not registered, so the target cannot be reached at all. Reported instead of
[`relation-target-not-found`](#relation-target-not-found), which is about a Source that could answer and
did not.

Only a violation the edit introduces blocks it, so a Subject that already carries such a target stays
editable.

It is also the one code that is not Schema-scoped: it is reported for every relation value on the
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.

`args`: `[targetId]`. `valuePartIndex`: the offending target. `severity`: `error` (fixed).

## Out-of-schema Statements

A Statement whose property is not declared on the current Schema is ignored — no violation. This is
schema-drift tolerance: a property may have been removed from the Schema while Subjects still carry
old Statements.
old Statements. The one exception is
[`relation-target-unresolvable-source`](#relation-target-unresolvable-source), reported even for
Statements the Schema does not declare.

## Adding a new validation code

Expand Down
Loading