finos / finos/architecture-as-code
feat(calm-hub): navigate detailed-architecture references via $id resolution
- Dominant language
- TypeScript
- Stars
- 399
- Forks
- 138
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 38
Description
## Feature Proposal
### Target Project:
`calm-hub` (Java/Quarkus backend) and `calm-hub-ui` (React frontend) — primarily a new backend resolution capability, consumed by the UI.
### Description of Feature:
Enable users to **navigate between architectures in CALM Hub by following `detailed-architecture` references**, mirroring the behaviour that already exists in the VSCode extension.
A CALM node (or timeline moment) may carry `details.detailed-architecture`, a reference to a more detailed sub-architecture document. In the VSCode extension this is a first-class navigation affordance: clicking the node resolves the reference and opens the target architecture. CALM Hub today **detects** such references and renders an indicator (a `ZoomIn` icon in `CustomNode.tsx` / a badge in `NodeDetails.tsx`), but the indicator is inert — there is no drill-down.
The intent of this proposal is to close that gap so that CALM Hub's behaviour matches the VSCode extension: a `detailed-architecture` reference that points at a document **hosted within the same CALM Hub instance** should be navigable, loading that architecture into the visualiser.
### User Stories:
- As an architect browsing an architecture in CALM Hub, I want to click a node that has a detailed architecture and have CALM Hub load that sub-architecture, so that I can explore composed/layered designs without leaving the tool.
- As an architect, I want a way back to the parent architecture (breadcrumb / back), so that drilling down is reversible.
- As a platform owner, I want navigation to be **restricted to documents hosted in my own CALM Hub instance**, so that references cannot silently pull content from arbitrary external locations.
### Current Limitations:
The VSCode extension resolves a `detailed-architecture` reference by treating it as the **`$id` of the target CALM document** and looking that `$id` up. It does this via a *calm mapping* document (`calm-mapping.json`, configured through the `calm.urlMapping` setting): a flat `{ "<$id-or-url>": "" }` map. The `MappedDocumentLoader` pre-loads each mapped document and indexes it **by its `$id`** (`shared/src/document-loader/mapped-document-loader.ts`, storing into a `SchemaDirectory` keyed on `$id`), so navigation is fundamentally a **`$id` → document** resolution.
CALM Hub has **no equivalent `$id`-based resolution**:
- The "front controller" (`FrontControllerResource`, `GET /calm/namespaces/{namespace}/{customId}/versions/{version}`) resolves a **namespace-scoped `customId` slug** (`^[a-z][a-z0-9]*(-[a-z0-9]+)*$`) via the `resource_mappings` table → `(resourceType, numericId)`. It does **not** accept or resolve an arbitrary `$id` URI.
- A document's `$id` is **never indexed or stored as a queryable field** — it is persisted as part of the JSON blob only. There is no index on `$id` in Mongo or Nitrite (`MongoIndexInitializer`).
- The `calm.hub.base-url` config only **writes** `$id` on output (rewriting it to `{base-url}/calm/namespaces/{ns}/{customId}/versions/{version}`); it is never used to parse an incoming URI back to a document.
- The `mappings` endpoint filters by numeric id / type only — there is no lookup by `$id` / URI.
Net: there is currently **no reliable path to resolve a `detailed-architecture` reference (a `$id` URI) to a stored document**, unless that reference happens to already be in the canonical front-controller path form. A genuinely arbitrary but same-instance `$id` (e.g. `http://calm.example.com/some-namespace/some-random-thing/architecture`) cannot be resolved today.
### Proposed Implementation:
The core need is a **`$id` reverse-lookup in CALM Hub** so that a `detailed-architecture` reference can be resolved to the stored document, scoped to this instance.
**Backend (calm-hub):**
- Index each architecture document's `$id` on write (new queryable field / index across Mongo + Nitrite stores; backfill existing documents via a migration).
- Add a resolution endpoint, e.g. `GET /calm/resolve?uri=` (or equivalent), that returns the stored document (or its mapping) for an **exact `$id` match within this instance**, and 404s for anything not hosted here. This is the same-instance guarantee — the hub only ever returns its own documents.
- Decide the uniqueness model (see open question below).
**Frontend (calm-hub-ui):**
- Make the existing detailed-architecture indicator interactive (`CustomNode.tsx` / `NodeDetails.tsx`).
- On click: same-instance gate (`new URL(ref).origin === window.location.origin`, accepting relative refs), call the resolve endpoint, feed `res.data` into the existing `parseCALMData` → `ArchitectureGraph` pipeline.
- Add a breadcrumb / back affordance to return to the parent architecture.
**Key design decision — how we guarantee a `$id` resolves to exactly one document.**
A `$id` is a URI and is currently not constrained to be unique. Two architecture documents in **different namespaces** could share the same `$id`, which makes "resolve by `$id`" ambiguous. I would like input from the other maintainers here on which of the following options we adopt:
- **Option A — Enforce `$id` uniqueness instance-wide.** Add a uniqueness constraint so the Hub rejects (or globally de-duplicates) any document whose `$id` collides with an existing one. `$id` → document is then unambiguous across the whole instance, and `GET /calm/resolve?uri=…` is a direct index lookup. Cost: a write-time validation + constraint, and a remediation story for any existing data that already violates it.
- **Option B — Require `$id` to be the canonical CALM Hub reference.** Constrain/normalise architecture `$id`s to the `/calm/namespaces/{ns}/{customId}/versions/{version}` form (already namespace-scoped and unique), and resolve through the existing front controller — no new `$id` index needed. Cost: pushes a constraint onto how `$id`s are authored/assigned, and means non-conforming `$id`s are not navigable.
- **Option C — Resolve `$id` within a namespace scope only.** Keep `$id` unconstrained globally but require uniqueness only within a namespace, and resolve as `(namespace, $id)`. Cost: the reference must carry/imply a namespace, which the current `detailed-architecture` value does not always do.
My current leaning is **Option A** (it most directly matches the VSCode `$id`-based model and keeps references portable), but I would like input from the other maintainers here before committing.
### Alternatives Considered:
- **UI-only, canonical refs only:** wire drill-down only for references already matching `/calm/namespaces/{ns}/{customId}/versions/{version}` and resolve via the existing `fetchResourceByCustomId`. No backend change, but does not satisfy the requirement that *any* same-instance `$id` be navigable — arbitrary `$id`s stay inert.
- **Client-side mapping file (VSCode parity, literally):** ship a `calm-mapping.json`-style map in the UI. Rejected: CALM Hub is a live server with a database; a static client map duplicates state the server already holds and goes stale.
- **Full-text `/calm/search` to find by `$id`:** rejected as imprecise — search is not an exact-identity lookup and gives no uniqueness/same-instance guarantee.
### Testing Strategy:
- **Backend unit + integration (calm-hub):** `$id` indexing on write; resolve endpoint returns the correct document for an exact `$id`; 404 for unknown / non-hosted `$id`; behaviour under the chosen uniqueness model (collision rejected, or canonical-form enforced); migration/backfill correctness. Cover both Mongo and Nitrite stores.
- **Frontend (calm-hub-ui):** same-instance gate (relative accepted, foreign origin rejected); click → resolve → render into graph; breadcrumb/back; inert indicator when a reference is unresolvable.
- **Regression:** existing front-controller and numeric-id endpoints unaffected.
### Documentation Requirements:
- CALM Hub API docs (Swagger) for the new resolution endpoint.
- Guidance on `$id` authoring expectations and the chosen uniqueness/canonicalisation rule.
- calm-hub-ui user docs describing detailed-architecture navigation.
### Implementation Checklist:
- [ ] Design reviewed and approved
- [ ] Implementation completed
- [ ] Tests written and passing
- [ ] Documentation updated
- [ ] Relevant workflows updated (if needed)
- [ ] Performance impact assessed
### Additional Context:
Reference behaviour in the VSCode extension:
- `calm-plugins/vscode/src/core/services/navigation-service.ts` (`navigateToDetailedArchitecture`) — resolves the reference and opens the target.
- `shared/src/document-loader/mapped-document-loader.ts` + `shared/src/schema-directory.ts` — index documents by `$id`; `calm-mapping.json` provides the `$id`/URL → document map.
Current CALM Hub state (detection only, no navigation):
- `calm-hub-ui/src/visualizer/components/reactflow/CustomNode.tsx` (`ZoomIn` indicator), `.../sidebar/NodeDetails.tsx` (badge).
- `calm-hub/.../FrontControllerResource.java` (slug-based resolution), `MongoIndexInitializer` (no `$id` index).
Contributor guide
Assessment
This issue has not been assessed yet.