feat(forms): EKKW metadata form — contributor output, name-only isPartOf, end-to-end proof #82

Open
laoc wants to merge 4 commits from feat/ekkw-metadata-form into feat/field-descriptions
Owner

Closes the two code gaps from the EKKW research and proves the whole form end-to-end:

  • Herausgeber ≠ Autor: creator fields can target amb:contributor; serializer, reverse edit-mapping and the builder picker follow the field output; legacy fields still normalize to amb:creator (contributor is the one round-trip exception).
  • Erschienen in as free text: relation entries may carry {name} → isPartOf {name, type: CreativeWork}, editable both directions, input commits on Enter AND blur. Band/Heft needed zero code (ext outputs).
  • e2e: a four-section 30168 template built with the app's own encoder drives the paged wizard on /forms//create-resource; the published kind-30142 is read back off the relay and asserted per-tag (contributor+ORCID URI, isPartOf name, ext band/heft, taxonomy child URI in about:id, creator p-tag, form back-reference).

TestOER-verified at 56a1a373; their measured read-back race fix (predicate now matches this run's title, 5/5 serial green) adopted as ddc3173e.

Deployment (template + Theologie scheme + variant entry) follows separately — dev first.

Stacked on feat/field-descriptions. Buzz thread: #edufeed-app.

🤖 Generated with Claude Code

Closes the two code gaps from the EKKW research and proves the whole form end-to-end: - Herausgeber ≠ Autor: creator fields can target amb:contributor; serializer, reverse edit-mapping and the builder picker follow the field output; legacy fields still normalize to amb:creator (contributor is the one round-trip exception). - Erschienen in as free text: relation entries may carry {name} → isPartOf {name, type: CreativeWork}, editable both directions, input commits on Enter AND blur. Band/Heft needed zero code (ext outputs). - e2e: a four-section 30168 template built with the app's own encoder drives the paged wizard on /forms/<naddr>/create-resource; the published kind-30142 is read back off the relay and asserted per-tag (contributor+ORCID URI, isPartOf name, ext band/heft, taxonomy child URI in about:id, creator p-tag, form back-reference). TestOER-verified at 56a1a373; their measured read-back race fix (predicate now matches this run's title, 5/5 serial green) adopted as ddc3173e. Deployment (template + Theologie scheme + variant entry) follows separately — dev first. Stacked on feat/field-descriptions. Buzz thread: #edufeed-app. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two gaps from the EKKW metadata-form research (Buzz #edufeed-app 2026-08-03):

Herausgeber != Autor: a creator-typed field can now target amb:contributor.
The serializer keys the person-list property off the field's output (default
unchanged: amb:creator), the reverse edit mapping reads the same property so
editing a resource no longer injects the authors into the editors field, and
creator leaves LOCKED_FIELD_OUTPUTS — locking it would silently revert a
contributor field to creator via fieldToState on every form edit. The builder
shows a two-option picker instead of the disabled select.

Erschienen in as free text: an amb-relation entry may now carry {name}
instead of a nostr coordinate; the serializer emits isPartOf/hasPart
{name, type: CreativeWork} and the reverse mapping restores it on edit. The
adapter gains a name input committing on Enter AND blur (same never-lose-
typed-text contract as the options editor) with whitespace/duplicate guards.
Band/Heft need no code: ext outputs already carry them.

Tests first: 9 serializer/reverse/edit-cycle assertions red at 5c9ce139
(2 controls green pre-fix), 4 adapter assertions red, all green now. The old
'locked creator select' component test is rewritten to pin the new contract
(enabled picker, exactly amb:creator|amb:contributor).
The deliverable of the EKKW research thread, proven in the sandbox: a
four-section template (built with the app's own encoder so the fixture
cannot drift from the builder), driven through the paged wizard on
/forms/<naddr>/create-resource, publishing a kind-30142 the spec reads
back off the relay and verifies:

- sections as steps with per-page validation
- a Theologie taxonomy with a broader relation, the CHILD picked from
  the rendered tree, its URI asserted in about:id
- Autoren (amb:creator, self p-tag) next to Herausgeber
  (amb:contributor, manual person with normalized ORCID URI)
