@hippo-digital/hippocampus 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/assets/dist/hippocampus.css +1 -0
- package/assets/hippocampus.scss +896 -0
- package/assets/javascript/blueprint.js +123 -0
- package/bin/hippocampus.js +106 -0
- package/docs/agent-roadmap.md +217 -0
- package/docs/agent-workflow.md +56 -0
- package/docs/agentic-patterns-to-port.md +273 -0
- package/docs/colour-pairings.md +41 -0
- package/docs/components.md +52 -0
- package/docs/copilot-ncrs-research-curator-agent.md +298 -0
- package/docs/custom-agents-usage.md +95 -0
- package/docs/design-actions-and-decisions.md +95 -0
- package/docs/gp-connect-real-data-to-production.md +66 -0
- package/docs/hippocampus-for-designers-and-researchers.md +261 -0
- package/docs/hippocampus-technical.md +373 -0
- package/docs/prompt-architecture-guardrails.md +39 -0
- package/docs/repeatable-research-round-ingestion.md +239 -0
- package/docs/research-knowledge-graph.md +552 -0
- package/docs/research-source-file-conventions.md +121 -0
- package/docs/source-model.md +252 -0
- package/index.js +47 -0
- package/lib/config.js +46 -0
- package/lib/context.js +59 -0
- package/lib/create-router.js +187 -0
- package/lib/discover-projects.js +45 -0
- package/lib/docs.js +72 -0
- package/lib/host-edit.js +151 -0
- package/lib/lint-knowledge.js +133 -0
- package/lib/load-knowledge.js +629 -0
- package/lib/package-paths.js +32 -0
- package/lib/paths.js +33 -0
- package/lib/resolve-root.js +84 -0
- package/lib/schema-version.js +96 -0
- package/lib/schema.js +346 -0
- package/package.json +70 -0
- package/scripts/audit-provenance.js +147 -0
- package/scripts/doctor.js +114 -0
- package/scripts/eject.js +51 -0
- package/scripts/import-blueprint-spreadsheet.js +242 -0
- package/scripts/import-design-actions.js +230 -0
- package/scripts/import-project-context.js +427 -0
- package/scripts/import-research-batch.js +160 -0
- package/scripts/import-research-deck.js +448 -0
- package/scripts/index-source-artefacts.js +261 -0
- package/scripts/init.js +232 -0
- package/scripts/install-skills.js +160 -0
- package/scripts/lib/artefact-text.js +347 -0
- package/scripts/lib/xlsx-grid.js +114 -0
- package/scripts/link-insights-to-needs.js +209 -0
- package/scripts/migrate-knowledge.js +73 -0
- package/scripts/promote-project-context-draft.js +125 -0
- package/scripts/promote-research-draft.js +236 -0
- package/scripts/smoke-routes.js +157 -0
- package/scripts/sync-project-context.js +236 -0
- package/scripts/sync-research-artefacts.js +294 -0
- package/scripts/triage-research-corpus.js +274 -0
- package/scripts/validate-knowledge.js +49 -0
- package/scripts/validate-skills.js +247 -0
- package/skills/COVERAGE-MATRIX.md +39 -0
- package/skills/README.md +77 -0
- package/skills/SKILL-FORMAT.md +100 -0
- package/skills/add-prototype-screen.skill.md +54 -0
- package/skills/add-scenario.skill.md +47 -0
- package/skills/add-user-needs.skill.md +51 -0
- package/skills/audit-knowledge-source.skill.md +47 -0
- package/skills/capture-route-review-pack.skill.md +52 -0
- package/skills/create-journey-from-scenario.skill.md +50 -0
- package/skills/deliver-service-slice.skill.md +65 -0
- package/skills/examples/README.md +15 -0
- package/skills/examples/non-copilot-research-ingestion.md +68 -0
- package/skills/examples/non-copilot-service-slice.md +64 -0
- package/skills/generate-service-slice.skill.md +57 -0
- package/skills/ingest-project-context.skill.md +58 -0
- package/skills/ingest-research-round.skill.md +60 -0
- package/skills/map-research-to-graph.skill.md +58 -0
- package/skills/record-accessibility-review.skill.md +52 -0
- package/skills/record-design-decision.skill.md +53 -0
- package/skills/review-research-import-draft.skill.md +45 -0
- package/skills/skills.json +464 -0
- package/skills/skills.schema.json +111 -0
- package/skills/structure-project-context-draft.skill.md +51 -0
- package/skills/structure-research-draft.skill.md +51 -0
- package/skills/triage-research-corpus.skill.md +51 -0
- package/skills/write-delivery-summary.skill.md +51 -0
- package/skills/write-route-review-summary.skill.md +55 -0
- package/views/hippocampus/_layout.html +45 -0
- package/views/hippocampus/blueprint-classic.html +39 -0
- package/views/hippocampus/blueprint.html +52 -0
- package/views/hippocampus/components/_blueprint-cell.njk +42 -0
- package/views/hippocampus/components/_blueprint-controls.njk +20 -0
- package/views/hippocampus/components/_blueprint-minimap.njk +14 -0
- package/views/hippocampus/components/_empty-state.njk +12 -0
- package/views/hippocampus/components/_journey-step.njk +21 -0
- package/views/hippocampus/components/_list.njk +23 -0
- package/views/hippocampus/components/_summary-card.njk +15 -0
- package/views/hippocampus/doc.html +11 -0
- package/views/hippocampus/docs.html +19 -0
- package/views/hippocampus/evidence-impact.html +53 -0
- package/views/hippocampus/graph.html +48 -0
- package/views/hippocampus/handover-empty.html +12 -0
- package/views/hippocampus/handover.html +56 -0
- package/views/hippocampus/index.html +46 -0
- package/views/hippocampus/journey-flow.html +31 -0
- package/views/hippocampus/journey.html +23 -0
- package/views/hippocampus/not-found.html +10 -0
- package/views/hippocampus/project.html +102 -0
- package/views/hippocampus/research.html +68 -0
- package/views/hippocampus/route-review.html +83 -0
- package/views/hippocampus/scenarios.html +56 -0
- package/views/hippocampus/trace.html +113 -0
- package/views/hippocampus/user-needs.html +32 -0
- package/views/hippocampus/user.html +157 -0
- package/views/hippocampus/users.html +32 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Source Model
|
|
2
|
+
|
|
3
|
+
The knowledge base is driven by structured JSON in `hippocampus/source`.
|
|
4
|
+
|
|
5
|
+
The source model is intentionally small. It should be easy for humans to inspect and easy for agents to update safely.
|
|
6
|
+
|
|
7
|
+
For the explicit graph layer, research entities and research artefact import/promotion process, read `docs/research-knowledge-graph.md`.
|
|
8
|
+
|
|
9
|
+
## Validation
|
|
10
|
+
|
|
11
|
+
Run:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx hippocampus validate
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Validation has two layers:
|
|
18
|
+
|
|
19
|
+
1. Schema validation in `node_modules/@hippo-digital/hippocampus/lib/schema.js`
|
|
20
|
+
2. Relationship validation in `node_modules/@hippo-digital/hippocampus/lib/load-knowledge.js`
|
|
21
|
+
|
|
22
|
+
Schema validation checks:
|
|
23
|
+
|
|
24
|
+
- required fields
|
|
25
|
+
- lowercase kebab-case IDs
|
|
26
|
+
- enum values
|
|
27
|
+
- route format
|
|
28
|
+
- nested journey-step structure
|
|
29
|
+
|
|
30
|
+
Relationship validation checks:
|
|
31
|
+
|
|
32
|
+
- user needs link to users and evidence
|
|
33
|
+
- scenarios link to users and needs
|
|
34
|
+
- journeys link to scenarios
|
|
35
|
+
- journey steps link to needs, screens, evidence and decisions
|
|
36
|
+
- screens link to user needs
|
|
37
|
+
|
|
38
|
+
## Records
|
|
39
|
+
|
|
40
|
+
### Project
|
|
41
|
+
|
|
42
|
+
File: `hippocampus/source/project.json`
|
|
43
|
+
|
|
44
|
+
Defines the small project identity used by the viewer:
|
|
45
|
+
|
|
46
|
+
- project ID
|
|
47
|
+
- name
|
|
48
|
+
- description
|
|
49
|
+
- phase
|
|
50
|
+
- service area
|
|
51
|
+
- prototype slice
|
|
52
|
+
|
|
53
|
+
### Project context
|
|
54
|
+
|
|
55
|
+
File: `hippocampus/source/project-context.json`
|
|
56
|
+
|
|
57
|
+
Defines broader project-level material that is useful across the whole prototype but is not itself research insight:
|
|
58
|
+
|
|
59
|
+
- purpose
|
|
60
|
+
- vision
|
|
61
|
+
- problem statement
|
|
62
|
+
- service proposition
|
|
63
|
+
- project-level users
|
|
64
|
+
- service channels
|
|
65
|
+
- policies and regulations
|
|
66
|
+
- constraints
|
|
67
|
+
- risks
|
|
68
|
+
- milestones
|
|
69
|
+
- source facts
|
|
70
|
+
- project overview artefacts
|
|
71
|
+
|
|
72
|
+
Use project context for overview boards, propositions, service maps, policy summaries and other generic briefing material.
|
|
73
|
+
|
|
74
|
+
Do not force this material into `research-rounds.json` unless it is genuinely a research session or research artefact.
|
|
75
|
+
|
|
76
|
+
### Users
|
|
77
|
+
|
|
78
|
+
File: `hippocampus/source/users.json`
|
|
79
|
+
|
|
80
|
+
Defines user groups. Users are linked from needs and scenarios.
|
|
81
|
+
|
|
82
|
+
Each user includes:
|
|
83
|
+
|
|
84
|
+
- responsibilities
|
|
85
|
+
- access needs
|
|
86
|
+
- service context
|
|
87
|
+
|
|
88
|
+
### User needs
|
|
89
|
+
|
|
90
|
+
File: `hippocampus/source/needs.json`
|
|
91
|
+
|
|
92
|
+
Needs are linked to:
|
|
93
|
+
|
|
94
|
+
- one user
|
|
95
|
+
- one or more evidence items
|
|
96
|
+
- scenarios through `needIds`
|
|
97
|
+
- journey steps through `needIds`
|
|
98
|
+
- screens through `needIds`
|
|
99
|
+
|
|
100
|
+
Needs should be specific enough to influence journeys and screens.
|
|
101
|
+
|
|
102
|
+
A need may optionally carry a `reference` (e.g. an external ticket ID such as a Jira key) when it was imported from a source that already tracks it that way.
|
|
103
|
+
|
|
104
|
+
### Scenarios
|
|
105
|
+
|
|
106
|
+
File: `hippocampus/source/scenarios.json`
|
|
107
|
+
|
|
108
|
+
Scenarios describe a user context that can produce a journey.
|
|
109
|
+
|
|
110
|
+
Each scenario links to:
|
|
111
|
+
|
|
112
|
+
- one user
|
|
113
|
+
- one or more user needs
|
|
114
|
+
|
|
115
|
+
Scenarios include:
|
|
116
|
+
|
|
117
|
+
- trigger
|
|
118
|
+
- context
|
|
119
|
+
- risk level
|
|
120
|
+
- happy path
|
|
121
|
+
- edge cases
|
|
122
|
+
- optional location search detail, when a scenario map contains address, postcode, household or resident-history behaviour
|
|
123
|
+
|
|
124
|
+
Location search detail captures:
|
|
125
|
+
|
|
126
|
+
- primary intent
|
|
127
|
+
- search scope: `household`, `postcode-area` or `both`
|
|
128
|
+
- trigger conditions
|
|
129
|
+
- available information
|
|
130
|
+
- role of the address
|
|
131
|
+
- what users learn from residents
|
|
132
|
+
- current and historic resident needs
|
|
133
|
+
- outcomes
|
|
134
|
+
- risks
|
|
135
|
+
|
|
136
|
+
### Journeys
|
|
137
|
+
|
|
138
|
+
File: `hippocampus/source/journeys.json`
|
|
139
|
+
|
|
140
|
+
Journeys are the main source for generated journey pages and blueprints.
|
|
141
|
+
|
|
142
|
+
Each journey links to:
|
|
143
|
+
|
|
144
|
+
- one scenario
|
|
145
|
+
- journey steps
|
|
146
|
+
|
|
147
|
+
Each journey step includes the blueprint lanes:
|
|
148
|
+
|
|
149
|
+
- user action
|
|
150
|
+
- frontstage
|
|
151
|
+
- backstage
|
|
152
|
+
- support processes
|
|
153
|
+
- data dependencies
|
|
154
|
+
- security and governance
|
|
155
|
+
- pain points
|
|
156
|
+
- exception handling
|
|
157
|
+
- evidence and decisions
|
|
158
|
+
|
|
159
|
+
A step may optionally include `futureState`: proposed or target-state changes that are not yet true of the current journey (e.g. a planned self-serve flow, or an as-is/to-be process still being redesigned). Keep this separate from the as-is lanes above rather than blending proposed changes into them.
|
|
160
|
+
|
|
161
|
+
`/hippocampus/journeys/:journeyId/flow` generates a Mermaid flowchart directly from a journey's steps (see `buildJourneyFlowDiagram` in `node_modules/@hippo-digital/hippocampus/lib/load-knowledge.js`) - one node per step in sequence, plus a dashed branch to its own node for each real `exceptionHandling` entry. A step's exception text is skipped rather than drawn as a branch if it's exactly `"No exception path was captured for this step in the source blueprint"` - an honest placeholder from importing the NHS Notify blueprint, not a real exception.
|
|
162
|
+
|
|
163
|
+
### Screens
|
|
164
|
+
|
|
165
|
+
File: `hippocampus/source/screens.json`
|
|
166
|
+
|
|
167
|
+
Screens define prototype routes and link prototype UI back to user needs.
|
|
168
|
+
|
|
169
|
+
Each screen includes:
|
|
170
|
+
|
|
171
|
+
- route
|
|
172
|
+
- title
|
|
173
|
+
- purpose
|
|
174
|
+
- linked needs
|
|
175
|
+
- expected NHS components
|
|
176
|
+
|
|
177
|
+
### Evidence
|
|
178
|
+
|
|
179
|
+
File: `hippocampus/source/evidence.json`
|
|
180
|
+
|
|
181
|
+
Evidence supports needs and journey steps.
|
|
182
|
+
|
|
183
|
+
Each evidence item includes:
|
|
184
|
+
|
|
185
|
+
- source type
|
|
186
|
+
- summary
|
|
187
|
+
- confidence
|
|
188
|
+
- optional source path
|
|
189
|
+
- optional supporting quotes
|
|
190
|
+
|
|
191
|
+
Use `sourcePath` when the evidence comes from a copied artefact in the repo.
|
|
192
|
+
|
|
193
|
+
Do not invent evidence. If a claim is not evidenced, add an assumption instead.
|
|
194
|
+
|
|
195
|
+
### Decisions
|
|
196
|
+
|
|
197
|
+
File: `hippocampus/source/decisions.json`
|
|
198
|
+
|
|
199
|
+
Decisions explain modelling or product choices.
|
|
200
|
+
|
|
201
|
+
Allowed statuses:
|
|
202
|
+
|
|
203
|
+
- `proposed`
|
|
204
|
+
- `accepted`
|
|
205
|
+
- `superseded`
|
|
206
|
+
- `rejected`
|
|
207
|
+
|
|
208
|
+
### Assumptions
|
|
209
|
+
|
|
210
|
+
File: `hippocampus/source/assumptions.json`
|
|
211
|
+
|
|
212
|
+
Assumptions capture claims or constraints that need later validation.
|
|
213
|
+
|
|
214
|
+
Use assumptions when:
|
|
215
|
+
|
|
216
|
+
- evidence is missing
|
|
217
|
+
- a service rule is plausible but unconfirmed
|
|
218
|
+
- the spike makes a deliberate simplification
|
|
219
|
+
|
|
220
|
+
## Relationship Map
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
users
|
|
224
|
+
-> needs
|
|
225
|
+
-> scenarios
|
|
226
|
+
-> journeys
|
|
227
|
+
-> journey steps
|
|
228
|
+
-> screens
|
|
229
|
+
-> evidence
|
|
230
|
+
-> decisions
|
|
231
|
+
|
|
232
|
+
screens
|
|
233
|
+
-> needs
|
|
234
|
+
|
|
235
|
+
needs
|
|
236
|
+
-> evidence
|
|
237
|
+
|
|
238
|
+
assumptions
|
|
239
|
+
-> currently standalone review items
|
|
240
|
+
|
|
241
|
+
project-context
|
|
242
|
+
-> project-level briefing material outside the graph
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Editing Rules
|
|
246
|
+
|
|
247
|
+
- Preserve existing IDs unless renaming is explicitly requested.
|
|
248
|
+
- Add new IDs in lowercase kebab-case.
|
|
249
|
+
- Update links whenever adding, splitting or deleting records.
|
|
250
|
+
- Run validation before committing.
|
|
251
|
+
- Keep fictional data fictional.
|
|
252
|
+
- Prefer adding an assumption over pretending there is evidence.
|
package/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const express = require('express')
|
|
2
|
+
const { createContext } = require('./lib/context')
|
|
3
|
+
const { createHippocampusRouter } = require('./lib/create-router')
|
|
4
|
+
const packagePaths = require('./lib/package-paths')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The whole public surface. A host wires the viewer with two lines:
|
|
8
|
+
*
|
|
9
|
+
* const hippocampus = require('@hippo-digital/hippocampus').createHippocampus()
|
|
10
|
+
* router.use(hippocampus.router)
|
|
11
|
+
*
|
|
12
|
+
* and adds `hippocampus.viewsPath` to the kit's viewsPath array. Everything
|
|
13
|
+
* else - stylesheet, client JS, layout - the viewer serves itself, so there is
|
|
14
|
+
* no build step and no stylesheet link for the host to get wrong.
|
|
15
|
+
*/
|
|
16
|
+
function createHippocampus (options = {}) {
|
|
17
|
+
const ctx = createContext(options)
|
|
18
|
+
const projects = ctx.projects()
|
|
19
|
+
const router = express.Router()
|
|
20
|
+
|
|
21
|
+
for (const project of projects) {
|
|
22
|
+
router.use(project.basePath, createHippocampusRouter({
|
|
23
|
+
basePath: project.basePath,
|
|
24
|
+
knowledgeStore: project.store
|
|
25
|
+
}))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
router,
|
|
30
|
+
viewsPath: packagePaths.viewsDir,
|
|
31
|
+
context: ctx,
|
|
32
|
+
projects
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
createHippocampus,
|
|
38
|
+
createContext,
|
|
39
|
+
createHippocampusRouter,
|
|
40
|
+
packagePaths,
|
|
41
|
+
createKnowledgeStore: require('./lib/load-knowledge').createKnowledgeStore,
|
|
42
|
+
createPaths: require('./lib/paths').createPaths,
|
|
43
|
+
resolveRoot: require('./lib/resolve-root').resolveRoot,
|
|
44
|
+
lintKnowledge: require('./lib/lint-knowledge').lintKnowledge,
|
|
45
|
+
schema: require('./lib/schema'),
|
|
46
|
+
schemaVersion: require('./lib/schema-version')
|
|
47
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { CONFIG_FILENAME } = require('./resolve-root')
|
|
4
|
+
|
|
5
|
+
// hippocampus.config.json is the host's anchor: it marks the project root and
|
|
6
|
+
// says where the data lives. It is optional - the defaults below are what an
|
|
7
|
+
// unconfigured project gets - but its presence is also the "is this
|
|
8
|
+
// initialised?" check, so it is never invented on read.
|
|
9
|
+
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
configVersion: 1,
|
|
12
|
+
dataDir: 'hippocampus',
|
|
13
|
+
artefactsDir: 'artefacts',
|
|
14
|
+
viewer: { basePath: '/hippocampus' }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readConfig (root) {
|
|
18
|
+
const configPath = path.join(root, CONFIG_FILENAME)
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
return { ...DEFAULTS, viewer: { ...DEFAULTS.viewer }, exists: false, configPath }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let raw
|
|
24
|
+
try {
|
|
25
|
+
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(`${CONFIG_FILENAME} is not valid JSON: ${configPath}\n${error.message}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (raw.configVersion && raw.configVersion > DEFAULTS.configVersion) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`${CONFIG_FILENAME} declares configVersion ${raw.configVersion}, but this ` +
|
|
33
|
+
`version of Hippocampus understands ${DEFAULTS.configVersion}. Upgrade the package.`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...DEFAULTS,
|
|
39
|
+
...raw,
|
|
40
|
+
viewer: { ...DEFAULTS.viewer, ...(raw.viewer || {}) },
|
|
41
|
+
exists: true,
|
|
42
|
+
configPath
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { readConfig, CONFIG_DEFAULTS: DEFAULTS }
|
package/lib/context.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { resolveRoot } = require('./resolve-root')
|
|
2
|
+
const { readConfig } = require('./config')
|
|
3
|
+
const { createPaths } = require('./paths')
|
|
4
|
+
const { discoverProjects } = require('./discover-projects')
|
|
5
|
+
const packagePaths = require('./package-paths')
|
|
6
|
+
|
|
7
|
+
// One place that turns "wherever this command was run from" into everything a
|
|
8
|
+
// script or the router needs. Scripts call this once at the top instead of
|
|
9
|
+
// requiring paths directly, which is what lets the same code run from the host
|
|
10
|
+
// root, from anywhere with --root, or from a cron job with HIPPOCAMPUS_ROOT set.
|
|
11
|
+
|
|
12
|
+
function createContext ({ argv, env, cwd } = {}) {
|
|
13
|
+
const { root, source, configPath } = resolveRoot({ argv, env, cwd })
|
|
14
|
+
const config = readConfig(root)
|
|
15
|
+
const paths = createPaths({
|
|
16
|
+
root,
|
|
17
|
+
dataDir: config.dataDir,
|
|
18
|
+
artefactsDir: config.artefactsDir
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
root,
|
|
23
|
+
rootSource: source,
|
|
24
|
+
configPath,
|
|
25
|
+
config,
|
|
26
|
+
paths,
|
|
27
|
+
package: packagePaths,
|
|
28
|
+
projects: () => discoverProjects({ root, config }),
|
|
29
|
+
|
|
30
|
+
// Commands that operate on a knowledge base call this instead of projects().
|
|
31
|
+
// Finding none is not "nothing to do" - it means the root is wrong, or the
|
|
32
|
+
// project was never initialised, and both deserve to be said out loud with
|
|
33
|
+
// the root printed. Exiting 0 on an empty result is how a misrooted command
|
|
34
|
+
// reports success for work it never did.
|
|
35
|
+
requireProjects: () => {
|
|
36
|
+
const found = discoverProjects({ root, config })
|
|
37
|
+
if (found.length) return found
|
|
38
|
+
throw new Error(
|
|
39
|
+
`No Hippocampus knowledge base found in ${root}\n` +
|
|
40
|
+
`Root resolved from: ${source}\n` +
|
|
41
|
+
`Looked for ${config.dataDir}/source/project.json (and any sibling hippocampus* directory).\n` +
|
|
42
|
+
'Run this from your project, or pass --root <project> / set HIPPOCAMPUS_ROOT.'
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Scripts are CLIs, not libraries. A misrooted command should print one clear
|
|
49
|
+
// line about where it looked and why, not a stack trace through resolve-root.
|
|
50
|
+
function createContextOrExit (options) {
|
|
51
|
+
try {
|
|
52
|
+
return createContext(options)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(error.message)
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { createContext, createContextOrExit }
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const express = require('express')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const packagePaths = require('./package-paths')
|
|
4
|
+
const { listDocs, readDoc, packageVersion } = require('./docs')
|
|
5
|
+
|
|
6
|
+
function createHippocampusRouter ({ basePath, knowledgeStore }) {
|
|
7
|
+
const {
|
|
8
|
+
getKnowledge,
|
|
9
|
+
getJourney,
|
|
10
|
+
getUserKnowledgeGraph,
|
|
11
|
+
getGraphExplorer,
|
|
12
|
+
getTraceExplorer,
|
|
13
|
+
getEvidenceImpact,
|
|
14
|
+
getResearchViewModel,
|
|
15
|
+
buildBlueprintViewModel,
|
|
16
|
+
buildJourneyFlowDiagram,
|
|
17
|
+
validateKnowledge
|
|
18
|
+
} = knowledgeStore
|
|
19
|
+
|
|
20
|
+
const router = express.Router()
|
|
21
|
+
|
|
22
|
+
// The viewer serves its own stylesheet and client JS. Copying them into a
|
|
23
|
+
// host's build is how a page ends up styled by a version of the library the
|
|
24
|
+
// host no longer has installed - and it costs the host two file edits it
|
|
25
|
+
// should not need to make.
|
|
26
|
+
router.use('/assets', express.static(path.join(packagePaths.assetsDir, 'dist'), { fallthrough: true }))
|
|
27
|
+
router.use('/assets', express.static(path.join(packagePaths.assetsDir, 'javascript'), { fallthrough: true }))
|
|
28
|
+
|
|
29
|
+
router.use((req, res, next) => {
|
|
30
|
+
res.locals.basePath = basePath
|
|
31
|
+
res.locals.hippocampusAssets = `${basePath}/assets`
|
|
32
|
+
// Data-browsing pages benefit from the full viewport width rather than
|
|
33
|
+
// the NHS.UK content-page measure - nhsuk-width-container-fluid is the
|
|
34
|
+
// frontend's own utility for this (see node_modules/nhsuk-frontend's
|
|
35
|
+
// template.njk, which already supports a containerClasses override).
|
|
36
|
+
res.locals.containerClasses = 'nhsuk-width-container-fluid'
|
|
37
|
+
// A visual identity distinct from the citizen-facing NHS.UK look, scoped
|
|
38
|
+
// to this body class so /patient and /policy prototype screens (which
|
|
39
|
+
// should still look like a real NHS.UK page) are unaffected.
|
|
40
|
+
res.locals.bodyClasses = 'knowledge-admin-theme'
|
|
41
|
+
const result = validateKnowledge()
|
|
42
|
+
res.locals.knowledgeValidation = result
|
|
43
|
+
next()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
router.get('/', (req, res) => {
|
|
47
|
+
const knowledge = getKnowledge()
|
|
48
|
+
res.render('hippocampus/index', { knowledge })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
router.get('/users', (req, res) => {
|
|
52
|
+
const knowledge = getKnowledge()
|
|
53
|
+
res.render('hippocampus/users', { knowledge })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
router.get('/project', (req, res) => {
|
|
57
|
+
const knowledge = getKnowledge()
|
|
58
|
+
res.render('hippocampus/project', { knowledge })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
router.get('/users/:userId', (req, res) => {
|
|
62
|
+
const knowledge = getKnowledge()
|
|
63
|
+
const userGraph = getUserKnowledgeGraph(req.params.userId)
|
|
64
|
+
if (!userGraph) return res.status(404).render('hippocampus/not-found')
|
|
65
|
+
res.render('hippocampus/user', { knowledge, userGraph })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
router.get('/user-needs', (req, res) => {
|
|
69
|
+
const knowledge = getKnowledge()
|
|
70
|
+
res.render('hippocampus/user-needs', { knowledge })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
router.get('/scenarios', (req, res) => {
|
|
74
|
+
const knowledge = getKnowledge()
|
|
75
|
+
res.render('hippocampus/scenarios', { knowledge })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
router.get('/research', (req, res) => {
|
|
79
|
+
const knowledge = getKnowledge()
|
|
80
|
+
const research = getResearchViewModel()
|
|
81
|
+
res.render('hippocampus/research', { knowledge, research })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
router.get('/graph', (req, res) => {
|
|
85
|
+
const knowledge = getKnowledge()
|
|
86
|
+
const graph = getGraphExplorer()
|
|
87
|
+
res.render('hippocampus/graph', { knowledge, graph })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
router.get('/trace', (req, res) => {
|
|
91
|
+
const knowledge = getKnowledge()
|
|
92
|
+
const trace = getTraceExplorer(req.query.type, req.query.entity)
|
|
93
|
+
res.render('hippocampus/trace', { knowledge, trace })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
router.get('/evidence/:evidenceId/impact', (req, res) => {
|
|
97
|
+
const knowledge = getKnowledge()
|
|
98
|
+
const impact = getEvidenceImpact(req.params.evidenceId)
|
|
99
|
+
if (!impact) return res.status(404).render('hippocampus/not-found')
|
|
100
|
+
res.render('hippocampus/evidence-impact', { knowledge, impact })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
router.get('/journeys/:journeyId', (req, res) => {
|
|
104
|
+
const knowledge = getKnowledge()
|
|
105
|
+
const journey = getJourney(req.params.journeyId)
|
|
106
|
+
if (!journey) return res.status(404).render('hippocampus/not-found')
|
|
107
|
+
res.render('hippocampus/journey', { knowledge, journey })
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
router.get('/journeys/:journeyId/blueprint', (req, res) => {
|
|
111
|
+
const knowledge = getKnowledge()
|
|
112
|
+
const blueprint = buildBlueprintViewModel(req.params.journeyId)
|
|
113
|
+
if (!blueprint) return res.status(404).render('hippocampus/not-found')
|
|
114
|
+
res.render('hippocampus/blueprint', { knowledge, blueprint })
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
router.get('/journeys/:journeyId/blueprint/classic', (req, res) => {
|
|
118
|
+
const knowledge = getKnowledge()
|
|
119
|
+
const blueprint = buildBlueprintViewModel(req.params.journeyId)
|
|
120
|
+
if (!blueprint) return res.status(404).render('hippocampus/not-found')
|
|
121
|
+
res.render('hippocampus/blueprint-classic', { knowledge, blueprint })
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
router.get('/journeys/:journeyId/flow', (req, res) => {
|
|
125
|
+
const knowledge = getKnowledge()
|
|
126
|
+
const flow = buildJourneyFlowDiagram(req.params.journeyId)
|
|
127
|
+
if (!flow) return res.status(404).render('hippocampus/not-found')
|
|
128
|
+
res.render('hippocampus/journey-flow', { knowledge, flow })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
router.get('/route-review', (req, res) => {
|
|
132
|
+
const knowledge = getKnowledge()
|
|
133
|
+
const screen = knowledge.index.screens['screen-route-review-workbench']
|
|
134
|
+
const journey = getJourney('route-review-workflow')
|
|
135
|
+
|
|
136
|
+
if (!screen || !journey) return res.status(404).render('hippocampus/not-found')
|
|
137
|
+
|
|
138
|
+
const linkedNeeds = screen.needIds
|
|
139
|
+
.map((needId) => knowledge.index.needs[needId])
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
|
|
142
|
+
res.render('hippocampus/route-review', { knowledge, screen, journey, linkedNeeds })
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Documentation is served from the package rather than copied into the host,
|
|
146
|
+
// so what a team reads always matches the version they are running.
|
|
147
|
+
router.get('/docs', (req, res) => {
|
|
148
|
+
res.render('hippocampus/docs', { docs: listDocs(), packageVersion: packageVersion() })
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
router.get('/docs/:slug', (req, res) => {
|
|
152
|
+
const doc = readDoc(req.params.slug)
|
|
153
|
+
if (!doc) return res.status(404).render('hippocampus/not-found')
|
|
154
|
+
res.render('hippocampus/doc', { doc, packageVersion: packageVersion() })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
router.get('/handover', (req, res) => {
|
|
158
|
+
const knowledge = getKnowledge()
|
|
159
|
+
// With no journeys at all this is an empty knowledge base, not a bad URL.
|
|
160
|
+
// 404 here would read as "the viewer is broken" on somebody's first run.
|
|
161
|
+
if (!knowledge.journeys.length) {
|
|
162
|
+
return res.render('hippocampus/handover-empty', { knowledge })
|
|
163
|
+
}
|
|
164
|
+
renderHandover(req, res, knowledge.project.prototypeSlice)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
router.get('/journeys/:journeyId/handover', (req, res) => {
|
|
168
|
+
renderHandover(req, res, req.params.journeyId)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
function renderHandover (req, res, journeyId) {
|
|
172
|
+
const knowledge = getKnowledge()
|
|
173
|
+
const blueprint = buildBlueprintViewModel(journeyId)
|
|
174
|
+
if (!blueprint) return res.status(404).render('hippocampus/not-found')
|
|
175
|
+
|
|
176
|
+
const screens = uniqueById(blueprint.steps.flatMap((step) => step.screens))
|
|
177
|
+
res.render('hippocampus/handover', { knowledge, blueprint, screens })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return router
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function uniqueById (items) {
|
|
184
|
+
return Object.values(Object.fromEntries(items.map((item) => [item.id, item])))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { createHippocampusRouter }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { createPaths } = require('./paths')
|
|
4
|
+
const { createKnowledgeStore } = require('./load-knowledge')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Finds every Hippocampus knowledge base in a host project.
|
|
8
|
+
*
|
|
9
|
+
* An explicit `knowledgeBases` list in hippocampus.config.json wins when it is
|
|
10
|
+
* present, so a project can name its bases and their mount points. Otherwise
|
|
11
|
+
* the directory scan is the fallback: sibling `hippocampus*` directories under
|
|
12
|
+
* the root that contain a `source/project.json`, which means adding a base
|
|
13
|
+
* never requires editing the scripts that validate and smoke-test them all.
|
|
14
|
+
*/
|
|
15
|
+
function discoverProjects ({ root, config = {} } = {}) {
|
|
16
|
+
if (!root) throw new Error('discoverProjects requires a resolved root - see resolve-root.js')
|
|
17
|
+
|
|
18
|
+
const artefactsDir = config.artefactsDir || 'artefacts'
|
|
19
|
+
const declared = Array.isArray(config.knowledgeBases) ? config.knowledgeBases : null
|
|
20
|
+
|
|
21
|
+
const entries = declared
|
|
22
|
+
? declared.map((base) => ({
|
|
23
|
+
name: base.name || path.basename(base.dir),
|
|
24
|
+
dir: path.isAbsolute(base.dir) ? base.dir : path.join(root, base.dir),
|
|
25
|
+
basePath: base.basePath || `/${base.name || path.basename(base.dir)}`
|
|
26
|
+
}))
|
|
27
|
+
: fs.readdirSync(root, { withFileTypes: true })
|
|
28
|
+
.filter((entry) => entry.isDirectory() && /^hippocampus(-|$)/.test(entry.name))
|
|
29
|
+
.map((entry) => ({
|
|
30
|
+
name: entry.name,
|
|
31
|
+
dir: path.join(root, entry.name),
|
|
32
|
+
basePath: `/${entry.name}`
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
return entries
|
|
36
|
+
.filter((entry) => fs.existsSync(path.join(entry.dir, 'source/project.json')))
|
|
37
|
+
.map((entry) => ({
|
|
38
|
+
name: entry.name,
|
|
39
|
+
basePath: entry.basePath,
|
|
40
|
+
dir: entry.dir,
|
|
41
|
+
store: createKnowledgeStore(createPaths({ root, dataDir: entry.dir, artefactsDir }))
|
|
42
|
+
}))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { discoverProjects }
|