@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.
- package/bin/fit-universe.js +31 -30
- package/package.json +3 -3
- package/pipeline.js +10 -13
- package/test/fixtures/minimal.dsl +103 -0
- package/test/pipeline.test.js +106 -0
- package/.prose-cache.json +0 -572
package/bin/fit-universe.js
CHANGED
|
@@ -52,7 +52,11 @@ async function main() {
|
|
|
52
52
|
SUPABASE_SERVICE_ROLE_KEY: null,
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
const mode = args.
|
|
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 =
|
|
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.
|
|
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
|
|
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 === "--
|
|
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("--
|
|
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
|
-
--
|
|
286
|
-
--strict Fail on cache miss (use with
|
|
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
|
-
--
|
|
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)
|
|
295
|
-
--
|
|
296
|
-
--
|
|
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 (
|
|
300
|
-
pathway YAML framework files (
|
|
301
|
-
raw Roster, GitHub events, evidence (
|
|
302
|
-
markdown Briefings, notes, KB content (
|
|
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 #
|
|
306
|
-
npx fit-universe --generate #
|
|
307
|
-
npx fit-universe --
|
|
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 --
|
|
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.
|
|
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.
|
|
31
|
-
"prettier": "^3.
|
|
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
|
|
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("
|
|
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("
|
|
145
|
+
files.set(join("data/knowledge", name), content);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
files.set(
|
|
150
|
-
"
|
|
150
|
+
"data/knowledge/README.md",
|
|
151
151
|
this.renderer.renderReadme(entities, prose),
|
|
152
152
|
);
|
|
153
153
|
files.set(
|
|
154
|
-
"
|
|
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("
|
|
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(`
|
|
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("
|
|
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("
|
|
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
|
+
});
|