@forwardimpact/libuniverse 0.1.1 → 0.1.3

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.
@@ -52,7 +52,11 @@ async function main() {
52
52
  SUPABASE_SERVICE_ROLE_KEY: null,
53
53
  });
54
54
 
55
- const mode = args.cached ? "cached" : args.generate ? "generate" : "no-prose";
55
+ const mode = args.noProse
56
+ ? "no-prose"
57
+ : args.generate
58
+ ? "generate"
59
+ : "cached";
56
60
 
57
61
  let llmApi = null;
58
62
  if (mode === "generate") {
@@ -75,7 +79,8 @@ async function main() {
75
79
 
76
80
  const monorepoRoot = resolve(__dirname, "../../..");
77
81
  const schemaDir = join(monorepoRoot, "products/map/schema/json");
78
- const cachePath = join(__dirname, "..", ".prose-cache.json");
82
+ const cachePath =
83
+ args.cache || join(monorepoRoot, "data", "synthetic", "prose-cache.json");
79
84
 
80
85
  const libsyntheticproseDir = dirname(
81
86
  fileURLToPath(import.meta.resolve("@forwardimpact/libsyntheticprose")),
@@ -156,7 +161,7 @@ async function main() {
156
161
 
157
162
  const result = await pipeline.run({
158
163
  universePath:
159
- args.universe || join(monorepoRoot, "examples", "universe.dsl"),
164
+ args.story || join(monorepoRoot, "data", "synthetic", "story.dsl"),
160
165
  only: args.only || null,
161
166
  schemaDir,
162
167
  });
@@ -190,26 +195,19 @@ async function main() {
190
195
  }
191
196
  } else if (!args.dryRun) {
192
197
  for (const [storagePath, content] of result.rawDocuments) {
193
- const fullPath = join(
194
- monorepoRoot,
195
- "examples/activity/raw",
196
- storagePath,
197
- );
198
+ const fullPath = join(monorepoRoot, "data/activity/raw", storagePath);
198
199
  await mkdir(dirname(fullPath), { recursive: true });
199
200
  await writeFile(fullPath, content);
200
201
  }
201
202
  console.log(
202
- `${result.rawDocuments.size} raw documents written to examples/activity/raw/`,
203
+ `${result.rawDocuments.size} raw documents written to data/activity/raw/`,
203
204
  );
204
205
  }
205
206
 
206
207
  // Write evidence directly (no raw source system for evidence)
207
208
  const evidence = result.entities.activity?.evidence;
208
209
  if (evidence && !args.dryRun && !args.load) {
209
- const evidencePath = join(
210
- monorepoRoot,
211
- "examples/activity/evidence.json",
212
- );
210
+ const evidencePath = join(monorepoRoot, "data/activity/evidence.json");
213
211
  await mkdir(dirname(evidencePath), { recursive: true });
214
212
  const formatted = await formatContent(
215
213
  evidencePath,
@@ -263,13 +261,14 @@ function parseArgs(argv) {
263
261
  const args = {};
264
262
  for (const arg of argv) {
265
263
  if (arg === "--help" || arg === "-h") args.help = true;
266
- else if (arg === "--cached") args.cached = true;
264
+ else if (arg === "--no-prose") args.noProse = true;
267
265
  else if (arg === "--generate") args.generate = true;
268
266
  else if (arg === "--strict") args.strict = true;
269
267
  else if (arg === "--dry-run") args.dryRun = true;
270
268
  else if (arg === "--load") args.load = true;
271
269
  else if (arg.startsWith("--only=")) args.only = arg.slice(7);
272
- else if (arg.startsWith("--universe=")) args.universe = arg.slice(11);
270
+ else if (arg.startsWith("--story=")) args.story = arg.slice(8);
271
+ else if (arg.startsWith("--cache=")) args.cache = arg.slice(8);
273
272
  }
274
273
  return args;
275
274
  }
@@ -281,32 +280,34 @@ Usage:
281
280
  npx fit-universe [options]
282
281
 
283
282
  Options:
284
- --generate Generate prose via LLM (requires LLM_TOKEN)
285
- --cached Use cached prose from .prose-cache.json
286
- --strict Fail on cache miss (use with --cached)
283
+ --generate Generate prose via LLM and update cache (requires LLM_TOKEN)
284
+ --no-prose Skip prose entirely (structural scaffolding only)
285
+ --strict Fail on cache miss (use with default cached mode)
287
286
  --dry-run Show what would be written without writing
288
287
  --load Load raw documents to Supabase Storage
289
288
  --only=<type> Render only one content type (html|pathway|raw|markdown)
290
- --universe=<path> Path to a custom universe DSL file
289
+ --story=<path> Path to a custom story DSL file
290
+ --cache=<path> Path to prose cache file
291
291
  -h, --help Show this help message
292
292
 
293
293
  Prose modes:
294
- (default) Structural generation only, no LLM calls
295
- --cached Read prose from .prose-cache.json
296
- --generate Call LLM to generate prose, write to cache
294
+ (default) Use cached prose from prose-cache.json
295
+ --generate Call LLM to generate prose and update the cache
296
+ --no-prose No prose produces minimal structural data only
297
297
 
298
298
  Content types:
299
- html Organizational articles, guides, FAQs (examples/organizational)
300
- pathway YAML framework files (examples/pathway)
301
- raw Roster, GitHub events, evidence (examples/activity)
302
- markdown Briefings, notes, KB content (examples/personal)
299
+ html Organizational articles, guides, FAQs (data/knowledge)
300
+ pathway YAML framework files (data/pathway)
301
+ raw Roster, GitHub events, evidence (data/activity)
302
+ markdown Briefings, notes, KB content (data/personal)
303
303
 
304
304
  Examples:
305
- npx fit-universe # Structural only
306
- npx fit-universe --generate # Full generation with LLM prose
307
- npx fit-universe --cached --strict # Cached prose, fail on miss
305
+ npx fit-universe # Cached prose (default)
306
+ npx fit-universe --generate # Generate new prose via LLM
307
+ npx fit-universe --strict # Cached prose, fail on miss
308
+ npx fit-universe --no-prose # Structural only, no prose
308
309
  npx fit-universe --only=pathway # Generate pathway data only
309
- npx fit-universe --universe=custom.dsl # Use custom DSL file
310
+ npx fit-universe --story=custom.dsl # Use custom DSL file
310
311
  `);
311
312
  }
312
313
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libuniverse",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Synthetic data universe DSL and generation engine",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -27,8 +27,8 @@
27
27
  "@forwardimpact/libsyntheticrender": "^0.1.0",
28
28
  "@forwardimpact/libtelemetry": "^0.1.23",
29
29
  "@forwardimpact/libtemplate": "^0.2.0",
30
- "@supabase/supabase-js": "^2.0.0",
31
- "prettier": "^3.7.4"
30
+ "@supabase/supabase-js": "^2.100.1",
31
+ "prettier": "^3.8.1"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=18.0.0"
package/pipeline.js CHANGED
@@ -66,7 +66,7 @@ export class Pipeline {
66
66
  * Run the full generation pipeline.
67
67
  *
68
68
  * @param {object} options
69
- * @param {string} options.universePath - Path to the universe.dsl file
69
+ * @param {string} options.universePath - Path to the story DSL file
70
70
  * @param {string} [options.only=null] - Render only a specific content type
71
71
  * @param {string|null} [options.schemaDir=null] - Path to JSON schema directory
72
72
  * @returns {Promise<{files: Map<string,string>, rawDocuments: Map<string,string>, entities: object, validation: object, stats: {prose: {hits: number, misses: number, generated: number}, files: number, rawDocuments: number}}>}
@@ -138,25 +138,25 @@ export class Pipeline {
138
138
  entities.domain,
139
139
  );
140
140
  for (const [name, content] of enriched) {
141
- files.set(join("examples/organizational", name), content);
141
+ files.set(join("data/knowledge", name), content);
142
142
  }
143
143
  } else {
144
144
  for (const [name, content] of htmlFiles) {
145
- files.set(join("examples/organizational", name), content);
145
+ files.set(join("data/knowledge", name), content);
146
146
  }
147
147
  }
148
148
 
149
149
  files.set(
150
- "examples/organizational/README.md",
150
+ "data/knowledge/README.md",
151
151
  this.renderer.renderReadme(entities, prose),
152
152
  );
153
153
  files.set(
154
- "examples/organizational/ONTOLOGY.md",
154
+ "data/knowledge/ONTOLOGY.md",
155
155
  this.renderer.renderOntology(entities),
156
156
  );
157
157
 
158
158
  const htmlCount = [...files.keys()].filter((p) =>
159
- p.startsWith("examples/organizational/"),
159
+ p.startsWith("data/knowledge/"),
160
160
  ).length;
161
161
  log.info("render", `HTML: ${htmlCount} files`);
162
162
  }
@@ -177,7 +177,7 @@ export class Pipeline {
177
177
  });
178
178
  const pathwayFiles = this.renderer.renderPathway(pathwayData);
179
179
  for (const [name, content] of pathwayFiles) {
180
- files.set(`examples/pathway/${name}`, content);
180
+ files.set(`data/pathway/${name}`, content);
181
181
  }
182
182
  log.info("render", `Pathway: ${pathwayFiles.size} files`);
183
183
  }
@@ -192,7 +192,7 @@ export class Pipeline {
192
192
 
193
193
  const activityFiles = this.renderer.renderActivity(entities);
194
194
  for (const [name, content] of activityFiles) {
195
- files.set(join("examples/activity", name), content);
195
+ files.set(join("data/activity", name), content);
196
196
  }
197
197
  log.info(
198
198
  "render",
@@ -204,7 +204,7 @@ export class Pipeline {
204
204
  log.info("render", "Rendering markdown");
205
205
  const md = this.renderer.renderMarkdown(entities, prose);
206
206
  for (const [name, content] of md) {
207
- files.set(join("examples/personal", name), content);
207
+ files.set(join("data/personal", name), content);
208
208
  }
209
209
  log.info("render", `Markdown: ${md.size} files`);
210
210
  }
@@ -292,10 +292,7 @@ export class Pipeline {
292
292
 
293
293
  const orgFiles = new Map();
294
294
  for (const [path, content] of formattedFiles) {
295
- if (
296
- path.startsWith("examples/organizational/") &&
297
- path.endsWith(".html")
298
- ) {
295
+ if (path.startsWith("data/knowledge/") && path.endsWith(".html")) {
299
296
  orgFiles.set(path, content);
300
297
  }
301
298
  }
@@ -0,0 +1,103 @@
1
+ universe minimal {
2
+ domain "test.example"
3
+ industry "technology"
4
+ seed 42
5
+
6
+ org testorg {
7
+ name "Test Organization"
8
+ }
9
+
10
+ department eng {
11
+ name "Engineering"
12
+ parent testorg
13
+ headcount 5
14
+
15
+ team alpha {
16
+ name "Alpha Team"
17
+ size 5
18
+ manager @alpha_lead
19
+ repos ["alpha-service"]
20
+ }
21
+ }
22
+
23
+ people {
24
+ count 5
25
+ distribution {
26
+ L3 40%
27
+ L4 40%
28
+ L5 20%
29
+ }
30
+ disciplines {
31
+ software_engineering 100%
32
+ }
33
+ }
34
+
35
+ project testproj {
36
+ name "Test Project"
37
+ type "drug"
38
+ teams [alpha]
39
+ prose_topic "Testing synthetic generation"
40
+ prose_tone "technical"
41
+ }
42
+
43
+ framework {
44
+ proficiencies [awareness, foundational, working, practitioner, expert]
45
+ maturities [emerging, developing, practicing, role_modeling, exemplifying]
46
+ stages [specify, plan, code, review]
47
+
48
+ levels {
49
+ J040 { title "Software Engineer" rank 1 experience "0-2 years" }
50
+ J050 { title "Senior Engineer" rank 2 experience "2-5 years" }
51
+ }
52
+
53
+ capabilities {
54
+ coding { name "Coding" skills [python_dev, code_review] }
55
+ }
56
+
57
+ behaviours {
58
+ collaboration { name "Collaboration" }
59
+ }
60
+
61
+ disciplines {
62
+ software_engineering {
63
+ roleTitle "Software Engineer"
64
+ core [python_dev]
65
+ supporting [code_review]
66
+ }
67
+ }
68
+
69
+ tracks {
70
+ backend { name "Backend" }
71
+ }
72
+
73
+ drivers {
74
+ clear_direction {
75
+ name "Clear Direction"
76
+ skills [python_dev]
77
+ behaviours [collaboration]
78
+ }
79
+ }
80
+ }
81
+
82
+ scenario baseline {
83
+ name "Baseline Scenario"
84
+ timerange_start 2025-01
85
+ timerange_end 2025-06
86
+
87
+ affect alpha {
88
+ github_commits "moderate"
89
+ github_prs "moderate"
90
+ dx_drivers {
91
+ clear_direction { trajectory "rising" magnitude 3 }
92
+ }
93
+ evidence_skills [python_dev]
94
+ evidence_floor "foundational"
95
+ }
96
+ }
97
+
98
+ content guide_html {
99
+ courses 2
100
+ events 1
101
+ blogs 3
102
+ }
103
+ }
@@ -0,0 +1,106 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import {
6
+ createDslParser,
7
+ createEntityGenerator,
8
+ } from "@forwardimpact/libsyntheticgen";
9
+ import { validateCrossContent } from "@forwardimpact/libsyntheticrender";
10
+ import { readFileSync } from "fs";
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const FIXTURE_PATH = join(__dirname, "fixtures", "minimal.dsl");
14
+
15
+ function makeLogger() {
16
+ return {
17
+ info: () => {},
18
+ debug: () => {},
19
+ warn: () => {},
20
+ error: () => {},
21
+ };
22
+ }
23
+
24
+ describe("Pipeline integration", () => {
25
+ test("parses minimal DSL fixture", () => {
26
+ const source = readFileSync(FIXTURE_PATH, "utf-8");
27
+ const parser = createDslParser();
28
+ const ast = parser.parse(source);
29
+
30
+ assert.strictEqual(ast.domain, "test.example");
31
+ assert.strictEqual(ast.industry, "technology");
32
+ assert.ok(ast.people);
33
+ assert.ok(ast.teams.length > 0);
34
+ assert.ok(ast.projects.length > 0);
35
+ });
36
+
37
+ test("generates entities from minimal DSL", () => {
38
+ const source = readFileSync(FIXTURE_PATH, "utf-8");
39
+ const parser = createDslParser();
40
+ const ast = parser.parse(source);
41
+ const generator = createEntityGenerator(makeLogger());
42
+ const entities = generator.generate(ast);
43
+
44
+ assert.ok(entities.orgs.length > 0);
45
+ assert.ok(entities.departments.length > 0);
46
+ assert.ok(entities.teams.length > 0);
47
+ assert.ok(entities.people.length > 0);
48
+ assert.ok(entities.projects.length > 0);
49
+ assert.ok(entities.domain);
50
+ });
51
+
52
+ test("entity IRIs use consistent /id/ namespace", () => {
53
+ const source = readFileSync(FIXTURE_PATH, "utf-8");
54
+ const parser = createDslParser();
55
+ const ast = parser.parse(source);
56
+ const generator = createEntityGenerator(makeLogger());
57
+ const entities = generator.generate(ast);
58
+
59
+ for (const org of entities.orgs) {
60
+ assert.ok(org.iri.includes("/id/org/"), `Bad org IRI: ${org.iri}`);
61
+ }
62
+ for (const dept of entities.departments) {
63
+ assert.ok(
64
+ dept.iri.includes("/id/department/"),
65
+ `Bad dept IRI: ${dept.iri}`,
66
+ );
67
+ }
68
+ for (const team of entities.teams) {
69
+ assert.ok(team.iri.includes("/id/team/"), `Bad team IRI: ${team.iri}`);
70
+ }
71
+ for (const person of entities.people) {
72
+ assert.ok(
73
+ person.iri.includes("/id/person/"),
74
+ `Bad person IRI: ${person.iri}`,
75
+ );
76
+ }
77
+ for (const proj of entities.projects) {
78
+ assert.ok(
79
+ proj.iri.includes("/id/project/"),
80
+ `Bad project IRI: ${proj.iri}`,
81
+ );
82
+ }
83
+ });
84
+
85
+ test("cross-content validation passes on generated entities", () => {
86
+ const source = readFileSync(FIXTURE_PATH, "utf-8");
87
+ const parser = createDslParser();
88
+ const ast = parser.parse(source);
89
+ const generator = createEntityGenerator(makeLogger());
90
+ const entities = generator.generate(ast);
91
+ const result = validateCrossContent(entities);
92
+
93
+ // Minimal fixture has no snapshots block, so snapshot checks are expected to fail
94
+ const snapshotChecks = new Set([
95
+ "getdx_snapshots_list_response",
96
+ "getdx_snapshots_info_responses",
97
+ ]);
98
+ const failures = result.checks.filter(
99
+ (c) => !c.passed && !snapshotChecks.has(c.name),
100
+ );
101
+ if (failures.length > 0) {
102
+ const names = failures.map((f) => f.name).join(", ");
103
+ assert.fail(`Validation failures: ${names}`);
104
+ }
105
+ });
106
+ });