@forwardimpact/libuniverse 0.1.0

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/pipeline.js ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Pipeline orchestrator — parse → generate → prose → render → validate.
3
+ *
4
+ * @module libuniverse/pipeline
5
+ */
6
+
7
+ import { readFile } from "fs/promises";
8
+ import { join } from "path";
9
+ import { validateLinks, validateHTML } from "@forwardimpact/libsyntheticrender";
10
+ import { collectProseKeys } from "@forwardimpact/libsyntheticgen";
11
+ import { loadSchemas } from "@forwardimpact/libsyntheticprose/pathway";
12
+
13
+ /**
14
+ * Pipeline class that orchestrates the full generation pipeline.
15
+ * All collaborators are injected via constructor.
16
+ */
17
+ export class Pipeline {
18
+ /**
19
+ * @param {object} deps
20
+ * @param {import('@forwardimpact/libsyntheticgen').DslParser} deps.dslParser - DSL parser
21
+ * @param {import('@forwardimpact/libsyntheticgen').EntityGenerator} deps.entityGenerator - Entity generator
22
+ * @param {import('@forwardimpact/libsyntheticprose').ProseEngine} deps.proseEngine - Prose engine
23
+ * @param {import('@forwardimpact/libsyntheticprose').PathwayGenerator} deps.pathwayGenerator - Pathway generator
24
+ * @param {import('@forwardimpact/libsyntheticrender').Renderer} deps.renderer - Renderer
25
+ * @param {import('@forwardimpact/libsyntheticrender').ContentValidator} deps.validator - Content validator
26
+ * @param {import('@forwardimpact/libsyntheticrender').ContentFormatter} deps.formatter - Content formatter
27
+ * @param {object} deps.logger - Logger instance
28
+ */
29
+ constructor({
30
+ dslParser,
31
+ entityGenerator,
32
+ proseEngine,
33
+ pathwayGenerator,
34
+ renderer,
35
+ validator,
36
+ formatter,
37
+ logger,
38
+ }) {
39
+ if (!dslParser) throw new Error("dslParser is required");
40
+ if (!entityGenerator) throw new Error("entityGenerator is required");
41
+ if (!proseEngine) throw new Error("proseEngine is required");
42
+ if (!pathwayGenerator) throw new Error("pathwayGenerator is required");
43
+ if (!renderer) throw new Error("renderer is required");
44
+ if (!validator) throw new Error("validator is required");
45
+ if (!formatter) throw new Error("formatter is required");
46
+ if (!logger) throw new Error("logger is required");
47
+
48
+ this.dslParser = dslParser;
49
+ this.entityGenerator = entityGenerator;
50
+ this.proseEngine = proseEngine;
51
+ this.pathwayGenerator = pathwayGenerator;
52
+ this.renderer = renderer;
53
+ this.validator = validator;
54
+ this.formatter = formatter;
55
+ this.logger = logger;
56
+ }
57
+
58
+ /**
59
+ * Run the full generation pipeline.
60
+ *
61
+ * @param {object} options
62
+ * @param {string} options.universePath - Path to the universe.dsl file
63
+ * @param {string} [options.only=null] - Render only a specific content type
64
+ * @param {string|null} [options.schemaDir=null] - Path to JSON schema directory
65
+ * @returns {Promise<{files: Map<string,string>, rawDocuments: Map<string,string>, entities: object, validation: object}>}
66
+ */
67
+ async run(options) {
68
+ const { universePath, only = null, schemaDir = null } = options;
69
+ const log = this.logger;
70
+
71
+ // 1. Parse DSL
72
+ log.info("pipeline", "Parsing universe DSL");
73
+ const source = await readFile(universePath, "utf-8");
74
+ const ast = this.dslParser.parse(source);
75
+
76
+ // 2. Generate entity graph (Tier 0)
77
+ log.info("pipeline", "Generating entity graph");
78
+ const entities = this.entityGenerator.generate(ast);
79
+
80
+ // 3. Prose generation (Tier 1/2)
81
+ const proseKeys = collectProseKeys(entities);
82
+ const prose = new Map();
83
+ const totalKeys = proseKeys.size;
84
+ let keyIndex = 0;
85
+ if (this.proseEngine.mode !== "no-prose") {
86
+ log.info(
87
+ "pipeline",
88
+ `Generating prose (${this.proseEngine.mode} mode, ${totalKeys} keys)`,
89
+ );
90
+ }
91
+ for (const [key, context] of proseKeys) {
92
+ keyIndex++;
93
+ const result = await this.proseEngine.generateProse(key, context);
94
+ if (result) prose.set(key, result);
95
+ if (this.proseEngine.mode !== "no-prose") {
96
+ log.info("prose", `[${keyIndex}/${totalKeys}] ${key}`);
97
+ }
98
+ }
99
+
100
+ // 4. Render outputs
101
+ const files = new Map();
102
+ const rawDocuments = new Map();
103
+ let htmlLinked = null;
104
+
105
+ const shouldRender = (type) => !only || only === type;
106
+
107
+ if (shouldRender("html")) {
108
+ log.info("render", "Rendering HTML (Pass 1: deterministic skeleton)");
109
+ const { files: htmlFiles, linked } = this.renderer.renderHtml(
110
+ entities,
111
+ prose,
112
+ );
113
+ htmlLinked = linked;
114
+
115
+ // Pass 2: LLM enrichment of prose blocks
116
+ if (this.proseEngine.mode !== "no-prose") {
117
+ log.info("render", "Enriching HTML (Pass 2: LLM prose enrichment)");
118
+ const enriched = await this.renderer.enrichHtml(
119
+ htmlFiles,
120
+ linked,
121
+ this.proseEngine,
122
+ entities.domain,
123
+ );
124
+ for (const [name, content] of enriched) {
125
+ files.set(join("examples/organizational", name), content);
126
+ }
127
+ } else {
128
+ for (const [name, content] of htmlFiles) {
129
+ files.set(join("examples/organizational", name), content);
130
+ }
131
+ }
132
+
133
+ files.set(
134
+ "examples/organizational/README.md",
135
+ this.renderer.renderReadme(entities, prose),
136
+ );
137
+ files.set(
138
+ "examples/organizational/ONTOLOGY.md",
139
+ this.renderer.renderOntology(entities),
140
+ );
141
+ }
142
+
143
+ if (shouldRender("pathway")) {
144
+ log.info("render", "Rendering pathway");
145
+ const hasPathwayFramework =
146
+ entities.framework?.capabilities?.length > 0 &&
147
+ typeof entities.framework.capabilities[0] === "object";
148
+
149
+ if (hasPathwayFramework && schemaDir) {
150
+ const schemas = loadSchemas(schemaDir);
151
+ const pathwayData = await this.pathwayGenerator.generate({
152
+ framework: entities.framework,
153
+ domain: entities.domain,
154
+ industry: entities.industry,
155
+ schemas,
156
+ });
157
+ const pathwayFiles = this.renderer.renderPathway(pathwayData);
158
+ for (const [name, content] of pathwayFiles) {
159
+ files.set(`examples/pathway/${name}`, content);
160
+ }
161
+ }
162
+ }
163
+
164
+ if (shouldRender("raw")) {
165
+ log.info("render", "Rendering raw documents");
166
+ const raw = this.renderer.renderRaw(entities, prose);
167
+ for (const [path, content] of raw) {
168
+ rawDocuments.set(path, content);
169
+ }
170
+
171
+ const activityFiles = this.renderer.renderActivity(entities);
172
+ for (const [name, content] of activityFiles) {
173
+ files.set(join("examples/activity", name), content);
174
+ }
175
+ }
176
+
177
+ if (shouldRender("markdown")) {
178
+ log.info("render", "Rendering markdown");
179
+ const md = this.renderer.renderMarkdown(entities, prose);
180
+ for (const [name, content] of md) {
181
+ files.set(join("examples/personal", name), content);
182
+ }
183
+ }
184
+
185
+ // Save prose cache after all generation
186
+ this.proseEngine.saveCache();
187
+
188
+ // 5. Format outputs with Prettier
189
+ log.info("format", "Formatting output files with Prettier");
190
+ const formattedFiles = await this.formatter.format(files);
191
+ const formattedRawDocuments = await this.formatter.format(rawDocuments);
192
+
193
+ // 6. Validate
194
+ const validation = this.validator.validate(entities);
195
+
196
+ if (htmlLinked) {
197
+ const linkValidation = validateLinks(htmlLinked, entities.domain);
198
+ validation.checks.push({
199
+ name: "link_density",
200
+ passed: linkValidation.passed,
201
+ });
202
+ if (!linkValidation.passed) {
203
+ validation.failures++;
204
+ validation.passed = false;
205
+ log.error(
206
+ "validate",
207
+ `Link validation: ${linkValidation.failures} failures`,
208
+ );
209
+ }
210
+
211
+ const orgFiles = new Map();
212
+ for (const [path, content] of formattedFiles) {
213
+ if (
214
+ path.startsWith("examples/organizational/") &&
215
+ path.endsWith(".html")
216
+ ) {
217
+ orgFiles.set(path, content);
218
+ }
219
+ }
220
+ const htmlValidation = validateHTML(orgFiles, entities.domain);
221
+ for (const check of htmlValidation.checks) {
222
+ validation.checks.push(check);
223
+ }
224
+ if (!htmlValidation.passed) {
225
+ validation.failures += htmlValidation.failures;
226
+ validation.passed = false;
227
+ for (const c of htmlValidation.checks.filter((c) => !c.passed)) {
228
+ log.error("validate", c.message);
229
+ }
230
+ }
231
+ }
232
+
233
+ return {
234
+ files: formattedFiles,
235
+ rawDocuments: formattedRawDocuments,
236
+ entities,
237
+ validation,
238
+ };
239
+ }
240
+ }