Skip to content

Feature/reconcile - #386

Open
awagner-mainz wants to merge 35 commits into
eeditiones:mainfrom
mpilhlt:feature/reconcile
Open

awagner-mainz wants to merge 35 commits into
eeditiones:mainfrom
mpilhlt:feature/reconcile

Conversation

@awagner-mainz

Copy link
Copy Markdown

Reconciliation update

The main point of the major reconciliation overhaul is adding a "reconcile" profile that makes TEI Publisher act as a reconciliation server. Thus, the data that a project maintains can be used by other projects (or internally, for that matter) as authority data. Often these data are quite specialized and the project is well-established in its research domain and community, but the data don't meet all (e.g. notability or entity type) requirements that large and "official" authority databases require. Then, the domain community can reconcile their OpenRefine or TP annotation activities against the project's TEI Publisher instance.

A second point is an update of the existing "annotate" profile for reconciliation services. This goes along with a PR (390) for tei-publisher-components. It adds explicit support for more recent versions of the Reconciliation API (v0.2 and v1.0-draft), plus some new features. Most notably, data extensions, which allows reconciliation clients to augment their data with more properties of the reconciled entities that are maintained and supplied by the authority database. This required revising how fields are mapped to TEI attributes: while formerly, it was hardcoded that the retrieved entity id would go to the @key attribute, it is now possible to specify a mapping in the authority lookup definition like this:

<pb-authority connector="gnd" name="organization" fields="ref=id,link=extend:link,target_attribute=source_field">

For more details, see PR 390 in tei-publisher-components.

New reconcile profile for adding the API

  • profiles/reconcile/README.md
  • profiles/docs/data/doc/reconcile*: documentation for the reconciliation API server
  • profiles/reconcile/config.json: profile config
  • profiles/reconcile/modules/reconcile-api.tpl.json: OpenAPI-compatible spec (template)
  • profiles/reconcile/modules/reconcile-api.xql: implementing the various endpoint functions (manifest, query, preview, suggest, extend etc.)
  • profiles/reconcile/modules/reconcile-config.xql: configure $reconc-config:TYPE map specifying how to handle, query, rank and offer different entity types in reconciliation (and some limits)
  • profiles/reconcile/modules/reconcile-conditions.xql: harmonize/normalize query conditions between various query request shapes of the different supported API versions
  • profiles/reconcile/modules/reconcile-fulltext.xql: add (optionally extend) fuzzy Lucene fulltext search that can be used in reconciliation queries
  • profiles/reconcile/modules/reconcile-scoring.xql: add (optionally extend) scoring functions that can take into account Levenshtein distances
  • cypress and xqsuite tests

Update annotate profile

  • profiles/annotate/config.json:
    • adds the keyMap map to identify per entity which attribute the id should go to by default (itself @key by default) and add an icon to the theme
  • profiles/annotate/modules/annotations/tei-annotation-config.tpl.xqm:
    • add more complex handling of key-or-other-attribute for where the id should reside
    • add all available properties as attributes to the various entities
    • add a sanitizing function guarding against empty JSON objects where strings are expected (and log such events) (see below, profiles/annotate/templates/pages/annotate.html)
  • profiles/annotate/resources/css/annotate.css:
    • fix layout problems independent of the reconciliation update. PROBABLY THIS SHOULD BE CHECKED ON MULTIPLE BROWSERS/RESOLUTIONS?
  • profiles/annotate/resources/scripts/annotations/annotations.js:
    • add more complex handling of properties upon entity selection
    • pipe multiple properties to form fields
    • catch XSS vulnerability in setting the status line and in document.addEventListener("pb-page-loaded"...)
  • profiles/annotate/templates/annotation-blocks.html:
    • improve doctype detection. CAN SOMEONE CHECK THIS?
  • profiles/annotate/templates/pages/annotate-jats.html:
    • add (demo) reconciliation connector
  • profiles/annotate/templates/pages/annotate-tei.html:
    • fix wiring of different "key" variables (the confusion of those "key" variables has not been resolved yet)
    • add more form fields for more properties provided by authority dbs
    • add fields mapping to custom geonames/gnd connectors
    • make the custom person authority definition combine two remote authority dbs (the gnd plus a reconciliation endpoint)
  • profiles/annotate/templates/pages/annotate.html:
    • fix icons inclusion
    • handle configurable id-containing attribute
    • add cancel/close button in multiple places
    • fix display of annotation status (options fields)
    • prevent sending empty JSON objects instead of strings (see above, profiles/annotate/modules/annotations/tei-annotation-config.tpl.xqm)
  • cypress api and gui tests for the annotate profile
  • docs

Other changes

  • profiles/forms/config.json:
    • add enabled switch

Andreas Wagner and others added 30 commits August 30, 2026 11:32
… lookup

Add the generic OpenRefine Reconciliation API connector as a fallback
alongside the existing GND connector for the person register, in both
annotate-tei.html and annotate-jats.html's authority-config template
(previously only present in the orphaned annotate-authorities.html
demo file). Endpoint is decoupled/configurable, defaulting here to
this repo's own local "reconcile" demo app.
… caveat

New doc/reconcile.xml explains the reconcile profile's server-side
Reconciliation Service API (endpoints, adding it to an app, customizing
the entity/property model), cross-linked from the Reference Manual's
"Extending the API" section and the annotation editor docs.

The Reconciliation Service connector docs claimed it "cannot be used as
a data provider for the Custom connector" / "it is not possible to use
[it]... in the context of the Custom connector" - stale since 2021;
Custom's createConnectors() has always dispatched on nested connectors
generically, with no special-casing, and this has been actively
exercised since 2023. Replaced both caveats with a positive example, and
fixed connector="Reconciliation" -> the actual attribute value
"ReconciliationService" recognized by connectors.js (a separate,
pre-existing inaccuracy in the same examples).
…ency

Matches reconcile/config.json dropping "registers" from depends: the
"view" field's redirect is now checked against the app's actual
installed-profiles list at request time, falling back to a built-in
preview otherwise. Update the extends example, endpoint description,
and add a "view" customization subsection covering the new
function-item override.
…e-filtering

Matches reconcile/modules/reconcile-config.xql's new "labels" and
"fulltext-search" fields and reconcile-scoring.xql's edit-distance fallback
tier.
Matches reconcile/modules/reconcile-conditions.xql - a query can now
disambiguate/refine via property conditions or match directly by id, not
just a plain name, reusing the same "properties" already defined for
/extend.
Mirrors the new "Production hardening" section in
profiles/reconcile/doc/README.md: request-size caps, reverse-proxy
rate limiting, and opt-in auth via x-constraints.
…tributes

The Reconciliation Service connector section didn't warn that an
unrecognized `connector` attribute value silently falls back to the
unrelated Metagrid connector, with no error - a real, easy-to-hit footgun
(e.g. `connector="Reconciliation"` instead of the exact
`connector="ReconciliationService"`; the docs' own prior example used the
wrong, non-"Service" name). Add a caution note pointing at the exact,
case-sensitive value required and the concrete symptom (queries going to
api.metagrid.ch) so this is diagnosable from the docs alone next time.

Also document the connector's `type` and `limit` attributes (verified
against the actual reconciliation.js source, not assumed), and that results
are now labeled with the connector's `prefix` when set, to disambiguate
multiple reconciliation-style authorities in the same app.

reconcile.xml: note that malformed batch entries (e.g. a blank spreadsheet
cell serialized as JSON null) degrade to an empty query rather than failing
or misaligning the whole batch, preserving the spec's required positional
correspondence between queries and results.
…ibutes

Generalize the entity-authority editor to write any number of a selected
match's properties into separate output attributes instead of always
writing just the id into one hardcoded field. annotations.js's
authoritySelected() now fans out a properties object rather than assuming
a single bare ref string; tei-annotation-config.tpl.xqm copies whatever
attributes the client sent instead of hardcoding [$key]. Demo config in
annotate-tei.html shows an example person mapping (escaped preferred name
as @key, id as @ref, GND identifier fetched via /extend as a third
attribute). Also documents this, plus the suggest-endpoint type-ahead and
extend-sourcing behavior, in the annotations doc page.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…on UI

Document that OpenRefine's own reconciliation client only understands
the 0.2 manifest field names (extend.propose_properties.service_url/
service_path, suggest.property.service_url/service_path) for its "Add
columns from reconciled values" dialog - it has no code path for
1.0-draft's boolean-only equivalents, so no manifest content on our
side can make that feature work against the default 1.0-draft
manifest. Recommends adding the service with ?version=0.2 instead of
mixing 0.2-only fields into the spec-pure 1.0-draft manifest. Also
documents the new /suggest/flyout endpoint and why it exists (a
related OpenRefine client quirk around the 0.2 spec's
flyout_service_path field).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
New subsection under "Customizing what gets reconciled" explaining that a
property's "value" is a plain function item with full XQuery/collection
access, not restricted to the matched entity's own subtree - covers the
mechanism, a verified worked example (average title length of documents
mentioning a person, via this demo's persName/@key convention), and
performance guidance (index the lookup or cache the aggregate once it's
actually a bottleneck, same options already used for fulltext-search).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…y list, images

Explains that /preview now rewrites every link/image URL to an absolute
one regardless of how relative the underlying ODD transform's own
output is, why that was needed (the shipped connector injects the
fetched HTML into an existing page via innerHTML rather than loading it
as its own document, so a relative link resolved against the wrong
page), and that the default preview now lists every configured property
with a value, rendering image-shaped values as thumbnails
(image: true()/false() to override the guess).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…ain preview overflow

Root cause of a bug reported 2026-07-24 (real repro: a user-created
persName ref="kbga-actors-329" key="Rade-Martin-1857-1940" showed
"Entity not found" in its detail popup, while an old-style
persName key="kbga-actors-27" - id directly in @key, no @ref - worked
fine): pb-view-annotate's own detail-lookup reads the entity id from
whichever annotation attribute its "key"/"key-map" config names, default
"key". That was correct under the OLD single-attribute convention this
profile shipped before this session's field-mapping feature, where @key
always held the id - but a fields="key=label,ref=id,..." mapping (the
profile's own shipped "person" example, see annotate-tei.html) repurposes
@key for the *label* instead, with the id moving to @ref. The lookup
never got told about that: it kept reading @key, tried to look up a
label-slug as if it were an id, and correctly reported "not found".
Fixed with a "person": "ref" entry in config.json's
features.annotate.configs.tei.keyMap, wired onto <pb-view-annotate
key-map="..."> in annotate.html - pb-view-annotate.js's own getKey(type)
already supported this per-type override, it just wasn't configured.
Other types (place/organization/work) keep the old key=id default
unaffected, since they don't use the new field-mapping convention.

Also fixes a second, unrelated, previously-latent bug this investigation
surfaced: annotate.css's containment rule for the entity-info preview
area used an ID selector ("#authority-info") that never matched
anything - the template only ever sets class="authority-info" - so the
intended max-height/overflow:auto never applied. Harmless while preview
content was a line or two; once the /preview response started listing
every configured property (see the reconcile profile's own preview
enhancement, same investigation), the unbounded growth pushed the
reconciliation search results below the visible viewport, making a
pre-existing Cypress test intermittently/consistently fail on a
"clipped by an overflow: hidden ancestor" visibility check - not a
regression in the search itself, just a long-dormant CSS selector typo
finally mattering. Fixed the selector to match the actual class.
Also caps the info area of the small "click to view" Tippy popup, which
had no size bound at all, for the same reason.

One new Cypress regression test reproduces the original bug against a
real, previously-user-created live entity (sermons/27003.xml's "Rade"
annotation) rather than a synthetic fixture. Full suite re-verified:
25 XQSuite, 43 Cypress API+GUI (was intermittently failing 1 before
either CSS fix, now green with no retries needed).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…ce/work

Only "person" actually used fields="key=label,ref=id,..." and the
matching keyMap override - organization/place/work still used the
implicit default (key=id, the old single-attribute convention), and so
had not hit the "Entity not found" bug fixed for person, but also
weren't getting the richer escaped-name-as-key/id-as-ref split.
Registry.buildProperties (tei-publisher-components) already made this
mechanism generic across every connector, including "Custom" (which
these three types use, wrapping GND/GeoNames as a fallback) - only this
config needed catching up.

Adds fields="key=label,ref=id" to the outer Custom connector for
organization/place/work (skipping an extend:-sourced property for any
of them: neither Custom's own local-register results nor its GND/
GeoNames fallbacks currently support real data-extension fetching, so
an extend: mapping would just silently never resolve - unlike person's
gnd=extend:gnd, which is meaningful there because it can be nested with
a real ReconciliationService connector). Adds matching "ref" entries
for all three types to config.json's keyMap.

Verified live: a real "place" search-and-select (Castilla -> Madrid)
correctly writes key="Madrid"/ref="geo-3117735" via the shared
buildProperties mechanism, and a place linked via @ref correctly shows
its real preview in the click-to-view detail popup instead of "Entity
not found" - confirming the keyMap fix generalizes, not just the
mechanism. See the annotate_select_save_not_verified project memory for
a separate, pre-existing gap this investigation surfaced (not fixed
here): Cypress's forced-click re-selection of an already-annotated span
doesn't reliably persist to the document, for any type - worked around
in the new regression test by setting up the linked state directly via
XQuery instead.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Explains why moving the id off the reference/key attribute (e.g.
fields="key=label,ref=id") needs a matching keyMap entry in the app's
own config.json, or the click-to-view detail popup shows "Entity not
found" for an otherwise perfectly valid annotation - the bug fixed
2026-07-25, now documented so the next person configuring a custom
fields mapping doesn't have to rediscover it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds a hidden <input name="link"> to the shared annotation-form template
(alongside the existing ref/gnd ones) and extends organization/place/
work's fields= to "key=label,ref=id,link=extend:link", exercising
tei-publisher-components' newly-added GND.fetchExtend/GeoNames.
fetchExtend (an external reference URL fetched from that connector's own
record) - the GND/GeoNames equivalent of person's existing
"gnd=extend:gnd" sourced from a nested ReconciliationService.

Also the fix half of this session's `jinks update --reinstall`
verification: this profile's Jinks-registered copy had never actually
been kept in sync with local edits all session (only the live app was
patched) - a real regeneration silently reverted every annotate fix from
today. Deployed every changed annotate profile file to
/db/apps/jinks/profiles/annotate/... to match the pattern already used
for the reconcile profile, then re-verified with --reinstall that
everything (keyMap, fields=, the CSS fix) now correctly survives a clean
regeneration. See the jinks_profile_sync_and_annotate_template_gotchas
project memory for the full story, including a second, separate finding
(templates/pages/annotate.html is a base template never meant to be
requested directly - use annotate-tei.html, fixed in the test suite
separately).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Person's Custom connector wraps both a GND connector and a nested
ReconciliationService, and a single search can surface a match from
either one - the field mapping only resolves consistently across both
because of a matching fix on the tei-publisher-components side
(GND.getRecord() now aliases its "link" property as "gnd" too). Expand
the doc comment to explain why, so a future edit doesn't "simplify"
gnd=extend:gnd to extend:link and reintroduce the bug.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
Full read-through pass over every annotate-profile file touched during this
project (config wiring for connectors/fields/keyMap per document type),
aimed at a reader unfamiliar with the session history. Clarifies that the
server-side $anno:reference-key/anno:get-key (always literal @key) and the
client-side keyMap (per-type, drives the editor's popup) are two separate
mechanisms; moves the person-specific "why gnd=extend:gnd needs GND.js's
dual alias" explanation to sit directly above the person connector block
instead of a shared comment above all four types. No logic/markup/config
value changes.

Flags one thing worth a follow-up, not fixed here (comments-only pass):
anno:occurrences groups by literal @key, which for a field-mapped type now
holds a slugified label rather than a stable id - two different people with
the same name would incorrectly count as the same entity's occurrences.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
…dd screenshots

- annotations.xml: update the keyMap example to all four field-mapped types
  (was person-only, stale since organization/place/work were added); soften
  the "Entity not found" caution now that pb-view-annotate.js falls back to
  @key when a type's configured id attribute is missing; add a tip on the
  Custom connector explaining that an extend:propertyId field source is
  resolved by property id alone, so every nested connector needs to expose
  a value under the same name (the concrete reason GND.js aliases "link" as
  "gnd" for person).
- reconcile.xml / annotations.xml: add real screenshots (captured live
  against tp-reconc) for the service manifest, the annotate editor's
  federated search results (local + GND badges), and the click-to-view
  detail popup - the reconcile-specific docs previously had none, unlike
  the rest of annotations.xml's extensive ann-*.png screenshots.

Verified via the docs site's own content-fetch API and a full page render
after syncing to the running apps/tei-publisher app.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
The third issue flagged during the recent comment-audit pass: for a
field-mapped type (see annotate-tei.html's `fields=` attributes), an
entity's id lands in @ref while @key holds a slugified label instead -
anno:occurrences only ever matched @key, so it undercounted to zero for
any field-mapped type's occurrence badge in the search results list.

NOTE: this app's live annotation-config.xqm (imported directly by
annotations.xql) is actually a separate, flat, doctype-agnostic module
bundled in the existdb/teipublisher base image, not generated from this
file - the dispatcher that's supposed to delegate to it
(annotation-config.tpl.xqm) was never registered with Jinks. This fix is
applied here as the correct source-of-truth for when that gets wired up
properly, and was additionally live-patched directly into the deployed
flat module so the running demo app actually reflects it today - see the
annotation_config_dispatcher_dormant project memory for the full
investigation and why fully swapping in the dispatcher wasn't attempted
here (its anno:annotations has a different, incompatible arity from what's
actually deployed).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
…in tests

Three of the annotate profile's own shipped Cypress tests
(api/annotations.cy.js, gui/annotate.cy.js, gui/custom-annotate.cy.js) all
called cy.uploadXml(path, xmlContent) - an old 2-arg convention that no
longer matches commands.js's actual signature, cy.uploadXml(url, filename,
xml, opts) (multipart, POSTs to an upload endpoint). Every one of these
tests therefore failed in its own before()/beforeEach() hook (405 posting
to the literal fixture path) before ever reaching its real assertions -
meaning this test coverage, including 11 real regression cases for
/api/annotations/merge (delete+wrap, nested notes, choice/abbr/expan,
app/lem/rdg, standOff/listAnnotation presence), had likely never actually
run/passed in this project's context. Fixed all three call sites to a
direct REST PUT instead, matching this project's own established
fixture-deployment pattern.

Also fixed gui/annotate.cy.js's separate, already-known-stale
`?template=annotate.html` (base template, empty blocks) to
`annotate-tei.html` (see the jinks_profile_sync_and_annotate_template_gotchas
project memory).

Found while thoroughly re-verifying the annotation-config dispatcher fix
(see prior commits + the annotation_config_dispatcher_dormant project
memory) - this is exactly the previously-missing save-path coverage that
investigation needed and didn't have.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
…ences

The "Configuring Annotations" section (and the "Custom Connector" section's
local-authority-file note, and the authority-config location note earlier
in the page) still told readers to edit modules/annotation-config.xqm
directly and templates/pages/annotate.html - both wrong now that the
per-doctype dispatch split (annotation-config.tpl.xqm delegating to
tei-annotation-config.xqm, annotate.html as an empty base template extended
by annotate-tei.html) is actually wired up and working, not dormant. Six
reference fixes total, plus a caution note that the walkthrough's toolbar
HTML/class names (header-toolbar, icon-group) predate TEI Publisher 10's
current page-template layout and may not match annotate-tei.html verbatim -
flagged rather than rewritten, since verifying/rewriting the full
walkthrough is a separate, larger task.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
annotation-blocks.html's toggle link built its URL from $context?doc?type,
which comes back empty in this app's actually-deployed base10 (drifted from
this repo's own history, same class of issue as the
annotation_config_dispatcher_dormant finding) - producing a broken
"template=annotate-.html" link. Compute the doctype directly from the
reliably-present $context?doc?content node instead.

forms/config.json never set features.forms.enabled, the flag
forms-blocks.html gates fore.js/fore.css loading on - so the annotation
editor's <fx-fore> elements never upgraded and raw Fore markup (JS bodies,
instance data, a stuck "Cannot save to local register" message) rendered as
plain visible text. Confirmed via a Playwright check
(customElements.get('fx-fore') was undefined) and fixed by setting the flag.

