feat(publish): check whether a minted ID has already been used - #669
Conversation
|
This is very good! I am only thinking that maybe we should give the template creators control over where to apply this. Maybe the best solution is to have an explicit NewUriPlaceholder tag that can be applied together with the existing placeholder types, and exactly for those we check and report errors wrt pre-existing usage (irrespective of whether prefix is used or not). What do you think? |
Reworked after review (#669): whether a value names a resource that does not exist yet is the template author's call, declared by tagging the placeholder, rather than something Nanodash works out from the shape of the form. The previous rule read intent out of mechanics -- an IRI built from a prefix, declared as an introduced resource, not auto-escaped. A prefix is a formatting device, and nt:introduces is attached by templates that take the IRI of a thing that already exists too, so the rule could refuse a publication its author was entitled to make. It also missed the opposite case, a new identifier typed out in full with no prefix involved. A placeholder typed nt:NewUriPlaceholder is now checked however its value was formed, and an untagged one is never checked, whatever its IRI looks like and whether or not it already exists. The special cases that existed only to correct the old inference go with it: an auto-escaped AIDA sentence is simply not tagged. Still exempt when tagged are identifiers under the nanopublication's own namespace, whose artifact code is substituted at signing, and the supersede and override fill modes, where keeping the source's identifier is the point. Since the check now does nothing until template authors ask for it, the term is documented in docs/new-uri-placeholder.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NnGpEQkFqCaf4AEQwwhFFn
Reworked after review (#669): whether a value names a resource that does not exist yet is the template author's call, declared by tagging the placeholder, rather than something Nanodash works out from the shape of the form. The previous rule read intent out of mechanics -- an IRI built from a prefix, declared as an introduced resource, not auto-escaped. A prefix is a formatting device, and nt:introduces is attached by templates that take the IRI of a thing that already exists too, so the rule could refuse a publication its author was entitled to make. It also missed the opposite case, a new identifier typed out in full with no prefix involved. A placeholder typed nt:NewUriPlaceholder is now checked however its value was formed, and an untagged one is never checked, whatever its IRI looks like and whether or not it already exists. The special cases that existed only to correct the old inference go with it: an auto-escaped AIDA sentence is simply not tagged. Still exempt when tagged are identifiers under the nanopublication's own namespace, whose artifact code is substituted at signing, and the supersede and override fill modes, where keeping the source's identifier is the point. Since the check now does nothing until template authors ask for it, the term is documented in docs/new-uri-placeholder.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NnGpEQkFqCaf4AEQwwhFFn
9f50738 to
d20fcb8
Compare
An identifier minted under a fixed prefix -- a new space's IRI, say -- carries no artifact code, so nothing makes it unique: the same form filled with the same name twice yields the same IRI, and the second nanopublication silently extends the first one's resource instead of defining a new one. A nanopublication cannot be edited afterwards, so the collision is worth catching before publishing rather than after. The publish form now asks the query API whether any identifier it minted is already introduced, and refuses to publish (or to build a preview, which the preview page publishes without coming back through the form) naming the one that is taken. Which identifiers those are is knowable only from how the value was built, so TemplateContext records them as it mints them. Four kinds are deliberately left alone: - an IRI typed out in full, which names an existing thing rather than minting one -- "Defining an open-ended Space with existing URI" exists precisely to do that; - one minted under the new nanopublication's own namespace, whose artifact code is substituted at signing time; - one built by an auto-escaping placeholder, where the IRI is derived from the text itself: two people writing the same AIDA sentence are meant to arrive at the same IRI, so an existing one is agreement, not a collision; - supersede and override, where keeping the source's identifier is the point (docs/fill-modes.md). The carve-outs are drawn from the published templates rather than guessed: of the templates combining nt:IntroducedResource with nt:hasPrefix, every one mints a new resource except the AIDA family, which is exactly the auto-escaping kind. A query service that cannot be reached answers "not taken": a check that cannot be made is not evidence of a collision, and publishing should not depend on the query services being up. Closes #646 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K55p8T6uQoSv48C5SXueve
Reworked after review (#669): whether a value names a resource that does not exist yet is the template author's call, declared by tagging the placeholder, rather than something Nanodash works out from the shape of the form. The previous rule read intent out of mechanics -- an IRI built from a prefix, declared as an introduced resource, not auto-escaped. A prefix is a formatting device, and nt:introduces is attached by templates that take the IRI of a thing that already exists too, so the rule could refuse a publication its author was entitled to make. It also missed the opposite case, a new identifier typed out in full with no prefix involved. A placeholder typed nt:NewUriPlaceholder is now checked however its value was formed, and an untagged one is never checked, whatever its IRI looks like and whether or not it already exists. The special cases that existed only to correct the old inference go with it: an auto-escaped AIDA sentence is simply not tagged. Still exempt when tagged are identifiers under the nanopublication's own namespace, whose artifact code is substituted at signing, and the supersede and override fill modes, where keeping the source's identifier is the point. Since the check now does nothing until template authors ask for it, the term is documented in docs/new-uri-placeholder.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NnGpEQkFqCaf4AEQwwhFFn
Superseding and overriding skipped the existing-ID check outright, on the reasoning that a new version keeps the resource it is a version of. That holds for the identifier the source already carries, but not for the fill mode as a whole. Nothing re-mints a prefix-minted identifier for a new version -- there is no artifact code in it to change -- and a tagged placeholder is re-formed from the prefix and whatever is in the model, so it stays editable while superseding. Renaming it there defines a resource the source never had, which can collide like any other, and the blanket exemption let exactly that through unchecked: the silent collision this check exists to prevent (#646). Identifiers the source already mentions are still exempt, now by comparison rather than by fill mode. Identifiers minted under the nanopublication's own namespace do change with the new artifact code, but they are excluded before this point and are unique by construction anyway. With no source to compare against nothing is checked, so an unrecognised fill publishes as it did before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NnGpEQkFqCaf4AEQwwhFFn
d20fcb8 to
d0c8516
Compare
@tkuhn agree. I defined the |
Closes #646
The problem
When a new ID is minted in a way that doesn't include the artifact code of the nanopublication — defining a new space, say — nothing makes it unique. Filling the same form with the same name twice yields the same IRI, and the second nanopublication silently extends the first one's resource instead of defining a new one. A nanopublication can't be edited afterwards, so the mistake is only undone by a corrected version.
This is not hypothetical:
https://w3id.org/spaces/example/barcurrently has two introducing nanopublications, the second published while this was being written.The change
The publish form asks the query API (
get-introducing-nanopub, on the indexed meta repo) whether any identifier it minted is already introduced, and refuses to publish — naming the one that is taken — before signing. The same check guards the preview button, since the preview page publishes the nanopublication it was given without coming back through the form.Which identifiers those are is knowable only from how the value was built, so
TemplateContextrecords them as it mints them, kept apart from the introduced IRIs.What is deliberately not checked
A blanket check over introduced IRIs breaks working flows, so four cases are exempt:
"Defining an open-ended Space with existing URI"exists precisely for thisdocs/fill-modes.md)The carve-outs are drawn from the published templates rather than guessed. A survey of every template combining
nt:IntroducedResourcewithnt:hasPrefixreturns 30 prefix/placeholder-type pairs: all are genuine minting cases (spaces, I-ADOPT variables, Nanodash projects, 3PFF,~~SPACE~~/) except the 17 AIDA templates, which are allAutoEscapeUriPlaceholder. The onehttps://orcid.org/hit is the superseded "Introducing a user" template; the current one — the one Nanodash links to — uses a prefix-less agent placeholder, so re-introducing yourself with a new key is unaffected.A query service that cannot be reached answers "not taken" and logs: a check that cannot be made is not evidence of a collision, and publishing shouldn't depend on the query services being up.
Testing
./mvnw -o test→ 1243 tests, 0 failures.PrefixMintedIdTest(8 tests): one per row of the table above, plus the guard itself (taken / free / supersede-asks-nothing).QueryApiAccessTestfor the lookup, including the fail-open path.get-introducing-nanopubseparates a taken space IRI (2 rows) from a free one (0 rows).🤖 Generated with Claude Code
https://claude.ai/code/session_01K55p8T6uQoSv48C5SXueve