Skip to content

feat(pipeline)!: add beforeDatasetWrite hook; split namespace normalization#577

Merged
ddeboer merged 3 commits into
mainfrom
feat/before-dataset-write-hook
Jul 9, 2026
Merged

feat(pipeline)!: add beforeDatasetWrite hook; split namespace normalization#577
ddeboer merged 3 commits into
mainfrom
feat/before-dataset-write-hook

Conversation

@ddeboer

@ddeboer ddeboer commented Jul 8, 2026

Copy link
Copy Markdown
Member

Reworks the schema.org namespace normalization from #568 so the VoID analysis core no longer knows about namespace aliases – normalization is one opt-in plugin at the pipeline edge again, and the namespace handling is split by generality.

Why

#568 fixed the duplicate void:classPartition (netwerk-digitaal-erfgoed/dataset-knowledge-graph#334) but threaded namespace-alias awareness through the core: a per-stage merge transform, a class selector that canonicalized and co-located variants per batch, query markers, and self-describing void-ext chains. That coupled a consumer-specific concern into the analysis queries and stage machinery.

Two facts reframe it: merging per-class partitions needs a class’s http/https variants seen together, which beforeStageWrite (per stage/batch) can’t provide – hence the co-location leaking into the core; and datasets typically use a single schema.org namespace, so the cross-variant merge is for the rare mixed dataset, not the common case.

What

@lde/pipeline – a generic per-dataset hook, plus a generic namespace rewrite:

  • New PipelinePlugin.beforeDatasetWrite: a QuadTransform run once over a whole dataset’s cross-stage output before write – the dataset-scoped sibling of beforeStageWrite. Implemented as a RunWriter decorator that streams every stage’s output through one AsyncQueue and honors the flush/reset/commit/abort run lifecycle; nothing is materialized. Domain-agnostic – dedup, roll-ups, or partition merging can use it.
  • Keeps namespaceNormalizationPlugin / schemaOrgNormalizationPlugin, now generalized into a blanket, vocabulary-agnostic beforeStageWrite rewrite of a namespace’s IRIs in every term position (previously only void:class/void:property objects). This serves consumers that just standardize a namespace across a dataset’s own quads – e.g. mapping instance data to an application profile – with no VoID involved.

@lde/pipeline-void – the VoID partition merge, core reverted:

  • New schemaOrgPartitionMergePlugin() (and generic namespacePartitionMergePlugin(aliases)): a beforeDatasetWrite plugin that canonicalizes void:class/void:property and re-mints/merges the duplicate partition nodes from their canonical components. A whole-dataset view resolves each partition’s cp → pp → dp chain from the other stages, so the queries need no markers or self-describe chains. Renamed so a VoID consumer can’t pick the plain rewrite and leave the duplicate nodes unmerged.
  • The six analysis queries revert to plain SPARQL; the namespaceAliases stage option, class-selector canonicalization/expansion, query markers and void-ext self-describe chains are gone.
  • The merge streams: only partition quads are buffered (bounded by the summary), everything else passes through.

Why the split

The generic rewrite and the merge share only the canonicalizeIri primitive. Rewriting void:class objects alone would still leave two void:classPartition nodes for one class, so the merge (VoID-specific: it knows the partition vocabulary and the shared partition-IRI minting) can’t be replaced by the rewrite. And the merge’s canonicalization is deliberately selective – it leaves void:vocabulary and entity-properties in the source namespace – whereas the generic rewrite is a blanket pass. So the two stay separate plugins, in the package each belongs to.

Correctness

Single-namespace datasets (the common case) keep every count exact – the plugin only renames and re-keys. A dataset that genuinely mixes both namespaces on one property has its void:distinctObjects summed (an over-count for shared objects), which is documented and not optimized for; entities/triples stay exact under subject-disjointness.

Tested: the beforeDatasetWrite hook (streaming, flush, reset, abort) in @lde/pipeline; the generic rewrite plugin (all term positions) in @lde/pipeline; the merge transform + plugin (incl. the MD5 minting matching the queries, single-namespace exactness, and the mixed over-count) in @lde/pipeline-void; and an endpoint-backed test running the real plain queries through the plugin end-to-end.

See ADR 7. Follow-up: dataset-knowledge-graph switches to plugins: [schemaOrgPartitionMergePlugin(), provenancePlugin()].