Found while verifying the reconciliation demo Docker image against a fully
clean rebuild.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
The annotation toolbar's buttons (person, save, undo, ...) rendered as
blank pills - their <svg><use href="#person-fill"> markup needs the icon
sprite's <symbol> defs present somewhere in the document. annotate.html's
frontmatter already declared theme.icons: ["annotate-icons.svg"], the
convention base10's layouts/base.html is meant to consume (the same
pattern metadata-editor uses successfully) - but this app's actually-
deployed base10 has no such include loop at all (confirmed by fetching the
live templates/layouts/base.html: no icon-related include, unlike its
menu.html/toolbar.html ones).

Fixed pragmatically: annotate's own content-top template now directly
includes the sprite itself, independent of base10 support. Left the
config.json theme.icons declaration in place too since it's harmless and
correct for any base10 that does implement the loop.

Found while re-verifying the reconciliation demo Docker image after fixing
the XQDY0025 crash that was previously masking this bug.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
…panel

The annotation editor's right-hand panel (search/edit forms, occurrences,
commit) looked completely broken - misshapen "cards", missing borders,
oversized empty space around normal-sized icons, invisible labels. Root
cause: annotate.css's `fx-fore fx-group { display: grid; ... }` rule
(specificity 0,0,2) that drives each card's grow/shrink animation was
losing the cascade to @jinntec/fore's own generic `[relevant] { display:
block; }` rule (specificity 0,1,0, wins ties regardless of load order
since attribute selectors always outrank type selectors) - every group
Fore marks relevant="" silently fell back to block flow instead of the
grid layout the whole card design depends on.

Confirmed via Playwright: before the fix, a "card" fx-group computed to
display:block/height:36px while its own child header was 54px tall
(overflowing); after, the panel renders as a normal, properly laid out
form with visible borders, labels, and correctly sized icons.

Reproduced and fixed outside Docker first (a plain dev-container app on
the base image), then confirmed the same fix resolves it in the
reconciliation demo Docker image too - this bug predates and is unrelated
to that packaging work; it's been latent since the annotate profile
started depending on the "forms"/Fore integration (2026-02-18), just
never visually verified until now since the automated GUI regression
tests only assert functional behavior (request URLs, form values), not
CSS.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
Two more polish issues found by real use of the panel now that it
actually renders (see the CSS-specificity commit): rounded corners were
huge (90px, looking like speech bubbles) and there was no way to dismiss
an in-progress edit/annotation once opened.

Corners: this app's deployed base10 controls.css ships (undrifted from
the local tei-publisher-jinks history's own fix) a bare `[role="group"]
{ border-radius: var(--jinks-form-border-radius) }` rule (a 90px pill
meant for small toolbar segments, normally excluded via `:not(fx-group)`
- the deployed copy lacks that exclusion). Every fx-group card here also
carries role="group" (Fore's own convention), so it inherited the pill
radius instead of base.css's normal 1rem .card radius. Fixed the same way
as the earlier display:grid fix: fx-group[role="group"] (specificity
0,1,1) reliably beats the base10 rule (0,1,0).

Close button: added a small "x" (reusing the existing #dialog-close
sprite icon) to each of the four card headers (commit, annotation,
authority, occurrences), wired via <fx-trigger><button>...<fx-call
action="reset-panels"></fx-call></fx-trigger> - the exact same pattern
already used by the proven "save" button in the commit panel, calling the
same pre-existing reset-panels action every other panel-opening flow in
this file already relies on.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
Custom.query() (tei-publisher-components/src/authority/custom.js)
concatenates nested-connector results in document order, so this
profile's own reconciliation service now appears ahead of GND in the
candidate list for person annotations, instead of after.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
Two bugs found via real interaction (both confirmed by the user, one
initially misdiagnosed by me as a test-harness artifact before finding
the real cause with a corrected element selector):

1. Clicking a card's close button set the right instance values
   (confirmed directly) but the [relevant]/[nonrelevant] DOM attributes
   never updated, leaving the panel visibly open. reset-panels' plain
   <fx-refresh> (Fore's default, unforced form) doesn't reliably
   re-render when invoked via <fx-call> from *inside* one of the very
   cards it's about to hide - a forced fx-fore.refresh(true) cleared it
   correctly every time in direct testing. Added force="" to both
   <fx-refresh> calls in annotate.html (reset-panels and pb-before-save) -
   a strict superset of the unforced form, safe for every existing caller.

2. Adding the close button as a 4th icon to headers that already held 3
   (occurrences' mark-all/search-collection/save-all) tipped an
   unprotected flex row into squeezing its icons instead of the text
   label - confirmed directly: iron-icon width collapsing to 8px while
   height stayed 24px (visibly squashed, not just smaller). Added
   flex-shrink:0 to header icon buttons and let the label truncate
   instead, which degrades better than an illegible icon.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
Four more issues found by real use, all confirmed live before/after:

- Card headers ("Annotation Details", "Revision Description", ...) and
  the annotation-form's plain <input> (not wrapped in <fx-control>, so it
  never got that selector's border/background) were visually
  indistinguishable from surrounding text - both now get a real title-bar
  treatment and a visible bordered input respectively.
- The commit panel's Status <select> always rendered two blank options:
  <option> can only reliably host text, not a nested custom element, and
  <pb-i18n> inside it never rendered. Switched to data-i18n's plain
  (no [attr] prefix) form, which pb-page.js applies via a direct
  innerHTML set - works on any element, <option> included.
- The #form-save icon (and any other icon-only button outside a <header>)
  suffered the same flex-squeeze already fixed for header icons.
  Broadened the flex-shrink:0 rule from ".preview header" to ".preview"
  generally instead of special-casing each container.
- The real bug: selecting a reconciliation match for a person already
  present in the local KBGA register created a second, separate,
  near-empty register entry under a "reconc-"-prefixed id instead of
  resolving to the existing rich entry - confirmed live (the resulting
  persName showed only its raw @key and "entity not found", while a
  pre-existing mention of the literal same person, tagged with the
  unprefixed id, showed full name/bio). Root cause: this profile's own
  reconcile server matches directly against the same local register
  (reconcile-config.xql's "person" entities are exactly
  collection(...)/id(...)//tei:person[@xml:id]), so a candidate's id
  already *is* the register's own xml:id - prefixing it was giving two
  different-looking ids to the identical entity. Removed prefix="reconc"
  from the ReconciliationService connector; verified end-to-end
  (Custom.query()/select()/buildProperties()) that a match now correctly
  resolves to the existing entry (POST .../kbga-actors-27 -> "found", no
  duplicate) and produces ref="kbga-actors-27", not a new id.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
…w info

You were right to suspect @key vs @ref. Two distinct, compounding bugs,
both now confirmed fixed live:

1. Live "Entity not found" preview immediately after selecting a
   candidate. The visible, user-facing reference input (class="form-ref",
   the one whose "input" event triggers the live pb-authority-lookup
   preview) was named from features.annotate.configs.tei.key ("key", a
   single, type-agnostic value) - NOT from keyMap, which correctly maps
   person/organization/place/work to "ref". A separate <input type="hidden"
   name="ref"> already existed and silently received the real id
   correctly; the visible field instead received "key=label"'s
   human-readable slug (e.g. "Barth-Peter-1888-1940") and ran the live
   lookup against *that*, which 404s. Fixed by swapping which field
   carries the "form-ref" class/visible role: the one the user actually
   sees and that drives the live lookup is now literally named "ref"
   (matching pb-view-annotate's own default key - confirmed via its
   source: `this.key = 'ref'` - so this is correct for every type sharing
   this form, not just the four in keyMap).

2. Full entity info missing from an already-saved annotation's
   click/hover popup after a page reload - a *different* rendering path
   than (1): base10's generic teipublisher.odd (shared by
   persName/placeName/orgName/rs, not owned by this project) resolves an
   entity via `head((@key, @ref))` - @key if present, else @ref - the
   opposite priority from this app's own keyMap. With a real id in @ref
   but a human-readable slug in @key (from "key=label"), that expression
   picks the slug, fails to resolve it as an id, and falls back to
   displaying the bare slug string with no lookup at all - confirmed
   exactly reproducible/fixable by toggling @key's presence on an
   existing saved persName, live, before touching any source. Rather than
   overriding a shared, comparatively risky base10 ODD rule, removed the
   "key=label" mapping outright: with @key never written for these types,
   head((@key, @ref)) simply returns @ref, matching this app's
   convention. The existing "already-linked entity" Cypress regression
   test is unaffected - it builds its own test fixture directly via
   XQuery, independent of this fields= mapping.

Verified end-to-end together: selecting "Barth, Peter (1888-1940)" now
shows the real name/bio in the live preview immediately (no more "Entity
not found"), and produces properties {ref: "kbga-actors-27"} with no
"key" property at all.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
…odes

annotate.html's pb-commit dispatch called string() on the "message"
fx-property but not on "user", "export", or "status", so those carried
raw XPath attribute-node results into the CustomEvent detail object.
JSON.stringify() collapses such a node to "{}" (DOM/Fore node wrappers
expose no own enumerable properties), so the server received log.user/
log.status as an empty JSON object instead of a string or absent value.

tei-annotation-config.xqm's anno:extend-header() then tried to atomize
that empty map inside a `{...}` attribute value template
(who="{$log?user}" status="{$log?status}"), and eXist throws err:FOTY0013
"A function item other than an array cannot be atomized" - maps are
function items in XQuery 3.1, and only arrays are exempted from that
error. Reproduced the exact error via an ad-hoc query atomizing a bare
map{} the same way, then confirmed the client dispatch was the source.

Fixed at the client (string() on all four fx-property expr attributes)
and added anno:sanitize-log-value() server-side as defense in depth, so
a malformed request body can never 500 this endpoint regardless of what
a future caller sends. Verified against the live merge endpoint with the
exact crash-inducing payload (log.user/status as {}): now returns 200
with who=""/status="" instead of 500.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
Andreas Wagner and others added 5 commits August 30, 2026 11:34
…xpansion

The previous commit's doc comment on anno:sanitize-log-value used `{...}`
(backtick immediately followed by a curly brace) to describe an XQuery
attribute value template. That's not just markdown styling here - Jinks's
templates.xqm wraps an entire .tpl.xqm file's source in an eXist string
constructor and treats that exact character sequence as the start of a
real interpolation, regardless of whether it's inside a comment. Building
a new app (jinks create, i.e. every container's first boot) failed outright
with err:XPST0003 "unexpected token: ." while template-expanding this file,
so the demo container never started - a full regression, not just a
degraded feature.

Reworded the comment to avoid the sequence and added a warning against
reintroducing it. Swept both the reconcile and annotate profile's .tpl.*
files for the same pattern - no other occurrences. Verified end-to-end:
rebuilt docker/Dockerfile locally, the generator step (jinks create) now
completes cleanly with no FOTY0013/XPST0003, and the running container
answers the manifest, suggest, and merge endpoints correctly (re-tested
the exact payload that motivated the previous fix).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GJ79F6UanhKHMctVm12ATN
… literals

Two real, confirmed stored-XSS spots in the annotate editor, both the same
class of bug: raw template-literal HTML assigned straight to .innerHTML
with an interpolated value that isn't developer-controlled.

1. The .form-ref input's "loading"/"loading failed" status messages built
   `<pb-i18n key="..."/> ${ref}...` (and "...${msg}") directly. `ref` is
   pre-populated from the document's own @ref/@key attribute when opening
   an already-tagged entity - a malicious @ref value in a document would
   fire for any user who later opened that entity's panel, not just
   someone typing it themselves. Fixed with a new setStatusMessage() helper
   that appends the interpolated text as a real text node instead.

2. The "note" annotation-detail popup set `container.innerHTML =
   data.properties.note` - plain document text pulled from the ODD's own
   [[note]] template interpolation, not pre-sanitized markup. Changed to
   .textContent. Also changed the sibling error-message innerHTML
   assignment (h.innerHTML = msg) to .textContent/a real <pb-i18n> element
   for the same reason, even though that one's less directly
   attacker-reachable.
Moves the reconciliation server profile from its standalone home in
mpilhlt/tei-publisher-reconcile into this repo as profiles/reconcile/,
matching the structure every other feature profile already uses (compare
to profiles/dts/: config.json with $schema/type/category/depends/api,
modules/, doc/README.md). Dropped expath-pkg.xml/repo.xml/build.xml and
the now-redundant "skipSource" config.json entry - standalone-repo
packaging scaffolding a profile living inside this monorepo doesn't need.

Tests moved to fit this repo's existing per-profile Cypress layout
(test/cypress/e2e/{api,gui}/*.cy.js, test/cypress/fixtures/schemas/ - see
base10's own test/cypress/schemas/openapi-3.0.json for the established
"a profile can ship its own JSON Schema fixtures" precedent):
- test/cypress/e2e/api/reconcile.cy.js and test/cypress/fixtures/schemas/
  {0.2,1.0}/*.json - the reconciliation protocol's own API tests, staying
  with the server profile.
- test/cypress/e2e/gui/annotate-reconciliation.cy.js moved to
  profiles/annotate/test/cypress/e2e/gui/ instead, alongside that
  profile's other GUI tests - it exercises the annotate editor's client
  behavior (via a live ReconciliationService connector), not the
  reconcile profile's own server code.
- test/xqsuite/*.xqm kept as-is (no other profile here uses XQSuite, but
  these are pure-logic unit tests - matches-value/normalize-1.0/normalize-
  0.2/evaluate - with no natural Cypress equivalent; dropping them would
  be a real coverage loss, not actual convention-following).

Scrubbed a handful of doc-comment references to paths and files specific
to the origin repo's own layout (skills/teipublisher-reconciliation-
testing/SKILL.md, reconc-specs/ as a local sibling clone,
README_MANUAL_TESTING.md) in favor of repo-agnostic wording (e.g.
pointing at https://github.com/reconciliation-api/specs directly) - none
of those paths exist here.

No code inside the .xql modules needed changing: every cross-module
import (config.xqm, pm-config.xql, etc.) already resolves against the
generated app's flattened modules/ directory regardless of which
profile's source tree it came from, exactly like every other profile's
own modules already do.

Not yet verified end-to-end in this repo (podman networking in the
current sandbox can't set up rootless containers right now - unrelated
infrastructure issue, not a code problem); the moved profile is otherwise
identical in behavior to the extensively-tested copy still living at
mpilhlt/tei-publisher-reconcile's own reconcile/ - see that repo's
AGENTS.md and README_MANUAL_TESTING.md for the full conformance history.
Re-run the full suite (XQSuite + Cypress API/GUI + cors-check.sh + both
local reconciliation-api testbenches) against a throwaway app generated
from this profile before treating this as PR-ready.

Prep work for a future PR to eeditiones/jinks - see the
fork_rebases_aug2026 and related project memory in
mpilhlt/tei-publisher-reconcile for the full upstreaming context. The
original copy stays in place at mpilhlt/tei-publisher-reconcile's own
reconcile/ for now; that repo's Docker demo image keeps building from
there until this is actually merged upstream.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XhBiqPTawcDo2p7niXwNgR
Refactor comments for clarity and readability.
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.

1 participant