Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/api/crate.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ Source: `src/lightcone/engine/crate.py` (converged by
never-materialized project) and must override rocrate's
construction-time default. Entities build in sorted order,
serialization is `sort_keys` — render-twice-identical is the one
byte-level claim, and it is what makes convergence sound.
byte-level claim, and it is what makes convergence sound. The
serialization also compacts every one-element array to its value,
as RO-Crate 1.1 recommends: which properties hold one value depends
on the project, so the rule lives in one place, not in each builder.
- **Maintenance is derived, never configured.** RO-Crate requires a
license; materialize must not refuse to run science over a missing
key, and inventing one asserts terms over someone's data. Absent ⇒
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dev = [
"ruff",
"mypy",
"datalad",
"roc-validator>=0.11.3",
"roc-validator>=0.11.4",
]
docs = [
"zensical>=0.0.33",
Expand Down
15 changes: 12 additions & 3 deletions src/lightcone/engine/crate.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,18 @@ def document(self) -> str:
self._control(key, self._action(key, manifest))
self._runs(workflow)
self._root()
text: str = json.dumps(
self.crate.metadata.generate(), indent=1, sort_keys=True, ensure_ascii=False
)
document = self.crate.metadata.generate()
# Compacted JSON-LD: a property with one value is that value, not
# a one-element array. Applied here rather than at each builder
# because the arrays come from three places — rocrate's
# `append_to`, and two literal lists — and which of them hold one
# element depends on the project (one output, one author). The
# same rule rocrate itself applies to `@context`.
for entity in document["@graph"]:
for name, value in entity.items():
if isinstance(value, list) and len(value) == 1:
entity[name] = value[0]
text: str = json.dumps(document, indent=1, sort_keys=True, ensure_ascii=False)
return text + "\n"

# ----- the workflow and its structure -----
Expand Down
20 changes: 18 additions & 2 deletions tests/test_crate.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,22 @@ def test_rendering_twice_at_the_same_state_is_byte_identical(project: Path) -> N
assert first == second


def test_a_single_value_is_never_a_singleton_array(project: Path) -> None:
"""Compacted JSON-LD: one author, one parameter, one result are plain
values — RO-Crate 1.1's recommendation, and what its validator checks.
The arrays come from three builders, so this pins the serialization."""
_made(project, "baseline", "first", git_sha="aaa111")
rendered = crate.render(
project, _graph(project), license="MIT", dsid=_DSID, writer=_writer, keys={}
)
document = json.loads(rendered)
for entity in document["@graph"]:
for name, value in entity.items():
assert not (isinstance(value, list) and len(value) == 1), (entity["@id"], name)
entities = _entities(document)
assert entities["./"]["author"] == {"@id": "mailto:ada@example.org"}


def test_the_clock_never_enters_the_document(project: Path) -> None:
"""`datePublished` is the newest recorded instant — rocrate's own
default stamps the current time, and this pins the override."""
Expand Down Expand Up @@ -230,7 +246,7 @@ def test_an_action_chains_its_inputs_and_its_environment(project: Path) -> None:
first_objects = {ref["@id"] for ref in first["object"]}
assert {"uv.lock", ".python-version", "pyproject.toml", "data/catalog.csv"} <= first_objects
assert "results/baseline/first.txt" in {ref["@id"] for ref in second["object"]}
assert second["result"] == [{"@id": "results/baseline/second.txt"}]
assert second["result"] == {"@id": "results/baseline/second.txt"}
assert second["description"] == "make second"
assert entities["results/baseline/second.txt"]["version"] == "sha256:baseline-second"

Expand Down Expand Up @@ -280,7 +296,7 @@ def test_the_person_is_the_saving_commits_author(project: Path) -> None:
assert person["name"] == "Ada Lovelace"
action = next(e for e in entities.values() if e["@type"] == "CreateAction")
assert action["agent"] == {"@id": "mailto:ada@example.org"}
assert {"@id": "mailto:ada@example.org"} in entities["./"]["author"]
assert {"@id": "mailto:ada@example.org"} in _as_list(entities["./"]["author"])


def test_decision_values_point_back_at_their_parameter(project: Path) -> None:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_crate_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"process-run-crate-0.5_13.2",
# lc knows no publishing organization and no author affiliation
"ro-crate-1.1_22.3",
"ro-crate-1.1_29.2",
"ro-crate-1.1_29.3",
"ro-crate-1.1_30.2",
"ro-crate-1.1_30.3",
}

_SPEC = """
Expand Down
Loading