BREAKING CHANGE: @lde/pipeline’s namespaceNormalizationPlugin / schemaOrgNormalizationPlugin now rewrite every matching IRI in any term position via beforeStageWrite, not just void:class/void:property objects. The VoID partition merge is now schemaOrgPartitionMergePlugin / namespacePartitionMergePlugin in @lde/pipeline-void, and voidStages no longer accepts namespaceAliases – add the plugin to the pipeline plugins instead.

https://claude.ai/code/session_01H8MVbsXSoNbYREYMFLy6Eg

@ddeboer ddeboer force-pushed the feat/before-dataset-write-hook branch 3 times, most recently from 745c367 to 133c76d Compare July 9, 2026 11:00
… a plugin

Adds a generic per-dataset plugin hook and moves schema.org namespace
normalization onto it, so the VoID analysis core no longer knows about
namespace aliases.

@lde/pipeline:
- Add PipelinePlugin.beforeDatasetWrite: a QuadTransform run once over a
  whole dataset's cross-stage output before write. Implemented as a
  RunWriter decorator that streams every stage's output through one
  AsyncQueue and honors the flush/reset/commit/abort run lifecycle; nothing
  is materialized.
- Remove the superseded object-only schemaOrgNormalizationPlugin and
  namespaceNormalizationPlugin (replaced by the pipeline-void plugin below).

@lde/pipeline-void:
- Add schemaOrgNormalizationPlugin (and generic namespaceNormalizationPlugin):
  a beforeDatasetWrite plugin that canonicalizes void:class/void:property and
  re-mints/merges partition IRIs from their canonical components. A
  whole-dataset view resolves each partition's cp->pp->dp chain from the other
  stages, so the queries need no markers or self-describe chains.
- Revert the analysis queries to plain SPARQL; remove the namespaceAliases
  stage option, the class-selector canonicalization/expansion, query markers
  and void-ext self-describe chains added for the per-stage approach.
- The merge streams: only partition quads are buffered (bounded by the
  summary), everything else passes through.

Single-namespace datasets (the common case) keep every count exact; a dataset
mixing both namespaces on one property has its void:distinctObjects summed
(an over-count), which is documented and not optimized for. See ADR 8.

BREAKING CHANGE: @lde/pipeline no longer exports schemaOrgNormalizationPlugin
or namespaceNormalizationPlugin; use schemaOrgNormalizationPlugin from
@lde/pipeline-void. @lde/pipeline-void's voidStages no longer accepts
namespaceAliases; add schemaOrgNormalizationPlugin to the pipeline plugins
instead.
@ddeboer ddeboer force-pushed the feat/before-dataset-write-hook branch from 133c76d to db0d8bb Compare July 9, 2026 11:04
ddeboer added 2 commits July 9, 2026 13:05
…tays in pipeline-void

Split schema.org namespace handling by generality instead of removing the
generic capability, so a non-VoID consumer (e.g. mapping instance data to an
application profile) can still normalize a namespace.

- @lde/pipeline keeps namespaceNormalizationPlugin / schemaOrgNormalizationPlugin,
  now a blanket, vocabulary-agnostic beforeStageWrite rewrite of a namespace’s
  IRIs in every term position (previously only void:class/void:property objects,
  and only in this package before this PR removed them).
