@forwardimpact/pathway 0.25.20 → 0.25.22

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.
@@ -44,7 +44,11 @@ function formatJob(view, _options, entities, jobTemplate) {
44
44
  */
45
45
  function printJobList(filteredJobs) {
46
46
  for (const job of filteredJobs) {
47
- const title = generateJobTitle(job.discipline, job.level, job.track);
47
+ const title = generateJobTitle({
48
+ discipline: job.discipline,
49
+ level: job.level,
50
+ track: job.track,
51
+ });
48
52
  if (job.track) {
49
53
  console.log(
50
54
  `${job.discipline.id} ${job.level.id} ${job.track.id}, ${title}`,
@@ -79,7 +79,7 @@ async function formatAgentDetail(skill, stages, templateLoader, dataDir) {
79
79
  }
80
80
 
81
81
  const template = templateLoader.load("skill.template.md", dataDir);
82
- const skillMd = generateSkillMarkdown(skill, stages);
82
+ const skillMd = generateSkillMarkdown({ skillData: skill, stages });
83
83
  const output = formatAgentSkill(skillMd, template);
84
84
  console.log(output);
85
85
  }
@@ -33,6 +33,54 @@
33
33
  gap: var(--space-xl);
34
34
  }
35
35
 
36
+ /* Install section — ecosystem-tool install commands shown between the
37
+ dropdowns and the preview cards. Uses the same surface treatment as
38
+ the form so the two blocks read as a matched pair. */
39
+ .agent-install-section {
40
+ background: var(--color-surface);
41
+ border-radius: var(--radius-lg);
42
+ padding: var(--space-xl);
43
+ box-shadow: var(--shadow-md);
44
+ margin-bottom: var(--space-xl);
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: var(--space-lg);
48
+ }
49
+
50
+ .agent-install-header h2 {
51
+ margin: 0 0 var(--space-sm) 0;
52
+ }
53
+
54
+ .agent-install-description {
55
+ margin: 0;
56
+ }
57
+
58
+ .agent-install-commands {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: var(--space-md);
62
+ }
63
+
64
+ .agent-install-command {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: var(--space-xs);
68
+ }
69
+
70
+ .agent-install-command-label {
71
+ margin: 0;
72
+ font-size: var(--font-size-sm);
73
+ font-weight: 600;
74
+ color: var(--color-text-secondary);
75
+ }
76
+
77
+ /* Allow the command prompts inside the install section to grow beyond the
78
+ 600px center-aligned default used on the landing hero. */
79
+ .agent-install-command .command-prompt {
80
+ max-width: none;
81
+ margin: 0;
82
+ }
83
+
36
84
  /* Agent section — spacing container, no surface */
37
85
  .agent-section {
38
86
  margin-bottom: var(--space-lg);
@@ -7,7 +7,6 @@
7
7
 
8
8
  // Shared utilities
9
9
  export * from "./shared.js";
10
- export * from "./microdata-shared.js";
11
10
 
12
11
  // Job formatters
13
12
  export { jobToMarkdown } from "./job/markdown.js";
@@ -23,15 +22,10 @@ export { progressToDOM } from "./progress/dom.js";
23
22
 
24
23
  // Driver formatters
25
24
  export { driverToDOM } from "./driver/dom.js";
26
- export {
27
- driverListToMicrodata,
28
- driverToMicrodata,
29
- } from "./driver/microdata.js";
30
25
 
31
26
  // Skill formatters
32
27
  export { skillListToMarkdown, skillToMarkdown } from "./skill/markdown.js";
33
28
  export { skillToDOM } from "./skill/dom.js";
34
- export { skillListToMicrodata, skillToMicrodata } from "./skill/microdata.js";
35
29
 
36
30
  // Behaviour formatters
37
31
  export {
@@ -39,10 +33,6 @@ export {
39
33
  behaviourToMarkdown,
40
34
  } from "./behaviour/markdown.js";
41
35
  export { behaviourToDOM } from "./behaviour/dom.js";
42
- export {
43
- behaviourListToMicrodata,
44
- behaviourToMicrodata,
45
- } from "./behaviour/microdata.js";
46
36
 
47
37
  // Discipline formatters
48
38
  export {
@@ -50,23 +40,14 @@ export {
50
40
  disciplineToMarkdown,
51
41
  } from "./discipline/markdown.js";
52
42
  export { disciplineToDOM } from "./discipline/dom.js";
53
- export {
54
- disciplineListToMicrodata,
55
- disciplineToMicrodata,
56
- } from "./discipline/microdata.js";
57
43
 
58
44
  // Level formatters
59
45
  export { levelListToMarkdown, levelToMarkdown } from "./level/markdown.js";
60
46
  export { levelToDOM } from "./level/dom.js";
61
- export { levelListToMicrodata, levelToMicrodata } from "./level/microdata.js";
62
47
 
63
48
  // Track formatters
64
49
  export { trackListToMarkdown, trackToMarkdown } from "./track/markdown.js";
65
50
  export { trackToDOM } from "./track/dom.js";
66
- export { trackListToMicrodata, trackToMicrodata } from "./track/microdata.js";
67
-
68
- // Stage formatters
69
- export { stageListToMicrodata, stageToMicrodata } from "./stage/microdata.js";
70
51
 
71
52
  // JSON-LD formatters
72
53
  export {
@@ -14,7 +14,9 @@ import {
14
14
  deriveDecompositionInterview,
15
15
  deriveStakeholderInterview,
16
16
  } from "@forwardimpact/libskill/interview";
17
- import { getOrCreateJob } from "@forwardimpact/libskill/job-cache";
17
+ import { createJobCache } from "@forwardimpact/libskill/job-cache";
18
+
19
+ const jobCache = createJobCache();
18
20
 
19
21
  /**
20
22
  * Interview type configurations
@@ -140,7 +142,7 @@ export function prepareInterviewDetail({
140
142
  }) {
141
143
  if (!discipline || !level) return null;
142
144
 
143
- const job = getOrCreateJob({
145
+ const job = jobCache.getOrCreate({
144
146
  discipline,
145
147
  level,
146
148
  track,
@@ -275,7 +277,7 @@ export function prepareInterviewBuilderPreview({
275
277
  };
276
278
  }
277
279
 
278
- const title = generateJobTitle(discipline, level, track);
280
+ const title = generateJobTitle({ discipline, level, track });
279
281
  const totalSkills = getDisciplineSkillIds(discipline).length;
280
282
 
281
283
  return {
@@ -320,7 +322,7 @@ export function prepareAllInterviews({
320
322
  // Track is optional (null = generalist)
321
323
  if (!discipline || !level) return null;
322
324
 
323
- const job = getOrCreateJob({
325
+ const job = jobCache.getOrCreate({
324
326
  discipline,
325
327
  level,
326
328
  track,
@@ -13,7 +13,9 @@ import {
13
13
  analyzeCustomProgression,
14
14
  getNextLevel,
15
15
  } from "@forwardimpact/libskill/progression";
16
- import { getOrCreateJob } from "@forwardimpact/libskill/job-cache";
16
+ import { createJobCache } from "@forwardimpact/libskill/job-cache";
17
+
18
+ const jobCache = createJobCache();
17
19
 
18
20
  /**
19
21
  * Get the next level for progression
@@ -22,20 +24,7 @@ import { getOrCreateJob } from "@forwardimpact/libskill/job-cache";
22
24
  * @returns {Object|null}
23
25
  */
24
26
  export function getDefaultTargetLevel(currentLevel, levels) {
25
- return getNextLevel(currentLevel, levels);
26
- }
27
-
28
- /**
29
- * Check if a job combination is valid
30
- * @param {Object} params
31
- * @param {Object} params.discipline
32
- * @param {Object} params.level
33
- * @param {Object} params.track
34
- * @param {Array} [params.levels] - All levels for validation
35
- * @returns {boolean}
36
- */
37
- export function isValidCombination({ discipline, level, track, levels }) {
38
- return isValidJobCombination({ discipline, level, track, levels });
27
+ return getNextLevel({ level: currentLevel, levels });
39
28
  }
40
29
 
41
30
  /**
@@ -69,7 +58,7 @@ export function prepareCurrentJob({
69
58
  }) {
70
59
  if (!discipline || !level) return null;
71
60
 
72
- const job = getOrCreateJob({
61
+ const job = jobCache.getOrCreate({
73
62
  discipline,
74
63
  level,
75
64
  track,
@@ -148,8 +137,8 @@ export function prepareCareerProgressPreview({
148
137
  };
149
138
  }
150
139
 
151
- const title = generateJobTitle(discipline, level, track);
152
- const nextLevel = getNextLevel(level, levels);
140
+ const title = generateJobTitle({ discipline, level, track });
141
+ const nextLevel = getNextLevel({ level, levels });
153
142
 
154
143
  // Find other valid tracks for comparison (exclude current track if any)
155
144
  const validTracks = tracks.filter(
@@ -209,7 +198,7 @@ export function prepareProgressDetail({
209
198
  if (!fromDiscipline || !fromLevel) return null;
210
199
  if (!toDiscipline || !toLevel) return null;
211
200
 
212
- const fromJob = getOrCreateJob({
201
+ const fromJob = jobCache.getOrCreate({
213
202
  discipline: fromDiscipline,
214
203
  level: fromLevel,
215
204
  track: fromTrack,
@@ -218,7 +207,7 @@ export function prepareProgressDetail({
218
207
  capabilities,
219
208
  });
220
209
 
221
- const toJob = getOrCreateJob({
210
+ const toJob = jobCache.getOrCreate({
222
211
  discipline: toDiscipline,
223
212
  level: toLevel,
224
213
  track: toTrack,
@@ -95,11 +95,18 @@ export function prepareSkillDetail(
95
95
  if (!skill) return null;
96
96
 
97
97
  const relatedDisciplines = disciplines
98
- .filter((d) => getSkillTypeForDiscipline(d, skill.id) !== null)
98
+ .filter(
99
+ (d) =>
100
+ getSkillTypeForDiscipline({ discipline: d, skillId: skill.id }) !==
101
+ null,
102
+ )
99
103
  .map((d) => ({
100
104
  id: d.id,
101
105
  name: d.specialization || d.name,
102
- skillType: getSkillTypeForDiscipline(d, skill.id),
106
+ skillType: getSkillTypeForDiscipline({
107
+ discipline: d,
108
+ skillId: skill.id,
109
+ }),
103
110
  }));
104
111
 
105
112
  const relatedTracks = tracks
@@ -88,7 +88,10 @@ export function prepareTrackDetail(track, { skills, behaviours }) {
88
88
  const skillModifiers = track.skillModifiers
89
89
  ? Object.entries(track.skillModifiers).map(([key, modifier]) => {
90
90
  if (isCapability(key)) {
91
- const capabilitySkills = getSkillsByCapability(skills, key);
91
+ const capabilitySkills = getSkillsByCapability({
92
+ skills,
93
+ capability: key,
94
+ });
92
95
  return {
93
96
  id: key,
94
97
  name: key.charAt(0).toUpperCase() + key.slice(1),
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Agent builder install section
3
+ *
4
+ * Surfaces the ecosystem-tool install commands (Microsoft APM and
5
+ * `npx skills`) for the currently selected discipline/track pack. The packs
6
+ * themselves are emitted by `fit-pathway build` when
7
+ * `framework.distribution.siteUrl` is configured — see spec 320 and
8
+ * `products/pathway/src/commands/build-packs.js`. The pack name derivation
9
+ * here must stay in sync with that generator so the command points at an
10
+ * archive that actually exists on the deployed site.
11
+ */
12
+
13
+ import { code, div, h2, p, section } from "../lib/render.js";
14
+ import {
15
+ getDisciplineAbbreviation,
16
+ toKebabCase,
17
+ } from "@forwardimpact/libskill/agent";
18
+ import { createCommandPrompt } from "../components/command-prompt.js";
19
+
20
+ /** Stable id for the install section heading (for aria-labelledby). */
21
+ const INSTALL_HEADING_ID = "agent-builder-install-heading";
22
+
23
+ /**
24
+ * Derive the pack archive name for a discipline/track combination.
25
+ * Must match `build-packs.js` → `${abbrev}-${toKebabCase(track.id)}`.
26
+ * @param {{id: string}} discipline
27
+ * @param {{id: string}} track
28
+ * @returns {string}
29
+ */
30
+ export function getPackName(discipline, track) {
31
+ return `${getDisciplineAbbreviation(discipline.id)}-${toKebabCase(track.id)}`;
32
+ }
33
+
34
+ /**
35
+ * Normalize a site URL by stripping a trailing slash. Matches the
36
+ * normalization applied by `generatePacks` so the displayed URL lines up
37
+ * with the manifest entries.
38
+ * @param {string} siteUrl
39
+ * @returns {string}
40
+ */
41
+ function normalizeSiteUrl(siteUrl) {
42
+ return siteUrl.replace(/\/$/, "");
43
+ }
44
+
45
+ /**
46
+ * Build the `apm install` command for a specific pack archive. Targets the
47
+ * direct archive URL (rather than a registry-style shorthand) because it is
48
+ * the most durable path through APM's evolving resolution logic and matches
49
+ * the URL listed in the generated `apm.yml` manifest.
50
+ * @param {string} siteUrl
51
+ * @param {string} packName
52
+ * @returns {string}
53
+ */
54
+ export function getApmInstallCommand(siteUrl, packName) {
55
+ return `apm install ${normalizeSiteUrl(siteUrl)}/packs/${packName}.tar.gz`;
56
+ }
57
+
58
+ /**
59
+ * Build the `npx skills add` command that discovers the published pack
60
+ * registry at `<siteUrl>/.well-known/agent-skills/index.json`.
61
+ * @param {string} siteUrl
62
+ * @returns {string}
63
+ */
64
+ export function getSkillsAddCommand(siteUrl) {
65
+ return `npx skills add ${normalizeSiteUrl(siteUrl)}`;
66
+ }
67
+
68
+ /**
69
+ * Render the install section for the selected agent combination. Returns
70
+ * `null` when no site URL is configured (no packs have been published, so
71
+ * there is nothing meaningful to install) so the caller can skip rendering.
72
+ * @param {Object} params
73
+ * @param {{id: string}} params.discipline - Selected human discipline
74
+ * @param {{id: string}} params.track - Selected human track
75
+ * @param {string|undefined} params.siteUrl - Framework distribution site URL
76
+ * @returns {HTMLElement|null}
77
+ */
78
+ export function createInstallSection({ discipline, track, siteUrl }) {
79
+ if (!siteUrl) return null;
80
+
81
+ const packName = getPackName(discipline, track);
82
+ const apmCommand = getApmInstallCommand(siteUrl, packName);
83
+ const skillsCommand = getSkillsAddCommand(siteUrl);
84
+
85
+ return section(
86
+ {
87
+ className: "agent-install-section",
88
+ "aria-labelledby": INSTALL_HEADING_ID,
89
+ },
90
+ div(
91
+ { className: "agent-install-header" },
92
+ h2({ id: INSTALL_HEADING_ID }, "📦 Install This Agent Team"),
93
+ p(
94
+ { className: "text-muted agent-install-description" },
95
+ "Install the pre-built pack for this discipline × track combination " +
96
+ "directly through an ecosystem package manager. The pack contains " +
97
+ "the same stage agents, skills, team instructions, and Claude Code " +
98
+ "settings shown below — installed into your project's ",
99
+ code({}, ".claude/"),
100
+ " directory.",
101
+ ),
102
+ ),
103
+ div(
104
+ { className: "agent-install-commands" },
105
+ div(
106
+ { className: "agent-install-command" },
107
+ p({ className: "agent-install-command-label" }, "Microsoft APM"),
108
+ createCommandPrompt(apmCommand),
109
+ ),
110
+ div(
111
+ { className: "agent-install-command" },
112
+ p({ className: "agent-install-command-label" }, "npx skills"),
113
+ createCommandPrompt(skillsCommand),
114
+ ),
115
+ ),
116
+ );
117
+ }
@@ -10,8 +10,8 @@ import {
10
10
  deriveStageAgent,
11
11
  generateSkillMarkdown,
12
12
  deriveAgentSkills,
13
- deriveToolkit,
14
- } from "@forwardimpact/libskill";
13
+ } from "@forwardimpact/libskill/agent";
14
+ import { deriveToolkit } from "@forwardimpact/libskill/toolkit";
15
15
  import { getStageEmoji } from "../formatters/stage/shared.js";
16
16
  import { formatAgentProfile } from "../formatters/agent/profile.js";
17
17
  import {
@@ -115,7 +115,7 @@ function deriveSkillData(context) {
115
115
  const skillFiles = derivedSkills
116
116
  .map((d) => skills.find((s) => s.id === d.skillId))
117
117
  .filter((skill) => skill?.agent)
118
- .map((skill) => generateSkillMarkdown(skill, stages));
118
+ .map((skill) => generateSkillMarkdown({ skillData: skill, stages }));
119
119
 
120
120
  const toolkit = deriveToolkit({
121
121
  skillMatrix: derivedSkills,
@@ -18,7 +18,7 @@ import {
18
18
  } from "../lib/render.js";
19
19
  import { getState } from "../lib/state.js";
20
20
  import { loadAgentDataBrowser } from "../lib/yaml-loader.js";
21
- import { deriveReferenceLevel } from "@forwardimpact/libskill";
21
+ import { deriveReferenceLevel } from "@forwardimpact/libskill/agent";
22
22
  import {
23
23
  createSelectWithValue,
24
24
  createDisciplineSelect,
@@ -30,6 +30,7 @@ import {
30
30
  createSingleStagePreview,
31
31
  createHelpSection,
32
32
  } from "./agent-builder-preview.js";
33
+ import { createInstallSection } from "./agent-builder-install.js";
33
34
 
34
35
  /** All stages option value */
35
36
  const ALL_STAGES_VALUE = "all";
@@ -79,6 +80,7 @@ async function getTemplates() {
79
80
  */
80
81
  export async function renderAgentBuilder() {
81
82
  const { data } = getState();
83
+ const siteUrl = data.framework?.distribution?.siteUrl;
82
84
 
83
85
  // Show loading state
84
86
  render(
@@ -260,8 +262,27 @@ export async function renderAgentBuilder() {
260
262
  templates,
261
263
  };
262
264
 
265
+ // Install section (ecosystem-tool install commands) — appears above the
266
+ // preview cards so the install action is visible before users scroll
267
+ // through the generated files. Only rendered when the framework has a
268
+ // published distribution site URL, since the packs only exist at that
269
+ // URL after a `fit-pathway build`. Must come after the stage-validity
270
+ // guard below so an invalid stage id (e.g. from a stale bookmark) does
271
+ // not pair the install card with a "Stage not found" error.
272
+ function appendInstallSection() {
273
+ const installSection = createInstallSection({
274
+ discipline: humanDiscipline,
275
+ track: humanTrack,
276
+ siteUrl,
277
+ });
278
+ if (installSection) {
279
+ previewContainer.appendChild(installSection);
280
+ }
281
+ }
282
+
263
283
  // Generate preview based on stage selection
264
284
  if (stage === ALL_STAGES_VALUE) {
285
+ appendInstallSection();
265
286
  previewContainer.appendChild(createAllStagesPreview(context));
266
287
  } else {
267
288
  const stageObj = stages.find((s) => s.id === stage);
@@ -274,6 +295,7 @@ export async function renderAgentBuilder() {
274
295
  );
275
296
  return;
276
297
  }
298
+ appendInstallSection();
277
299
  previewContainer.appendChild(createSingleStagePreview(context, stageObj));
278
300
  }
279
301
  }
@@ -16,8 +16,8 @@ import {
16
16
  prepareCurrentJob,
17
17
  prepareCustomProgression,
18
18
  getDefaultTargetLevel,
19
- isValidCombination,
20
19
  } from "../formatters/progress/shared.js";
20
+ import { isValidJobCombination } from "@forwardimpact/libskill/derivation";
21
21
  import { buildComparisonResult } from "./progress-comparison.js";
22
22
 
23
23
  /**
@@ -188,7 +188,7 @@ function createComparisonSelectorsSection({
188
188
  for (const level of data.levels) {
189
189
  // Check trackless combination
190
190
  if (
191
- isValidCombination({ discipline: selectedDisc, level, track: null })
191
+ isValidJobCombination({ discipline: selectedDisc, level, track: null })
192
192
  ) {
193
193
  if (!validLevels.find((g) => g.id === level.id)) {
194
194
  validLevels.push(level);
@@ -197,7 +197,7 @@ function createComparisonSelectorsSection({
197
197
  }
198
198
  // Check each track combination
199
199
  for (const track of data.tracks) {
200
- if (isValidCombination({ discipline: selectedDisc, level, track })) {
200
+ if (isValidJobCombination({ discipline: selectedDisc, level, track })) {
201
201
  if (!validLevels.find((g) => g.id === level.id)) {
202
202
  validLevels.push(level);
203
203
  }
@@ -11,7 +11,7 @@ import { prepareSkillsList } from "../formatters/skill/shared.js";
11
11
  import { skillToDOM } from "../formatters/skill/dom.js";
12
12
  import { skillToCardConfig } from "../lib/card-mappers.js";
13
13
  import { getCapabilityEmoji, getConceptEmoji } from "@forwardimpact/map/levels";
14
- import { generateSkillMarkdown } from "@forwardimpact/libskill";
14
+ import { generateSkillMarkdown } from "@forwardimpact/libskill/agent";
15
15
  import { formatAgentSkill } from "../formatters/agent/skill.js";
16
16
 
17
17
  /** @type {string|null} Cached skill template */
@@ -101,7 +101,10 @@ export async function renderSkillDetail(params) {
101
101
  let agentSkillContent;
102
102
  if (skill.agent) {
103
103
  const template = await getSkillTemplate();
104
- const skillData = generateSkillMarkdown(skill, data.stages);
104
+ const skillData = generateSkillMarkdown({
105
+ skillData: skill,
106
+ stages: data.stages,
107
+ });
105
108
  agentSkillContent = formatAgentSkill(skillData, template);
106
109
  }
107
110
 
@@ -1,106 +0,0 @@
1
- /**
2
- * Behaviour formatting for microdata HTML output
3
- *
4
- * Generates clean, class-less HTML with microdata aligned with behaviour.schema.json
5
- * RDF vocab: https://www.forwardimpact.team/schema/rdf/
6
- */
7
-
8
- import {
9
- openTag,
10
- prop,
11
- propRaw,
12
- metaTag,
13
- section,
14
- dl,
15
- ul,
16
- escapeHtml,
17
- formatLevelName,
18
- htmlDocument,
19
- } from "../microdata-shared.js";
20
- import { prepareBehavioursList, prepareBehaviourDetail } from "./shared.js";
21
-
22
- /**
23
- * Format behaviour list as microdata HTML
24
- * @param {Array} behaviours - Raw behaviour entities
25
- * @returns {string} HTML with microdata
26
- */
27
- export function behaviourListToMicrodata(behaviours) {
28
- const { items } = prepareBehavioursList(behaviours);
29
-
30
- const content = items
31
- .map(
32
- (
33
- behaviour,
34
- ) => `${openTag("article", { itemtype: "Behaviour", itemid: `#${behaviour.id}` })}
35
- ${prop("h2", "name", behaviour.name)}
36
- ${prop("p", "description", behaviour.truncatedDescription)}
37
- </article>`,
38
- )
39
- .join("\n");
40
-
41
- return htmlDocument(
42
- "Behaviours",
43
- `<main>
44
- <h1>Behaviours</h1>
45
- ${content}
46
- </main>`,
47
- );
48
- }
49
-
50
- /**
51
- * Format behaviour detail as microdata HTML
52
- * @param {Object} behaviour - Raw behaviour entity
53
- * @param {Object} context - Additional context
54
- * @param {Array} context.drivers - All drivers
55
- * @returns {string} HTML with microdata
56
- */
57
- export function behaviourToMicrodata(behaviour, { drivers }) {
58
- const view = prepareBehaviourDetail(behaviour, { drivers });
59
-
60
- if (!view) return "";
61
-
62
- const sections = [];
63
-
64
- // Maturity descriptions - uses MaturityDescriptions itemtype
65
- const maturityPairs = Object.entries(view.maturityDescriptions).map(
66
- ([maturity, desc]) => ({
67
- term: formatLevelName(maturity),
68
- definition: desc,
69
- itemprop: `${maturity.replace(/_([a-z])/g, (_, c) => c.toUpperCase())}Description`,
70
- }),
71
- );
72
- sections.push(
73
- section(
74
- "Maturity Levels",
75
- `${openTag("div", { itemtype: "MaturityDescriptions", itemprop: "maturityDescriptions" })}
76
- ${dl(maturityPairs)}
77
- </div>`,
78
- 2,
79
- ),
80
- );
81
-
82
- // Related drivers
83
- if (view.relatedDrivers.length > 0) {
84
- const driverItems = view.relatedDrivers.map(
85
- (d) => `<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a>`,
86
- );
87
- sections.push(section("Linked to Drivers", ul(driverItems), 2));
88
- }
89
-
90
- const body = `<main>
91
- ${openTag("article", { itemtype: "Behaviour", itemid: `#${view.id}` })}
92
- ${prop("h1", "name", view.name)}
93
- ${metaTag("id", view.id)}
94
- ${propRaw(
95
- "div",
96
- "human",
97
- `${openTag("div", { itemtype: "BehaviourHumanSection" })}
98
- ${prop("p", "description", view.description)}
99
- ${sections.join("\n")}
100
- </div>`,
101
- )}
102
- </article>
103
- </main>`;
104
-
105
- return htmlDocument(view.name, body);
106
- }