@forwardimpact/libsyntheticgen 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/dsl/index.js +36 -0
- package/dsl/parser.js +728 -0
- package/dsl/tokenizer.js +282 -0
- package/engine/activity.js +956 -0
- package/engine/entities.js +144 -0
- package/engine/names.js +290 -0
- package/engine/prose-keys.js +182 -0
- package/engine/rng.js +43 -0
- package/engine/tier0.js +63 -0
- package/index.js +7 -0
- package/package.json +35 -0
- package/test/activity.test.js +322 -0
- package/test/faker.test.js +98 -0
- package/test/parser-dataset.test.js +142 -0
- package/test/parser.test.js +596 -0
- package/test/rng.test.js +236 -0
- package/test/sdv.test.js +67 -0
- package/test/synthea.test.js +95 -0
- package/test/tokenizer.test.js +266 -0
- package/tools/faker.js +83 -0
- package/tools/sdv.js +93 -0
- package/tools/sdv_generate.py +29 -0
- package/tools/synthea.js +126 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity generation — builds orgs, departments, teams, people, projects.
|
|
3
|
+
*
|
|
4
|
+
* @module libuniverse/engine/entities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
GREEK_NAMES,
|
|
9
|
+
MANAGER_NAMES,
|
|
10
|
+
toGithubUsername,
|
|
11
|
+
toEmail,
|
|
12
|
+
} from "./names.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build all entities from AST and RNG.
|
|
16
|
+
* @param {import('../dsl/parser.js').UniverseAST} ast
|
|
17
|
+
* @param {import('./rng.js').SeededRNG} rng
|
|
18
|
+
* @returns {{ orgs: object[], departments: object[], teams: object[], people: object[], projects: object[] }}
|
|
19
|
+
*/
|
|
20
|
+
export function buildEntities(ast, rng) {
|
|
21
|
+
const domain = ast.domain;
|
|
22
|
+
const orgs = ast.orgs.map((o) => ({
|
|
23
|
+
...o,
|
|
24
|
+
iri: `https://${domain}/org/${o.id}`,
|
|
25
|
+
}));
|
|
26
|
+
const departments = ast.departments.map((d) => ({
|
|
27
|
+
...d,
|
|
28
|
+
iri: `https://${domain}/department/${d.id}`,
|
|
29
|
+
}));
|
|
30
|
+
const teams = ast.teams.map((t) => ({
|
|
31
|
+
...t,
|
|
32
|
+
repos: t.repos || [],
|
|
33
|
+
iri: `https://${domain}/team/${t.id}`,
|
|
34
|
+
getdx_team_id: `gdx_team_${t.id}`,
|
|
35
|
+
}));
|
|
36
|
+
const people = generatePeople(ast, rng, teams, domain);
|
|
37
|
+
const projects = ast.projects.map((p) => ({
|
|
38
|
+
...p,
|
|
39
|
+
teams: p.teams || [],
|
|
40
|
+
phase: p.phase || null,
|
|
41
|
+
prose_topic: p.prose_topic || null,
|
|
42
|
+
prose_tone: p.prose_tone || null,
|
|
43
|
+
iri: `https://${domain}/project/${p.id}`,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
return { orgs, departments, teams, people, projects };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function generatePeople(ast, rng, teams, domain) {
|
|
50
|
+
const { count, distribution, disciplines } = ast.people;
|
|
51
|
+
const people = [];
|
|
52
|
+
const usedNames = new Set();
|
|
53
|
+
|
|
54
|
+
// Reserve manager names
|
|
55
|
+
const managerAssignments = new Map();
|
|
56
|
+
for (const team of teams) {
|
|
57
|
+
if (team.manager) {
|
|
58
|
+
const name = MANAGER_NAMES[team.manager] || team.manager;
|
|
59
|
+
managerAssignments.set(team.id, name);
|
|
60
|
+
usedNames.add(name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const levelKeys = Object.keys(distribution);
|
|
65
|
+
const levelWeights = Object.values(distribution);
|
|
66
|
+
const discKeys = Object.keys(disciplines);
|
|
67
|
+
const discWeights = Object.values(disciplines);
|
|
68
|
+
const available = rng.shuffle(GREEK_NAMES.filter((n) => !usedNames.has(n)));
|
|
69
|
+
|
|
70
|
+
// Create managers
|
|
71
|
+
for (const team of teams) {
|
|
72
|
+
if (!team.manager) continue;
|
|
73
|
+
const name = managerAssignments.get(team.id);
|
|
74
|
+
people.push(
|
|
75
|
+
makePerson(
|
|
76
|
+
name,
|
|
77
|
+
rng.pick(["L3", "L4", "L5"]),
|
|
78
|
+
discKeys[rng.weightedPick(discWeights)] || "software_engineering",
|
|
79
|
+
team,
|
|
80
|
+
domain,
|
|
81
|
+
true,
|
|
82
|
+
null,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fill remaining people
|
|
88
|
+
let idx = 0;
|
|
89
|
+
while (people.length < count && idx < available.length) {
|
|
90
|
+
const name = available[idx++];
|
|
91
|
+
const level = levelKeys[rng.weightedPick(levelWeights)];
|
|
92
|
+
const disc = discKeys[rng.weightedPick(discWeights)];
|
|
93
|
+
const team = rng.pick(teams);
|
|
94
|
+
const mgr = people.find((p) => p.is_manager && p.team_id === team.id);
|
|
95
|
+
people.push(
|
|
96
|
+
makePerson(
|
|
97
|
+
name,
|
|
98
|
+
level,
|
|
99
|
+
disc,
|
|
100
|
+
team,
|
|
101
|
+
domain,
|
|
102
|
+
false,
|
|
103
|
+
mgr?.email || null,
|
|
104
|
+
`2023-${pad2(rng.randomInt(1, 12))}-${pad2(rng.randomInt(1, 28))}`,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return people;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function makePerson(
|
|
113
|
+
name,
|
|
114
|
+
level,
|
|
115
|
+
discipline,
|
|
116
|
+
team,
|
|
117
|
+
domain,
|
|
118
|
+
isManager,
|
|
119
|
+
managerEmail,
|
|
120
|
+
hireDate = "2023-01-15",
|
|
121
|
+
) {
|
|
122
|
+
const id = name.toLowerCase().replace(/\s+/g, "-");
|
|
123
|
+
return {
|
|
124
|
+
id,
|
|
125
|
+
name,
|
|
126
|
+
email: toEmail(name, domain),
|
|
127
|
+
github: toGithubUsername(name),
|
|
128
|
+
github_username: toGithubUsername(name),
|
|
129
|
+
discipline,
|
|
130
|
+
level,
|
|
131
|
+
track: null,
|
|
132
|
+
team_id: team.id,
|
|
133
|
+
department: team.department,
|
|
134
|
+
is_manager: isManager,
|
|
135
|
+
manager_email: managerEmail,
|
|
136
|
+
hire_date: hireDate,
|
|
137
|
+
iri: `https://${domain}/person/${id}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @param {number} n */
|
|
142
|
+
function pad2(n) {
|
|
143
|
+
return String(n).padStart(2, "0");
|
|
144
|
+
}
|
package/engine/names.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Greek mythology name pool for synthetic people generation.
|
|
3
|
+
* Names are deterministically assigned based on seed order.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const GREEK_NAMES = [
|
|
7
|
+
// Olympians and major deities
|
|
8
|
+
"Zeus",
|
|
9
|
+
"Hera",
|
|
10
|
+
"Poseidon",
|
|
11
|
+
"Demeter",
|
|
12
|
+
"Athena",
|
|
13
|
+
"Apollo",
|
|
14
|
+
"Artemis",
|
|
15
|
+
"Ares",
|
|
16
|
+
"Aphrodite",
|
|
17
|
+
"Hephaestus",
|
|
18
|
+
"Hermes",
|
|
19
|
+
"Dionysus",
|
|
20
|
+
"Hades",
|
|
21
|
+
"Persephone",
|
|
22
|
+
"Hestia",
|
|
23
|
+
// Titans
|
|
24
|
+
"Prometheus",
|
|
25
|
+
"Epimetheus",
|
|
26
|
+
"Atlas",
|
|
27
|
+
"Cronus",
|
|
28
|
+
"Rhea",
|
|
29
|
+
"Hyperion",
|
|
30
|
+
"Theia",
|
|
31
|
+
"Coeus",
|
|
32
|
+
"Phoebe",
|
|
33
|
+
"Oceanus",
|
|
34
|
+
"Tethys",
|
|
35
|
+
"Mnemosyne",
|
|
36
|
+
"Themis",
|
|
37
|
+
"Crius",
|
|
38
|
+
"Iapetus",
|
|
39
|
+
// Heroes and mortals
|
|
40
|
+
"Achilles",
|
|
41
|
+
"Odysseus",
|
|
42
|
+
"Perseus",
|
|
43
|
+
"Heracles",
|
|
44
|
+
"Theseus",
|
|
45
|
+
"Jason",
|
|
46
|
+
"Orpheus",
|
|
47
|
+
"Atalanta",
|
|
48
|
+
"Bellerophon",
|
|
49
|
+
"Cadmus",
|
|
50
|
+
"Daedalus",
|
|
51
|
+
"Icarus",
|
|
52
|
+
"Minos",
|
|
53
|
+
"Ariadne",
|
|
54
|
+
"Andromeda",
|
|
55
|
+
"Antigone",
|
|
56
|
+
"Electra",
|
|
57
|
+
"Medea",
|
|
58
|
+
"Penelope",
|
|
59
|
+
"Telemachus",
|
|
60
|
+
"Nestor",
|
|
61
|
+
"Ajax",
|
|
62
|
+
"Agamemnon",
|
|
63
|
+
"Menelaus",
|
|
64
|
+
"Helen",
|
|
65
|
+
"Paris",
|
|
66
|
+
"Hector",
|
|
67
|
+
"Priam",
|
|
68
|
+
"Cassandra",
|
|
69
|
+
"Hecuba",
|
|
70
|
+
// Lesser deities and spirits
|
|
71
|
+
"Nike",
|
|
72
|
+
"Iris",
|
|
73
|
+
"Eros",
|
|
74
|
+
"Psyche",
|
|
75
|
+
"Tyche",
|
|
76
|
+
"Nemesis",
|
|
77
|
+
"Hecate",
|
|
78
|
+
"Selene",
|
|
79
|
+
"Eos",
|
|
80
|
+
"Helios",
|
|
81
|
+
"Pan",
|
|
82
|
+
"Triton",
|
|
83
|
+
"Proteus",
|
|
84
|
+
"Nereus",
|
|
85
|
+
"Amphitrite",
|
|
86
|
+
"Galatea",
|
|
87
|
+
"Thetis",
|
|
88
|
+
"Calliope",
|
|
89
|
+
"Clio",
|
|
90
|
+
"Erato",
|
|
91
|
+
"Euterpe",
|
|
92
|
+
"Melpomene",
|
|
93
|
+
"Polyhymnia",
|
|
94
|
+
"Terpsichore",
|
|
95
|
+
"Thalia",
|
|
96
|
+
"Urania",
|
|
97
|
+
// Physicians and wisdom
|
|
98
|
+
"Asclepius",
|
|
99
|
+
"Hygieia",
|
|
100
|
+
"Panacea",
|
|
101
|
+
"Chiron",
|
|
102
|
+
"Metis",
|
|
103
|
+
// Egyptian-Greek crossover (Thoth)
|
|
104
|
+
"Thoth",
|
|
105
|
+
"Chronos",
|
|
106
|
+
"Astraea",
|
|
107
|
+
"Plutus",
|
|
108
|
+
// More heroes and mythological
|
|
109
|
+
"Patroclus",
|
|
110
|
+
"Diomedes",
|
|
111
|
+
"Philoctetes",
|
|
112
|
+
"Pyrrhus",
|
|
113
|
+
"Aeneas",
|
|
114
|
+
"Deucalion",
|
|
115
|
+
"Pyrrha",
|
|
116
|
+
"Ganymede",
|
|
117
|
+
"Endymion",
|
|
118
|
+
"Narcissus",
|
|
119
|
+
"Echo",
|
|
120
|
+
"Adonis",
|
|
121
|
+
"Actaeon",
|
|
122
|
+
"Orion",
|
|
123
|
+
"Callisto",
|
|
124
|
+
"Io",
|
|
125
|
+
"Europa",
|
|
126
|
+
"Leda",
|
|
127
|
+
"Danae",
|
|
128
|
+
"Semele",
|
|
129
|
+
"Alcmene",
|
|
130
|
+
"Amphion",
|
|
131
|
+
"Zethus",
|
|
132
|
+
"Castor",
|
|
133
|
+
"Pollux",
|
|
134
|
+
"Clytemnestra",
|
|
135
|
+
"Iphigenia",
|
|
136
|
+
"Aegisthus",
|
|
137
|
+
"Pylades",
|
|
138
|
+
"Hermione",
|
|
139
|
+
"Neoptolemus",
|
|
140
|
+
"Astyanax",
|
|
141
|
+
"Polyxena",
|
|
142
|
+
"Briseis",
|
|
143
|
+
"Chryseis",
|
|
144
|
+
// Argonauts and companions
|
|
145
|
+
"Peleus",
|
|
146
|
+
"Teucer",
|
|
147
|
+
"Idomeneus",
|
|
148
|
+
"Meriones",
|
|
149
|
+
"Machaon",
|
|
150
|
+
"Podalirius",
|
|
151
|
+
"Antilochus",
|
|
152
|
+
"Eurypylus",
|
|
153
|
+
"Thrasymedes",
|
|
154
|
+
"Peisistratus",
|
|
155
|
+
// Mythological creatures (personified)
|
|
156
|
+
"Sphinx",
|
|
157
|
+
"Phoenix",
|
|
158
|
+
"Griffin",
|
|
159
|
+
"Pegasus",
|
|
160
|
+
"Cerberus",
|
|
161
|
+
// More names for 211 people
|
|
162
|
+
"Laertes",
|
|
163
|
+
"Autolycus",
|
|
164
|
+
"Sisyphus",
|
|
165
|
+
"Tantalus",
|
|
166
|
+
"Pelops",
|
|
167
|
+
"Atreus",
|
|
168
|
+
"Thyestes",
|
|
169
|
+
"Aegeus",
|
|
170
|
+
"Medus",
|
|
171
|
+
"Circe",
|
|
172
|
+
"Calypso",
|
|
173
|
+
"Nausicaa",
|
|
174
|
+
"Alcinous",
|
|
175
|
+
"Arete",
|
|
176
|
+
"Eumaeus",
|
|
177
|
+
"Philoetius",
|
|
178
|
+
"Melanthius",
|
|
179
|
+
"Eurycleia",
|
|
180
|
+
"Mentor",
|
|
181
|
+
"Halitherses",
|
|
182
|
+
"Tiresias",
|
|
183
|
+
"Polyphemus",
|
|
184
|
+
"Aeolus",
|
|
185
|
+
"Scylla",
|
|
186
|
+
"Charybdis",
|
|
187
|
+
"Leucothea",
|
|
188
|
+
"Phaeacian",
|
|
189
|
+
"Antiope",
|
|
190
|
+
"Hippolyta",
|
|
191
|
+
"Phaedra",
|
|
192
|
+
"Procris",
|
|
193
|
+
"Cephalus",
|
|
194
|
+
"Meleager",
|
|
195
|
+
"Althaea",
|
|
196
|
+
"Oeneus",
|
|
197
|
+
"Tydeus",
|
|
198
|
+
"Capaneus",
|
|
199
|
+
"Hippomedon",
|
|
200
|
+
"Parthenopaeus",
|
|
201
|
+
"Adrastus",
|
|
202
|
+
"Polynices",
|
|
203
|
+
"Eteocles",
|
|
204
|
+
"Ismene",
|
|
205
|
+
"Haemon",
|
|
206
|
+
"Eurydice",
|
|
207
|
+
"Creon",
|
|
208
|
+
"Jocasta",
|
|
209
|
+
"Laius",
|
|
210
|
+
"Oedipus",
|
|
211
|
+
"Chrysippus",
|
|
212
|
+
"Niobe",
|
|
213
|
+
"Amphion2",
|
|
214
|
+
"Leto",
|
|
215
|
+
"Asteria",
|
|
216
|
+
"Hecate2",
|
|
217
|
+
"Perses",
|
|
218
|
+
"Pallas",
|
|
219
|
+
"Styx",
|
|
220
|
+
"Zelus",
|
|
221
|
+
"Kratos",
|
|
222
|
+
"Bia",
|
|
223
|
+
"Boreas",
|
|
224
|
+
"Zephyrus",
|
|
225
|
+
"Notus",
|
|
226
|
+
"Eurus",
|
|
227
|
+
"Aether",
|
|
228
|
+
"Hemera",
|
|
229
|
+
"Nyx",
|
|
230
|
+
"Erebus",
|
|
231
|
+
"Gaia",
|
|
232
|
+
"Uranus",
|
|
233
|
+
"Pontus",
|
|
234
|
+
"Tartarus",
|
|
235
|
+
"Ananke",
|
|
236
|
+
"Chronos2",
|
|
237
|
+
"Phanes",
|
|
238
|
+
"Thalassa",
|
|
239
|
+
"Ourea",
|
|
240
|
+
"Nesoi",
|
|
241
|
+
"Achelous",
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Stable manager name lookup (used for @references in DSL).
|
|
246
|
+
* Maps DSL @name to a canonical full name.
|
|
247
|
+
*/
|
|
248
|
+
export const MANAGER_NAMES = {
|
|
249
|
+
thoth: "Thoth",
|
|
250
|
+
chronos: "Chronos",
|
|
251
|
+
apollo: "Apollo",
|
|
252
|
+
hygieia: "Hygieia",
|
|
253
|
+
themis: "Themis",
|
|
254
|
+
athena: "Athena",
|
|
255
|
+
prometheus: "Prometheus",
|
|
256
|
+
hephaestus: "Hephaestus",
|
|
257
|
+
ares: "Ares",
|
|
258
|
+
hermes: "Hermes",
|
|
259
|
+
iris: "Iris",
|
|
260
|
+
demeter: "Demeter",
|
|
261
|
+
astraea: "Astraea",
|
|
262
|
+
tyche: "Tyche",
|
|
263
|
+
daedalus: "Daedalus",
|
|
264
|
+
asclepius: "Asclepius",
|
|
265
|
+
plutus: "Plutus",
|
|
266
|
+
aphrodite: "Aphrodite",
|
|
267
|
+
artemis: "Artemis",
|
|
268
|
+
hestia: "Hestia",
|
|
269
|
+
metis: "Metis",
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get a GitHub username from a person name.
|
|
274
|
+
* @param {string} name
|
|
275
|
+
* @param {string} domain - organization domain
|
|
276
|
+
* @returns {string}
|
|
277
|
+
*/
|
|
278
|
+
export function toGithubUsername(name) {
|
|
279
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "") + "-bio";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get an email from a person name.
|
|
284
|
+
* @param {string} name
|
|
285
|
+
* @param {string} domain
|
|
286
|
+
* @returns {string}
|
|
287
|
+
*/
|
|
288
|
+
export function toEmail(name, domain) {
|
|
289
|
+
return `${name.toLowerCase().replace(/[^a-z0-9]/g, "")}@${domain}`;
|
|
290
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prose Keys — collects all keys that need LLM-generated prose.
|
|
3
|
+
*
|
|
4
|
+
* Each key maps to a context object that guides the LLM prompt.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collect all prose keys from the entity graph.
|
|
9
|
+
* @param {object} entities - Generated entity graph from tier0
|
|
10
|
+
* @returns {Map<string, object>} key → context for prose generation
|
|
11
|
+
*/
|
|
12
|
+
export function collectProseKeys(entities) {
|
|
13
|
+
const keys = new Map();
|
|
14
|
+
|
|
15
|
+
// Organization README prose
|
|
16
|
+
keys.set("org_readme", {
|
|
17
|
+
topic: `${entities.orgs[0]?.name || "BioNova"} company overview`,
|
|
18
|
+
tone: "corporate, informative",
|
|
19
|
+
length: "3-4 paragraphs",
|
|
20
|
+
domain: entities.domain,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Project descriptions
|
|
24
|
+
for (const proj of entities.projects) {
|
|
25
|
+
if (proj.prose_topic) {
|
|
26
|
+
keys.set(`project_${proj.id}`, {
|
|
27
|
+
topic: proj.prose_topic,
|
|
28
|
+
tone: proj.prose_tone || "technical",
|
|
29
|
+
length: "2-3 paragraphs",
|
|
30
|
+
domain: entities.domain,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// HTML content — articles
|
|
36
|
+
const guideContent = entities.content.find((c) => c.id === "guide_html");
|
|
37
|
+
if (guideContent) {
|
|
38
|
+
for (const topic of guideContent.article_topics || []) {
|
|
39
|
+
keys.set(`article_${topic}`, {
|
|
40
|
+
topic: `${topic.replace(/_/g, " ")} in pharmaceutical industry`,
|
|
41
|
+
tone: "technical, informative",
|
|
42
|
+
length: "6-8 paragraphs",
|
|
43
|
+
domain: entities.domain,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Blog posts
|
|
48
|
+
for (let i = 0; i < (guideContent.blogs || 0); i++) {
|
|
49
|
+
keys.set(`blog_${i}`, {
|
|
50
|
+
topic: "pharmaceutical engineering blog post",
|
|
51
|
+
tone: "conversational, technical",
|
|
52
|
+
length: "4-5 paragraphs",
|
|
53
|
+
domain: entities.domain,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// FAQs
|
|
58
|
+
for (let i = 0; i < (guideContent.faqs || 0); i++) {
|
|
59
|
+
keys.set(`faq_${i}`, {
|
|
60
|
+
topic: "pharmaceutical engineering FAQ",
|
|
61
|
+
tone: "helpful, concise",
|
|
62
|
+
length: "1 paragraph",
|
|
63
|
+
domain: entities.domain,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// HowTos
|
|
68
|
+
for (const topic of guideContent.howto_topics || []) {
|
|
69
|
+
keys.set(`howto_${topic}`, {
|
|
70
|
+
topic: `how-to guide for ${topic.replace(/_/g, " ")}`,
|
|
71
|
+
tone: "instructional",
|
|
72
|
+
length: "5-6 paragraphs",
|
|
73
|
+
domain: entities.domain,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Reviews
|
|
78
|
+
for (let i = 0; i < (guideContent.reviews || 0); i++) {
|
|
79
|
+
keys.set(`review_${i}`, {
|
|
80
|
+
topic: "peer review comment on engineering work",
|
|
81
|
+
tone: "professional, constructive",
|
|
82
|
+
length: "1-2 sentences",
|
|
83
|
+
maxTokens: 100,
|
|
84
|
+
domain: entities.domain,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Comments
|
|
89
|
+
for (let i = 0; i < (guideContent.comments || 0); i++) {
|
|
90
|
+
keys.set(`comment_${i}`, {
|
|
91
|
+
topic: "discussion comment on engineering topic",
|
|
92
|
+
tone: "casual, technical",
|
|
93
|
+
length: "1-2 sentences",
|
|
94
|
+
maxTokens: 80,
|
|
95
|
+
domain: entities.domain,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Basecamp personas
|
|
101
|
+
const basecampContent = entities.content.find(
|
|
102
|
+
(c) => c.id === "basecamp_markdown",
|
|
103
|
+
);
|
|
104
|
+
if (basecampContent) {
|
|
105
|
+
const personas = selectPersonaNames(entities, basecampContent);
|
|
106
|
+
for (const persona of personas) {
|
|
107
|
+
// Briefings
|
|
108
|
+
for (let i = 0; i < (basecampContent.briefings_per_persona || 0); i++) {
|
|
109
|
+
keys.set(`briefing_${persona.name}_${i}`, {
|
|
110
|
+
topic: `daily briefing for ${persona.name}, a ${persona.level} ${persona.discipline}`,
|
|
111
|
+
tone: "professional, concise",
|
|
112
|
+
length: "2-3 paragraphs",
|
|
113
|
+
domain: entities.domain,
|
|
114
|
+
role: `${persona.level} ${persona.discipline}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Notes
|
|
119
|
+
for (let i = 0; i < (basecampContent.notes_per_persona || 0); i++) {
|
|
120
|
+
keys.set(`note_${persona.name}_${i}`, {
|
|
121
|
+
topic: `engineering knowledge note by ${persona.name}`,
|
|
122
|
+
tone: "personal, technical",
|
|
123
|
+
length: "1-2 paragraphs",
|
|
124
|
+
domain: entities.domain,
|
|
125
|
+
role: `${persona.level} ${persona.discipline}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Snapshot comments (GetDX engineer voice)
|
|
132
|
+
if (entities.activity?.commentKeys) {
|
|
133
|
+
for (const ck of entities.activity.commentKeys) {
|
|
134
|
+
const direction =
|
|
135
|
+
ck.trajectory === "declining" ? "declining" : "improving";
|
|
136
|
+
keys.set(
|
|
137
|
+
`snapshot_comment_${ck.snapshot_id}_${ck.email.replace(/[@.]/g, "_")}`,
|
|
138
|
+
{
|
|
139
|
+
topic: `GetDX snapshot survey comment about ${ck.driver_name.toLowerCase()}`,
|
|
140
|
+
tone: "authentic, first-person developer voice",
|
|
141
|
+
length: "1-2 sentences",
|
|
142
|
+
maxTokens: 80,
|
|
143
|
+
domain: entities.domain,
|
|
144
|
+
role: `${ck.person_level} ${ck.person_discipline.replace(/_/g, " ")} on the ${ck.team_name}`,
|
|
145
|
+
scenario: ck.scenario_name,
|
|
146
|
+
driver: ck.driver_name,
|
|
147
|
+
direction,
|
|
148
|
+
magnitude: ck.magnitude,
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return keys;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Select persona representatives from people.
|
|
159
|
+
*/
|
|
160
|
+
function selectPersonaNames(entities, basecampContent) {
|
|
161
|
+
const levels = basecampContent.persona_levels || [
|
|
162
|
+
"L1",
|
|
163
|
+
"L2",
|
|
164
|
+
"L3",
|
|
165
|
+
"L4",
|
|
166
|
+
"L5",
|
|
167
|
+
];
|
|
168
|
+
const personas = [];
|
|
169
|
+
for (const level of levels) {
|
|
170
|
+
const person = entities.people.find((p) => p.level === level);
|
|
171
|
+
if (person) {
|
|
172
|
+
personas.push({
|
|
173
|
+
name: person.name,
|
|
174
|
+
level: person.level,
|
|
175
|
+
discipline: person.discipline,
|
|
176
|
+
email: person.email,
|
|
177
|
+
team_id: person.team_id,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return personas;
|
|
182
|
+
}
|
package/engine/rng.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import seedrandom from "seedrandom";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a seeded RNG with convenience methods.
|
|
5
|
+
* @param {number|string} seed
|
|
6
|
+
* @returns {{ random: () => number, randomInt: (min: number, max: number) => number, pick: <T>(arr: T[]) => T, shuffle: <T>(arr: T[]) => T[], weightedPick: (items: Array<{weight: number}>) => number, gaussian: (mean: number, std: number) => number }}
|
|
7
|
+
*/
|
|
8
|
+
export function createSeededRNG(seed) {
|
|
9
|
+
const rng = seedrandom(String(seed));
|
|
10
|
+
|
|
11
|
+
const random = () => rng();
|
|
12
|
+
const randomInt = (min, max) => Math.floor(random() * (max - min + 1)) + min;
|
|
13
|
+
const pick = (arr) => arr[Math.floor(random() * arr.length)];
|
|
14
|
+
|
|
15
|
+
const shuffle = (arr) => {
|
|
16
|
+
const result = [...arr];
|
|
17
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
18
|
+
const j = Math.floor(random() * (i + 1));
|
|
19
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const weightedPick = (weights) => {
|
|
25
|
+
const total = weights.reduce((s, w) => s + w, 0);
|
|
26
|
+
let r = random() * total;
|
|
27
|
+
for (let i = 0; i < weights.length; i++) {
|
|
28
|
+
r -= weights[i];
|
|
29
|
+
if (r <= 0) return i;
|
|
30
|
+
}
|
|
31
|
+
return weights.length - 1;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const gaussian = (mean, std) => {
|
|
35
|
+
const u1 = random();
|
|
36
|
+
const u2 = random();
|
|
37
|
+
const z =
|
|
38
|
+
Math.sqrt(-2 * Math.log(u1 || 1e-10)) * Math.cos(2 * Math.PI * u2);
|
|
39
|
+
return mean + z * std;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return { random, randomInt, pick, shuffle, weightedPick, gaussian };
|
|
43
|
+
}
|
package/engine/tier0.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 0 — Deterministic Entity & Activity Generation (no LLM).
|
|
3
|
+
*
|
|
4
|
+
* @module libuniverse/engine/tier0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createSeededRNG } from "./rng.js";
|
|
8
|
+
import { buildEntities } from "./entities.js";
|
|
9
|
+
import { generateActivity } from "./activity.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Entity generator that wraps deterministic generation from a parsed AST.
|
|
13
|
+
*/
|
|
14
|
+
export class EntityGenerator {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Function} rngFactory - Factory function that creates a seeded RNG
|
|
17
|
+
* @param {object} logger - Logger instance
|
|
18
|
+
*/
|
|
19
|
+
constructor(rngFactory, logger) {
|
|
20
|
+
if (!rngFactory) throw new Error("rngFactory is required");
|
|
21
|
+
if (!logger) throw new Error("logger is required");
|
|
22
|
+
this.rngFactory = rngFactory;
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate all entities and activity from a parsed AST.
|
|
28
|
+
* @param {import('../dsl/parser.js').UniverseAST} ast
|
|
29
|
+
* @returns {object} Entity graph with activity data
|
|
30
|
+
*/
|
|
31
|
+
generate(ast) {
|
|
32
|
+
const rng = this.rngFactory(ast.seed);
|
|
33
|
+
const { orgs, departments, teams, people, projects } = buildEntities(
|
|
34
|
+
ast,
|
|
35
|
+
rng,
|
|
36
|
+
);
|
|
37
|
+
const activity = generateActivity(ast, rng, people, teams);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
orgs,
|
|
41
|
+
departments,
|
|
42
|
+
teams,
|
|
43
|
+
people,
|
|
44
|
+
projects,
|
|
45
|
+
scenarios: ast.scenarios,
|
|
46
|
+
snapshots: ast.snapshots,
|
|
47
|
+
framework: { ...ast.framework, seed: ast.seed },
|
|
48
|
+
content: ast.content,
|
|
49
|
+
activity,
|
|
50
|
+
domain: ast.domain,
|
|
51
|
+
industry: ast.industry,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates an EntityGenerator with the built-in seeded RNG factory.
|
|
58
|
+
* @param {object} logger - Logger instance
|
|
59
|
+
* @returns {EntityGenerator}
|
|
60
|
+
*/
|
|
61
|
+
export function createEntityGenerator(logger) {
|
|
62
|
+
return new EntityGenerator(createSeededRNG, logger);
|
|
63
|
+
}
|