feat: CTDL-ASN registry import, alignment, and publish to the Credential Registry - #9
Draft
jeff-grann wants to merge 14 commits into
Draft
feat: CTDL-ASN registry import, alignment, and publish to the Credential Registry#9jeff-grann wants to merge 14 commits into
jeff-grann wants to merge 14 commits into
Conversation
Adds server-side import of a competency framework from a CTDL-ASN registry resource into a CASE CFPackage, plus the supporting configuration. - CredentialRegistryClient fetches the CTDL-ASN @graph. The default registry origin is configurable via CREDENTIAL_REGISTRY_BASE_URL; a full resource URL passed to import overrides it per request (enables prod/sandbox/self-hosted registries without reconfiguration). - CtdlAsnToCaseMapper maps ceasn:CompetencyFramework / ceasn:Competency graphs to a CASE CFPackage, recording registry provenance under a unified ext:opencase.source block (uri / ctid / registry / format) on the CFDocument and every CFItem, and setting officialSourceURL + sourcePackageURI. - ImportFrameworkFromRegistry and PreviewRegistryFramework endpoints, wired through the management controller/routes and DI container. - The document index lifts isModifiedFromSource / sourcePackageURI from ext:opencase; CreateFramework no longer force-marks registry-managed frameworks modified on a layout-only save (it trusts the editor's explicit fork flag; legacy CASE-package imports keep the previous behaviour). - docker-compose + docs/env.example expose CREDENTIAL_REGISTRY_BASE_URL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nal alignment Data/mapping layer for importing registry frameworks and aligning CASE items to external competencies, with round-trip integrity. - Preserve registry provenance across the editor round-trip: carry ext:opencase on the CFDocument snapshot (normalize + snapshot type) and reconcile the nested vs flattened extension shapes on export so provenance survives saves. - Unified provenance: ext:opencase.source on documents and items; forking an imported framework rewrites source -> derivedFrom (provenance.ts) and the reducer's framework/enableEditing action applies it across all nodes. - Alignment associations to BOTH registry and external-framework reference nodes are emitted on export (previously external edges were dropped); the reference nodes are persisted in the CFDocument extension and reconstructed on load, and alignment edges are re-linked to the reconstructed nodes by destination URI (fixes edges/associations being lost across a reload). - Official-format (toOpenCaseFormat) URI fixes: a top-level item's isChildOf now targets the CFDocument (not a bogus /CFItems/<docId> URI), and a registry alignment's destination identifier is the CTID's UUID rather than a hash of the "ce-" string. - Tests: toOpenCaseFormat (destination URIs), externalAlignment (association emitted + node persisted), alignmentRelink (edges re-link on reload). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Import-from-registry dialog and a read-only registry reference node type, wired into the home screen, app, and canvas (floating add menu). - Imported registry frameworks open read-only: content edits and structural changes (add/remove items and associations) are blocked while layout changes stay allowed, enforced centrally in EditorContext. A header "Enable editing" action forks the framework after confirmation, recording derivation. - Node properties panel shows registry provenance (CTID / CTDL URI / source registry) read-only, and the header/home reflect Imported vs Forked state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Walkthrough for importing a CTDL-ASN framework from a Credential Engine registry, aligning CASE items to registry/external competencies, and the read-only/fork workflow — with expected results at each step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…try (dry-run)
First slice of publishing a framework from OpenCASE to the Credential Registry
via the Registry Assistant API. Validation/dry-run only — nothing is published
and no CTIDs are persisted yet.
- RegistryAssistantClient: POST to the Registry Assistant
competencyframework/{format,publish} endpoints with `Authorization: ApiToken`;
sandbox (default) / production base URLs selectable per request.
- CaseToCompetencyFrameworkRequestMapper: reverse of the CTDL-ASN import mapper —
CASE CFDocument/CFItems/CFAssociations → a Registry Assistant
CompetencyFrameworkRequest. Maps isChildOf → IsChildOf/HasTopChild and the
cross-framework alignment associations → ExactAlignment (exactMatchOf) /
AlignTo (isRelatedTo).
- ctid.ts: OpenCASE mints ce-<uuid> CTIDs client-side and sends them.
- PreviewPublishToRegistry + POST …/CFPackages/:docId/preview-publish (v1p0/v1p1):
load the stored package, mint CTIDs, map, and call /format; returns the request
we would send plus the RA validation response.
- Config + docker-compose + env.example: REGISTRY_ASSISTANT_* settings
(environment, sandbox/production base URLs, per-org API key, org CTID). API key
is blank by default (publishing disabled until configured).
Still to come: real publish + persisting CTID/RegistryEnvelopeId into
ext:opencase.published, and the editor Publish UI (gated to authored/forked
frameworks, sandbox default with a production toggle).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Local walkthrough for the Registry Assistant /format dry-run: configure sandbox credentials, rebuild, obtain a bearer token (password grants are disabled), call POST …/CFPackages/:docId/preview-publish, and interpret the request + format response. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an editor affordance to dry-run publishing to the Credential Registry so it can be tested without curl/token-copying. - CaseApiClient.previewPublish → POST …/CFPackages/:docId/preview-publish. - PublishPreviewDialog: sandbox/production toggle, runs the dry-run, and shows the Registry Assistant validation result (ok + Messages) and the request payload. - NodePropertiesPanel Export section gains a "Preview publish to registry…" button. - Gated in EditorCanvas to saved (published-to-OpenCASE), editable (non-locked) frameworks; wired through App with the authenticated CaseApiClient. - Testing guide updated with the editor button as the primary path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…configurable While testing the dry-run, a hang in the Registry Assistant surfaced only as a bare "HTTP 400" with no detail. Improve observability and control: - Editor HTTP client appends the server's error message to the thrown error, so the UI shows the real cause (e.g. "Registry Assistant request timed out"). - RegistryAssistantClient logs timeouts/aborts (previously silent) with the environment, action, and elapsed context. - Registry Assistant request timeout is configurable via REGISTRY_ASSISTANT_TIMEOUT_MS (default 60000), wired through config, container, docker-compose, and env.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ChildOf) Per Credential Engine feedback on the Registry Assistant schema, each competency must declare its relationship to the framework. The mapper set the framework's HasTopChild but omitted the competency side. - Every competency now sets IsPartOf = framework CTID. - Top-level competencies also set IsTopChildOf = framework CTID. - (Nested competencies keep IsChildOf → parent competency CTIDs.) Note: adding these relations did not clear the sandbox /format hang on re-test — confirming the hang is a Registry Assistant-side issue (tracked in CE issue 4014), independent of the payload's membership relations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the write publish path alongside the existing dry-run: - publishState.ts: durable publish identity - mints/reuses ce-<uuid> CTIDs from ext:opencase.published, tracks the registry envelope id per environment, and merges publish results back into the package so re-publishing UPDATES the same resource instead of duplicating it. - PublishFrameworkToRegistry use case: loads the saved package, maps it to a Registry Assistant request (reusing prior CTIDs + envelope id when present), calls client.publish, persists the minted CTIDs + envelope as a new version, and returns the resource URL / create-vs-update. - POST /CFPackages/:docId/publish (v1p0 + v1p1), wired through the controller and container (503 when no API key/org CTID configured). - Editor: CaseApiClient.publish; PublishPreviewDialog gains a Publish button that appears after a passing dry-run for the selected environment, with a production confirmation gate and a result panel (resource link, CTID, envelope). Renamed the panel action to "Publish to registry...". Backend: 268 jest pass (incl. new publishState tests). Editor: 133 vitest pass, tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Splits the guide into dry-run and publish sections: editor + API steps for the write path, the create-vs-update round-trip check, the response shape, and where the persisted CTID + per-environment envelope id live under ext:opencase.published. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… updates Re-publishing after editing a framework created a DUPLICATE in the registry instead of updating it. Root cause: the publish endpoint persists the minted CTIDs + per-environment registry envelope id under ext:opencase.published, but the editor doesn't round-trip that block, so a normal save between publishes overwrote the stored version without it. The next publish then found no prior identity and minted all-new CTIDs. Fix: CreateFramework now carries the prior version's ext:opencase.published block forward onto any save that lacks it - the document, and each item matched by identifier - via carryForwardPublishState(). Incoming values still win (explicit republish/clear works), and newly added items get no prior match so publish mints fresh CTIDs for them. The existing version is loaded once and reused for both the carry-forward and the idempotency check. Result: framework CTID + existing competency CTIDs stay stable across edit/save/publish cycles, a newly added statement gets its own CTID, and the persisted envelope id makes the second publish an UPDATE of the same registry resource. Backend: full jest suite pass (incl. new carryForwardPublishState tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gistry Adds visibility into what's published and a way to remove it. Status: - frameworkContentHash() snapshots a framework's publishable content (excluding timestamps + publish bookkeeping). Saved on every save as ext:opencase.contentHash and snapshotted at publish as published.contentHash; a drift between them means "changed since published". Excluded from the idempotency comparison so it doesn't create spurious versions. - Framework list metadata + GetAllCFDocuments/ListFrameworks now expose a `publish` summary (ctid, per-environment resource URLs + status, needsUpdate). - Editor: "In Registry" / "Registry: changed" / "Registry: deprecated" badges on library cards; a status block in the framework side panel with per-environment resource links and a re-publish hint. Remove: - RegistryAssistantClient.delete() (HTTP DELETE /competencyframework/delete). - Mapper gains PublicationStatusType so deprecate re-publishes the same resource as Deprecated (CE's recommended practice over hard delete). - RemoveFrameworkFromRegistry use case + POST .../CFPackages/:docId/unpublish (v1p0 + v1p1). Delete clears the local publish link (drops the published block, incl. CTID, when no environments remain, so a later publish mints fresh CTIDs); deprecate keeps the link and flags the environment Deprecated. publishState gains clearPublishEnvironment() and markPublishDeprecated(). - Editor: the publish dialog gains a "Currently in the registry" section with Deprecate and Delete (delete behind an inline confirm; production behind the existing confirm). CaseApiClient.unpublish; App refreshes status after publish/unpublish. Backend: full jest suite pass (incl. new publishState tests). Editor: 133 vitest pass, tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a "Publish status in the UI" section (card badges, side-panel status, how changed-since-publish is detected) and a "Remove from the registry" section (delete vs deprecate, editor + API steps, unpublish response shape). Updates the persisted-identity block to include contentHash + per-environment status and the carry-forward-on-save behavior, and extends common responses with unpublish_failed. Co-Authored-By: Claude Opus 4.8 <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
Adds full two-way interoperability between OpenCASE and the Credential Engine Registry, on top of OpenCASE's existing CASE import/export. A "no-wrong-door" approach: software can manage competencies regardless of whether they're represented as CASE or CTDL-ASN.
This PR supersedes draft #8 — it includes that PR's CTDL-ASN import + alignment work and builds the publish pathway on top of it. #8 can be closed in favor of this.
Two capabilities:
What's included
CTDL-ASN import + alignment (was PR #8)
fce44d2), mapping CTDL-ASN → CASE with provenance underext:opencase.sourceand support for registry/external alignment associations (0c0a1d9).source→derivedFromand marks the copy modified (23b0eb4).72fb413).Publish to the Registry
/format) and real publish (/publish) via the Registry Assistant (ApiTokenauth; sandbox + production, default sandbox with a per-request toggle). OpenCASE mints/reusesce-<uuid>CTIDs client-side and sends the priorRegistryEnvelopeIdso re-publish updates the same resource. (5316c3c,a8a7a5f)IsPartOf/IsTopChildOf/IsChildOf) and alignments (ExactAlignment/AlignTo) mapped for the Registry Assistant (e67ebb0).ext:opencase.published(ctid, per-environmentregistryEnvelopeId+publishedAt,contentHash). Because the editor doesn't round-trip this block,CreateFrameworkcarries it forward on every save (matching items by identifier) so an edit between publishes can't wipe the CTIDs and cause a duplicate (48f2144).42b62db)./unpublish): hard delete (clears the local publish link) or deprecate (republish asPublicationStatusType: Deprecated, CE's recommended practice). Editor dialog gains a "Currently in the registry" section with both actions (delete behind a confirm) (42b62db).4397580,b6119ef,4f508ff).Configuration
Per-tenant Registry Assistant config via env (
REGISTRY_ASSISTANT_ENVIRONMENT,REGISTRY_ASSISTANT_API_KEY,REGISTRY_ASSISTANT_ORG_CTID; registry base URL parameterized). The API key is a secret and is never committed. Publishing endpoints return503when unconfigured. Future: fetch keys/org CTIDs via a Keycloak sign-on event with the Registry.Testing
publishStatecoverage (CTID reuse, per-env envelope, carry-forward, content hash, clear/deprecate).tsc --noEmitclean on both apps.See
docs/PUBLISH_TO_REGISTRY_TESTING.mdand the CTDL-ASN import testing guide for local walkthroughs.Notes for reviewers
ext:opencase.*extensions; standard CASE consumers that strip extensions are unaffected./formatendpoint had a CE-side outage during development (Elasticsearch/GetResourceByUrl hang) that has since been resolved; no OpenCASE change was needed.🤖 Generated with Claude Code