Skip to content

feat: CTDL-ASN registry import, alignment, and publish to the Credential Registry - #9

Draft
jeff-grann wants to merge 14 commits into
1EdTech:mainfrom
jeff-grann:feat/publish-to-registry
Draft

feat: CTDL-ASN registry import, alignment, and publish to the Credential Registry#9
jeff-grann wants to merge 14 commits into
1EdTech:mainfrom
jeff-grann:feat/publish-to-registry

Conversation

@jeff-grann

Copy link
Copy Markdown

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:

  1. Import CTDL-ASN competency frameworks from the Registry into OpenCASE (read-only until forked), including CASE frameworks that carry alignments to CTDL-ASN competencies.
  2. Publish OpenCASE frameworks to the Registry via the Registry Assistant — with durable identity so re-publishing updates rather than duplicates, publish-status visibility, and delete/deprecate.

What's included

CTDL-ASN import + alignment (was PR #8)

  • Import a framework from the Credential Registry (fce44d2), mapping CTDL-ASN → CASE with provenance under ext:opencase.source and support for registry/external alignment associations (0c0a1d9).
  • Editor: registry import UI and a read-only / confirm-to-fork workflow — imported frameworks are not editable until the user forks, which rewrites sourcederivedFrom and marks the copy modified (23b0eb4).
  • Testing guide (72fb413).

Publish to the Registry

  • Dry-run (/format) and real publish (/publish) via the Registry Assistant (ApiToken auth; sandbox + production, default sandbox with a per-request toggle). OpenCASE mints/reuses ce-<uuid> CTIDs client-side and sends the prior RegistryEnvelopeId so re-publish updates the same resource. (5316c3c, a8a7a5f)
  • Competency → framework membership (IsPartOf / IsTopChildOf / IsChildOf) and alignments (ExactAlignment / AlignTo) mapped for the Registry Assistant (e67ebb0).
  • Durable publish identity persisted in ext:opencase.published (ctid, per-environment registryEnvelopeId + publishedAt, contentHash). Because the editor doesn't round-trip this block, CreateFramework carries it forward on every save (matching items by identifier) so an edit between publishes can't wipe the CTIDs and cause a duplicate (48f2144).
  • Publish status in the UI: library-card badges (In Registry / Registry: changed / Registry: deprecated) and a side-panel status block with resource links; "changed since publish" is detected via a content fingerprint that ignores timestamps + publish bookkeeping (42b62db).
  • Delete / deprecate from the Registry (/unpublish): hard delete (clears the local publish link) or deprecate (republish as PublicationStatusType: Deprecated, CE's recommended practice). Editor dialog gains a "Currently in the registry" section with both actions (delete behind a confirm) (42b62db).
  • Testing guide covering dry-run, publish, status, and delete/deprecate (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 return 503 when unconfigured. Future: fetch keys/org CTIDs via a Keycloak sign-on event with the Registry.

Testing

  • Backend: full Jest suite green (283 tests), including new publishState coverage (CTID reuse, per-env envelope, carry-forward, content hash, clear/deprecate).
  • Editor: 133 Vitest green; tsc --noEmit clean on both apps.
  • Publish, re-publish-as-update, status, and delete verified end-to-end against the live sandbox Registry.

See docs/PUBLISH_TO_REGISTRY_TESTING.md and the CTDL-ASN import testing guide for local walkthroughs.

Notes for reviewers

  • All Registry interop lives behind ext:opencase.* extensions; standard CASE consumers that strip extensions are unaffected.
  • The Registry Assistant /format endpoint had a CE-side outage during development (Elasticsearch/GetResourceByUrl hang) that has since been resolved; no OpenCASE change was needed.

🤖 Generated with Claude Code

jeff-grann and others added 14 commits August 6, 2026 12:02
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant