Skip to content
Open
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
50 changes: 31 additions & 19 deletions .claude/skills/update-dependency-graph/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
---
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.
description: Re-scan the solution and regenerate the dependency graph data files (Documentation/DependencyGraph/graph.yml + projects/*.yml) 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
Regenerate the data files — `Documentation/DependencyGraph/graph.yml` and
`projects/*.yml` — 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,
Read `Documentation/DependencyGraph/README.md`, then `graph.yml` and the
`projects/*.yml` files it lists. The data files are 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
Expand All @@ -39,8 +40,8 @@ not a rewrite. Preserve its modelling rules:
`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.
- Column map (0–8) is documented in `graph.yml` — keep new components
consistent with it.

## 2/ Re-scan the source

Expand Down Expand Up @@ -72,32 +73,43 @@ the `this.<field>.<Method>` calls between one declaration and the next
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
## 3/ Update the data files

- 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.
The YAML schema is documented in the README's "The data files" section —
components live in `projects/<project>.yml`, each with `methods` and its
outbound `calls` (`from: null` = header-level link); manifest-level lists
(`projects`, `roots`, `events`) live in `graph.yml`.

- A new component → add it to its project's file AND to `roots` in
`graph.yml` (project order; `shared` components must be roots).
- Externals with `deriveMethods: true` get their rows derived from inbound
edges at load time — never hand-list rows on them.
- Strings with characters beyond letters, digits, spaces and `_.-/()` must be
double-quoted JSON strings; the renderer parses a small YAML subset
(single-line scalars only, no anchors, no multi-line blocks).
- If an edit does not show up, load the page and watch for its "graph data
did not load" panel — it prints the parse error.

## 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
Serve the folder over HTTP — the page fetches graph.yml and the project
files, and browsers block those fetches from file:// pages:
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:
Verify BOTH views — the header toggle, or `window.__graph.setView("single")` /
`window.__graph.setView("duplicated")` from `javascript_tool` (the renderer
exposes `window.__graph` = { state, setView, select, selectRow,
clearSelection, rebuild, fit, tracePath }). 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
- No node-rect overlaps and no project-box overlaps — query `window.__graph.state.instances`
and `.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
Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ on:
- .github/workflows/pages.yml
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.
# Publishes Documentation/DependencyGraph as a static site. index.html
# carries both views (single-copy and per-consumer) and fetches its data
# from graph.yml + projects/*.yml as sibling files. 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
Expand All @@ -38,7 +38,8 @@ jobs:
run: |
mkdir -p dist
cp Documentation/DependencyGraph/index.html dist/
cp Documentation/DependencyGraph/graph-data.js dist/
cp Documentation/DependencyGraph/graph.yml dist/
cp -r Documentation/DependencyGraph/projects dist/projects
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload artifact
Expand Down
116 changes: 85 additions & 31 deletions Documentation/DependencyGraph/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
# 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.
An interactive dependency graph of the NHS Digital API Platform solution:
project boundaries, per-component method blocks, and colour-coded data flows.

Data and renderer are separate files, all in this folder:

- [graph.yml](./graph.yml) — the manifest: solution name, project list (which
also names each project's data file), root order, and the event registry.
- `projects/*.yml` — one file per project / package boundary, each declaring
that project's components with their methods and outbound flows.
- [index.html](./index.html) — the renderer. It fetches the manifest and the
project files, assembles them, and draws. No build step, but because the
data is fetched the page must be **served** rather than double-clicked:

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

then open `http://127.0.0.1:8731/` — or use the published GitHub Pages copy.

It carries two ways of drawing the same data, switched from the segmented
control in the header:
Expand Down Expand Up @@ -46,17 +60,18 @@ view you were on, and switching carries your current selection across.
- 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
toggle reveals the DateTime / Identifier / Logging broker copies and the logging external that are hidden by
default for readability.

At the last scan, 27 declared components and 84 declared edges draw as
At the last scan, 29 declared components and 86 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).
413 flows** per consumer (29 · 86 and 115 · 443 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).
compiled; `index.html`, `graph.yml` and `projects/` 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)

Expand All @@ -71,9 +86,15 @@ enabled once in the repository's Settings → Pages (source: GitHub Actions).
`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.
- **`LoggingBroker` has no inbound flows on this graph, by design.** Every
service takes `ILoggingBroker`, but it is only ever reached from the
`CreateAndLog*` exception factories, and this graph draws happy-path calls
only. It is a utility broker, so it is hidden behind the toggle along with
the DateTime and Identifier brokers.
- **Dependency failures are categorised by HTTP status.** Both foundation
services split `HttpRequestException`: a 4xx becomes a
`*DependencyValidationException` (the caller sent something the dependency
rejected), a 5xx or a transport failure becomes a `*DependencyException`.
- **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
Expand All @@ -100,7 +121,7 @@ enabled once in the repository's Settings → Pages (source: GitHub Actions).

## Modelling decisions

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

- **Happy-path calls are drawn; exception-path (`TryCatch` /
Expand All @@ -117,27 +138,60 @@ successive scans stay comparable.
right, because which one is live is a registration choice rather than a
call.

## The data files

All data is declarative YAML — no code runs to produce the model, and
[index.html](./index.html) is a pure renderer (it holds both views,
`buildSingleCopyInstances` / `layoutBands` and `buildDuplicatedInstances` /
`layoutTrees`, dispatched on `state.view`, and should rarely need changes).

**`graph.yml`** is the manifest:

- `projects` — id, name, kind (`internal` / `library` / `external`) and the
`file` holding that project's components. List order controls the
single-copy view's band order.
- `roots` — component ids in layout order for the per-consumer view. A
component flagged `shared` **must** appear here, or its inbound edges are
silently dropped.
- `events` / `eventBroker` — empty / `null` today; ready for an event bus.

**`projects/<name>.yml`** declares one project's components:

```yaml
- id: CIS.Foundation
name: CareIdentityService
layer: foundation # exposer|client|processing|orchestration|
# foundation|broker|external
col: 4 # layout column — map documented in graph.yml
shared: true # optional: consumers link to ONE copy
utility: true # optional: hidden behind the header toggle
deriveMethods: true # optional: rows derived from inbound edges
# (externals — rows can never drift from arrows)
description: "..."
methods: [...]
calls: # outbound flows, one per call
- from: <method or null> # null = header-level link
to: <component id>
method: <method or null>
Comment on lines +163 to +175
publishes: # for a future event bus
- method: M
event: E
subscribes:
- event: E
handler: H
```

Strings containing anything beyond letters, digits, spaces, `_.-/()` are
double-quoted JSON strings — the renderer parses a deliberately small YAML
subset, so stick to the shapes above (single-line scalars, no anchors, no
multi-line blocks).

## 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.
diffs against the current data files, updates them, and re-verifies the
rendered graph. Small changes (a new method, one new call) are comfortable
hand-edits in the project file that owns the calling component.
Loading
Loading