
After Excel Review: Finalize and Publish Your Package
Source:vignettes/post-review-package-publication.Rmd
post-review-package-publication.RmdOverview
This guide starts after you already ran
create_sdp(), opened the package in Excel (or another
spreadsheet editor), and saved your reviewed metadata back to CSV.
If you have not created the package yet, start with the 5-Minute Quickstart.
If you want to assemble dataset.csv,
tables.csv, and column_dictionary.csv manually
instead of continuing from a reviewed package, use Publishing Data
Packages.
This is the post-review path:
- reload the reviewed package from disk;
- validate the reviewed metadata and data structure;
- re-run semantic suggestions only for fields that are still unresolved;
- detect likely ontology gaps;
- decide whether each missing term belongs in the shared salmon-domain ontology or the DFO-specific bucket;
- produce a concrete list of term requests to file;
- rebuild EDH XML if needed;
- run strict final validation; and
- assemble the reviewed closure the EML/KNB path needs before publishing.
Step 9 is the one that surprises people. Strict validation is the last gate the Salmon Data Package has; it is not the last gate before a deposit.
1) Reload the reviewed package
library(metasalmon)
pkg_path <- "fraser-coho-2023-2024-sdp"
pkg <- read_salmon_datapackage(pkg_path)
names(pkg$resources)
pkg$tablesread_salmon_datapackage() reloads the package from the
canonical metadata/*.csv files, so you are checking the
same metadata you just reviewed in Excel.
2) Run a review-state validation pass
Start with the package-level validator in non-strict mode:
review_check <- validate_salmon_datapackage(pkg_path, require_iris = FALSE)This catches package/data mismatches now, before you worry about
final ontology coverage. Since 0.2.6 the same call also enforces any
declared primary_key (duplicate or missing
key values are hard issues) and warns about every unresolved
MISSING METADATA: placeholder — so a package fresh from
Excel review will usually warn until the placeholders are filled in.
If you also want the semantic details separated out, inspect the reloaded package directly:
review_semantics <- validate_semantics(pkg$dictionary, require_iris = FALSE)
review_semantics$issues
review_semantics$missing_termsUse this pass to answer three questions:
- Did the Excel edits break the package structure?
- Which required metadata is still a placeholder? (the warnings preview them, deduplicated by file and field — a long list is truncated, so treat it as a pointer, not an inventory)
- Which measurement rows still lack a final semantic term?
Do not switch to require_iris = TRUE
yet unless you believe the package is fully finalized. Strict validation
is the last gate, not the first one.
3) Re-run semantic suggestions for only the unresolved pieces
The package-root semantic_suggestions.csv file is the
original evidence trail from create_sdp(). After review, it
is usually more useful to re-run suggest_semantics()
against the current package state so you only inspect
what is still unresolved.
reviewed_dict <- suggest_semantics(
df = pkg$resources,
dict = pkg$dictionary,
codes = pkg$codes,
table_meta = pkg$tables,
dataset_meta = pkg$dataset
)
remaining_suggestions <- attr(reviewed_dict, "semantic_suggestions")
remaining_suggestions |>
dplyr::select(
target_scope,
target_row_key,
target_sdp_field,
dictionary_role,
search_query,
label,
source,
score
)That usually produces a much shorter shortlist than the original package export, because rows you already resolved in Excel are no longer the target.
4) Detect likely ontology gaps
Now detect the places where metasalmon still cannot find
a shared SMN match, but did find useful fallback candidates
elsewhere:
gaps <- detect_semantic_term_gaps(reviewed_dict)
gaps |>
dplyr::select(
target_scope,
target_row_key,
target_sdp_field,
dictionary_role,
search_query,
top_non_smn_label,
top_non_smn_source,
gap_detection_basis,
llm_decision,
llm_new_term_label,
llm_escalated_from,
placement_recommendation,
placement_confidence
)This is the key post-review gap table:
- rows here are still unresolved;
-
top_non_smn_*columns show the best non-SMN evidence the package found; -
gap_detection_basisdistinguishes candidate evidence, a final LLM term-gap decision, or both; and -
placement_recommendationgives a first-pass routing guess.
When suggestions is omitted,
detect_semantic_term_gaps() reads both the
semantic_suggestions and
semantic_llm_assessments dictionary attributes. Final
request_new_term decisions are therefore included
automatically, even when candidate rows contain an SMN match. An
unresolved reject_shortlist escalation remains traceable
through llm_escalated_from.
If you pass suggestions explicitly, only candidate
evidence and the embedded llm_* columns in that table are
used. The dictionary’s assessment attribute is intentionally ignored in
that form.
If you only want a narrower slice, filter by role or scope:
Start from a commons gap register
The same request pipeline can start from a recorded domain gap rather
than a dataset. From a salmon-knowledge-commons checkout,
export the current register and confirm the validator exits
successfully:
Then use the exported file in R:
commons_gaps <- detect_semantic_term_gaps(commons_gaps = "gaps.json")
commons_requests <- render_ontology_term_request(commons_gaps, ask = FALSE)
commons_preview <- submit_term_request_issues(commons_requests, dry_run = TRUE)
commons_previewThe reader retains every row, source order and repeated concepts.
Fields starting with commons_ preserve the card, registry,
mint target, lifecycle, draft note, dependencies, conflicts and
verification state. Dataset, role, candidate and model evidence remain
missing; this input cannot be combined with a dictionary, suggestions or
SDP-only filters.
Only an open gap with an explicit SMN or GCDFO target and no hold
reaches the issue preview. Proposed/rejected and do-not-mint records,
unresolved blockers, conflicts, contested meanings, evidence needs and
deprecated cards remain visible with commons_hold_reason.
PSC CV, new-scheme and undecided targets are held because this pipeline
has no defined route for them. scope,
scope_overrides and ask cannot reopen or
redirect these source declarations. Draft/unverified cards can produce
drafts; they do not establish approval.
Bodies retain the exact draft gap note and identify its concept card. A curator must still supply the term definition and type. This preview makes no issue or authentication request. The matching Python reader remains owed under B-279; this R capability does not establish parity on Python main.
5) Decide shared salmon-domain vs DFO-specific routing
Use this plain-English rule:
-
Shared salmon-domain ontology (
smn): use this when the term describes a reusable salmon science concept that another program, region, or organization could reasonably use too. -
DFO Salmon Ontology (
gcdfo): use this when the concept is clearly tied to DFO policy, operations, internal workflow, local identifiers, program-specific statuses, or other context that is not broadly reusable outside that setting. -
Profile term (
profile): use this for a local, program, or organization profile when neither shared SMN nor DFO-wide GCDFO governance is appropriate.
A good default test is:
- if the term would still make sense in a non-DFO salmon dataset, lean SMN;
- if the term only makes sense because of a DFO or local program process, lean DFO-specific.
How routing is decided
render_ontology_term_request() accepts smn,
gcdfo, profile, uncertain, and
skip row outcomes. Its precedence is:
- an explicit
scope_overridesvalue for the row; - a forced
scope; - a recognized
llm_new_term_namespace; and - the deterministic placement heuristic.
Treat llm_new_term_namespace as evidence, not authority.
A human reviewer must still decide whether a DFO-specific proposal
belongs in GCDFO, a narrower profile, or nowhere. Non-interactive
uncertainty becomes skip; interactive use asks for a
routing decision.
6) Produce a concrete term-request list
First render a dry, non-interactive plan and inspect every route:
requests <- render_ontology_term_request(
gaps,
scope = "auto",
ask = FALSE,
profile_name = "local-program"
)
request_plan <- requests |>
dplyr::select(
dataset_id,
table_id,
target_scope,
target_row_key,
target_sdp_field,
dictionary_role,
search_query,
top_non_smn_label,
top_non_smn_source,
gap_detection_basis,
llm_new_term_namespace,
placement_recommendation,
placement_confidence,
request_scope,
ontology_repo
)
request_plan
readr::write_csv(request_plan, file.path(pkg_path, "term-request-plan.csv"))That gives you a concrete list of which package row still needs help,
the best fallback evidence, and the proposed route. Review
skip rows and any route inferred from
llm_new_term_namespace.
To apply reviewed row-by-row decisions, pass an override vector. Edit
reviewed_scopes so every value is one of smn,
gcdfo, profile, or skip:
reviewed_scopes <- request_plan$request_scope
# reviewed_scopes[2] <- "gcdfo"
requests <- render_ontology_term_request(
gaps,
scope = "auto",
ask = FALSE,
profile_name = "local-program",
scope_overrides = reviewed_scopes
)
requests |>
dplyr::select(
request_scope,
ontology_repo,
request_title,
target_row_key,
search_query
)The override vector must have length one or one value per gap row. Rendering uses the active SMN and GCDFO repository templates and never submits an issue.
Preview issue drafts
Preview both SMN and GCDFO payloads before any submission:
issue_preview <- requests |>
dplyr::filter(request_scope %in% c("smn", "gcdfo")) |>
submit_term_request_issues(dry_run = TRUE)
issue_previewThe helper only posts after an explicit call with
dry_run = FALSE; it never submits during detection or
rendering. Keep confirm = TRUE for curator review. Profile
rows remain local governance artifacts unless you provide a repository
workflow for them.
7) Retain semantic mapping and decomposition evidence when available
Use SSSOM for reviewed relationships between whole vocabulary
concepts. That includes an explicit sssom:NoTermFound row
when a released PSC compound measurement has no defensible match in
another versioned vocabulary. Do not put property/entity/unit
decomposition columns into SSSOM.
write_sdp_sssom(
pkg_path,
mapping_sets = reviewed_sssom_paths
)
validate_sdp_sssom(pkg_path)Store the ordered property, entity, constraint, statistical-modifier, and unit evidence in the separate SDP measurement-decomposition artifact:
write_sdp_measurement_decompositions(
pkg_path,
decompositions = reviewed_components
)
validate_sdp_measurement_decompositions(pkg_path)metasalmon does not invent either record by default: both require reviewed inputs. When their manifests are present, later KNB planning validates them and includes them automatically inside the canonical SDP ZIP. This preserves the evidence as part of the package without claiming native I-ADOPT conformance or turning the component rows into concept mappings.
8) Finalize metadata and rebuild EDH XML if needed
Once the metadata is final and every surviving IRI is deliberate, regenerate EDH XML if your publication path needs it:
write_edh_xml_from_sdp(pkg_path)This is the reviewed-package wrapper around
edh_build_hnap_xml(). It refuses to rebuild while obvious
review-state markers still exist, including:
-
REVIEW:-prefixed IRIs; and - unresolved
MISSING DESCRIPTION:/MISSING METADATA:placeholders indescription,observation_unit, andtable_label.
A blank observation_unit_iri is not one of
those markers and does not block a rebuild; only a
REVIEW:-prefixed value does.
9) Run strict final validation
When the package is genuinely ready, switch to strict validation:
validate_salmon_datapackage(pkg_path, require_iris = TRUE)After this passes, the whole package folder can be shared as described in step 12. For EML/KNB export or deposit, complete the reviewed closure and EML facts in steps 10 and 11.
If strict validation still fails because a measurement term genuinely needs a new shared or DFO-specific ontology term, that is not a bug in the workflow — it means the package is not finalized yet. Keep the term-request plan with the package, resolve the ontology decision, then run strict validation again.
10) Assemble the reviewed closure
Strict validation is not the publication gate. A
package that passes
validate_salmon_datapackage(require_iris = TRUE) can still
be refused by write_eml_from_sdp() and
publish_sdp_to_knb(), because the EML/KNB path additionally
requires a reviewed closure: a standing record of which
vocabulary terms you accepted, on what evidence, and for which
target.
Three files carry it.
write_sdp_semantic_closure() writes the first two
and pins the digests the third one carries; the reviewed EML
facts in the third are yours (step 11):
| File | What it must contain | Written by |
|---|---|---|
metadata/semantic_vocabulary.csv |
One evidence row per canonical measurement IRI — exactly that set, no more and no less | write_sdp_semantic_closure() |
reviewed_semantic_selections.csv |
Exactly one accepted row per canonical review
target |
write_sdp_semantic_closure() |
metadata/eml-mapping.yml |
The reviewed EML facts, plus the SHA-256 of each of the two files above | you, except the two SHA-256 values |
The canonical measurement IRI set is every non-empty
value across term_iri, property_iri,
entity_iri, constraint_iri,
statistical_modifier_iri and unit_iri on your
measurement dictionary rows, plus any
tables.csv method_iri and any procedure IRIs
reached through codes.csv from an observation-structures
binding.
The canonical review target set is derived
independently, and it is not the measurement set plus anything.
It has one target for each filled slot among these: the same six
semantic fields on your measurement dictionary rows, plus
each table’s observation_unit_iri and its
method_iri.
The two sets differ in both directions, which is why neither can be reasoned from the other:
- an
observation_unit_iriis a review target and not a vocabulary term — a reviewed selection, but not something the EML measurement path emits; and - a
sosa:usedProcedurereached through a code value is a vocabulary term and not a review target — the EML method path emits it, but the ledger has no slot for it, because it is reached through the code values in your data rather than through any field the review target set reads.
Getting that second direction backwards is worth avoiding rather than
discovering: the ledger gate accepts exactly the
canonical target set, so a row added for a code-resolved procedure is
refused as unexpected. write_eml_from_sdp() and
publish_sdp_to_knb() stop and name the row, while
validate_salmon_datapackage(require_iris = TRUE) goes on
passing: the package stops being publishable because of a row you added
to make it publishable.
semantic_vocabulary.csv needs these columns, all
non-empty except type_iris and
source_artifact_sha256:
iri, label, definition, source, ontology, resource_kind, type_iris,
native_type, source_url, source_artifact_sha256, reviewed_snapshot_sha256
reviewed_semantic_selections.csv needs:
dataset_id, table_id, column_name, target_scope, target_sdp_field,
dictionary_role, decision, confidence, review_rationale, iri
Every decision must be accepted — a final
ledger holds no open decisions — and column_name is empty
only for table-scope rows.
Produce both files
write_sdp_semantic_closure() derives both canonical sets
from the package, re-runs find_terms() for each IRI to
resolve its evidence, computes every
reviewed_snapshot_sha256, writes both files, and rewrites
the two file digests in metadata/eml-mapping.yml.
No SHA-256 in a reviewed package is ever
hand-written.
Two kinds of value it cannot derive, and both go in
evidence:
-
Vocabularies
find_terms()cannot search. It resolvessmnandgcdfo. A QUDT unit row is hand-authored in full. It also cannot fillnative_typeorsource_url, which describe the ontology artifact rather than the term; those are derived from the resolved source and can be overridden. -
confidenceandreview_rationale, which are your judgement. They are read fromsemantic_suggestions.csvwhereapply_sdp_semantics()recorded a decision reason, and otherwise written as aREVIEW REQUIRED:marker with a warning naming each target that got one. Replace those before publishing.
# Your judgement on each accepted IRI. The term evidence is resolved for you.
judgements <- tibble::tribble(
~iri, ~confidence, ~review_rationale,
"https://w3id.org/gcdfo/salmon#SpawnerAbundance", "high",
"The column is an escapement estimate of adult spawners.",
"https://w3id.org/smn/Abundance", "high",
"The measured characteristic is abundance.",
"https://w3id.org/smn/Observation", "high",
"Each row is one population-year observation."
)
# A QUDT row is hand-authored end to end: the search cannot reach QUDT.
qudt <- tibble::tibble(
iri = "https://qudt.org/vocab/unit/INDIV",
label = "Individual",
definition = "A counting unit denoting one organism.",
source = "qudt",
ontology = "qudt",
resource_kind = "Unit",
type_iris = "http://qudt.org/schema/qudt/Unit",
native_type = "qudt:Unit",
source_url = "https://qudt.org/vocab/unit/",
confidence = "high",
review_rationale = "Values are whole counts of organisms."
)
closure <- write_sdp_semantic_closure(
"path/to/package",
evidence = dplyr::bind_rows(judgements, qudt)
)Copy the sidecar template (step 11) before running this, so the two digests are pinned in the same pass. If you produce the closure first, run it again once the sidecar exists.
An IRI it cannot resolve is reported, not guessed at, and not
fatal. closure$gaps is a term-gap table in the
same shape detect_semantic_term_gaps() returns, so it feeds
the request pipeline directly:
if (nrow(closure$gaps) > 0) {
requests <- render_ontology_term_request(closure$gaps, ask = FALSE)
submit_term_request_issues(requests, dry_run = TRUE)
}The unresolved row is left out of the vocabulary rather than
invented, so write_eml_from_sdp() will still refuse the
package and will name the same IRI. That is the intended sequence: the
gap is a term to mint or an IRI to correct, and the package is not
publishable until it is one or the other.
Two things that leave a row out are not gaps, and you should
not file them as term requests. A gap says the searched
vocabularies do not have the term, and that is what
render_ontology_term_request() acts on, so the cases that
do not establish absence are reported separately:
-
A lookup that did not answer — a network or parser
failure, or a
find_terms()result whose"diagnostics"attribute names a source that did not reply. The call aborts and writes nothing, naming each IRI and the silent sources. Re-run when the sources answer; nothing on disk changed. -
A term found with a required field blank, for
example a class with no definition. It comes back in
closure$incomplete, naming the missing field and the slot. The term exists, so supply the field throughevidenceor annotate it upstream rather than requesting a new term.
A worked end-to-end example is
scripts/build-fraser-coho-knb-rehearsal.R in the source
repository, which takes the shipped Fraser coho data from
create_sdp() to a clean KNB test-node dry run using only
exported functions.
11) Build reviewed EML and preview KNB publication when needed
KNB publication requires EML-specific facts that the canonical SDP
does not guess, including structured parties, rights, methods,
measurement scales, missing-value meanings, and access intent. Record
those reviewed decisions in metadata/eml-mapping.yml, then
build and validate EML 2.2.0.
Three of its fields are cross-checked against the package and will
abort the build if they drift, so set them from one source rather than
retyping them: source_provenance.source_citation and
source_provenance.provenance_note must byte-match
dataset.csv, and publication.public must equal
the public argument you pass to
publish_sdp_to_knb().
A live deposit also requires the sidecar to name exactly
one metadata-provider orcid, and it must be the
ORCID behind the DataONE token you authenticate with. A dry run accepts
any syntactically valid ORCID, so the placeholder in the shipped
template will pass a rehearsal and be refused by the live call.
# Copy once, then replace every example value and checksum with reviewed facts.
file.copy(
system.file(
"extdata",
"eml-mapping-template.yml",
package = "metasalmon"
),
file.path(pkg_path, "metadata", "eml-mapping.yml")
)
write_eml_from_sdp(pkg_path)That standalone call creates valid reviewed EML for inspection. The
later KNB dry run deterministically rebuilds it once more so the final
EML also describes each named canonical SDP artifact as an
otherEntity.
Preview the exact immutable DataONE objects, identifiers, checksums, access decision, and OAI-ORE relationships without reading credentials or making a network request:
publish_sdp_to_knb(
pkg_path,
public = FALSE,
dry_run = TRUE,
representation = "expanded"
)That dry run targets the KNB Test Node
(urn:node:mnTestKNB), which is what
knb_environment defaults to for a dry run. The golden path
develops a package against the test node first and posts to production
once it looks good there, so rehearse in that order:
- credential-free test dry run (the call above)
- optional authorized test deposit, with a
dataone_test_token - fresh production dry run, naming the environment explicitly
- optional private production deposit
- separate KNB Publish action for public release and DOI
Test objects are never promoted to production: a production deposit is planned separately, with production credentials, and mints its own identifiers.
publish_sdp_to_knb(
pkg_path,
public = FALSE,
dry_run = TRUE,
representation = "expanded",
knb_environment = "production"
)Switching the environment switches the DataONE network, member node,
coordinating node, resolver, and catalog endpoint together — they never
vary independently — along with which credential is read. Test artifacts
are written under publication/test/, so the reviewed
production metadata/eml.xml and
publication/knb-manifest.json are left exactly as they
were.
Live KNB publication is a separate explicit action. It requires a
short-lived DataONE JWT in the process-local option belonging to the
environment (dataone_token for production,
dataone_test_token for the test node — they are separate
credentials), an ORCID-authenticated subject matching the EML metadata
provider, an explicitly named knb_environment, and an
explicitly supplied confirm = TRUE. A live call never
infers its environment. The confirmation means both that the exact
pending manifest is approved and that the caller has authority to
redistribute the package.
Keep the JWT out of scripts, YAML, manifests, and shell history. Enter it into the current R process only and clear it immediately after the live call:
# `dataone_token` is the production credential; the test node reads
# `dataone_test_token` instead.
options(dataone_token = rstudioapi::askForPassword("Short-lived DataONE JWT"))
on.exit(options(dataone_token = NULL), add = TRUE)
publish_sdp_to_knb(
pkg_path,
public = FALSE,
dry_run = FALSE,
confirm = TRUE,
representation = "expanded",
knb_environment = "production"
)public = FALSE is the recommended review path, but it is
not a server-side draft: a live call creates persistent production KNB
objects. Completion requires authenticated byte-for-byte and
SystemMetadata readback for every object, anonymous denial for both
object bytes and SystemMetadata, a complete authenticated catalog graph,
and zero planned PIDs in the anonymous catalog. The reviewed private
plan also explicitly sets DataONE replication to disabled with zero
replicas, and live readback must match that KNB-only policy. Changing to
public = TRUE is a separate release decision: it requires
public redistribution authority and explicitly requests three DataONE
preservation replicas; metasalmon never infers that decision from a
private deposit.
An expanded plan contains four kinds of object: the original data
resources named by tables.csv, validated canonical SDP
artifacts with their exact package-relative paths, the EML
science-metadata record, and the OAI-ORE map. The EML record describes
raw files as data tables and named SDP artifacts as supplementary
entities; ORE prov:atLocation values preserve the hierarchy
for exact reconstruction. A closed inventory includes SSSOM, ordered
measurement decompositions, observation structures, and
manifest-declared reproducibility records without scanning arbitrary
files. There is no methods artifact to deposit: since sdp-0.3.0 a
package carrying a metadata/methods.csv is refused
outright, with a pointer to migrate_sdp_methods(). The
KNB-specific metadata/eml-mapping.yml, its authorization
evidence and party details, and all mutable publication/
receipts remain local and are not deposited.
KNB DOI minting is a later opt-in release action, not part of
publish_sdp_to_knb(). KNB assigns the DOI to the selected
science-metadata version when its Publish action makes that package
public; it does not assign a separate DOI to each raw or supplementary
object. Leaving a deposit private is therefore the practical review
workflow. Editing after deposit creates new immutable versions rather
than overwriting bytes.
For a corrected or otherwise revised private version, keep the same
series_key, add a new safe
publication.revision_key to the sidecar, and bind the new
dry run to the verified manifest for the preceding version. Build the
revision in a fresh versioned SDP directory; never reuse or overwrite
the prior package directory:
publish_sdp_to_knb(
revised_pkg_path,
public = FALSE,
manifest_path = file.path(
revised_pkg_path,
"publication",
"knb-manifest.json"
),
revision_manifest = previous_manifest_path,
dry_run = TRUE,
representation = "expanded",
knb_environment = "production"
)A revision continues the environment it started in; a manifest from one environment can never be revised into the other.
Review that new manifest before repeating the live call with
dry_run = FALSE, confirm = TRUE. An interrupted live call
is resumable: reuse the byte-identical package, revision manifest, and
output manifest. A completed upload whose coordinating-network catalog
check is delayed returns published_pending_catalog;
rerunning the same call performs fresh checks and does not create
duplicate objects.
If KNB’s separate Publish action later mints a DOI, KNB creates another science-metadata version. metasalmon does not yet import that KNB-created version into a local predecessor manifest. Do not base a later automated revision on the older pre-DOI manifest; use KNB’s editor for that next change or wait for an explicit manifest-import workflow.
Before any plan is written, metasalmon rejects referenced rows whose
source or ontology label identifies them as a
review-candidate. This is a deliberately offline
fail-closed check; it does not resolve public IRIs or by itself prove
that a vocabulary release was governed. The canonical transformation
record must separately pin the approved vocabulary release and verify
its public term IRIs before the SDP is deposited.
12) Publish or share the finished package
Once strict validation passes:
- share the whole package folder (or a zip of it),
not just
datapackage.json; - keep
metadata/*.csvwith the data files; and - keep
term-request-plan.csvonly as working governance support, not as part of the canonical final package unless you want that audit trail to travel with it.
In short: reload -> validate -> detect gaps -> route requests -> build the required export -> strict validate -> dry-run -> publish.