Skip to content

handling of "empty" views #305

Description

@keighrim

Bug Description

The handling of "empty" views is currently done at app level, on a case-by-case basis, with no guidance from the SDK. Some apps skip creating a view entirely when they have nothing to annotate; others create a view but leave it without annotations. Neither behavior is documented or enforced, so the actual behavior of any given app is whatever its author happened to write.

Definition of "empty"

A view is empty when it carries zero annotations, regardless of whether its contains metadata was populated. This includes two distinct situations that both need to be treated as "empty":

  1. no view was created at all for a given _annotate() call
  2. one or more views were created, new_contain() was called on them, but no Annotation objects were ever added

Note that Mmif.sanitize() already strips any contains entry with zero matching annotations before serialization, so an empty-in-sense-2 view serializes with contains: {} regardless of what was declared. A view that has an app-set error or warnings in its metadata is not "empty" in this sense, even if it carries zero annotations: it is already informative on its own and must not be treated the same way.

Impact

Downstream tooling that uses app output might rely on the workflow ID generation offered by the mmif-python SDK (mmif.utils.workflow_helper.generate_param_hash), based on app identifier plus parameter hash pulled from view metadata. When an app returns no view at all, the last view is still whatever the previous app produced, so the workflow identifier computed for this app's (missing) output ends up identical to the identifier for the pipeline one step earlier, i.e. the pipeline minus the current app. Any downstream consumer that relies on this identifier hits the same problem: two distinct pipeline stages resolve to the same ID, so whatever the consumer does with that ID (storage keying, caching, deduplication, ...) silently misattributes one stage's identity to another. mmif-storage in aapb-brandeis-datahousing is one such consumer: it keys stored output by this identifier, so the collision causes one output to be dropped or overwritten with no error raised.

Reproduction steps

Confirmed case: app-spacy-wrapper creates one view per input TextDocument inside a loop (app.py, _annotate()). When the input MMIF has zero TextDocuments, the loop body never runs, so _annotate() returns the input MMIF completely unchanged: no new view, no record that the app ran at all.

Expected behavior

Or more like proposed behavior

Move this policy to the SDK level, in ClamsApp.annotate(), so it applies uniformly regardless of what an individual _annotate() implementation does:

  1. after _annotate() returns, diff the new views (created during this call) against the views present before the call
  2. delete any new view that is empty, per the definition above (zero annotations, no app-set error, no app-set warnings)
  3. if every new view was deleted (including the case where none were created in the first place), add a single SDK-generated, signed warning view stating that the app ran but produced no annotations, and that the cause is unknown (could be missing/unsuitable input, could be a genuine absence of anything to annotate)

This guarantees that every annotate() call produces at least one view carrying that app's identity in its metadata, addressing the reproducibility issue. An app that partially succeeds (some inputs yield real output, others don't) keeps its real views untouched and gets no spurious warning; only a fully empty run gets the synthesized warning view.

Log output

Screenshots

No response

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    🐛BSomething isn't working

    Type

    No type

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions