@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +153 -0
  4. package/assets/dist/hippocampus.css +1 -0
  5. package/assets/hippocampus.scss +896 -0
  6. package/assets/javascript/blueprint.js +123 -0
  7. package/bin/hippocampus.js +106 -0
  8. package/docs/agent-roadmap.md +217 -0
  9. package/docs/agent-workflow.md +56 -0
  10. package/docs/agentic-patterns-to-port.md +273 -0
  11. package/docs/colour-pairings.md +41 -0
  12. package/docs/components.md +52 -0
  13. package/docs/copilot-ncrs-research-curator-agent.md +298 -0
  14. package/docs/custom-agents-usage.md +95 -0
  15. package/docs/design-actions-and-decisions.md +95 -0
  16. package/docs/gp-connect-real-data-to-production.md +66 -0
  17. package/docs/hippocampus-for-designers-and-researchers.md +261 -0
  18. package/docs/hippocampus-technical.md +373 -0
  19. package/docs/prompt-architecture-guardrails.md +39 -0
  20. package/docs/repeatable-research-round-ingestion.md +239 -0
  21. package/docs/research-knowledge-graph.md +552 -0
  22. package/docs/research-source-file-conventions.md +121 -0
  23. package/docs/source-model.md +252 -0
  24. package/index.js +47 -0
  25. package/lib/config.js +46 -0
  26. package/lib/context.js +59 -0
  27. package/lib/create-router.js +187 -0
  28. package/lib/discover-projects.js +45 -0
  29. package/lib/docs.js +72 -0
  30. package/lib/host-edit.js +151 -0
  31. package/lib/lint-knowledge.js +133 -0
  32. package/lib/load-knowledge.js +629 -0
  33. package/lib/package-paths.js +32 -0
  34. package/lib/paths.js +33 -0
  35. package/lib/resolve-root.js +84 -0
  36. package/lib/schema-version.js +96 -0
  37. package/lib/schema.js +346 -0
  38. package/package.json +70 -0
  39. package/scripts/audit-provenance.js +147 -0
  40. package/scripts/doctor.js +114 -0
  41. package/scripts/eject.js +51 -0
  42. package/scripts/import-blueprint-spreadsheet.js +242 -0
  43. package/scripts/import-design-actions.js +230 -0
  44. package/scripts/import-project-context.js +427 -0
  45. package/scripts/import-research-batch.js +160 -0
  46. package/scripts/import-research-deck.js +448 -0
  47. package/scripts/index-source-artefacts.js +261 -0
  48. package/scripts/init.js +232 -0
  49. package/scripts/install-skills.js +160 -0
  50. package/scripts/lib/artefact-text.js +347 -0
  51. package/scripts/lib/xlsx-grid.js +114 -0
  52. package/scripts/link-insights-to-needs.js +209 -0
  53. package/scripts/migrate-knowledge.js +73 -0
  54. package/scripts/promote-project-context-draft.js +125 -0
  55. package/scripts/promote-research-draft.js +236 -0
  56. package/scripts/smoke-routes.js +157 -0
  57. package/scripts/sync-project-context.js +236 -0
  58. package/scripts/sync-research-artefacts.js +294 -0
  59. package/scripts/triage-research-corpus.js +274 -0
  60. package/scripts/validate-knowledge.js +49 -0
  61. package/scripts/validate-skills.js +247 -0
  62. package/skills/COVERAGE-MATRIX.md +39 -0
  63. package/skills/README.md +77 -0
  64. package/skills/SKILL-FORMAT.md +100 -0
  65. package/skills/add-prototype-screen.skill.md +54 -0
  66. package/skills/add-scenario.skill.md +47 -0
  67. package/skills/add-user-needs.skill.md +51 -0
  68. package/skills/audit-knowledge-source.skill.md +47 -0
  69. package/skills/capture-route-review-pack.skill.md +52 -0
  70. package/skills/create-journey-from-scenario.skill.md +50 -0
  71. package/skills/deliver-service-slice.skill.md +65 -0
  72. package/skills/examples/README.md +15 -0
  73. package/skills/examples/non-copilot-research-ingestion.md +68 -0
  74. package/skills/examples/non-copilot-service-slice.md +64 -0
  75. package/skills/generate-service-slice.skill.md +57 -0
  76. package/skills/ingest-project-context.skill.md +58 -0
  77. package/skills/ingest-research-round.skill.md +60 -0
  78. package/skills/map-research-to-graph.skill.md +58 -0
  79. package/skills/record-accessibility-review.skill.md +52 -0
  80. package/skills/record-design-decision.skill.md +53 -0
  81. package/skills/review-research-import-draft.skill.md +45 -0
  82. package/skills/skills.json +464 -0
  83. package/skills/skills.schema.json +111 -0
  84. package/skills/structure-project-context-draft.skill.md +51 -0
  85. package/skills/structure-research-draft.skill.md +51 -0
  86. package/skills/triage-research-corpus.skill.md +51 -0
  87. package/skills/write-delivery-summary.skill.md +51 -0
  88. package/skills/write-route-review-summary.skill.md +55 -0
  89. package/views/hippocampus/_layout.html +45 -0
  90. package/views/hippocampus/blueprint-classic.html +39 -0
  91. package/views/hippocampus/blueprint.html +52 -0
  92. package/views/hippocampus/components/_blueprint-cell.njk +42 -0
  93. package/views/hippocampus/components/_blueprint-controls.njk +20 -0
  94. package/views/hippocampus/components/_blueprint-minimap.njk +14 -0
  95. package/views/hippocampus/components/_empty-state.njk +12 -0
  96. package/views/hippocampus/components/_journey-step.njk +21 -0
  97. package/views/hippocampus/components/_list.njk +23 -0
  98. package/views/hippocampus/components/_summary-card.njk +15 -0
  99. package/views/hippocampus/doc.html +11 -0
  100. package/views/hippocampus/docs.html +19 -0
  101. package/views/hippocampus/evidence-impact.html +53 -0
  102. package/views/hippocampus/graph.html +48 -0
  103. package/views/hippocampus/handover-empty.html +12 -0
  104. package/views/hippocampus/handover.html +56 -0
  105. package/views/hippocampus/index.html +46 -0
  106. package/views/hippocampus/journey-flow.html +31 -0
  107. package/views/hippocampus/journey.html +23 -0
  108. package/views/hippocampus/not-found.html +10 -0
  109. package/views/hippocampus/project.html +102 -0
  110. package/views/hippocampus/research.html +68 -0
  111. package/views/hippocampus/route-review.html +83 -0
  112. package/views/hippocampus/scenarios.html +56 -0
  113. package/views/hippocampus/trace.html +113 -0
  114. package/views/hippocampus/user-needs.html +32 -0
  115. package/views/hippocampus/user.html +157 -0
  116. 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 }