@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,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
|
+
}
|