@forwardimpact/libsyntheticrender 0.1.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/LICENSE +201 -0
- package/format.js +84 -0
- package/index.js +7 -0
- package/package.json +37 -0
- package/render/dataset-renderers.js +187 -0
- package/render/enricher.js +384 -0
- package/render/html.js +458 -0
- package/render/industry-data.js +434 -0
- package/render/link-assigner.js +350 -0
- package/render/markdown.js +126 -0
- package/render/pathway.js +124 -0
- package/render/raw.js +465 -0
- package/render/renderer.js +122 -0
- package/render/validate-links.js +329 -0
- package/templates/article.html +31 -0
- package/templates/blog-post.html +34 -0
- package/templates/blog.html +27 -0
- package/templates/briefing.md +20 -0
- package/templates/comments.html +15 -0
- package/templates/courses.html +37 -0
- package/templates/departments.html +29 -0
- package/templates/drugs.html +23 -0
- package/templates/events.html +38 -0
- package/templates/faq.html +22 -0
- package/templates/howto.html +8 -0
- package/templates/leadership.html +8 -0
- package/templates/ontology.md +34 -0
- package/templates/page.html +12 -0
- package/templates/platforms.html +23 -0
- package/templates/project-note.md +19 -0
- package/templates/projects.html +42 -0
- package/templates/readme.md +28 -0
- package/templates/reviews.html +19 -0
- package/templates/roles.html +11 -0
- package/templates/skill-reflection.md +18 -0
- package/templates/weekly.md +18 -0
- package/test/dataset-renderers.test.js +214 -0
- package/test/validate.test.js +396 -0
- package/validate.js +535 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Assigner — deterministic cross-link assignment between entities.
|
|
3
|
+
*
|
|
4
|
+
* Assigns drugs to projects, platforms to projects, people to events/courses,
|
|
5
|
+
* and builds all cross-entity relationships using seeded random for
|
|
6
|
+
* reproducibility.
|
|
7
|
+
*
|
|
8
|
+
* @module libuniverse/render/link-assigner
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createSeededRNG } from "@forwardimpact/libsyntheticgen/rng";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} LinkedEntities
|
|
15
|
+
* @property {object[]} drugs
|
|
16
|
+
* @property {object[]} platforms
|
|
17
|
+
* @property {object[]} projects - enriched with drug/platform/people links
|
|
18
|
+
* @property {object[]} courses - enriched with prereqs, attendees, platform/drug links
|
|
19
|
+
* @property {object[]} events - enriched with organizer, attendees, about links
|
|
20
|
+
* @property {object[]} blogPosts - enriched with author, about, mentions
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Assign cross-links between all entity types deterministically.
|
|
25
|
+
* @param {object} params
|
|
26
|
+
* @param {object[]} params.drugs
|
|
27
|
+
* @param {object[]} params.platforms
|
|
28
|
+
* @param {object[]} params.projects
|
|
29
|
+
* @param {object[]} params.people
|
|
30
|
+
* @param {object[]} params.teams
|
|
31
|
+
* @param {object[]} params.departments
|
|
32
|
+
* @param {string} params.domain
|
|
33
|
+
* @param {number} params.courseCount
|
|
34
|
+
* @param {number} params.eventCount
|
|
35
|
+
* @param {number} params.blogCount
|
|
36
|
+
* @param {string[]} [params.articleTopics=[]]
|
|
37
|
+
* @param {number} [params.seed=42]
|
|
38
|
+
* @returns {LinkedEntities}
|
|
39
|
+
*/
|
|
40
|
+
export function assignLinks({
|
|
41
|
+
drugs,
|
|
42
|
+
platforms,
|
|
43
|
+
projects,
|
|
44
|
+
people,
|
|
45
|
+
teams,
|
|
46
|
+
departments,
|
|
47
|
+
domain,
|
|
48
|
+
courseCount,
|
|
49
|
+
eventCount,
|
|
50
|
+
blogCount,
|
|
51
|
+
articleTopics = [],
|
|
52
|
+
seed = 42,
|
|
53
|
+
}) {
|
|
54
|
+
const rng = createSeededRNG(seed + 1000);
|
|
55
|
+
const base = `https://${domain}`;
|
|
56
|
+
|
|
57
|
+
// --- Project linking ---
|
|
58
|
+
const linkedProjects = projects.map((proj) => {
|
|
59
|
+
const projectTeams = teams.filter((t) => proj.teams.includes(t.id));
|
|
60
|
+
const projectPeople = people.filter((p) =>
|
|
61
|
+
projectTeams.some((t) => t.id === p.team_id),
|
|
62
|
+
);
|
|
63
|
+
const leader =
|
|
64
|
+
projectPeople.find((p) => p.is_manager) || rng.pick(projectPeople);
|
|
65
|
+
const members = rng
|
|
66
|
+
.shuffle(projectPeople)
|
|
67
|
+
.slice(0, Math.min(8, projectPeople.length));
|
|
68
|
+
|
|
69
|
+
// Assign drugs to drug/platform projects
|
|
70
|
+
const drugLinks =
|
|
71
|
+
proj.type === "drug"
|
|
72
|
+
? [drugs.find((d) => d.id === proj.id) || rng.pick(drugs)]
|
|
73
|
+
: [rng.pick(drugs)];
|
|
74
|
+
const platformLinks =
|
|
75
|
+
proj.type === "platform"
|
|
76
|
+
? [platforms.find((p) => p.id === proj.id) || rng.pick(platforms)]
|
|
77
|
+
: rng.shuffle(platforms).slice(0, 2);
|
|
78
|
+
const deptLinks = [...new Set(projectTeams.map((t) => t.department))]
|
|
79
|
+
.map((dId) => departments.find((d) => d.id === dId))
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...proj,
|
|
84
|
+
iri: `${base}/id/project/${proj.id}`,
|
|
85
|
+
leader,
|
|
86
|
+
members,
|
|
87
|
+
drugLinks,
|
|
88
|
+
platformLinks,
|
|
89
|
+
departmentLinks: deptLinks,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --- Course linking ---
|
|
94
|
+
const COURSE_CATALOG = [
|
|
95
|
+
{
|
|
96
|
+
id: "PHARM-101",
|
|
97
|
+
title: "Introduction to Drug Discovery",
|
|
98
|
+
category: "Drug Discovery",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "PHARM-201",
|
|
102
|
+
title: "Clinical Data Management",
|
|
103
|
+
category: "Clinical",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "PHARM-301",
|
|
107
|
+
title: "Advanced Pharmacology",
|
|
108
|
+
category: "Drug Discovery",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "GMP-101",
|
|
112
|
+
title: "GMP Compliance Essentials",
|
|
113
|
+
category: "Manufacturing",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "GMP-201",
|
|
117
|
+
title: "GMP Advanced Practices",
|
|
118
|
+
category: "Manufacturing",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "STAT-101",
|
|
122
|
+
title: "Pharmaceutical Statistics",
|
|
123
|
+
category: "Analytics",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "STAT-201",
|
|
127
|
+
title: "Biostatistics for Trials",
|
|
128
|
+
category: "Analytics",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "BIO-101",
|
|
132
|
+
title: "Molecular Biology Fundamentals",
|
|
133
|
+
category: "Genomics",
|
|
134
|
+
},
|
|
135
|
+
{ id: "BIO-201", title: "Genomics and Sequencing", category: "Genomics" },
|
|
136
|
+
{ id: "REG-101", title: "Regulatory Submissions", category: "Regulatory" },
|
|
137
|
+
{
|
|
138
|
+
id: "DATA-101",
|
|
139
|
+
title: "Data Engineering for Pharma",
|
|
140
|
+
category: "Data Infrastructure",
|
|
141
|
+
},
|
|
142
|
+
{ id: "DATA-201", title: "Machine Learning Pipelines", category: "AI/ML" },
|
|
143
|
+
{ id: "AI-101", title: "AI in Drug Development", category: "AI/ML" },
|
|
144
|
+
{ id: "QA-101", title: "Quality Assurance Methods", category: "Quality" },
|
|
145
|
+
{
|
|
146
|
+
id: "SEC-101",
|
|
147
|
+
title: "Cloud Infrastructure Security",
|
|
148
|
+
category: "Security",
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const coursesToGenerate = Math.min(courseCount, COURSE_CATALOG.length);
|
|
153
|
+
const linkedCourses = COURSE_CATALOG.slice(0, coursesToGenerate).map(
|
|
154
|
+
(course, i) => {
|
|
155
|
+
// Build prerequisite chains: 201 courses require 101 in same category
|
|
156
|
+
const prereqs = [];
|
|
157
|
+
if (course.id.includes("201")) {
|
|
158
|
+
const base101 = COURSE_CATALOG.find(
|
|
159
|
+
(c) => c.id.includes("101") && c.category === course.category,
|
|
160
|
+
);
|
|
161
|
+
if (base101) prereqs.push(base101.id);
|
|
162
|
+
}
|
|
163
|
+
if (course.id.includes("301")) {
|
|
164
|
+
const base201 = COURSE_CATALOG.find(
|
|
165
|
+
(c) => c.id.includes("201") && c.category === course.category,
|
|
166
|
+
);
|
|
167
|
+
if (base201) prereqs.push(base201.id);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Assign platform and drug links by category
|
|
171
|
+
const relatedPlatforms = platforms.filter(
|
|
172
|
+
(p) => p.category === course.category,
|
|
173
|
+
);
|
|
174
|
+
const platformLink =
|
|
175
|
+
relatedPlatforms.length > 0
|
|
176
|
+
? rng.pick(relatedPlatforms)
|
|
177
|
+
: rng.pick(platforms);
|
|
178
|
+
const drugLink = rng.pick(drugs);
|
|
179
|
+
|
|
180
|
+
// Assign attendees
|
|
181
|
+
const attendees = rng.shuffle(people).slice(0, rng.randomInt(3, 8));
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...course,
|
|
185
|
+
index: i + 1,
|
|
186
|
+
iri: `${base}/id/course/${course.id}`,
|
|
187
|
+
prerequisites: prereqs,
|
|
188
|
+
prerequisiteIris: prereqs.map((pid) => `${base}/id/course/${pid}`),
|
|
189
|
+
platformLink,
|
|
190
|
+
drugLink,
|
|
191
|
+
attendees,
|
|
192
|
+
date: `2025-${String((i % 12) + 1).padStart(2, "0")}-01`,
|
|
193
|
+
orgName: "BioNova",
|
|
194
|
+
orgIri: `${base}/org/headquarters`,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// --- Event linking ---
|
|
200
|
+
const EVENT_CATALOG = [
|
|
201
|
+
"Engineering All-Hands",
|
|
202
|
+
"Tech Talk: AI in Pharma",
|
|
203
|
+
"Hackathon 2025",
|
|
204
|
+
"Architecture Review Board",
|
|
205
|
+
"Sprint Demo Day",
|
|
206
|
+
"Drug Discovery Symposium",
|
|
207
|
+
"Compliance Training Day",
|
|
208
|
+
"Platform Migration Workshop",
|
|
209
|
+
"Data Science Summit",
|
|
210
|
+
"Security Awareness Week",
|
|
211
|
+
];
|
|
212
|
+
const eventsToGenerate = Math.min(eventCount, EVENT_CATALOG.length);
|
|
213
|
+
const linkedEvents = Array.from({ length: eventsToGenerate }, (_, i) => {
|
|
214
|
+
const organizer = rng.pick(people.filter((p) => p.is_manager));
|
|
215
|
+
const attendees = rng.shuffle(people).slice(0, rng.randomInt(5, 15));
|
|
216
|
+
const aboutProjects = rng
|
|
217
|
+
.shuffle(linkedProjects)
|
|
218
|
+
.slice(0, rng.randomInt(1, 3));
|
|
219
|
+
const aboutDrugs = rng.shuffle(drugs).slice(0, rng.randomInt(0, 2));
|
|
220
|
+
const aboutPlatforms = rng.shuffle(platforms).slice(0, rng.randomInt(1, 2));
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
index: i + 1,
|
|
224
|
+
title: EVENT_CATALOG[i % EVENT_CATALOG.length],
|
|
225
|
+
iri: `${base}/id/event/event-${i + 1}`,
|
|
226
|
+
organizer,
|
|
227
|
+
attendees,
|
|
228
|
+
aboutProjects,
|
|
229
|
+
aboutDrugs,
|
|
230
|
+
aboutPlatforms,
|
|
231
|
+
date: `2025-${String((i % 12) + 1).padStart(2, "0")}-15`,
|
|
232
|
+
location: "Cambridge, MA",
|
|
233
|
+
eventStatus: "EventScheduled",
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// --- Blog post linking ---
|
|
238
|
+
const BLOG_TOPICS = [
|
|
239
|
+
"AI-Driven Drug Discovery at BioNova",
|
|
240
|
+
"Our Journey to Cloud-Native Infrastructure",
|
|
241
|
+
"Building a Culture of Engineering Excellence",
|
|
242
|
+
"How We Scaled Our Clinical Data Pipeline",
|
|
243
|
+
"Machine Learning in Pharmaceutical Manufacturing",
|
|
244
|
+
"Security Best Practices for Life Sciences",
|
|
245
|
+
"Developer Experience: What We Learned",
|
|
246
|
+
"The Future of Genomics Technology",
|
|
247
|
+
"Cross-Functional Collaboration in Drug Development",
|
|
248
|
+
"Real-World Evidence and Data Analytics",
|
|
249
|
+
"Platform Engineering at Scale",
|
|
250
|
+
"Regulatory Technology Innovation",
|
|
251
|
+
"Quality Engineering in a GMP Environment",
|
|
252
|
+
"Data Mesh Architecture for Pharma",
|
|
253
|
+
"Open Source in Pharmaceutical R&D",
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const linkedBlogPosts = Array.from({ length: blogCount }, (_, i) => {
|
|
257
|
+
const author = rng.pick(people);
|
|
258
|
+
const aboutDrugs = rng.shuffle(drugs).slice(0, rng.randomInt(1, 3));
|
|
259
|
+
const aboutPlatforms = rng.shuffle(platforms).slice(0, rng.randomInt(1, 3));
|
|
260
|
+
const aboutProjects = rng
|
|
261
|
+
.shuffle(linkedProjects)
|
|
262
|
+
.slice(0, rng.randomInt(0, 2));
|
|
263
|
+
const mentionsPeople = rng
|
|
264
|
+
.shuffle(people.filter((p) => p.id !== author.id))
|
|
265
|
+
.slice(0, rng.randomInt(1, 4));
|
|
266
|
+
|
|
267
|
+
const keywords = [
|
|
268
|
+
...aboutDrugs.map((d) => d.name),
|
|
269
|
+
...aboutPlatforms.map((p) => p.name),
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
index: i + 1,
|
|
274
|
+
headline: BLOG_TOPICS[i % BLOG_TOPICS.length],
|
|
275
|
+
iri: `${base}/id/blog/blog-${i + 1}`,
|
|
276
|
+
identifier: `BLOG-2025-${String(i + 1).padStart(3, "0")}`,
|
|
277
|
+
author,
|
|
278
|
+
aboutDrugs,
|
|
279
|
+
aboutPlatforms,
|
|
280
|
+
aboutProjects,
|
|
281
|
+
mentionsPeople,
|
|
282
|
+
keywords: keywords.join(", "),
|
|
283
|
+
date: `2025-${String(Math.floor(i / 2) + 1).padStart(2, "0")}-${String(10 + (i % 20)).padStart(2, "0")}`,
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// --- Article linking ---
|
|
288
|
+
const TOPIC_ENTITY_MAP = {
|
|
289
|
+
clinical: {
|
|
290
|
+
drugFilter: (d) =>
|
|
291
|
+
["oncora", "cardioguard", "immunex-pro"].includes(d.id),
|
|
292
|
+
platformCategory: "Clinical",
|
|
293
|
+
},
|
|
294
|
+
data_ai: { drugFilter: () => true, platformCategory: "AI/ML" },
|
|
295
|
+
drug_discovery: {
|
|
296
|
+
drugFilter: () => true,
|
|
297
|
+
platformCategory: "Drug Discovery",
|
|
298
|
+
},
|
|
299
|
+
manufacturing: {
|
|
300
|
+
drugFilter: (d) => ["genova-rna", "dermashield"].includes(d.id),
|
|
301
|
+
platformCategory: "Manufacturing",
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const linkedArticles = articleTopics.map((topic, i) => {
|
|
306
|
+
const topicTitle = topic
|
|
307
|
+
.replace(/_/g, " ")
|
|
308
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
309
|
+
const mapping = TOPIC_ENTITY_MAP[topic] || {
|
|
310
|
+
drugFilter: () => true,
|
|
311
|
+
platformCategory: null,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const drugLinks = drugs.filter(mapping.drugFilter).slice(0, 3);
|
|
315
|
+
const platformLinks = mapping.platformCategory
|
|
316
|
+
? platforms
|
|
317
|
+
.filter((p) => p.category === mapping.platformCategory)
|
|
318
|
+
.slice(0, 3)
|
|
319
|
+
: rng.shuffle(platforms).slice(0, 3);
|
|
320
|
+
const projectLinks = rng.shuffle(linkedProjects).slice(0, 2);
|
|
321
|
+
const authorLinks = rng.shuffle(people).slice(0, 2);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
topic,
|
|
325
|
+
title: topicTitle,
|
|
326
|
+
iri: `${base}/id/article/${topic}`,
|
|
327
|
+
identifier: `ART-${topic.toUpperCase().replace(/_/g, "-")}-001`,
|
|
328
|
+
author: authorLinks[0],
|
|
329
|
+
authorLinks,
|
|
330
|
+
drugLinks,
|
|
331
|
+
platformLinks,
|
|
332
|
+
projectLinks,
|
|
333
|
+
keywords: [
|
|
334
|
+
...drugLinks.map((d) => d.name),
|
|
335
|
+
...platformLinks.map((p) => p.name),
|
|
336
|
+
].join(", "),
|
|
337
|
+
date: `2025-${String((i % 12) + 1).padStart(2, "0")}-01`,
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
drugs,
|
|
343
|
+
platforms,
|
|
344
|
+
projects: linkedProjects,
|
|
345
|
+
courses: linkedCourses,
|
|
346
|
+
events: linkedEvents,
|
|
347
|
+
blogPosts: linkedBlogPosts,
|
|
348
|
+
articles: linkedArticles,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Renderer — generates personal knowledge-base content
|
|
3
|
+
* for Basecamp personas.
|
|
4
|
+
*
|
|
5
|
+
* Uses TemplateLoader from libtemplate for all output.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const SKILL_NAMES = [
|
|
9
|
+
"version_control",
|
|
10
|
+
"code_review",
|
|
11
|
+
"testing",
|
|
12
|
+
"deployment",
|
|
13
|
+
"monitoring",
|
|
14
|
+
"documentation",
|
|
15
|
+
"architecture",
|
|
16
|
+
"security",
|
|
17
|
+
];
|
|
18
|
+
const PROFICIENCIES = [
|
|
19
|
+
"awareness",
|
|
20
|
+
"foundational",
|
|
21
|
+
"working",
|
|
22
|
+
"practitioner",
|
|
23
|
+
"expert",
|
|
24
|
+
];
|
|
25
|
+
const LEVEL_IDX = { L1: 0, L2: 1, L3: 2, L4: 3, L5: 4 };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render Markdown files for Basecamp personas.
|
|
29
|
+
* @param {object} entities
|
|
30
|
+
* @param {Map<string,string>} prose
|
|
31
|
+
* @param {import('@forwardimpact/libtemplate/loader').TemplateLoader} templates - Template loader
|
|
32
|
+
* @returns {Map<string,string>} path → Markdown content
|
|
33
|
+
*/
|
|
34
|
+
export function renderMarkdown(entities, prose, templates) {
|
|
35
|
+
if (!templates) throw new Error("templates is required");
|
|
36
|
+
const files = new Map();
|
|
37
|
+
const basecampContent = entities.content.find(
|
|
38
|
+
(c) => c.id === "basecamp_markdown",
|
|
39
|
+
);
|
|
40
|
+
if (!basecampContent) return files;
|
|
41
|
+
|
|
42
|
+
const personaCount = basecampContent.personas || 0;
|
|
43
|
+
const personaLevels = basecampContent.persona_levels || ["L2", "L3", "L4"];
|
|
44
|
+
const candidates = entities.people.filter((p) =>
|
|
45
|
+
personaLevels.includes(p.level),
|
|
46
|
+
);
|
|
47
|
+
const personas = candidates.slice(0, personaCount);
|
|
48
|
+
const date = new Date().toISOString().split("T")[0];
|
|
49
|
+
|
|
50
|
+
for (const person of personas) {
|
|
51
|
+
const team = entities.teams.find((t) => t.id === person.team_id);
|
|
52
|
+
const dept = entities.departments.find((d) => d.id === person.department);
|
|
53
|
+
const prefix = `personas/${person.id}`;
|
|
54
|
+
const baseIdx = LEVEL_IDX[person.level] || 0;
|
|
55
|
+
|
|
56
|
+
const ctx = {
|
|
57
|
+
personId: person.id,
|
|
58
|
+
personName: person.name,
|
|
59
|
+
discipline: person.discipline,
|
|
60
|
+
level: person.level,
|
|
61
|
+
email: person.email,
|
|
62
|
+
teamName: team?.name || "Unknown",
|
|
63
|
+
deptName: dept?.name || "Unknown",
|
|
64
|
+
teamSize: team?.size || "?",
|
|
65
|
+
isManager: person.is_manager ? "Yes" : "No",
|
|
66
|
+
date,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
files.set(
|
|
70
|
+
`${prefix}/daily-briefing.md`,
|
|
71
|
+
templates.render("briefing.md", {
|
|
72
|
+
...ctx,
|
|
73
|
+
briefing:
|
|
74
|
+
prose.get(`briefing_${person.id}`) ||
|
|
75
|
+
`Morning briefing for ${person.name}, ${person.discipline} ${person.level} on ${team?.name || "their team"}.`,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
files.set(
|
|
80
|
+
`${prefix}/weekly-notes.md`,
|
|
81
|
+
templates.render("weekly.md", {
|
|
82
|
+
...ctx,
|
|
83
|
+
weeklyNote:
|
|
84
|
+
prose.get(`weekly_${person.id}`) ||
|
|
85
|
+
`Weekly reflection for ${person.name}.`,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
files.set(
|
|
90
|
+
`${prefix}/skill-reflections.md`,
|
|
91
|
+
templates.render("skill-reflection.md", {
|
|
92
|
+
...ctx,
|
|
93
|
+
skills: SKILL_NAMES.map((s) => ({
|
|
94
|
+
label: s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
95
|
+
proficiency: PROFICIENCIES[Math.min(4, baseIdx)],
|
|
96
|
+
})),
|
|
97
|
+
growthNote:
|
|
98
|
+
baseIdx < 4
|
|
99
|
+
? `Target: Move from ${PROFICIENCIES[baseIdx]} to ${PROFICIENCIES[baseIdx + 1]} in key areas.`
|
|
100
|
+
: "Currently at expert level. Focus on mentoring and knowledge sharing.",
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const personProjects = entities.projects.filter((proj) =>
|
|
105
|
+
(proj.teams || []).includes(person.team_id),
|
|
106
|
+
);
|
|
107
|
+
for (const proj of personProjects) {
|
|
108
|
+
files.set(
|
|
109
|
+
`${prefix}/project-${proj.id}.md`,
|
|
110
|
+
templates.render("project-note.md", {
|
|
111
|
+
...ctx,
|
|
112
|
+
projectId: proj.id,
|
|
113
|
+
projectName: proj.name,
|
|
114
|
+
projectType: proj.type,
|
|
115
|
+
timeline_start: proj.timeline_start,
|
|
116
|
+
timeline_end: proj.timeline_end,
|
|
117
|
+
note:
|
|
118
|
+
prose.get(`project_note_${person.id}_${proj.id}`) ||
|
|
119
|
+
`Notes on ${proj.name} from ${person.name}'s perspective.`,
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pathway Renderer — converts LLM-generated JSON to YAML files.
|
|
3
|
+
*
|
|
4
|
+
* @module libuniverse/render/pathway
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import YAML from "yaml";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render pathway YAML files from generated entity data.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} pathwayData - Keyed by entity type
|
|
13
|
+
* @returns {Map<string,string>} path → YAML content
|
|
14
|
+
*/
|
|
15
|
+
export function renderPathway(pathwayData) {
|
|
16
|
+
const files = new Map();
|
|
17
|
+
|
|
18
|
+
// Single-file entities
|
|
19
|
+
if (pathwayData.framework) {
|
|
20
|
+
files.set("framework.yaml", toYaml(pathwayData.framework, "framework"));
|
|
21
|
+
}
|
|
22
|
+
if (pathwayData.levels) {
|
|
23
|
+
files.set("levels.yaml", toYaml(pathwayData.levels, "levels"));
|
|
24
|
+
}
|
|
25
|
+
if (pathwayData.stages) {
|
|
26
|
+
files.set("stages.yaml", toYaml(pathwayData.stages, "stages"));
|
|
27
|
+
}
|
|
28
|
+
if (pathwayData.drivers) {
|
|
29
|
+
files.set("drivers.yaml", toYaml(pathwayData.drivers, "drivers"));
|
|
30
|
+
}
|
|
31
|
+
if (pathwayData.selfAssessments) {
|
|
32
|
+
files.set(
|
|
33
|
+
"self-assessments.yaml",
|
|
34
|
+
toYaml(pathwayData.selfAssessments, "self-assessments"),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Multi-file entities (one YAML per entity)
|
|
39
|
+
if (pathwayData.capabilities) {
|
|
40
|
+
const capIds = [];
|
|
41
|
+
for (const cap of pathwayData.capabilities) {
|
|
42
|
+
const id = cap._id || cap.id;
|
|
43
|
+
const data = stripInternal(cap);
|
|
44
|
+
files.set(`capabilities/${id}.yaml`, toYaml(data, "capability"));
|
|
45
|
+
capIds.push(id);
|
|
46
|
+
}
|
|
47
|
+
files.set("capabilities/_index.yaml", renderIndex(capIds));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (pathwayData.behaviours) {
|
|
51
|
+
const behIds = [];
|
|
52
|
+
for (const beh of pathwayData.behaviours) {
|
|
53
|
+
const id = beh._id || beh.id;
|
|
54
|
+
const data = stripInternal(beh);
|
|
55
|
+
files.set(`behaviours/${id}.yaml`, toYaml(data, "behaviour"));
|
|
56
|
+
behIds.push(id);
|
|
57
|
+
}
|
|
58
|
+
files.set("behaviours/_index.yaml", renderIndex(behIds));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (pathwayData.disciplines) {
|
|
62
|
+
const discIds = [];
|
|
63
|
+
for (const disc of pathwayData.disciplines) {
|
|
64
|
+
const id = disc._id || disc.id;
|
|
65
|
+
const data = stripInternal(disc);
|
|
66
|
+
files.set(`disciplines/${id}.yaml`, toYaml(data, "discipline"));
|
|
67
|
+
discIds.push(id);
|
|
68
|
+
}
|
|
69
|
+
files.set("disciplines/_index.yaml", renderIndex(discIds));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (pathwayData.tracks) {
|
|
73
|
+
const trackIds = [];
|
|
74
|
+
for (const track of pathwayData.tracks) {
|
|
75
|
+
const id = track._id || track.id;
|
|
76
|
+
const data = stripInternal(track);
|
|
77
|
+
files.set(`tracks/${id}.yaml`, toYaml(data, "track"));
|
|
78
|
+
trackIds.push(id);
|
|
79
|
+
}
|
|
80
|
+
files.set("tracks/_index.yaml", renderIndex(trackIds));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert data to YAML with a schema comment header.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} data
|
|
90
|
+
* @param {string} schemaName
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
function toYaml(data, schemaName) {
|
|
94
|
+
const schemaComment = `# yaml-language-server: $schema=https://www.forwardimpact.team/schema/json/${schemaName}.schema.json\n\n`;
|
|
95
|
+
return schemaComment + YAML.stringify(data, { lineWidth: 80 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Render an _index.yaml file.
|
|
100
|
+
*
|
|
101
|
+
* @param {string[]} ids
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
function renderIndex(ids) {
|
|
105
|
+
const content = [
|
|
106
|
+
"# Auto-generated index for browser loading",
|
|
107
|
+
"# Do not edit manually - regenerate with: npx pathway --generate-index",
|
|
108
|
+
];
|
|
109
|
+
return content.join("\n") + "\n" + YAML.stringify({ files: ids });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Strip internal properties (prefixed with _) from an object.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} obj
|
|
116
|
+
* @returns {object}
|
|
117
|
+
*/
|
|
118
|
+
function stripInternal(obj) {
|
|
119
|
+
const result = {};
|
|
120
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
121
|
+
if (!key.startsWith("_")) result[key] = value;
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|