feat(equipment): Asset.persistent_id write path + DoiMinter port + Stub adapter (slice F.1)#38
Merged
Merged
Conversation
…ub adapter (slice F.1)
Closes the D-ASSIGN deferral from slice E.1. Adds the WRITE path for PIDINST
v1.0 Property 1 at the Asset tier: an aggregate-tier persistent_id field, a
PersistentIdentifier value object, the closed PersistentIdentifierScheme enum
{DOI, HANDLE}, the assign_asset_persistent_id mutation slice, the
AssetPersistentIdAssigned event, the DoiMinter Protocol port, and a
StubDoiMinter adapter that ships without DataCite credentials. The production
DataCite adapter (PUT /dois/{id}, HTTP Basic, 429 backoff, cassettes) is
deferred to F.2 behind facility-credential gating.
WHY: operators need a server-mint flow today so that downstream PIDINST
serialization swaps from urn:uuid: to the assigned DOI as soon as the assign
slice runs. Slice E.1 reserved the JSONB column and shipped the read closure;
slice F closes the write side end-to-end on a Stub so the whole flow can be
proven on the inert mint, then F.2 swaps the Stub for the production adapter
once credentials land at the pilot facility.
Set-once domain invariant enforced at the decider per F3.3 (DataCite Findable
immutability): once Asset.persistent_id is set, no further assign / clear /
reassign is accepted (slice G owns withdraw via a separate timestamp facet).
Server-mint posture per [[project-non-determinism-principle]]: the route
accepts (scheme, suffix | None) and the handler resolves the PersistentIdentifier
from the DoiMinter port before invoking the pure decider, keeping the decider
deterministic and trivially testable. BC-tier wiring per the operation BC
ControlPort precedent: wire_equipment(deps) stashes the StubDoiMinter on the
equipment-deps namespace, NOT through the cross-BC Kernel dataclass.
Error surface: 5 classes mapped uniformly. 400 InvalidPersistentIdentifierValueError,
404 AssetNotFoundError, 409 AssetPersistentIdAlreadyAssignedError +
AssetPersistentIdAssignmentForbiddenError, 502 PersistentIdentifierMintError
(error class lives at cora.equipment.ports.doi_minter next to the Protocol it
documents, NOT in aggregates/asset/state.py, because it is an adapter-tier
failure not a decider-raised genesis error).
Naming: slice directory renamed from assign_persistent_id to
assign_asset_persistent_id at implementation time per the
test_slice_dir_carries_subject architecture fitness (sibling pattern:
add_asset_owner, add_asset_alternate_identifier, add_asset_family,
add_asset_port). Command class AssignAssetPersistentId, Pydantic models
AssignAssetPersistentIdRequest / AssignAssetPersistentIdResponse follow. URL
stays terse (/assets/{asset_id}/assign-persistent-identifier) because the URL
path already carries the SUBJECT via /assets/.
Serializer: _build_identifier now uses match/case on PersistentIdentifierScheme
with NO default arm, so adding a third scheme member fires pyright as an
unhandled-case error at type-check time. URN fallback for None stays outside
the match.
Tach: adds cora.equipment.ports and cora.equipment.adapters rows.
Scale: 40 files, 3943 insertions / 6 deletions. 118 slice tests + 16571
architecture fitness tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
…data assertion The static test_projection_metadata assertion enumerates the full subscribed_event_types frozenset. Slice F added AssetPersistentIdAssigned to the projection writer but missed the corresponding test assertion update. CI caught it; targeted local pytest runs did not cover this file. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced Jun 5, 2026
xmap
added a commit
that referenced
this pull request
Jun 5, 2026
…ction metadata assertion The static test_projection_metadata assertion enumerates the full subscribed_event_types frozenset. Phase 2 added FixturePersistentIdAssigned to the projection writer but missed the corresponding test update. CI caught it; targeted local pytest runs did not cover this file. Same regression pattern as Asset slice F (PR #38 fixed by fe9430b); need to add the existing tests/unit/<bc>/test_*_projection.py files to the pre-push targeted run list when touching projection writers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the D-ASSIGN deferral from slice E.1 (PR #34). Adds the WRITE path for PIDINST v1.0 Property 1 at the Asset tier:
Asset.persistent_id: PersistentIdentifier | Noneaggregate field (additive; default None; placed as LAST field per the verified field-ordering check)PersistentIdentifierfrozen-dataclass VO with trim + 1-200 length invariantPersistentIdentifierSchemeStrEnum {DOI, HANDLE}, values matchingPidinstIdentifierTypebyte-for-byte (no translation map at the serializer)AssetPersistentIdAssignedevent (alphabetical position in__all__; grouped by domain-arm in the discriminated union per BC convention)assign_asset_persistent_idmutation slice (command + decider + handler + route + MCP tool)DoiMinterProtocol port atcora.equipment.ports.doi_minterStubDoiMinteradapter atcora.equipment.adapters.stub_doi_minter(ships in F.1, no DataCite credentials needed)_build_identifierwith NO default arm (adding a third scheme member fires pyright as an unhandled-case error)cora.equipment.ports+cora.equipment.adaptersThe production DataCite adapter (PUT /dois/{id} + HTTP Basic + 429 backoff + cassette tests) is deferred to F.2 behind facility-credential gating.
Why
Operators need a server-mint flow today so that downstream PIDINST serialization swaps from
urn:uuid:to the assigned DOI as soon as the assign slice runs. Slice E.1 reserved the JSONB column and shipped the read closure; this slice closes the write side end-to-end on a Stub so the whole flow can be proven on the inert mint, then F.2 swaps the Stub for the production adapter once credentials land at the pilot facility.Key locks
Asset.persistent_idis set, no further assign / clear / reassign is accepted. Slice G owns withdraw via a separate timestamp facet.(scheme, suffix | None)and the handler resolves thePersistentIdentifierfrom theDoiMinterport before invoking the pure decider, keeping the decider deterministic and trivially testable. ONE minter call site (the handler), not two (route + MCP tool).ControlPortprecedent:wire_equipment(deps)stashes theStubDoiMinteron the equipment-deps namespace, NOT through the cross-BCKerneldataclass.InvalidPersistentIdentifierValueError, 404AssetNotFoundError, 409AssetPersistentIdAlreadyAssignedError+AssetPersistentIdAssignmentForbiddenError, 502PersistentIdentifierMintError.PersistentIdentifierMintErrorlives atcora.equipment.ports.doi_minternext to the Protocol it documents, NOT inaggregates/asset/state.py, because it is an adapter-tier failure not a decider-raised genesis error.Naming note
The slice directory was renamed from
assign_persistent_idtoassign_asset_persistent_idat implementation time per thetest_slice_dir_carries_subjectarchitecture fitness (sibling pattern:add_asset_owner,add_asset_alternate_identifier,add_asset_family,add_asset_port). The command class follows:AssignAssetPersistentId. The Pydantic wire models follow:AssignAssetPersistentIdRequest/AssignAssetPersistentIdResponse. The URL stays terse (/assets/{asset_id}/assign-persistent-identifier) because the URL path already carries the SUBJECT via/assets/.Scale
40 files, 3943 insertions / 6 deletions. 118 slice tests + 16657 architecture-tier tests passing post-rebase.
Test plan
Follow-ups
project_fixture_configuration_hash_followup)Closes the D-ASSIGN deferral from slice E.1.
Generated with Claude Code