- @lde/pipeline-void renames its beforeDatasetWrite merge to
  schemaOrgPartitionMergePlugin / namespacePartitionMergePlugin, so a VoID
  consumer can’t pick the plain rewrite and leave the duplicate partition nodes
  (#334) unmerged.
- Both share the canonicalizeIri primitive; the merge keeps its own selective
  canonicalization (leaves void:vocabulary and entity-properties in the source
  namespace), which the blanket rewrite deliberately does not.

Docs: renumber the ADR to 7 and make it self-contained; document the split in
the ADR and both package READMEs.

BREAKING CHANGE: @lde/pipeline’s namespaceNormalizationPlugin and
schemaOrgNormalizationPlugin now rewrite every matching IRI in any term position
via beforeStageWrite, not just void:class/void:property objects. The VoID
partition merge is now schemaOrgPartitionMergePlugin / namespacePartitionMergePlugin
in @lde/pipeline-void.

Claude-Session: https://claude.ai/code/session_01H8MVbsXSoNbYREYMFLy6Eg
@ddeboer ddeboer changed the title feat(pipeline)!: add beforeDatasetWrite hook; normalize namespaces in a plugin feat(pipeline)!: add beforeDatasetWrite hook; split namespace normalization Jul 9, 2026
@ddeboer ddeboer merged commit 6467a49 into main Jul 9, 2026
2 checks passed
@ddeboer ddeboer deleted the feat/before-dataset-write-hook branch July 9, 2026 12:59
ddeboer added a commit to netwerk-digitaal-erfgoed/dataset-knowledge-graph that referenced this pull request Jul 9, 2026
Fixes the duplicate `void:classPartition` for schema.org classes (#334)
in production by switching to the merge plugin released in
ldelements/lde#577.

## What

- Bump `@lde/pipeline` `0.33.4 → 0.34.0` and `@lde/pipeline-void`
`0.31.4 → 0.32.0`.
- Swap the pipeline plugin from `@lde/pipeline`’s generic
`schemaOrgNormalizationPlugin` to `@lde/pipeline-void`’s
`schemaOrgPartitionMergePlugin`.

## Why

The old `@lde/pipeline` plugin only rewrote `void:class`/`void:property`
objects `http → https`, which left **two** `void:classPartition` nodes
for the same class when a dataset uses both `http://` and
`https://schema.org/` – the duplicate that crashed the Dataset Register
browser (#334). `schemaOrgPartitionMergePlugin` additionally re-mints
and merges those partition nodes into one canonical partition per class.
It runs as a `beforeDatasetWrite` transform over the whole dataset’s
output, so the VoID analysis queries stay unaware of namespace aliases
(see ldelements/lde#577 and its ADR 7).

`voidStages()` and the SHACL sampler’s own `namespaceAliases` are
unchanged.

## Reaching existing datasets

DKG skips datasets whose data dump is unchanged. `PIPELINE_VERSION` is
release-please–managed, so merging this `fix:` bumps the version,
rotates the token, and forces a full reprocess on the next run – which
is when the 4 currently-affected datasets (Venlo, literatuurmuseum,
nijmegen studiezaal, ldmax wo2net; 4 of 542 measured) get single
partitions.

## Validation

`npm run compile`, `npm run lint`, `npm test` (148 tests) all pass
locally.

https://claude.ai/code/session_01H8MVbsXSoNbYREYMFLy6Eg
ddeboer added a commit to netwerk-digitaal-erfgoed/dataset-knowledge-graph that referenced this pull request Jul 9, 2026
Fixes the duplicate `void:classPartition` for schema.org classes (#334)
in production by switching to the merge plugin released in
ldelements/lde#577.

## What

- Bump `@lde/pipeline` `0.33.4 → 0.34.0` and `@lde/pipeline-void`
`0.31.4 → 0.32.0`.
- Swap the pipeline plugin from `@lde/pipeline`’s generic
`schemaOrgNormalizationPlugin` to `@lde/pipeline-void`’s
`schemaOrgPartitionMergePlugin`.

## Why

The old `@lde/pipeline` plugin only rewrote `void:class`/`void:property`
objects `http → https`, which left **two** `void:classPartition` nodes
for the same class when a dataset uses both `http://` and
`https://schema.org/` – the duplicate that crashed the Dataset Register
browser (#334). `schemaOrgPartitionMergePlugin` additionally re-mints
and merges those partition nodes into one canonical partition per class.
It runs as a `beforeDatasetWrite` transform over the whole dataset’s
output, so the VoID analysis queries stay unaware of namespace aliases
(see ldelements/lde#577 and its ADR 7).

`voidStages()` and the SHACL sampler’s own `namespaceAliases` are
unchanged.

## Reaching existing datasets

DKG skips datasets whose data dump is unchanged. `PIPELINE_VERSION` is
release-please–managed, so merging this `fix:` bumps the version,
rotates the token, and forces a full reprocess on the next run – which
is when the 4 currently-affected datasets (Venlo, literatuurmuseum,
nijmegen studiezaal, ldmax wo2net; 4 of 542 measured) get single
partitions.

## Validation

`npm run compile`, `npm run lint`, `npm test` (148 tests) all pass
locally.
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