@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,84 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ const CONFIG_FILENAME = 'hippocampus.config.json'
5
+
6
+ // Where is the host project? Everything else - which knowledge base to read,
7
+ // where drafts land, where artefacts are written - hangs off this one answer,
8
+ // and a silently wrong answer produces an empty knowledge base with no
9
+ // explanation. So the precedence is explicit, the chosen source is reported,
10
+ // and a root inside node_modules is a hard error rather than a guess.
11
+ //
12
+ // --root <dir> > HIPPOCAMPUS_ROOT > nearest ancestor with
13
+ // hippocampus.config.json > nearest ancestor with package.json > cwd
14
+
15
+ function readArgRoot (argv) {
16
+ const flagIndex = argv.indexOf('--root')
17
+ if (flagIndex !== -1 && argv[flagIndex + 1] && !argv[flagIndex + 1].startsWith('--')) {
18
+ return argv[flagIndex + 1]
19
+ }
20
+ const inline = argv.find((arg) => arg.startsWith('--root='))
21
+ return inline ? inline.slice('--root='.length) : null
22
+ }
23
+
24
+ function isInsideNodeModules (dir) {
25
+ return path.resolve(dir).split(path.sep).includes('node_modules')
26
+ }
27
+
28
+ function nearestAncestor (start, predicate) {
29
+ let dir = path.resolve(start)
30
+ for (;;) {
31
+ if (!isInsideNodeModules(dir) && predicate(dir)) return dir
32
+ const parent = path.dirname(dir)
33
+ if (parent === dir) return null
34
+ dir = parent
35
+ }
36
+ }
37
+
38
+ function resolveRoot ({
39
+ argv = process.argv.slice(2),
40
+ env = process.env,
41
+ cwd = process.cwd()
42
+ } = {}) {
43
+ const argRoot = readArgRoot(argv)
44
+ const envRoot = env.HIPPOCAMPUS_ROOT || null
45
+
46
+ let root
47
+ let source
48
+
49
+ if (argRoot) {
50
+ root = path.resolve(cwd, argRoot)
51
+ source = '--root'
52
+ } else if (envRoot) {
53
+ root = path.resolve(cwd, envRoot)
54
+ source = 'HIPPOCAMPUS_ROOT'
55
+ } else {
56
+ const configured = nearestAncestor(cwd, (dir) => fs.existsSync(path.join(dir, CONFIG_FILENAME)))
57
+ const packaged = configured || nearestAncestor(cwd, (dir) => fs.existsSync(path.join(dir, 'package.json')))
58
+ root = packaged || path.resolve(cwd)
59
+ source = configured ? CONFIG_FILENAME : (packaged ? 'package.json' : 'cwd')
60
+ }
61
+
62
+ if (!fs.existsSync(root)) {
63
+ throw new Error(`Hippocampus root does not exist: ${root} (from ${source})`)
64
+ }
65
+
66
+ if (!fs.statSync(root).isDirectory()) {
67
+ throw new Error(`Hippocampus root is not a directory: ${root} (from ${source})`)
68
+ }
69
+
70
+ // The library ships inside the host's node_modules. If the root resolves in
71
+ // there, something has walked up from __dirname instead of from the host -
72
+ // and the symptom would be an empty knowledge base, not an error.
73
+ if (isInsideNodeModules(root)) {
74
+ throw new Error(
75
+ `Hippocampus root resolved inside node_modules: ${root} (from ${source}).\n` +
76
+ 'This is the installed package, not your project. Run the command from your ' +
77
+ 'project directory, or pass --root <project> / set HIPPOCAMPUS_ROOT.'
78
+ )
79
+ }
80
+
81
+ return { root, source, configPath: path.join(root, CONFIG_FILENAME) }
82
+ }
83
+
84
+ module.exports = { resolveRoot, CONFIG_FILENAME, isInsideNodeModules }
@@ -0,0 +1,96 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ // The data contract. Within a schemaVersion changes are additive only, and
5
+ // unknown fields survive a read/write round trip (every record schema is
6
+ // passthrough) - that pair is what actually makes data written by version N
7
+ // readable by N+1 and back again. Anything non-additive bumps this number,
8
+ // ships with a package major, and gets a migration below.
9
+ const CURRENT_SCHEMA_VERSION = 1
10
+ const META_FILENAME = '_meta.json'
11
+
12
+ // Keyed by the version they migrate FROM. Empty on purpose: version 1 is the
13
+ // first, and the runner exists so the first real migration is a data change
14
+ // rather than an architecture change made under pressure.
15
+ const migrations = {}
16
+
17
+ function readMeta (knowledgeSourceDir) {
18
+ const metaPath = path.join(knowledgeSourceDir, META_FILENAME)
19
+ if (!fs.existsSync(metaPath)) {
20
+ // Absent means version 1 - every knowledge base written before _meta.json
21
+ // existed is a valid version 1 base, and inventing a file on read would
22
+ // make an unwritable directory fail for no reason.
23
+ return { schemaVersion: 1, implicit: true, metaPath }
24
+ }
25
+
26
+ let meta
27
+ try {
28
+ meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'))
29
+ } catch (error) {
30
+ throw new Error(`${META_FILENAME} is not valid JSON: ${metaPath}\n${error.message}`)
31
+ }
32
+
33
+ const schemaVersion = Number(meta.schemaVersion)
34
+ if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
35
+ throw new Error(`${META_FILENAME} has no usable schemaVersion: ${metaPath}`)
36
+ }
37
+
38
+ return { ...meta, schemaVersion, implicit: false, metaPath }
39
+ }
40
+
41
+ /**
42
+ * Brings data up to the version this code understands, in memory.
43
+ *
44
+ * Data newer than the code is refused outright. Reading a file forward - "the
45
+ * fields I recognise are enough" - is how a knowledge base gets silently
46
+ * destroyed by the next write, and an error the day someone downgrades is far
47
+ * cheaper than losing a round of research quietly.
48
+ */
49
+ function migrateInMemory (knowledge, meta) {
50
+ if (meta.schemaVersion > CURRENT_SCHEMA_VERSION) {
51
+ throw new Error(
52
+ `This knowledge base is at schemaVersion ${meta.schemaVersion}, but this ` +
53
+ `version of Hippocampus understands ${CURRENT_SCHEMA_VERSION}.\n` +
54
+ `${meta.metaPath}\nUpgrade the package rather than reading it with older code.`
55
+ )
56
+ }
57
+
58
+ const warnings = []
59
+ let data = knowledge
60
+ let version = meta.schemaVersion
61
+
62
+ while (version < CURRENT_SCHEMA_VERSION) {
63
+ const migrate = migrations[version]
64
+ if (!migrate) {
65
+ throw new Error(`No migration from schemaVersion ${version} to ${version + 1}`)
66
+ }
67
+ data = migrate(data)
68
+ version += 1
69
+ warnings.push(
70
+ `Knowledge base read as schemaVersion ${version - 1} and migrated to ${version} in memory. ` +
71
+ 'Run `npm run migrate:knowledge` to write the upgrade back.'
72
+ )
73
+ }
74
+
75
+ return { data, version, warnings }
76
+ }
77
+
78
+ function writeMeta (knowledgeSourceDir, { generator } = {}) {
79
+ const metaPath = path.join(knowledgeSourceDir, META_FILENAME)
80
+ const meta = {
81
+ schemaVersion: CURRENT_SCHEMA_VERSION,
82
+ generator: generator || 'hippocampus',
83
+ updatedAt: new Date().toISOString()
84
+ }
85
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n')
86
+ return meta
87
+ }
88
+
89
+ module.exports = {
90
+ CURRENT_SCHEMA_VERSION,
91
+ META_FILENAME,
92
+ migrations,
93
+ readMeta,
94
+ migrateInMemory,
95
+ writeMeta
96
+ }
package/lib/schema.js ADDED
@@ -0,0 +1,346 @@
1
+ const { z } = require('zod')
2
+
3
+ const id = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Use lowercase kebab-case IDs')
4
+ const nonEmptyString = z.string().trim().min(1)
5
+ const stringList = z.array(nonEmptyString).min(1)
6
+
7
+ const ProjectSchema = z.object({
8
+ id,
9
+ name: nonEmptyString,
10
+ description: nonEmptyString,
11
+ phase: nonEmptyString,
12
+ serviceArea: nonEmptyString,
13
+ prototypeSlice: id
14
+ }).passthrough()
15
+
16
+ const ProjectContextUserSchema = z.object({
17
+ id,
18
+ label: nonEmptyString,
19
+ group: nonEmptyString.optional(),
20
+ description: nonEmptyString.optional()
21
+ }).passthrough()
22
+
23
+ const ProjectContextChannelSchema = z.object({
24
+ id,
25
+ label: nonEmptyString,
26
+ description: nonEmptyString.optional(),
27
+ usage: nonEmptyString.optional()
28
+ }).passthrough()
29
+
30
+ const ProjectContextPolicySchema = z.object({
31
+ id,
32
+ title: nonEmptyString,
33
+ summary: nonEmptyString.optional(),
34
+ sourceUrl: nonEmptyString.optional()
35
+ }).passthrough()
36
+
37
+ const ProjectContextConstraintSchema = z.object({
38
+ id,
39
+ label: nonEmptyString,
40
+ detail: nonEmptyString,
41
+ impact: nonEmptyString.optional()
42
+ }).passthrough()
43
+
44
+ const ProjectContextRiskSchema = z.object({
45
+ id,
46
+ label: nonEmptyString,
47
+ detail: nonEmptyString.optional(),
48
+ severity: z.enum(['high', 'medium', 'low']).optional()
49
+ }).passthrough()
50
+
51
+ const ProjectContextMilestoneSchema = z.object({
52
+ id,
53
+ label: nonEmptyString,
54
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD dates').optional(),
55
+ detail: nonEmptyString.optional()
56
+ }).passthrough()
57
+
58
+ const ProjectContextFactSchema = z.object({
59
+ id,
60
+ label: nonEmptyString,
61
+ value: nonEmptyString,
62
+ detail: nonEmptyString.optional()
63
+ }).passthrough()
64
+
65
+ const ProjectContextArtefactSchema = z.object({
66
+ id,
67
+ title: nonEmptyString,
68
+ sourcePath: nonEmptyString,
69
+ sourceType: nonEmptyString,
70
+ importedAt: nonEmptyString.optional()
71
+ }).passthrough()
72
+
73
+ const ProjectContextSchema = z.object({
74
+ id,
75
+ title: nonEmptyString,
76
+ summary: nonEmptyString,
77
+ purpose: nonEmptyString.optional(),
78
+ vision: nonEmptyString.optional(),
79
+ problemStatement: nonEmptyString.optional(),
80
+ serviceProposition: nonEmptyString.optional(),
81
+ users: z.array(ProjectContextUserSchema).default([]),
82
+ channels: z.array(ProjectContextChannelSchema).default([]),
83
+ policies: z.array(ProjectContextPolicySchema).default([]),
84
+ constraints: z.array(ProjectContextConstraintSchema).default([]),
85
+ risks: z.array(ProjectContextRiskSchema).default([]),
86
+ milestones: z.array(ProjectContextMilestoneSchema).default([]),
87
+ facts: z.array(ProjectContextFactSchema).default([]),
88
+ sourceArtefacts: z.array(ProjectContextArtefactSchema).default([])
89
+ }).passthrough()
90
+
91
+ const UserSchema = z.object({
92
+ id,
93
+ label: nonEmptyString,
94
+ description: nonEmptyString,
95
+ responsibilities: stringList,
96
+ accessNeeds: stringList
97
+ }).passthrough()
98
+
99
+ const NeedSchema = z.object({
100
+ id,
101
+ userId: id,
102
+ statement: nonEmptyString,
103
+ priority: z.enum(['high', 'medium', 'low']),
104
+ evidenceIds: z.array(id).min(1),
105
+ reference: nonEmptyString.optional()
106
+ }).passthrough()
107
+
108
+ const LocationSearchSchema = z.object({
109
+ primaryIntent: nonEmptyString,
110
+ searchScope: z.enum(['household', 'postcode-area', 'both']),
111
+ triggerConditions: stringList,
112
+ availableInformation: stringList,
113
+ addressRole: nonEmptyString,
114
+ learnedFromResidents: stringList,
115
+ currentAndHistoricResidents: stringList,
116
+ outcomes: stringList,
117
+ risks: stringList
118
+ }).passthrough()
119
+
120
+ const ScenarioSchema = z.object({
121
+ id,
122
+ title: nonEmptyString,
123
+ userId: id,
124
+ trigger: nonEmptyString,
125
+ context: nonEmptyString,
126
+ riskLevel: z.enum(['high', 'medium', 'low']),
127
+ needIds: z.array(id).min(1),
128
+ happyPath: stringList,
129
+ edgeCases: stringList,
130
+ locationSearch: LocationSearchSchema.optional()
131
+ }).passthrough()
132
+
133
+ const PainPointSchema = z.object({
134
+ id,
135
+ label: nonEmptyString,
136
+ severity: z.enum(['high', 'medium', 'low'])
137
+ }).passthrough()
138
+
139
+ const JourneyStepSchema = z.object({
140
+ id,
141
+ label: nonEmptyString,
142
+ channel: z.enum(['app', 'staff', 'external', 'hybrid']),
143
+ userAction: nonEmptyString,
144
+ frontstage: stringList,
145
+ backstage: stringList,
146
+ supportProcesses: stringList,
147
+ dataDependencies: stringList,
148
+ securityAndGovernance: stringList,
149
+ painPoints: z.array(PainPointSchema),
150
+ exceptionHandling: stringList,
151
+ futureState: stringList.optional(),
152
+ needIds: z.array(id).min(1),
153
+ screenIds: z.array(id),
154
+ evidenceIds: z.array(id).min(1),
155
+ decisionIds: z.array(id)
156
+ }).passthrough()
157
+
158
+ const JourneySchema = z.object({
159
+ id,
160
+ title: nonEmptyString,
161
+ description: nonEmptyString,
162
+ scenarioId: id,
163
+ entryPoint: nonEmptyString,
164
+ exitPoint: nonEmptyString,
165
+ steps: z.array(JourneyStepSchema).min(1)
166
+ }).passthrough()
167
+
168
+ const ScreenSchema = z.object({
169
+ id,
170
+ route: z.string().regex(/^\/[a-z0-9][a-z0-9\-\/]*$/, 'Routes must be absolute lowercase paths'),
171
+ title: nonEmptyString,
172
+ purpose: nonEmptyString,
173
+ needIds: z.array(id).min(1),
174
+ components: stringList,
175
+ // A screen in the real NCRS prototype rather than in this viewer. The
176
+ // knowledge base models the service, not only the screens this repo happens
177
+ // to render, so the route smoke test skips these instead of failing on them.
178
+ external: z.boolean().optional(),
179
+ sourcePath: nonEmptyString.optional()
180
+ }).passthrough()
181
+
182
+ const EvidenceSchema = z.object({
183
+ id,
184
+ title: nonEmptyString,
185
+ sourceType: nonEmptyString,
186
+ summary: nonEmptyString,
187
+ confidence: z.enum(['high', 'medium', 'low']),
188
+ sourcePath: nonEmptyString.optional(),
189
+ quotes: z.array(nonEmptyString).optional()
190
+ }).passthrough()
191
+
192
+ const DecisionSchema = z.object({
193
+ id,
194
+ title: nonEmptyString,
195
+ status: z.enum(['proposed', 'accepted', 'superseded', 'rejected']),
196
+ decision: nonEmptyString,
197
+ consequence: nonEmptyString
198
+ }).passthrough()
199
+
200
+ const AssumptionSchema = z.object({
201
+ id,
202
+ statement: nonEmptyString,
203
+ risk: z.enum(['high', 'medium', 'low'])
204
+ }).passthrough()
205
+
206
+ const ResearchRoundSchema = z.object({
207
+ id,
208
+ title: nonEmptyString,
209
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD dates'),
210
+ sourceType: nonEmptyString,
211
+ sourcePath: nonEmptyString.optional(),
212
+ summary: nonEmptyString
213
+ }).passthrough()
214
+
215
+ const ParticipantSchema = z.object({
216
+ id,
217
+ label: nonEmptyString,
218
+ role: nonEmptyString,
219
+ organisation: nonEmptyString.optional(),
220
+ roundIds: z.array(id).min(1)
221
+ }).passthrough()
222
+
223
+ const InsightSchema = z.object({
224
+ id,
225
+ roundId: id,
226
+ title: nonEmptyString,
227
+ summary: nonEmptyString,
228
+ confidence: z.enum(['high', 'medium', 'low']),
229
+ participantIds: z.array(id),
230
+ evidenceIds: z.array(id)
231
+ }).passthrough()
232
+
233
+ const DesignActionSchema = z.object({
234
+ id,
235
+ title: nonEmptyString,
236
+ status: z.enum(['proposed', 'in-progress', 'done', 'parked']),
237
+ action: nonEmptyString,
238
+ rationale: nonEmptyString,
239
+ insightIds: z.array(id).min(1)
240
+ }).passthrough()
241
+
242
+ const RelationshipSchema = z.object({
243
+ id,
244
+ from: id,
245
+ type: z.enum([
246
+ 'has-scenario',
247
+ 'has-need',
248
+ 'tests-need',
249
+ 'supported-by',
250
+ 'drives-journey',
251
+ 'renders-screen',
252
+ 'addresses-need',
253
+ 'uses-evidence',
254
+ 'records-decision',
255
+ 'has-participant',
256
+ 'produced-insight',
257
+ 'contributed-to',
258
+ 'supports-insight',
259
+ // Dependents point at what they rely on, because evidence impact walks
260
+ // relationship.to -> relationship.from. A design action relies on the
261
+ // insight that prompted it, and a decision relies on the action it
262
+ // resolves, so both point backwards down the chain. Written the other way
263
+ // round the impact walk stops at the insight, which is exactly where the
264
+ // question "if this research is wrong, what design work is affected?"
265
+ // becomes worth asking.
266
+ 'responds-to-insight',
267
+ 'resolves-action',
268
+ // Retained so older drafts still validate. New links use
269
+ // responds-to-insight, which traces.
270
+ 'leads-to-action'
271
+ ]),
272
+ to: id
273
+ }).passthrough()
274
+
275
+ const ResearchImportDraftSchema = z.object({
276
+ sourceDeck: nonEmptyString,
277
+ importedAt: nonEmptyString,
278
+ warnings: z.array(nonEmptyString).optional(),
279
+ researchRounds: z.array(ResearchRoundSchema).min(1),
280
+ participants: z.array(ParticipantSchema).default([]),
281
+ insights: z.array(InsightSchema).default([]),
282
+ designActions: z.array(DesignActionSchema).default([]),
283
+ relationships: z.array(RelationshipSchema).default([]),
284
+ slides: z.array(z.object({
285
+ number: z.number().int().positive(),
286
+ title: nonEmptyString,
287
+ text: z.array(nonEmptyString)
288
+ }).strict()).min(1)
289
+ }).strict()
290
+
291
+ const ProjectContextImportDraftSchema = z.object({
292
+ sourceDocument: nonEmptyString,
293
+ importedAt: nonEmptyString,
294
+ warnings: z.array(nonEmptyString).optional(),
295
+ projectContext: ProjectContextSchema,
296
+ sections: z.array(z.object({
297
+ number: z.number().int().positive(),
298
+ title: nonEmptyString,
299
+ text: z.array(nonEmptyString)
300
+ }).strict()).min(1)
301
+ }).strict()
302
+
303
+ const KnowledgeSchema = z.object({
304
+ project: ProjectSchema,
305
+ projectContext: ProjectContextSchema,
306
+ users: z.array(UserSchema).default([]),
307
+ needs: z.array(NeedSchema).default([]),
308
+ scenarios: z.array(ScenarioSchema).default([]),
309
+ journeys: z.array(JourneySchema).default([]),
310
+ screens: z.array(ScreenSchema).default([]),
311
+ evidence: z.array(EvidenceSchema).default([]),
312
+ decisions: z.array(DecisionSchema).default([]),
313
+ assumptions: z.array(AssumptionSchema).default([]),
314
+ researchRounds: z.array(ResearchRoundSchema).default([]),
315
+ participants: z.array(ParticipantSchema).default([]),
316
+ insights: z.array(InsightSchema).default([]),
317
+ designActions: z.array(DesignActionSchema).default([]),
318
+ relationships: z.array(RelationshipSchema).default([])
319
+ }).strict()
320
+
321
+ // Exported so the linter can tell a deliberate extra field from a typo. Record
322
+ // schemas are passthrough, which is what lets data written by a slightly newer
323
+ // package round-trip; the cost is that `titel` no longer fails the parse, so the
324
+ // linter has to notice it instead.
325
+ const recordSchemas = {
326
+ users: UserSchema,
327
+ needs: NeedSchema,
328
+ scenarios: ScenarioSchema,
329
+ journeys: JourneySchema,
330
+ screens: ScreenSchema,
331
+ evidence: EvidenceSchema,
332
+ decisions: DecisionSchema,
333
+ assumptions: AssumptionSchema,
334
+ researchRounds: ResearchRoundSchema,
335
+ participants: ParticipantSchema,
336
+ insights: InsightSchema,
337
+ designActions: DesignActionSchema,
338
+ relationships: RelationshipSchema
339
+ }
340
+
341
+ module.exports = {
342
+ KnowledgeSchema,
343
+ ResearchImportDraftSchema,
344
+ ProjectContextImportDraftSchema,
345
+ recordSchemas
346
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@hippo-digital/hippocampus",
3
+ "version": "1.0.0-rc.1",
4
+ "description": "A design-knowledge base for NHS Prototype Kit projects: research, insights, needs, journeys and screens, traceable end to end.",
5
+ "license": "MIT",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "hippocampus": "bin/hippocampus.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "bin",
13
+ "lib",
14
+ "views",
15
+ "assets",
16
+ "scripts",
17
+ "skills",
18
+ "templates",
19
+ "docs",
20
+ "README.md",
21
+ "LICENSE",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "scripts": {
25
+ "build:assets": "sass --no-source-map --style=compressed assets/hippocampus.scss assets/dist/hippocampus.css",
26
+ "validate:skills": "node scripts/validate-skills.js",
27
+ "prepack": "npm run build:assets",
28
+ "prepublishOnly": "node ../../scripts/check-release.js"
29
+ },
30
+ "dependencies": {
31
+ "adm-zip": "^0.5.18",
32
+ "fast-xml-parser": "^5.9.3",
33
+ "marked": "^18.0.13",
34
+ "pdf-parse": "^2.4.5",
35
+ "zod": "^4.4.3"
36
+ },
37
+ "devDependencies": {
38
+ "sass": "^1.80.0"
39
+ },
40
+ "peerDependencies": {
41
+ "nhsuk-frontend": ">=10.5.1",
42
+ "nhsuk-prototype-kit": ">=8.3.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=22.16.0"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/hippo-digital/nhs-prototype-hippocampus.git",
50
+ "directory": "packages/hippocampus"
51
+ },
52
+ "homepage": "https://github.com/hippo-digital/nhs-prototype-hippocampus/tree/main/packages/hippocampus#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/hippo-digital/nhs-prototype-hippocampus/issues"
55
+ },
56
+ "keywords": [
57
+ "nhs",
58
+ "nhsuk",
59
+ "prototype-kit",
60
+ "design",
61
+ "user-research",
62
+ "knowledge-base",
63
+ "service-design",
64
+ "accessibility"
65
+ ],
66
+ "author": "Hippo Digital",
67
+ "publishConfig": {
68
+ "access": "public"
69
+ }
70
+ }