BDQ Workbench is a Java 17 application for policy-driven Biodiversity Data Quality (BDQ) execution over Darwin Core Archives (DwC-A) and Darwin Core Data Packages.
BDQ Workbench takes DarwinCore Archive files or Darwin Core Data Package files as input, identifies tests that apply to a bdqffdq:UseCase (purpose to which data are to be put and need to have fitness for) that are available in the implemntation, then runs those tests on the data (in pre-amendment, amendment, and post-amendment phases) and produces a data quality report (in several formats). It also evaluates the binding of information elements in the input data with the test implementations, can provide parameters to parameterized tests, and can run tests from a use case individually.
mvn -q clean packagemvn -q testRun integration tests only (failsafe):
mvn -q verify -Preleasemvn -q package
java -jar target/bdq_workbench-0.1.0-SNAPSHOT.jarLaunching the jar without options opens a desktop GUI for entering parameters and monitoring execution. The startup screen includes a dataset file picker, use-case selection, and advanced options for custom use-case/test-definition/ontology sources, discovery packages, and threads. By default the GUI caches:
https://bdq.tdwg.org/draft/dist/bdquc.xml(use cases)https://bdq.tdwg.org/draft/dist/bdqtest.ttl(test definitions)https://bdq.tdwg.org/draft/vocabulary/bdqffdq.ttl(ontology)
Use-case and RDF definition inputs support RDF/XML, Turtle, and JSON-LD serializations.
Logging is configured to the console with a default DEBUG root level in src/main/resources/logback.xml.
Show command-line help:
java -jar target/bdq_workbench-0.1.0-SNAPSHOT.jar --helpConfiguration defaults are in src/main/resources/application.properties and can be overridden with CLI options, for example:
java -jar target/bdq_workbench-0.1.0-SNAPSHOT.jar --dataset path/to/dataset.zipA dataset with more than one table (a Darwin Core Archive with extensions, a data package with
several resources) always runs through a dataset view. You can supply one with --dataset-view
(bdq.dataset.view in config), or build it interactively in the GUI with Build Dataset View...:
java -jar target/bdq_workbench-0.1.0-SNAPSHOT.jar \
--dataset path/to/datapackage.json \
--dataset-view path/to/bdq-dataset-view.jsonA view picks a grain table (one execution record per row), joins directly related tables, and maps each Darwin Core term to one source table and column. Each join says what to do when a grain record has more than one related row (for example, several identifications of one occurrence):
| Policy | Effect |
|---|---|
FIRST_ROW (default) |
Flatten: use the first related row and ignore the rest. |
EXPAND |
Keep every related row. A test that reads any term mapped from this table runs once per related row, with the grain record's (and flattened tables') terms reused for each. Every evaluation is kept and reported; the grain record's result for a VALIDATION or ISSUE test is derived from all of them — COMPLIANT only when every evaluation is, NOT_COMPLIANT when any is — and multi-record measures count grain records. A grain record with no related rows is tested once with those terms blank. |
AGGREGATE |
Flatten: join all related rows' values with " | ". Combined values usually fail tests on that term. |
REJECT |
Flatten: leave the mapped terms empty, with a warning, for grain records that have more than one related row. |
For example, with occurrence as the grain, join event with FIRST_ROW and identification with
EXPAND: a test such as VALIDATION_DATEIDENTIFIED_INRANGE then runs once per identification, each
time with that identification's dateIdentified and the occurrence's event date. A test whose inputs
come from two different EXPAND tables cannot run and reports an error, so expand only the tables
whose rows each need testing. The builder shows the multiplicity observed in the data for each join,
previews expanded rows beneath each grain record, and warns where a policy drops or garbles rows.
When a term is mapped both from the grain and from an EXPAND table — for example, an occurrence
core that carries its current identification and an identification-history extension — both are
tested: the grain's value once, and each expanded row's value once, all counting toward the record's result.
The structured reports' "Input data view" section then draws the execution view in two parts — the
flat grain record (with the tables flattened into it) and, for each EXPAND table, its expanded
rows joined 1 : n — lists the tests evaluated once per related row with their evaluation and record
counts, and names the offending rows (data file and lines) in the records needing attention. When
--record-filter selects some records, table counts and multiplicity are given for the selected
records ("132 of 5827").
Without --dataset-view, the workbench builds the view itself: the grain is the table it would read
anyway (occurrence when there is one), every column of the grain and its directly related tables is
mapped, and related tables with at most one row per grain record (such as the event of an
occurrence) are joined FIRST_ROW. For each related table that has more than one row for some
record, you decide how to handle it:
java -jar target/bdq_workbench-0.1.0-SNAPSHOT.jar \
--dataset path/to/dataset.zip \
--join-policy identification=EXPAND --join-policy multimedia.txt=FIRST_ROW--join-policy is repeatable (bdq.dataset.join.policies=identification=EXPAND,multimedia.txt=FIRST_ROW
in config). Any table still undecided is asked about on the console when running interactively; in
a script or CI the run stops before testing and lists each such table with its observed rows per
record. In the GUI, the dataset view builder opens with those tables highlighted, and Use View
saves the view to reports/bdq-dataset-view.json and continues the run.
Configuration precedence is:
- command-line or GUI-supplied overrides
src/main/resources/application.properties- built-in fallback defaults in
ConfigLoader
Pre-execution record filtering can be configured from the CLI with repeatable --record-filter flags, or in the GUI with the Build Record Filters... dialog after selecting a dataset. Filter syntax is:
java -jar target/bdq_workbench-0.1.0-SNAPSHOT.jar \
--record-filter 'dwc:genus=Abies|Pinus' \
--record-filter dwc:country=CanadaWithin a record-filter, search terms are related by OR; across record-filters, terms are related by AND. The example above is interpreted as dwc:genus=(Abies OR Pinus) AND dwc:country=Canada. Field names match case-insensitively by full name or local name (for example dwc:country and country), while search term values are matched exactly and case-sensitively against the canonicalized input values.
The current desktop/CLI flow can be summarized as:
Dataset input
|
+--> Load DwC-A zip / Data Package CSV
|
+--> [optional] Build dataset view and/or record filters from dataset terms/values (GUI:
| schema/relationship pane, use-case-driven term mapping pane, flattened preview)
|
+--> [optional] Apply record filters
|
+--> Resolve use case / policy tests
|
+--> Discover implementations on classpath
|
+--> Bind tests + validate parameter mappings
| |
| +--> unresolved tests may still be shown in preflight
| +--> GUI can continue with runnable tests only ("Start Available Tests")
| +--> GUI can edit/load/save parameter values before execution
|
+--> [optional] Distinct-value aggregation (`bdq.execution.dedup=true`)
|
+--> PRE_AMENDMENT tests
|
+--> AMENDMENT tests
|
+--> POST_AMENDMENT tests
|
+--> Post-process responses
| |
| +--> built-in COUNT multi-record measures
| +--> synthesized `UNABLE_TO_RUN` responses for unresolved/unbound tests
|
+--> Export text, RDF/Turtle, XLSX, and unresolved-response XLSX reports
Current flow-control options are intentionally modest:
- Dataset choice: choose a DwC-A zip or Darwin Core Data Package.
- Record filtering: optional exact-match filtering before binding or execution; in the GUI the filter builder inspects the selected dataset and lets the user choose only from terms present in the data.
- Use case/test-definition/ontology sources: GUI advanced options and CLI/config can override the default RDF sources.
- Implementation packages: CLI/config/GUI advanced options control the discovery package roots.
- Thread count: CLI/config/GUI advanced options control the worker pool size.
- Continue with unresolved tests: GUI preflight can proceed with runnable tests even when some policy or implementation bindings remain unresolved.
- Parameter overrides: GUI preflight supports per-test parameter editing plus saving/loading parameter settings.
- Isolated test execution: GUI preflight/debug tools can run one bound test independently against the prepared dataset.
- Distinct-value reduction: CLI/config
bdq.execution.dedupand the GUI'sReduce repeated test calls by distinct input valuescheckbox toggle whether eligible bindings run once per distinct input-value group instead of once per record. - External-service scheduling and resilience:
bdq.execution.*settings (and the CLI's--external-concurrency,--retries,--reuse-pre-results,--execution-override,--lane-limit) control per-resource concurrency lanes, adaptive throttling, circuit breaking, retries, and PRE-to-POST result reuse; see Execution scheduling and resilience. - Not currently supported: there is still no user-facing phase skip/select control, and the main run UI still does not expose cancellation.
The codebase is organized under org.filteredpush.bdq_workbench with explicit module boundaries:
app: bootstrap, configuration loading, orchestration, exception handlingmodel: domain model (UseCase,Policy,TestDefinition,ImplementationBinding,Phase,Response)ingest: DwC-A and Data Package ingestion into canonical recordsrdf_policy: use-case/policy/test RDF resolutiontest_discovery: annotation-based discovery (@Provides,@Validation,@Issue,@Measure,@Amendment, etc.) and bindingexecution: parallel phase orchestration (pre-amendment, amendment, post-amendment) with deterministic ordering and distinct-value test call reductionreporting: summary output, normalized response stream export, RDF export, and XLSX spreadsheet export (via kurator-ffdq)
Extension points are interfaces for discovery, binding, execution adapters, and report exporters.
Execution binding is reflection-driven and annotation-aware:
@ActedUpon("dwc:term")and@Consulted("dwc:term")are matched against canonical record fields.- Matching is deterministic across exact values,
dwc:prefixvalues, and local-name forms such aseventDate. @Parameter(name = "bdq:...")values come from the selected test definition / UI parameter editor.- Legacy implementations that accept
(Map record)or(Map record, Map parameters)are still supported for backward compatibility.
For every candidate implementation method the workbench records:
- implementation status:
FOUND,MISSING, or deterministic resolution of an ambiguous set - binding status:
BOUND,PARTIAL, orUNBOUND - per-parameter diagnostics for missing Darwin Core terms, missing user parameters, or unsupported parameter types
When both default and parameterized implementations exist for the same test, the workbench prefers:
- the parameterized method when the user provides parameter values
- the default method when no parameter values are provided
The preflight grid shows the chosen method, parameterization capability, and whether the selected run is using default values.
Each execution result is normalized into a response stream entry with:
- record id
- test id and test type
- implementation class/method provenance
- phase (
PRE_AMENDMENT,AMENDMENT,POST_AMENDMENT) - parameter values used for the invocation
responseStatusresponseResultcomment- amendment payload, when present
DQResponse objects are adapted reflectively by reading getResultState(), getValue().getObject(), and getComment(). Amendment results are preserved as normalized amendment maps and then applied to the amendment working copy before post-amendment execution.
Reports include:
reports/bdq-report-summary.txtHuman readable summary of test execution results.reports/bdq-report-responses.txtHuman readable list of test execution Response values.reports/bdq-report-rdf.ttlRDF test responses serialized as Turtle.reports/bdq-report-xls.xlsxSpreadsheet report produced via kurator-ffdq'sXLSXPostProcessor(see below).reports/bdq-report-xls-unresolved.xlsxSpreadsheet companion listing unresolved, unbound, and other sentinel-record responses excluded from the main per-record workbook.
The structured HTML and Markdown reports are quality-control summaries sized for real datasets:
high-impact action items (issues, non-compliance and its most frequent causes, the amendment
proposals that most reduced problems, empty terms), pre/post measure differences, records with quality for the use case (all multi-record QA measures COMPLETE), a per-test table of
problems before and after amendment, information elements empty in every record, proposed
amendments ranked by how many records they affect, and a capped list of records needing
attention, in which each related row that needs attention (such as one identification of an
occurrence, under an EXPAND join) is listed on its own line beneath its record, with its data
file and line, the values tested, and its problems and amendments. Tests are named by their
labels, and records by values from the original data —
institutionCode:collectionCode:catalogNumber (or dataset and catalog number; without a catalog
number, the codes and the record's identifier, e.g. occurrenceID …) plus the data file and line
within the archive or package. These terms are read from the record's related rows when the grain
table lacks them — a Darwin Core Data Package keeps them on the material table — whether or not
the view maps them, and such a table is reported as used to identify records rather than ignored. Report headers, and the text summary, state any results
with external prerequisites not met. The full per-record results remain in the spreadsheet and
the response list (bdq-report-responses.txt, which leads with record and test label columns).
The input is checked for the record-level markers of the BDQ
Guide to Marking and Identifying Synthetic and Modified Data:
collectionCode "Synthetic Example" / "Modified Example", the guide's two collectionID UUIDs,
relationshipOfResource "source for modified example record", and the example.org
institutionCode/institutionID. The raw input rows are scanned (the grain table and its related
rows, before any view mapping), so markers are found even when a view does not map those terms.
When any record is marked, the structured HTML and Markdown reports open with a warning, the text
summary starts with one, the RDF report carries the counts and an rdfs:comment, the GUI preflight
summary and the CLI output show it too; every one of these also states the result ("none detected
in N input record(s)") when nothing is marked.
A BDQ test is specified as a pure function of the Darwin Core terms it declares as input
(@ActedUpon/@Consulted), so records that share identical values for exactly those terms must
produce identical results. Rather than invoking a test once per record, ParallelPhaseExecutionService
partitions each phase's records into distinct-value groups per binding (via RecordGroupPartitioner),
invokes the test once against one representative record per group, and copies that one response to
every record in the group — the final response list has exactly the same shape (one response per
record per test) as running per-record would, just with fewer real invocations.
A few behaviors worth knowing about:
- Grouping is shared across tests, not just per test. Two bindings that happen to declare the
same set of term names — regardless of test type, or whether a term is
ACTED_UPONfor one andCONSULTEDfor the other — share the same partitioning work for a phase via aPhaseGroupCache, rather than each recomputing it from scratch. E.g. ten different validations that each act upon onlydwc:countryall reuse one partition of the dataset's distinct country values. - Grouping is exact-match only. Term values are compared with plain string equality — no case-folding or other normalization — since many BDQ tests are sensitive to exact formatting.
- Amendments are sequenced correctly within the AMENDMENT phase. Since one amendment test's output can change values a later amendment test in the same phase groups or reads by, AMENDMENT-phase bindings are processed one at a time: each binding's groups are computed, invoked, and its resulting amendments applied to every group member, and any cached partition touching the changed fields is discarded, before the next binding's groups are computed. PRE_AMENDMENT and POST_AMENDMENT never mutate records mid-phase, so their bindings' groups are all computed up front and dispatched round-robin across bindings (see below).
- Not every binding is eligible. Implementations bound via the legacy
(Map record)/(Map record, Map parameters)signatures read the whole record or parameter map rather than specific declared terms, so the workbench can't know what subset of fields they actually depend on — these always run once per record. - Configurable via
bdq.execution.dedup(CLI--dedup true|false, defaulttrue). Disabling it runs every binding once per record exactly as if none were dedup-eligible, useful for debugging or comparing behavior against the pre-reduction execution path.
The workbench cannot know beforehand whether a discovered test implementation calls an external
service (WoRMS, IRMNG, GBIF, a geography layer server, ...). Running every group of one test on
every worker at once can send a burst of simultaneous requests to that service; the service then
throttles or rejects them, implementation libraries retry immediately, and whole test runs end in
EXTERNAL_PREREQUISITES_NOT_MET. ParallelPhaseExecutionService therefore schedules invocations
as follows (all without changing which responses are produced or their order):
- Fair dispatch. In PRE_AMENDMENT and POST_AMENDMENT every binding's distinct-value groups are planned first and then dispatched round-robin — group 1 of test A, group 1 of B, group 1 of C, group 2 of A, ... — so one test cannot fill every worker. AMENDMENT bindings still run one at a time, in order.
- Resource lanes. Each binding is assigned a resource lane (
ExecutionResourceClassifier), in this order of precedence: an explicit override for its test ID, implementation signature,class#method, or class; then a source-authority-like parameter (bdq:sourceAuthority,bdq:taxonIsMarine,bdq:geospatialLand, ...), which marks it likely external and names the lane after the authority (source:worms), so different tests using one service share a lane; then clearly local method names (...Notempty,...Inrangewithout a source authority), which run local; everything else is unclassified and gets a lane perclass#method. A lane runs at most its limit of invocations at once; work waiting on a lane stays in the scheduler's queue and never occupies a worker, so unrelated lanes and local work keep running. These static hints are only a starting point: overrides replace them, and a lane not pinned by an override that has run many invocations without an external failure (a "source authority" can be a bundled local data layer) is promoted to the local limit. - Runtime adaptation and circuit breaker.
ResponseFailureClassifiersorts each response into completed, likely transient external (timeouts, connection failures, unknown host, HTTP 429/5xx), non-transient configuration (invalid/unsupported source authority, HTTP 400/401/403/404), ambiguous external (EXTERNAL_PREREQUISITES_NOT_METwithout a clear diagnostic), or internal. A transient or ambiguous external failure halves the lane's limit (minimum 1); successes restore it one slot at a time. After consecutive external failures the lane's circuit opens: its queued work pauses, without holding workers, for a cooldown that doubles on each reopening; then one probe runs, and a successful probe closes the circuit and resumes the lane gradually. A lane that keeps failing after several openings is given up for the run, and its remaining work is reported asEXTERNAL_PREREQUISITES_NOT_METwithout being invoked. Configuration failures never throttle, open a circuit, or retry. - Retries. A group whose response is a likely transient external failure is retried (the
group, not each record), after an exponential backoff with jitter scheduled on a timer rather
than a sleeping worker; an ambiguous failure is retried at most once. The retry rejoins its
lane's queue and obeys the lane's current limit and circuit. Only the final attempt's response
is fanned out to the group's records; its message notes the attempt count (the comment and
status are the implementation's own), so a group that never recovers still reports the latest
EXTERNAL_PREREQUISITES_NOT_METdiagnostic. An AMENDMENT group's retries settle before its amendments are applied and before the next amendment binding runs. - PRE-to-POST reuse. POST_AMENDMENT re-runs every PRE_AMENDMENT validation, issue, and
measure that has no explicit POST_AMENDMENT binding. When amendments left all of a group's
inputs unchanged, the successful PRE_AMENDMENT result is copied into POST_AMENDMENT (with the
POST_AMENDMENT phase, fresh timestamps, and a note in its message) instead of invoking the
test again. An invocation's fingerprint covers the implementation signature, test ID and type,
parameter values, the governing relation of the evaluated subject, and the value of every
declared
ACTED_UPON/CONSULTEDterm. Errors, unable-to-run results, and everyEXTERNAL_PREREQUISITES_NOT_METresult are never reused; amendments, explicitly POST-bound tests, legacy whole-record bindings, and bindings with an unresolved input are always invoked; reuse is off whenbdq.execution.dedupisfalse.
Lane events (throttled, capacity restored, circuit opened/half-open/closed, retry scheduled,
succeeded, or exhausted) are logged and reported to ExecutionProgressListener.onResourceLaneEvent;
at the end of a run the per-resource and per-test statistics (invocations, maximum concurrency,
external failures, retries, recovered and exhausted retries, circuit openings, reused and skipped
calls) are logged and passed to ExecutionProgressListener.onExecutionStatistics.
Settings (application.properties, overridable like any other setting):
| Setting | Default | Meaning |
|---|---|---|
bdq.threads |
4 |
Worker pool size (unchanged) |
bdq.execution.lanes |
true |
Per-resource lanes (false: one shared lane using the whole pool, with no adaptive throttling or circuit breaking) |
bdq.execution.concurrency.external |
2 |
Limit of a likely external lane (CLI --external-concurrency) |
bdq.execution.concurrency.unclassified |
4 |
Limit of an unclassified lane |
bdq.execution.concurrency.local |
0 |
Limit of a local lane; 0 = the whole worker pool |
bdq.execution.adaptive |
true |
Adaptive throttling and circuit breaker |
bdq.execution.circuit.failures |
3 |
Consecutive external failures that open a circuit; 0 disables it |
bdq.execution.circuit.cooldown.ms / .max.ms |
5000 / 60000 |
Initial and maximum circuit cooldown |
bdq.execution.retries |
2 |
Workbench retries of a transient failure; 0 disables (CLI --retries) |
bdq.execution.retry.delay.ms / .max.ms |
500 / 8000 |
Base and maximum retry backoff (reduced by up to half at random) |
bdq.execution.reuse |
true |
PRE-to-POST result reuse (CLI --reuse-pre-results) |
bdq.execution.overrides |
empty | target=spec;..., spec from external/local/unclassified, lane:NAME, max:N (CLI --execution-override, repeatable) |
bdq.execution.lane.limits |
empty | laneKey=N;..., e.g. source:worms=1 (CLI --lane-limit, repeatable) |
Every limit is capped at the worker count. For example, to put every test of the georeference library that consults WoRMS into one lane that makes one call at a time:
java -jar target/bdq_workbench-0.1.0-SNAPSHOT.jar --dataset occurrences.zip \
--execution-override 'org.filteredpush.qc.georeference.DwCGeoRefDQDefaults#validationCoordinatesTerrestrialmarine=external,lane:worms' \
--lane-limit lane:worms=1Programmatically, pass an ExecutionPolicy (built with ExecutionPolicy.builder()) to the
ParallelPhaseExecutionService(threads, adapter, listener, dedup, policy) constructor. The older
constructors remain and use the same defaults except that PRE-to-POST reuse is off, so existing
callers see every POST_AMENDMENT invocation they did before; the CLI and GUI use the policy from
their configuration (the GUI takes it from application.properties and any --gui overrides).
Lanes, adaptive state, and statistics are scoped to one execute call, and the worker pool, the
timer, and every queued, delayed, or in-flight invocation are shut down when it returns, fails, or
is cancelled.
XlsxReportExporter builds an in-memory kurator-ffdq FFDQModel directly from the run's
ExecutionSummary — one data resource per input record and one response per Response — and
streams it directly to bdq-report-xls.xlsx with kurator-ffdq's XLSXPostProcessor, which
produces Summary, Initial Values, Final Values, Measures, Validations, Amendments, and
Issues sheets, with per-record rows color-coded by outcome.
A few behaviors worth knowing about:
- Missing information elements are padded, not omitted. If a use case's tests expect a Darwin
Core term as an input information element (acted upon or consulted) and that term isn't present
in the input data at all, it still appears as a column in the report, with an empty value for
every record — rather than being silently missing from the spreadsheet. This comes from the
run's test/implementation bindings (
ExecutionSummary.bindings()), not from re-resolving the ratified ontology, so it reflects exactly what the bound implementations look for. - Responses that don't apply to one record — built-in multi-record measures and synthesized
unresolved/unbound placeholder responses — are excluded from
bdq-report-xls.xlsxand instead listed in their own small companion workbook,bdq-report-xls-unresolved.xlsx(viaUnresolvedResponsesExporter), since kurator-ffdq's per-record model has no place for them. This is a separate file rather than an extra sheet appended afterward: appending would require reopening the (potentially very large) written workbook as a plainXSSFWorkbook, which for a large dataset can exceed Apache POI's single-zip-entry read cap and fail with aRecordFormatException. Keeping the two files separate means the main export is a pure, one-pass stream straight to disk regardless of dataset size. - Issues sheet coloring is a known gap. kurator-ffdq's
Issuecontext class lacks a no-arg constructor (unlikeMeasure/Validation/Amendment), which breaks RDFBeans deserialization if one is attached to a saved response.XlsxReportExporterleaves it unset, so ISSUE-type responses still get their row and every field column, just without per-cell acted-upon/consulted coloring. - Build dependency: this depends on kurator-ffdq's "restored and productized"
XLSXPostProcessor, which as of this writing only exists in a3.3.0-SNAPSHOTbuild (thepom.xmldependency is pinned there, with a comment to move it to a released3.3.0once one is cut). Building this project currently requires that SNAPSHOT installed locally.
ExecutionSummary exposes filtering and counting helpers over the normalized response stream so downstream multi-record measure work can count and filter by:
- phase
- test type
- response status
- response result
This is the initial plumbing layer for multi-record calculations. Full multi-record execution remains a follow-up item, but downstream code can now consume the normalized response stream instead of raw input rows.
The desktop GUI supports:
- selecting a dataset and use case
- optionally opening
Build Record Filters...to inspect dataset terms and common values, then assembling exact-match filters - running a preflight review that loads the dataset, applies filters, discovers implementations, and populates a test grid
- reviewing binding status, method selection, parameterization capability, and normalized filter counts before execution
- editing parameter values or keeping defaults before execution
- saving and loading parameters for parameterized tests
- continuing with runnable tests only when some tests remain unresolved (
Start Available Tests) - running a bound test in isolation
- monitoring a simple stage list plus live per-phase progress and response/result counters
- reviewing a post-run summary and saved output locations
The execution phases are fixed (PRE_AMENDMENT, AMENDMENT, POST_AMENDMENT) and are not currently user-skippable from either the CLI or the GUI.
When policy resolution or implementation binding cannot produce a runnable test, the workbench still emits synthesized UNABLE_TO_RUN responses so those tests appear in the final summary and unresolved workbook outputs.
This project has used GitHub Copilot and Claude Code as AI coding assistants during development.
Copilot and Claude contributions are limited to suggested code and documentation text.
All accepted changes were reviewed, edited as needed, and validated by human maintainers before
inclusion in the master branch.
- Human maintainers are responsible for all design decisions, semantics, and released content.
- AI-generated suggestions are treated as draft material and may contain errors.
- Ontology-aligned terminology and normative language in this project are curated by the tdwg/bdq maintainers.