- Erschienen in as a free-text isPartOf {name}
- Band/Heft as ext:<dtag>:band / :heft tags
- the Erlaeuterungssatz field descriptions rendering under labels

One trap encoded in a comment: waitForURL(/naddr1…/) resolves against
the create-resource URL itself (it contains the TEMPLATE naddr, kind
30168) — the wait must match a pathname that is ONLY an naddr, or the
assertion reads the wrong event kind. Found by running, not reading.

Deployment of the real form is deliberately NOT in this branch: publish
the production template + Theologie scheme, then one variant entry with
its templateNaddr (resource-form-variants.js) — no further code.
The EKKW spec's waitForEventOnRelay predicate was () => true, which
accepts the FIRST event at the resource coordinate — and the coordinate
is a constant (the fixture DOI maps to amb:id, which derives the d-tag).
On a relay already holding a prior run's event the poll returns the
stale one before the new publish lands: run 1 green, runs 2-5 red on
the run-stamped title assertion (TestOER, serial reproduction, worker
confound excluded).

Fix is TestOER's measured one-liner, adopted with the file's JSDoc
style: the predicate now requires THIS run's title tag. Their 5 serial
runs with exactly this predicate: 5/5 green — which also proves the
publish landed every time, so the product claim was intact and only
the reader was wrong. The predicate is the helper's entire reason to
exist; () => true disables it.

svelte-check at this tip: 0 errors / 4 warnings (baseline).
The deployable follow-up to the e2e proof: the actual form from the pad,
publishable with the existing pipeline.

- scripts/data: `fachsystematik-theologie` scheme (inline, hierarchical —
  Systematische/Praktische/Historische/Biblische Theologie with their
  sub-disciplines, typos from the pad normalised) and the `ekkw-hochschule`
  form: 4 sections in the pad's corrected order (Publikationstyp FIRST per
  "Seite 6 an den Anfang"), DOI, Erschienen-in (name-only isPartOf),
  Band/Heft as ext, trimmed Ressourcentyp/Bildungsstufe option lists
  ("Unterpunkte streichen", "Promotion rauslassen"), Autoren (amb:creator)
  and Herausgeber (amb:contributor) separated per the library feedback.
  Every field carries its Erläuterungssatz.
- publish-forms-build: map inline `selectOptions` and per-field
  `description` into the builder shape (tests red first at the data;
  sections already passed through). Round-trip through the app parser
  asserted.
- Both publish scripts take `--only d1,d2` — publishing one new entry must
  not re-sign and re-stamp `published_at` on all 25 others.
- `hochschule` variant registered (template-driven only; OFF everywhere
  until a deployment sets RESOURCE_FORM_VARIANTS and
  RESOURCE_FORM_TEMPLATE_NADDR_HOCHSCHULE).

Verified end-to-end on a dedicated sandbox relay (compose project
ekkw-demo, port 17013 — separate from the e2e stack so e2e teardowns
cannot reap it): published scheme+15 concepts+form via the scripts, read
back off the relay, and drove /create/resource/hochschule in real Chrome —
paged wizard 1/4-3/4, option lists, Erläuterungssätze, and the collapsible
Theologie tree all render. Full suite 489 files / 5483 / 0, exit 0;
svelte-check 0 errors / 4 warnings.

Note: the two scripts/data JSON files are re-indented wholesale (python
json.dump, 2-space) — the semantic change is exactly one added scheme and
one added form; every pre-existing entry is byte-identical after parse.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/ekkw-metadata-form:feat/ekkw-metadata-form
git switch feat/ekkw-metadata-form

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch feat/field-descriptions
git merge --no-ff feat/ekkw-metadata-form
git switch feat/ekkw-metadata-form
git rebase feat/field-descriptions
git switch feat/field-descriptions
git merge --ff-only feat/ekkw-metadata-form
git switch feat/ekkw-metadata-form
git rebase feat/field-descriptions
git switch feat/field-descriptions
git merge --no-ff feat/ekkw-metadata-form
git switch feat/field-descriptions
git merge --squash feat/ekkw-metadata-form
git switch feat/field-descriptions
git merge --ff-only feat/ekkw-metadata-form
git switch feat/field-descriptions
git merge feat/ekkw-metadata-form
git push origin feat/field-descriptions
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
edufeed/edufeed-app!82
No description provided.