Skip to content

feat: support optional statements inside statement groups#554

Merged
tkuhn merged 1 commit into
masterfrom
feat/optional-statements-in-groups
Jul 14, 2026
Merged

feat: support optional statements inside statement groups#554
tkuhn merged 1 commit into
masterfrom
feat/optional-statements-in-groups

Conversation

@tkuhn

@tkuhn tkuhn commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Implements member-level optionality inside statement groups per the design doc docs/optional-statements-in-groups.md (merged in #553's sibling d227dbc): a member of a nt:GroupedStatement marked nt:OptionalStatement may stay empty — its triple is then dropped — while the group's other members keep their own requirements. Repeatable members and nested groups remain out of scope (the group is the unit of repetition) and are now explicitly ignored with a warning.

Changes

  • Template parsing (Template): an all-optional group is normalized to a group-level optional with a warning — a group needs ≥1 required member, otherwise matching would be vacuous and repetition detection would not terminate. Out-of-scope member flags (nt:RepeatableStatement, nested nt:GroupedStatement) are stripped with a warning.
  • Publish path (StatementItem): hasEmptyElements/addTriplesTo skip empty optional members instead of blocking (required statement) or silently dropping the whole group (optional statement). Both gated on isGrouped() so top-level optional statements behave exactly as before.
  • Form UI (ValueItem, StatementPartItem.html, style.css): member-optional fields are non-required (per repetition — unlike group-level optionality, which turns off when repeated); a per-line "(optional)" mark renders inline after the statement via a new part-label slot, leaving the group-level mark and its repetition toggling untouched.
  • Fill/unification (StatementItem): assignParts gets a skip branch for optional members, tried only after all candidate statements fail, so fills stay maximal (match-over-skip). matches() additionally requires at least one consumed statement, guarding the trial-repetition loop against vacuous matches. Skipped members are tracked in unmatchedParts; their rows are hidden in read-only rendering and shown as empty editable fields in derive/update contexts.

Backwards compatibility

Existing templates parse and behave identically (the new paths only activate on the member-level flag, which nothing honored before). Old parsers treat member-optional statements as required — stricter but never invalid; fill of a nanopub legitimately omitting the optional triple falls back to the generic statement display there. All 196 live templates in AllAssertionTemplatesLoadingTest still load.

Tests

  • OptionalGroupMemberTest: parsing, all-optional normalization, out-of-scope flag stripping, memberless-group regression.
  • OptionalGroupMemberPublishTest: empty optional member dropped, filled member emitted, empty required member still fails.
  • OptionalGroupMemberFillTest: group matches a nanopub omitting the optional triple (skipped member tracked for row hiding), full match, required-member-missing non-match, repeatable group with per-repetition optionality, termination guard (@Timeout).

All test fixtures are built programmatically (no new network dependencies). Full suite: 771 tests, 0 failures.

Verified additionally against the live "Annotation as a discourse contribution" template (…RADVnxF4…): its selector group publishes correctly with oa:prefix/oa:suffix omitted. (That template independently needs republishing with nt:LocalResource declarations for statement/selector/anchor — unrelated to this PR.)

🤖 Generated with Claude Code

Individual members of a nt:GroupedStatement can now be marked
nt:OptionalStatement: the member's triple is emitted only if its
placeholders are filled, while the group's other members keep their own
requirements. Design doc: docs/optional-statements-in-groups.md.

- Template parsing: an all-optional group is normalized to a group-level
  optional with a warning (a group needs at least one required member,
  otherwise matching would be vacuous and repetition detection would not
  terminate); out-of-scope member flags (repeatable, nested group) are
  ignored with a warning instead of half-rendering.
- Publish path: hasEmptyElements/addTriplesTo skip empty optional
  members instead of blocking or dropping the whole group.
- Form UI: member-optional fields are non-required; a per-line
  "(optional)" mark is rendered inline via a new part-label slot,
  leaving the group-level mark untouched.
- Fill/unification: assignParts gets a skip branch for optional members
  (match-over-skip keeps fills maximal), matches() requires at least one
  consumed statement to guard the repetition loop, and skipped member
  rows are hidden in read-only rendering while staying editable in
  derive/update contexts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tkuhn tkuhn merged commit 093c8d9 into master Jul 14, 2026
8 checks passed
@tkuhn tkuhn deleted the feat/optional-statements-in-groups branch July 14, 2026 04:59
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 5.6.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant