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
117 changes: 117 additions & 0 deletions .claude/skills/update-dependency-graph/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
name: update-dependency-graph
description: Re-scan the solution and regenerate Documentation/DependencyGraph/graph-data.js so the interactive dependency graph matches the current source. Use when clients, services, brokers, storage implementations or cross-project wiring have changed, or when the user asks to refresh/rebuild the dependency graph.
version: 0.1.0
---

# Update Solution Dependency Graph

Regenerate `Documentation/DependencyGraph/graph-data.js` from the current
source. `index.html` is the renderer — do not change it unless a new concept
cannot be expressed in data (new edge kind, new layer). It carries BOTH views
behind `state.view`: `buildSingleCopyInstances` + `layoutBands` (the default)
and `buildDuplicatedInstances` + `layoutTrees`. Anything you change in one
builder usually needs the mirror change in the other.

## 1/ Load the current model

Read `Documentation/DependencyGraph/README.md` and `graph-data.js` first.
The data file is the previous scan's snapshot; your job is a diff-and-update,
not a rewrite. Preserve its modelling rules:

- Per-consumer duplication is done by the renderer — declare each component
ONCE; never hand-duplicate.
- `shared: true` on external surfaces. A `shared` component MUST also be in
`roots` or its inbound edges are silently dropped.
- `utility: true` on the DateTime / Identifier brokers (hidden behind a
toggle).
- Happy-path calls are drawn; exception-path (`TryCatch` / `CreateAndLog*`)
logging is NOT. The `.Validations.cs` partials here are pure argument
checks with no broker calls.
- Private helpers are attributed to the public method that reaches them. No
component links to itself — a self-edge means you modelled a private helper
as a row, which this graph does not do.
- A swappable interface is drawn once at the broker column with its
implementations to the right (`IApiPlatformStateBroker` /
`IApiPlatformTokenBroker` → memory + session), because which one is live is
a registration choice rather than a call.
- This solution has no event bus — `events` is empty, `eventBrokerId` is
`null`, and every edge is `kind: "direct"`. If an event broker ever lands,
the renderer already supports `P(...)` / `S(...)` and automatic
circular-flow detection; do not hand-colour anything.
- Column map (0–8) is documented at the top of `graph-data.js` — keep new
components consistent with it.

## 2/ Re-scan the source

Read the interfaces for the public surface and the implementation `.cs` for
the per-method calls. A quick way to get per-method dependency calls out of a
C# tree is a small throwaway script that finds method declarations and then
the `this.<field>.<Method>` calls between one declaration and the next
(whitespace-normalise first — calls wrap across lines).

1. **`NHSDigital.ApiPlatform.Sdk`** — the whole SDK:
- `Clients\*` — `ApiPlatformClient` (note its standalone `Create` path
builds its own `ServiceCollection`), the facade, and the two per-API
clients.
- `Services\Processings\*`, `Services\Orchestrations\*`,
`Services\Foundations\*` — dependencies and per-method calls.
- `Brokers\*` — public surface plus the external member each one wraps
(`IHttpClientFactory`, `System.Text.Json`, `RandomNumberGenerator`,
`Guid`, `DateTimeOffset`).
- `ServiceCollectionExtensions.cs` — the registration story, including
which lifetimes and which `TryAdd` calls decide who wins.
2. **`NHSDigital.ApiPlatform.Sdk.AspNetCore`** — the session-backed state and
token brokers and `AddApiPlatformSdkAspNetCore`.
3. **`NHSDigital.ApiPlatform.Infrastructure`** — `Program.Main` and
`ScriptGenerationService`. Remember `.github/workflows/build.yml` and
`prLinter.yml` are GENERATED from here; `pages.yml` is the one hand-authored
workflow.
4. **Unused surface is a headline.** Check for packages referenced in a
`.csproj` that no `.cs` file mentions, constructor dependencies that are
never called, and public members with no callers — several exist today and
they are recorded in the README's "Current truths".

## 3/ Update graph-data.js

- Components are declared explicitly with `C({...})`, edges with
`D(from, to)` (`null` method = header-level link).
- Add new roots to the `roots` list in project order (it controls layout).
- External components' method rows are DERIVED from the edges at the bottom of
the file — add the id to that loop rather than hand-listing rows.

## 4/ Verify in the browser

Serve the folder over HTTP — a sandboxed viewer can block `graph-data.js` as a
sub-resource, and the page then shows its "graph-data.js did not load" notice
instead of the graph:

```bash
python -m http.server 8731 --bind 127.0.0.1
```

Verify BOTH views — the header toggle, or `setView("single")` /
`setView("duplicated")` from `javascript_tool`. Confirm:

- No console errors; the header count is in the expected range (last scan:
25 components · 79 flows single-copy; 100 nodes · 413 flows per consumer,
27 · 84 and 113 · 441 with utility brokers on).
- No node-rect overlaps and no project-box overlaps — query `state.instances`
and `state.projBoxes` with `javascript_tool` and intersect pairwise, in each
view, with the utility toggle both off and on.
- No dropped edges: every `shared` component appears in `roots`.
- Click one client, one foundation service and one method row: the side-panel
flows in / out must match the scan.
- Selecting a header must light the component's whole fan-out (the same
upstream + downstream slice a method row gets, seeded from every row), not
just its first hop, and the selection must be outlined in amber. Clearing
the selection must restore the graph exactly — snapshot every node's
attributes before and after and compare.
- Switching view preserves the selection (by component id).

## 5/ Finish

Update the "Current truths" section and scan date in
`Documentation/DependencyGraph/README.md` (and the node/flow counts if they
moved), and summarize what changed since the previous snapshot — new
components, new flows, anything that became unreachable or newly consumed.
59 changes: 59 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Pages
on:
push:
branches:
- main
paths:
- Documentation/DependencyGraph/**
- .github/workflows/pages.yml
Comment on lines +3 to +8
workflow_dispatch:

# Publishes Documentation/DependencyGraph as a static site. The graph is
# self-contained — index.html carries both views (single-copy and
# per-consumer) and reads graph-data.js as a sibling file. There is nothing
# to compile: no npm, no bundler, no .NET build.
#
# UNLIKE build.yml and prLinter.yml this file is hand-authored, NOT generated
# by NHSDigital.ApiPlatform.Infrastructure — the GitHub Pages actions fall
# outside ADotNet 4.1.0's task model. Regenerating the other two workflows
# will not touch this one; keep it that way, or teach the generator first.

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4
- name: Stage the dependency graph
run: |
mkdir -p dist
cp Documentation/DependencyGraph/index.html dist/
cp Documentation/DependencyGraph/graph-data.js dist/
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: dist
Comment on lines +34 to +47

deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
143 changes: 143 additions & 0 deletions Documentation/DependencyGraph/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Solution Dependency Graph

An interactive, self-contained dependency graph of the NHS Digital API
Platform solution: project boundaries, per-component method blocks, and
colour-coded data flows. No build step and no server — open
[index.html](./index.html) in a browser.

It carries two ways of drawing the same data, switched from the segmented
control in the header:

- **single copy** *(default)* — every component appears exactly once with its
full method surface, and all consumers' flows converge on it. Best for
"who touches this?".
- **per consumer** — dependencies are duplicated once per consumer, each copy
showing only the method rows that consumer uses. Best for "what does this
one call path actually do?".

The choice lands in the URL (`#single` / `#duplicated`), so a link keeps the
view you were on, and switching carries your current selection across.

## Reading the graph

- **Left → right layering**: SDK entry point → clients → processings →
orchestrations → foundations → brokers → broker implementations →
external services.
- **Dashed boxes** are project boundaries. External surfaces show only the
public members this solution actually calls.
- **Edge colours**:
- **blue** — direct method call
- **green** — event publish, **purple** — event subscribe,
**red** — a publish/subscribe pair in a circular event flow. None appear
today: this solution has no event bus. The machinery is kept in the
renderer so an event broker can be modelled later without touching
`index.html`.
- **Duplication over line-spaghetti** (the *per consumer* view only): a
dependency is drawn once per consumer, showing only the method rows that
consumer uses, instead of many lines converging on one shared node. The
exception is components marked "shared" in the side panel — the external
surfaces. In the *single copy* view nothing is duplicated, so the `shared`
flag makes no difference there.
- **Click a method row** to trace that single method's path — the full
upstream + downstream slice lights up and everything else dims.
- **Click a component header** for the same slice seeded from *every* row of
that copy at once: the component's whole fan-out, not just its first hop.
Other copies of the same component stay half-lit so you can find them.
- Whatever is selected is outlined and lettered in **amber**; rows the traced
path passes through carry a faint blue tint. Click the background or Reset
to clear. Search finds components and methods. The **utility brokers**
toggle reveals the DateTime / Identifier broker copies that are hidden by
default for readability.

At the last scan, 27 declared components and 84 declared edges draw as
**25 components · 79 flows** in the single-copy view and **100 nodes ·
413 flows** per consumer (27 · 84 and 113 · 441 with utility brokers on).

`.github/workflows/pages.yml` publishes this folder to GitHub Pages on every
push to `main` that touches it — `index.html` is the site root. Nothing is
compiled; `index.html` and `graph-data.js` are copied as-is. Pages has to be
enabled once in the repository's Settings → Pages (source: GitHub Actions).

## Current truths captured in the data (scanned 2026-08-11)

- **This is an SDK, not a host.** There is no controller, no worker and no
database — the whole solution is a class library plus an ASP.NET Core
companion package. `ApiPlatformClient` is the only front door.
- **`ApiPlatformClient` can be used without a DI container.** The static
`Create` and the configurations-only constructor build their own
`ServiceCollection`, register the SDK core, and fall back to the in-memory
storage brokers — so a console app or a test can new it up directly.
- **`ApiPlatformClientFacade` is dead code.** It is an internal
`IApiPlatformClient` holding the same two sub-clients, but nothing
constructs or registers it: `AddApiPlatformSdkCore` registers a hand-built
`ApiPlatformClient` instead. It shows on the graph with no inbound flows.
- **`PdsOrchestrationService` takes `IApiPlatformTokenBroker` and never uses
it.** The access token comes from `CareIdentityService.GetAccessTokenAsync`;
the injected broker is unused.
- **The storage brokers are the extension seam.** `IApiPlatformStateBroker`
and `IApiPlatformTokenBroker` each have an in-memory implementation in the
Sdk and a session-backed one in Sdk.AspNetCore. Both are registered with
`TryAdd`, so whichever the host registers first wins — call
`AddApiPlatformSdkAspNetCore()` before `AddApiPlatformSdkInMemoryStorage()`
in a web host, or you get the process-wide singletons.
- **The in-memory brokers are singletons and hold one user's state.** Fine
for a console app or a test; wrong for a multi-user web host.
- **CIS2 runs without PKCE** — the code says so explicitly; only `client_id`,
`redirect_uri`, `response_type`, `state` and optional `acr_values` are sent.
- **`GetAccessTokenAsync` refreshes silently** and returns an *empty string*
rather than throwing when both tokens have expired. The orchestration is
what turns that into `UnauthorizedPdsOrchestrationException`.
- **PDS responses are never deserialised.** `PdsService` returns the raw FHIR
JSON string; the `Patient` / `Address` / `PatientLookup` models exist but
nothing maps onto them.
- **The `ISL.Providers.PDS.*` packages are referenced but unused.** All three
(`Abstractions`, `FakeFHIR`, `FHIR`) are in the Sdk's `.csproj` and not a
single `.cs` file mentions them — the PDS call is hand-rolled over
`IHttpBroker`.
- **`JsonBroker.Serialize` is on the surface but never called.**
- **`ReactApp1.Server` and `reactapp1.client` are empty scaffolding** — no
source files, not in the `.slnx` — so they are not modelled here.

## Modelling decisions

These are the judgement calls baked into `graph-data.js`; keep them stable so
successive scans stay comparable.

- **Happy-path calls are drawn; exception-path (`TryCatch` /
`CreateAndLog*`) logging is NOT.** The `.Validations.cs` partials in this
solution are pure argument checks with no broker calls, so they contribute
nothing.
- **Private helpers are attributed to the public method that reaches them** —
`CareIdentityService.CallbackAsync` carries `ExchangeCodeForTokenAsync`'s
calls, and `GetAccessTokenAsync` carries
`ExchangeRefreshTokenForTokenAsync`'s. No component links to itself.
- **A swappable interface is drawn once with its implementations behind it.**
`IApiPlatformStateBroker` / `IApiPlatformTokenBroker` each get one node at
the broker column, with the memory and session implementations to their
right, because which one is live is a registration choice rather than a
call.

## Updating the graph

The data is a scanned snapshot of the source, not a build artifact — refresh
it whenever clients, services, brokers or cross-project wiring change by
running the `/update-dependency-graph` skill in Claude Code (defined in
`.claude/skills/update-dependency-graph/SKILL.md`). It re-scans the solution,
diffs against the current data, updates `graph-data.js`, and re-verifies the
rendered graph.

For small changes you can also edit by hand: all data lives in
[graph-data.js](./graph-data.js) (`window.APIPLATFORM_DATA`);
[index.html](./index.html) is the renderer — it holds both views
(`buildSingleCopyInstances` / `layoutBands` and `buildDuplicatedInstances` /
`layoutTrees`, dispatched on `state.view`) and should rarely need changes.

- Components are declared explicitly with `C({...})` and edges with
`D(from, to)` (`null` method = header-level link). `P(component, method,
event)` / `S(event, component, handler)` exist for a future event bus.
- Component options: `col` (layout column), `utility: true` (hidden behind the
toggle), `shared: true` (consumers link to one copy instead of duplicating —
**must** also appear in `roots`, or its inbound edges are dropped).
- External components' method rows are DERIVED from the edges at the bottom of
the file — add the id to that loop rather than hand-listing rows.
- Add new roots to the `roots` list in project order; it controls layout.
Loading
Loading