@forwardimpact/libsyntheticprose 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 ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to the Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Dick Olsson
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Pathway Engine — orchestrates LLM calls to generate pathway entity data.
3
+ *
4
+ * Generates entities in dependency order:
5
+ * framework → levels → stages → behaviours → capabilities →
6
+ * drivers → disciplines → tracks → self-assessments
7
+ *
8
+ * @module libuniverse/engine/pathway
9
+ */
10
+
11
+ import { readFileSync } from "fs";
12
+ import { join } from "path";
13
+ import { buildFrameworkPrompt } from "../prompts/pathway/framework.js";
14
+ import { buildLevelPrompt } from "../prompts/pathway/level.js";
15
+ import { buildStagePrompt } from "../prompts/pathway/stage.js";
16
+ import { buildBehaviourPrompt } from "../prompts/pathway/behaviour.js";
17
+ import { buildCapabilityPrompt } from "../prompts/pathway/capability.js";
18
+ import { buildDriverPrompt } from "../prompts/pathway/driver.js";
19
+ import { buildDisciplinePrompt } from "../prompts/pathway/discipline.js";
20
+ import { buildTrackPrompt } from "../prompts/pathway/track.js";
21
+
22
+ /**
23
+ * Load JSON schemas from the schema directory.
24
+ * @param {string} schemaDir - Path to products/map/schema/json/
25
+ * @returns {object} schemas keyed by entity type
26
+ */
27
+ export function loadSchemas(schemaDir) {
28
+ const names = [
29
+ "framework",
30
+ "levels",
31
+ "stages",
32
+ "behaviour",
33
+ "capability",
34
+ "discipline",
35
+ "track",
36
+ "drivers",
37
+ "self-assessments",
38
+ "defs",
39
+ ];
40
+ const schemas = {};
41
+ for (const name of names) {
42
+ schemas[name] = JSON.parse(
43
+ readFileSync(join(schemaDir, `${name}.schema.json`), "utf-8"),
44
+ );
45
+ }
46
+ return schemas;
47
+ }
48
+
49
+ /**
50
+ * PathwayGenerator orchestrates LLM calls to generate pathway entity data.
51
+ */
52
+ export class PathwayGenerator {
53
+ /**
54
+ * @param {import('./prose.js').ProseEngine} proseEngine - Prose engine for LLM calls
55
+ * @param {object} logger - Logger instance
56
+ */
57
+ constructor(proseEngine, logger) {
58
+ if (!proseEngine) throw new Error("proseEngine is required");
59
+ if (!logger) throw new Error("logger is required");
60
+ this.proseEngine = proseEngine;
61
+ this.logger = logger;
62
+ }
63
+
64
+ /**
65
+ * Generate all pathway entity data via LLM calls in dependency order.
66
+ * @param {object} options
67
+ * @param {object} options.framework - Framework AST from DSL parser
68
+ * @param {string} options.domain - Universe domain
69
+ * @param {string} options.industry - Universe industry
70
+ * @param {object} options.schemas - Loaded JSON schemas
71
+ * @returns {Promise<object>} Generated pathway data keyed by entity type
72
+ */
73
+ async generate({ framework, domain, industry, schemas }) {
74
+ return generatePathwayData({
75
+ framework,
76
+ domain,
77
+ industry,
78
+ schemas,
79
+ proseEngine: this.proseEngine,
80
+ });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Generate all pathway entity data via LLM calls in dependency order.
86
+ *
87
+ * @param {object} options
88
+ * @param {object} options.framework - Framework AST from DSL parser
89
+ * @param {string} options.domain - Universe domain
90
+ * @param {string} options.industry - Universe industry
91
+ * @param {object} options.schemas - Loaded JSON schemas
92
+ * @param {import('./prose.js').ProseEngine} options.proseEngine - Prose engine for LLM calls
93
+ * @returns {Promise<object>} Generated pathway data keyed by entity type
94
+ */
95
+ async function generatePathwayData({
96
+ framework,
97
+ domain,
98
+ industry,
99
+ schemas,
100
+ proseEngine,
101
+ }) {
102
+ const ctx = { domain, industry };
103
+
104
+ // 1. Framework metadata
105
+ const fw = await generateEntity(
106
+ "framework",
107
+ "framework",
108
+ buildFrameworkPrompt(framework, ctx, schemas.framework),
109
+ proseEngine,
110
+ );
111
+
112
+ // 2. Levels
113
+ const levels = await generateEntity(
114
+ "levels",
115
+ "levels",
116
+ buildLevelPrompt(framework.levels, ctx, schemas.levels),
117
+ proseEngine,
118
+ );
119
+
120
+ // 3. Stages
121
+ const stages = await generateEntity(
122
+ "stages",
123
+ "stages",
124
+ buildStagePrompt(framework.stages, ctx, schemas.stages),
125
+ proseEngine,
126
+ );
127
+
128
+ // 4. Behaviours (parallel — no cross-references)
129
+ const behaviours = await Promise.all(
130
+ framework.behaviours.map((b) =>
131
+ generateEntity(
132
+ "behaviour",
133
+ b.id,
134
+ buildBehaviourPrompt(b, ctx, schemas.behaviour),
135
+ proseEngine,
136
+ ).then((data) => ({ ...data, _id: b.id })),
137
+ ),
138
+ );
139
+
140
+ // 5. Capabilities with skills (parallel)
141
+ const capabilities = await Promise.all(
142
+ framework.capabilities.map((c, i) =>
143
+ generateEntity(
144
+ "capability",
145
+ c.id,
146
+ buildCapabilityPrompt(
147
+ { ...c, ordinalRank: i + 1 },
148
+ ctx,
149
+ schemas.capability,
150
+ ),
151
+ proseEngine,
152
+ ).then((data) => ({ ...data, _id: c.id })),
153
+ ),
154
+ );
155
+
156
+ // Collect all skill IDs and behaviour IDs from DSL declarations
157
+ // (not from LLM output — these must be available even in no-prose mode)
158
+ const skillIds = framework.capabilities.flatMap((c) => c.skills || []);
159
+ const behaviourIds = framework.behaviours.map((b) => b.id);
160
+
161
+ // 6. Drivers (reference skills + behaviours)
162
+ const drivers = await generateEntity(
163
+ "drivers",
164
+ "drivers",
165
+ buildDriverPrompt(
166
+ framework.drivers,
167
+ { ...ctx, skillIds, behaviourIds },
168
+ schemas.drivers,
169
+ ),
170
+ proseEngine,
171
+ );
172
+
173
+ // 7. Disciplines (reference skills, behaviours, track IDs from DSL)
174
+ const trackIds = framework.tracks.map((t) => t.id);
175
+ const disciplines = await Promise.all(
176
+ framework.disciplines.map((d) =>
177
+ generateEntity(
178
+ "discipline",
179
+ d.id,
180
+ buildDisciplinePrompt(
181
+ d,
182
+ { ...ctx, skillIds, behaviourIds, trackIds },
183
+ schemas.discipline,
184
+ ),
185
+ proseEngine,
186
+ ).then((data) => ({ ...data, _id: d.id })),
187
+ ),
188
+ );
189
+
190
+ // 8. Tracks (reference capability IDs for skillModifiers)
191
+ const capabilityIds = framework.capabilities.map((c) => c.id);
192
+ const tracks = await Promise.all(
193
+ framework.tracks.map((t) =>
194
+ generateEntity(
195
+ "track",
196
+ t.id,
197
+ buildTrackPrompt(
198
+ t,
199
+ { ...ctx, capabilityIds, skillIds, behaviourIds },
200
+ schemas.track,
201
+ ),
202
+ proseEngine,
203
+ ).then((data) => ({ ...data, _id: t.id })),
204
+ ),
205
+ );
206
+
207
+ // 9. Self-assessments (deterministic — no LLM)
208
+ const selfAssessments = generateSelfAssessments(
209
+ framework,
210
+ skillIds,
211
+ behaviourIds,
212
+ );
213
+
214
+ return {
215
+ framework: fw,
216
+ levels,
217
+ stages,
218
+ behaviours,
219
+ capabilities,
220
+ drivers,
221
+ disciplines,
222
+ tracks,
223
+ selfAssessments,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Generate a single entity via the prose engine.
229
+ *
230
+ * @param {string} entityType - Entity type for cache key prefix
231
+ * @param {string} entityId - Entity ID for cache key
232
+ * @param {{ system: string, user: string }} prompt - Built prompt
233
+ * @param {import('./prose.js').ProseEngine} proseEngine - Prose engine
234
+ * @returns {Promise<object|null>} Parsed JSON data
235
+ */
236
+ async function generateEntity(entityType, entityId, prompt, proseEngine) {
237
+ const key = `pathway:${entityType}:${entityId}`;
238
+ const result = await proseEngine.generateJson(key, [
239
+ { role: "system", content: prompt.system },
240
+ { role: "user", content: prompt.user },
241
+ ]);
242
+ return result;
243
+ }
244
+
245
+ /**
246
+ * Simple seeded PRNG (mulberry32). Deterministic given the same seed.
247
+ * @param {number} seed
248
+ * @returns {() => number} Returns values in [0, 1)
249
+ */
250
+ function createRng(seed) {
251
+ let s = seed | 0;
252
+ return () => {
253
+ s = (s + 0x6d2b79f5) | 0;
254
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
255
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
256
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Pick a random index near a base, weighted toward ±1 with rare ±2 outliers.
262
+ * @param {() => number} rng - Seeded random function
263
+ * @param {number} base - Centre index for this level
264
+ * @param {number} max - Maximum valid index (inclusive)
265
+ * @returns {number} Clamped index
266
+ */
267
+ function jitter(rng, base, max) {
268
+ const r = rng();
269
+ // 50% same, 20% +1, 15% -1, 10% +2, 5% -2
270
+ let offset = 0;
271
+ if (r < 0.5) offset = 0;
272
+ else if (r < 0.7) offset = 1;
273
+ else if (r < 0.85) offset = -1;
274
+ else if (r < 0.95) offset = 2;
275
+ else offset = -2;
276
+ return Math.max(0, Math.min(max, base + offset));
277
+ }
278
+
279
+ /**
280
+ * Generate self-assessments with realistic randomized distributions.
281
+ *
282
+ * Each assessment centres skills around the expected proficiency for
283
+ * that level, then applies per-skill jitter so profiles look natural:
284
+ * most skills cluster near the base, with occasional outliers.
285
+ * Behaviours use tighter jitter (±1 only, less variance).
286
+ *
287
+ * @param {object} framework - Framework AST
288
+ * @param {string[]} skillIds - All skill IDs from capabilities
289
+ * @param {string[]} behaviourIds - All behaviour IDs
290
+ * @returns {object[]}
291
+ */
292
+ function generateSelfAssessments(framework, skillIds, behaviourIds) {
293
+ const proficiencies = framework.proficiencies || [
294
+ "awareness",
295
+ "foundational",
296
+ "working",
297
+ "practitioner",
298
+ "expert",
299
+ ];
300
+ const maturities = framework.maturities || [
301
+ "emerging",
302
+ "developing",
303
+ "practicing",
304
+ "role_modeling",
305
+ "exemplifying",
306
+ ];
307
+
308
+ const seed = framework.seed || 1;
309
+ const rng = createRng(seed);
310
+ const maxP = proficiencies.length - 1;
311
+ const maxM = maturities.length - 1;
312
+
313
+ const assessments = [];
314
+ const levelNames = ["junior", "mid", "senior", "staff", "principal"];
315
+
316
+ for (let i = 0; i < Math.min(levelNames.length, proficiencies.length); i++) {
317
+ const skillProficiencies = {};
318
+ for (const skillId of skillIds) {
319
+ skillProficiencies[skillId] = proficiencies[jitter(rng, i, maxP)];
320
+ }
321
+
322
+ const behaviourMaturities = {};
323
+ for (const behaviourId of behaviourIds) {
324
+ // Behaviours use tighter variance: ±1 only (no ±2 outliers)
325
+ const r = rng();
326
+ let offset = 0;
327
+ if (r < 0.55) offset = 0;
328
+ else if (r < 0.8) offset = 1;
329
+ else offset = -1;
330
+ behaviourMaturities[behaviourId] =
331
+ maturities[Math.max(0, Math.min(maxM, i + offset))];
332
+ }
333
+
334
+ assessments.push({
335
+ id: `example_${levelNames[i]}`,
336
+ skillProficiencies,
337
+ behaviourMaturities,
338
+ });
339
+ }
340
+
341
+ return assessments;
342
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Prose Engine — LLM-assisted prose generation with cache.
3
+ *
4
+ * Uses libllm for completions, libutil for cache key hashing,
5
+ * and libprompt for prompt template loading.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync } from "fs";
9
+ import { dirname, join } from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { generateHash } from "@forwardimpact/libutil";
12
+ import { createLogger } from "@forwardimpact/libtelemetry";
13
+ import { PromptLoader } from "@forwardimpact/libprompt";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+
17
+ export class ProseEngine {
18
+ /**
19
+ * @param {object} options
20
+ * @param {string} options.cachePath Path to .prose-cache.json
21
+ * @param {string} options.mode "cached" | "generate" | "no-prose"
22
+ * @param {boolean} [options.strict] Fail on cache miss
23
+ * @param {import('@forwardimpact/libllm').LlmApi} options.llmApi
24
+ * Pre-configured LLM client — required when mode is "generate"
25
+ * @param {import('@forwardimpact/libprompt').PromptLoader} options.promptLoader
26
+ * Prompt template loader
27
+ * @param {object} options.logger Logger instance
28
+ */
29
+ constructor({
30
+ cachePath,
31
+ mode,
32
+ strict = false,
33
+ llmApi,
34
+ promptLoader,
35
+ logger,
36
+ }) {
37
+ if (!cachePath) throw new Error("cachePath is required");
38
+ if (!mode) throw new Error("mode is required");
39
+ if (!promptLoader) throw new Error("promptLoader is required");
40
+ if (!logger) throw new Error("logger is required");
41
+ this.cachePath = cachePath;
42
+ this.mode = mode;
43
+ this.strict = strict;
44
+ this.llmApi = llmApi;
45
+ this.promptLoader = promptLoader;
46
+ this.logger = logger;
47
+ this.cache = this.#loadCache();
48
+ this.dirty = false;
49
+ this.stats = { hits: 0, misses: 0, generated: 0 };
50
+ }
51
+
52
+ /**
53
+ * Generate or retrieve prose for a key.
54
+ * @param {string} key
55
+ * @param {object} context
56
+ * @returns {Promise<string|null>}
57
+ */
58
+ async generateProse(key, context) {
59
+ if (this.mode === "no-prose") return null;
60
+
61
+ const cacheKey = generateHash(key, JSON.stringify(context));
62
+
63
+ if (this.cache.has(cacheKey)) {
64
+ this.stats.hits++;
65
+ return this.cache.get(cacheKey);
66
+ }
67
+
68
+ if (this.mode === "cached") {
69
+ this.stats.misses++;
70
+ if (this.strict) throw new Error(`Cache miss: '${key}'`);
71
+ return null;
72
+ }
73
+
74
+ // Tier 1: generate via libllm
75
+ const prose = await this.#callLlm(key, context);
76
+ this.stats.generated++;
77
+ if (prose) {
78
+ this.cache.set(cacheKey, prose);
79
+ this.dirty = true;
80
+ }
81
+ return prose;
82
+ }
83
+
84
+ /**
85
+ * @param {string} key
86
+ * @param {object} context
87
+ * @returns {Promise<string|null>}
88
+ */
89
+ async #callLlm(key, context) {
90
+ const prompt = this.#buildPrompt(key, context);
91
+ const response = await this.llmApi.createCompletions({
92
+ messages: [
93
+ { role: "system", content: this.promptLoader.load("prose-system") },
94
+ { role: "user", content: prompt },
95
+ ],
96
+ max_tokens: context.maxTokens || 500,
97
+ });
98
+ const content = response.choices?.[0]?.message?.content?.trim() || null;
99
+ this.logger.info("prose", `Generated: ${key}`, {
100
+ chars: content ? content.length : 0,
101
+ });
102
+ return content;
103
+ }
104
+
105
+ /**
106
+ * Generate or retrieve a structured response (pre-built messages).
107
+ * @param {string} key - Cache key
108
+ * @param {object[]} messages - Pre-built messages array [{role, content}]
109
+ * @returns {Promise<string|null>}
110
+ */
111
+ async generateStructured(key, messages) {
112
+ if (this.mode === "no-prose") return null;
113
+
114
+ const cacheKey = generateHash(key, JSON.stringify(messages));
115
+
116
+ if (this.cache.has(cacheKey)) {
117
+ this.stats.hits++;
118
+ this.logger.debug("prose", `Cache hit: ${key}`);
119
+ return this.cache.get(cacheKey);
120
+ }
121
+
122
+ if (this.mode === "cached") {
123
+ this.stats.misses++;
124
+ if (this.strict) throw new Error(`Cache miss: '${key}'`);
125
+ return null;
126
+ }
127
+
128
+ const response = await this.llmApi.createCompletions({
129
+ messages,
130
+ max_tokens: 4000,
131
+ });
132
+ const content = response.choices?.[0]?.message?.content?.trim() || null;
133
+ this.stats.generated++;
134
+ if (content) {
135
+ this.cache.set(cacheKey, content);
136
+ this.dirty = true;
137
+ }
138
+ this.logger.info("prose", `Generated structured: ${key}`, {
139
+ chars: content ? content.length : 0,
140
+ });
141
+ return content;
142
+ }
143
+
144
+ /**
145
+ * Generate structured output and parse as JSON.
146
+ * @param {string} key - Cache key
147
+ * @param {object[]} messages - Pre-built messages array
148
+ * @returns {Promise<object|null>}
149
+ */
150
+ async generateJson(key, messages) {
151
+ const raw = await this.generateStructured(key, messages);
152
+ if (!raw) return null;
153
+ const cleaned = raw
154
+ .replace(/^```(?:json)?\s*\n?/m, "")
155
+ .replace(/\n?```\s*$/m, "")
156
+ .trim();
157
+ return JSON.parse(cleaned);
158
+ }
159
+
160
+ /** @returns {Map<string, string>} */
161
+ getProseMap() {
162
+ return this.cache;
163
+ }
164
+
165
+ saveCache() {
166
+ if (!this.dirty || !this.cachePath) return;
167
+ writeFileSync(
168
+ this.cachePath,
169
+ JSON.stringify(Object.fromEntries(this.cache), null, 2),
170
+ );
171
+ this.dirty = false;
172
+ }
173
+
174
+ /**
175
+ * Build a prompt from key and context.
176
+ * @param {string} key
177
+ * @param {object} context
178
+ * @returns {string}
179
+ */
180
+ #buildPrompt(key, context) {
181
+ return this.promptLoader.render("prose-user", {
182
+ topic: context.topic || key.replace(/_/g, " ").replace(/-/g, " "),
183
+ tone: context.tone || "technical",
184
+ length: context.length || "2-3 paragraphs",
185
+ domain: context.domain,
186
+ role: context.role,
187
+ audience: context.audience,
188
+ scenario: context.scenario,
189
+ driver: context.driver,
190
+ direction: context.direction,
191
+ magnitude: context.magnitude,
192
+ });
193
+ }
194
+
195
+ #loadCache() {
196
+ try {
197
+ if (this.cachePath && existsSync(this.cachePath)) {
198
+ return new Map(
199
+ Object.entries(JSON.parse(readFileSync(this.cachePath, "utf-8"))),
200
+ );
201
+ }
202
+ } catch {
203
+ /* cache corrupt or missing */
204
+ }
205
+ return new Map();
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Creates a ProseEngine with real dependencies wired.
211
+ * @param {object} options
212
+ * @param {string} options.cachePath - Path to .prose-cache.json
213
+ * @param {string} options.mode - "cached" | "generate" | "no-prose"
214
+ * @param {boolean} [options.strict] - Fail on cache miss
215
+ * @param {import('@forwardimpact/libllm').LlmApi} [options.llmApi] - LLM client
216
+ * @returns {ProseEngine}
217
+ */
218
+ export function createProseEngine(options) {
219
+ const logger = createLogger("universe");
220
+ const promptLoader = new PromptLoader(join(__dirname, "..", "prompts"));
221
+ return new ProseEngine({ ...options, promptLoader, logger });
222
+ }
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { ProseEngine, createProseEngine } from "./engine/prose.js";
2
+ export { PathwayGenerator, loadSchemas } from "./engine/pathway.js";
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@forwardimpact/libsyntheticprose",
3
+ "version": "0.1.1",
4
+ "description": "LLM-based prose and pathway generation for synthetic data",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/forwardimpact/monorepo",
9
+ "directory": "libraries/libsyntheticprose"
10
+ },
11
+ "type": "module",
12
+ "main": "index.js",
13
+ "exports": {
14
+ ".": "./index.js",
15
+ "./prose": "./engine/prose.js",
16
+ "./pathway": "./engine/pathway.js"
17
+ },
18
+ "dependencies": {
19
+ "@forwardimpact/libutil": "^0.1.61",
20
+ "@forwardimpact/libtelemetry": "^0.1.23",
21
+ "@forwardimpact/libprompt": "^0.1.0",
22
+ "ajv": "^8.12.0",
23
+ "ajv-formats": "^2.1.1",
24
+ "yaml": "^2.3.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Prompt template for a single behaviour entity.
3
+ *
4
+ * @param {object} skeleton - Behaviour skeleton { id, name }
5
+ * @param {object} ctx - Universe context
6
+ * @param {object} schema - JSON schema for behaviour
7
+ * @returns {{ system: string, user: string }}
8
+ */
9
+ export function buildBehaviourPrompt(skeleton, ctx, schema) {
10
+ return {
11
+ system: [
12
+ "You are an expert career framework author.",
13
+ "Output ONLY valid JSON. No markdown fences, no explanations.",
14
+ `The organization domain is: ${ctx.domain}.`,
15
+ `Industry: ${ctx.industry}.`,
16
+ ].join(" "),
17
+
18
+ user: [
19
+ "Generate a behaviour definition for a career framework.",
20
+ "",
21
+ "## JSON Schema (you MUST conform to this exactly)",
22
+ "```json",
23
+ JSON.stringify(schema, null, 2),
24
+ "```",
25
+ "",
26
+ `## Behaviour: "${skeleton.name}" (ID: ${skeleton.id})`,
27
+ "",
28
+ "## Instructions",
29
+ "- name: Use the provided name exactly.",
30
+ "- human.description: 2-3 sentences describing this behaviour.",
31
+ "- human.maturityDescriptions: One paragraph per maturity level",
32
+ " (emerging, developing, practicing, role_modeling, exemplifying).",
33
+ ' Use second-person ("You..."). Each level must show clear',
34
+ " progression in depth, consistency, and influence.",
35
+ "- agent.title: Short title (2-4 words) for how the agent applies this behaviour.",
36
+ "- agent.workingStyle: 1-2 sentences describing how the AI agent should embody",
37
+ " this behaviour in its work style and communication.",
38
+ "",
39
+ "Output a single JSON object for this behaviour file.",
40
+ ].join("\n"),
41
+ };
42
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Prompt template for a single capability entity (with skills).
3
+ *
4
+ * @param {object} skeleton - Capability skeleton { id, name, skills, ordinalRank }
5
+ * @param {object} ctx - Universe context
6
+ * @param {object} schema - JSON schema for capability
7
+ * @returns {{ system: string, user: string }}
8
+ */
9
+ export function buildCapabilityPrompt(skeleton, ctx, schema) {
10
+ return {
11
+ system: [
12
+ "You are an expert career framework author.",
13
+ "Output ONLY valid JSON. No markdown fences, no explanations.",
14
+ `The organization domain is: ${ctx.domain}.`,
15
+ `Industry: ${ctx.industry}.`,
16
+ ].join(" "),
17
+
18
+ user: [
19
+ "Generate a capability definition for a career framework.",
20
+ "",
21
+ "## JSON Schema (you MUST conform to this exactly)",
22
+ "```json",
23
+ JSON.stringify(schema, null, 2),
24
+ "```",
25
+ "",
26
+ `## Skeleton`,
27
+ `Capability ID: ${skeleton.id}`,
28
+ `Capability name: ${skeleton.name}`,
29
+ `Skills to define: ${skeleton.skills.join(", ")}`,
30
+ `Ordinal rank: ${skeleton.ordinalRank}`,
31
+ "",
32
+ "## Instructions",
33
+ "- id: Use the provided capability ID.",
34
+ "- name: Use the provided name.",
35
+ "- emojiIcon: A single emoji representing this capability.",
36
+ `- ordinalRank: ${skeleton.ordinalRank}`,
37
+ "- description: 1-2 sentences describing this capability area.",
38
+ "- professionalResponsibilities: One sentence per proficiency level",
39
+ " (awareness through expert) describing IC expectations.",
40
+ "- managementResponsibilities: Same for management track.",
41
+ "- skills: For each skill ID listed above, generate:",
42
+ " - id: Use the provided skill ID exactly.",
43
+ " - name: Human-readable name (title case).",
44
+ " - human.description: 2-3 sentences.",
45
+ " - human.proficiencyDescriptions: One paragraph per level",
46
+ " (awareness, foundational, working, practitioner, expert).",
47
+ ' Use second-person ("You..."). Each level must show clear',
48
+ " progression in scope, autonomy, and complexity.",
49
+ "- For each skill, also generate an agent section:",
50
+ " - agent.name: kebab-case name (e.g., 'code-review', 'data-modeling').",
51
+ " - agent.description: 1 sentence describing what this agent skill provides.",
52
+ " - agent.useWhen: 1 sentence describing when/why an agent should use this skill.",
53
+ " - agent.stages: Object with ONLY the stages where this skill is meaningfully relevant.",
54
+ " Not all skills need all 6 stages. Use these criteria:",
55
+ " - specify: include if the skill informs what to build or constrains requirements",
56
+ " - plan: include if the skill drives architecture or design decisions",
57
+ " - onboard: include if the skill requires tooling, dependencies, or env setup",
58
+ " - code: include if the skill is directly exercised during implementation",
59
+ " - review: include if the skill has quality criteria to verify",
60
+ " - deploy: include if the skill has production or operational concerns",
61
+ " Each skill must have at least 2 stages. Omit stages where the skill has no specific guidance.",
62
+ " Each stage has:",
63
+ " - focus: 1 sentence — the primary focus for this skill in this stage.",
64
+ " - readChecklist: Array of 2-3 items — steps to read/understand before acting.",
65
+ " - confirmChecklist: Array of 2-3 items — items to verify after completing work.",
66
+ "",
67
+ "Output the JSON object for this single capability file.",
68
+ ].join("\n"),
69
+ };
70
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Prompt template for a single discipline entity.
3
+ *
4
+ * @param {object} skeleton - Discipline skeleton from DSL
5
+ * @param {object} ctx - Universe context (includes skillIds, behaviourIds, trackIds)
6
+ * @param {object} schema - JSON schema for discipline
7
+ * @returns {{ system: string, user: string }}
8
+ */
9
+ export function buildDisciplinePrompt(skeleton, ctx, schema) {
10
+ return {
11
+ system: [
12
+ "You are an expert career framework author.",
13
+ "Output ONLY valid JSON. No markdown fences, no explanations.",
14
+ `The organization domain is: ${ctx.domain}.`,
15
+ `Industry: ${ctx.industry}.`,
16
+ ].join(" "),
17
+
18
+ user: [
19
+ "Generate a discipline definition for a career framework.",
20
+ "",
21
+ "## JSON Schema (you MUST conform to this exactly)",
22
+ "```json",
23
+ JSON.stringify(schema, null, 2),
24
+ "```",
25
+ "",
26
+ `## Skeleton`,
27
+ `Discipline ID: ${skeleton.id}`,
28
+ `Role title: ${skeleton.roleTitle || skeleton.id.replace(/_/g, " ")}`,
29
+ `Specialization: ${skeleton.specialization || skeleton.roleTitle || skeleton.id.replace(/_/g, " ")}`,
30
+ `isProfessional: ${skeleton.isProfessional !== false}`,
31
+ `Core skills: ${(skeleton.core || []).join(", ")}`,
32
+ `Supporting skills: ${(skeleton.supporting || []).join(", ")}`,
33
+ `Broad skills: ${(skeleton.broad || []).join(", ")}`,
34
+ `Valid tracks: ${JSON.stringify(skeleton.validTracks || [null])}`,
35
+ "",
36
+ `## Available skill IDs: ${(ctx.skillIds || []).join(", ")}`,
37
+ `## Available behaviour IDs: ${(ctx.behaviourIds || []).join(", ")}`,
38
+ `## Available track IDs: ${(ctx.trackIds || []).join(", ")}`,
39
+ "",
40
+ "## Instructions",
41
+ "- specialization: Use the provided specialization or generate from role title.",
42
+ "- roleTitle: Use the provided role title.",
43
+ "- isProfessional: Use the provided value.",
44
+ "- isManagement: Set to true only for management disciplines.",
45
+ "- validTracks: Use the provided array. null means trackless/generalist is allowed.",
46
+ "- description: 2-3 sentences describing this discipline.",
47
+ "- coreSkills: Use the provided core skill IDs. Must all exist in available list.",
48
+ "- supportingSkills: Use the provided supporting skill IDs.",
49
+ "- broadSkills: Use the provided broad skill IDs.",
50
+ "- behaviourModifiers: Object mapping behaviour IDs to modifiers (-1, 0, or 1).",
51
+ " Include 2-3 behaviour modifiers relevant to this discipline.",
52
+ "- human.roleSummary: 2-3 sentences describing this role. May use {roleTitle} or {specialization}.",
53
+ "- agent.identity: 1-2 sentences defining the AI coding agent's core identity.",
54
+ " Frame as 'You are a {roleTitle} agent that...' May use {roleTitle} or {roleName} placeholder.",
55
+ "- agent.priority: 1 sentence stating the agent's top priority (e.g., code quality, system reliability).",
56
+ "- agent.constraints: 2-3 things the agent must avoid or never do.",
57
+ "",
58
+ "Output a single JSON object for this discipline.",
59
+ ].join("\n"),
60
+ };
61
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Prompt template for drivers.yaml — all drivers in a single call.
3
+ *
4
+ * @param {object[]} drivers - Driver skeletons from DSL
5
+ * @param {object} ctx - Universe context (includes skillIds, behaviourIds)
6
+ * @param {object} schema - JSON schema for drivers
7
+ * @returns {{ system: string, user: string }}
8
+ */
9
+ export function buildDriverPrompt(drivers, ctx, schema) {
10
+ const driverList = drivers
11
+ .map((d) => {
12
+ const parts = [` - id: ${d.id}, name: "${d.name}"`];
13
+ if (d.skills?.length) parts.push(` skills: [${d.skills.join(", ")}]`);
14
+ if (d.behaviours?.length)
15
+ parts.push(` behaviours: [${d.behaviours.join(", ")}]`);
16
+ return parts.join("\n");
17
+ })
18
+ .join("\n");
19
+
20
+ return {
21
+ system: [
22
+ "You are an expert career framework author.",
23
+ "Output ONLY valid JSON. No markdown fences, no explanations.",
24
+ `The organization domain is: ${ctx.domain}.`,
25
+ `Industry: ${ctx.industry}.`,
26
+ ].join(" "),
27
+
28
+ user: [
29
+ "Generate organizational driver definitions for a career framework.",
30
+ "",
31
+ "## JSON Schema (you MUST conform to this exactly)",
32
+ "```json",
33
+ JSON.stringify(schema, null, 2),
34
+ "```",
35
+ "",
36
+ "## Driver Skeletons",
37
+ driverList,
38
+ "",
39
+ `## Available skill IDs: ${(ctx.skillIds || []).join(", ")}`,
40
+ `## Available behaviour IDs: ${(ctx.behaviourIds || []).join(", ")}`,
41
+ "",
42
+ "## Instructions",
43
+ "- Output a JSON array of driver objects.",
44
+ "- For each driver:",
45
+ " - id: Use the provided ID.",
46
+ " - name: Use the provided name.",
47
+ " - description: 2-3 sentences describing this organizational outcome.",
48
+ " - contributingSkills: Use the skill IDs listed in the skeleton.",
49
+ " All referenced skill IDs MUST be from the available list above.",
50
+ " - contributingBehaviours: Use the behaviour IDs listed in the skeleton.",
51
+ " All referenced behaviour IDs MUST be from the available list above.",
52
+ "",
53
+ "Output a JSON array.",
54
+ ].join("\n"),
55
+ };
56
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Prompt template for framework.yaml metadata.
3
+ *
4
+ * @param {object} skeleton - Framework skeleton from DSL
5
+ * @param {object} ctx - Universe context (domain, industry)
6
+ * @param {object} schema - JSON schema for framework entity
7
+ * @returns {{ system: string, user: string }}
8
+ */
9
+ export function buildFrameworkPrompt(skeleton, ctx, schema) {
10
+ return {
11
+ system: [
12
+ "You are an expert career framework author.",
13
+ "Output ONLY valid JSON. No markdown fences, no explanations.",
14
+ `The organization domain is: ${ctx.domain}.`,
15
+ `Industry: ${ctx.industry}.`,
16
+ ].join(" "),
17
+
18
+ user: [
19
+ "Generate a framework metadata file for an engineering career framework.",
20
+ "",
21
+ "## JSON Schema (you MUST conform to this exactly)",
22
+ "```json",
23
+ JSON.stringify(schema, null, 2),
24
+ "```",
25
+ "",
26
+ "## Instructions",
27
+ '- title: A short, compelling title for this engineering pathway (e.g., "BioNova Engineering Pathway").',
28
+ "- emojiIcon: A single emoji representing engineering growth.",
29
+ '- tag: A short hashtag identifier (e.g., "#BioNova").',
30
+ "- description: 2-3 sentences describing the framework's purpose.",
31
+ `- distribution.siteUrl: Use "https://${ctx.domain}/pathway".`,
32
+ "- entityDefinitions: Provide definitions for these entity types:",
33
+ " driver, skill, behaviour, discipline, level, track, job, agent, stage, tool.",
34
+ " Each needs: title, emojiIcon, description (1 sentence).",
35
+ "",
36
+ "Output a single JSON object.",
37
+ ].join("\n"),
38
+ };
39
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Prompt template for levels.yaml — all levels in a single call.
3
+ *
4
+ * @param {object[]} levels - Level skeletons from DSL
5
+ * @param {object} ctx - Universe context
6
+ * @param {object} schema - JSON schema for levels
7
+ * @returns {{ system: string, user: string }}
8
+ */
9
+ export function buildLevelPrompt(levels, ctx, schema) {
10
+ const levelList = levels
11
+ .map(
12
+ (l) =>
13
+ ` - id: ${l.id}, professionalTitle: "${l.professionalTitle || ""}", rank: ${l.rank}, experience: "${l.experience || ""}"`,
14
+ )
15
+ .join("\n");
16
+
17
+ return {
18
+ system: [
19
+ "You are an expert career framework author.",
20
+ "Output ONLY valid JSON. No markdown fences, no explanations.",
21
+ `The organization domain is: ${ctx.domain}.`,
22
+ `Industry: ${ctx.industry}.`,
23
+ ].join(" "),
24
+
25
+ user: [
26
+ "Generate career level definitions for an engineering pathway.",
27
+ "",
28
+ "## JSON Schema (you MUST conform to this exactly)",
29
+ "```json",
30
+ JSON.stringify(schema, null, 2),
31
+ "```",
32
+ "",
33
+ "## Level Skeletons",
34
+ levelList,
35
+ "",
36
+ "## Instructions",
37
+ "- Output a JSON array of level objects.",
38
+ "- For each level, generate:",
39
+ " - id: Use the provided ID (uppercase, e.g., J040).",
40
+ " - professionalTitle: Use the provided title or generate one.",
41
+ " - managementTitle: Generate a management-track equivalent.",
42
+ " - ordinalRank: Use the provided rank.",
43
+ " - typicalExperienceRange: Use the provided experience range.",
44
+ " - qualificationSummary: 2-3 sentences describing qualifications.",
45
+ " May use {typicalExperienceRange} placeholder.",
46
+ " - baseSkillProficiencies: { primary, secondary, broad } using",
47
+ " awareness/foundational/working/practitioner/expert.",
48
+ " Increase across levels (L1→awareness, L5→expert for primary).",
49
+ " - baseBehaviourMaturity: emerging/developing/practicing/role_modeling/exemplifying.",
50
+ " Increase across levels.",
51
+ " - expectations: { impactScope, autonomyExpectation, influenceScope, complexityHandled }.",
52
+ " Each 1 sentence showing clear progression.",
53
+ " - breadthCriteria: Only for rank >= 4. Object mapping proficiency → min count.",
54
+ "",
55
+ "Output a JSON array.",
56
+ ].join("\n"),
57
+ };
58
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Prompt template for stages.yaml — all stages in a single call.
3
+ *
4
+ * @param {string[]} stageIds - Stage ID list from DSL
5
+ * @param {object} ctx - Universe context
6
+ * @param {object} schema - JSON schema for stages
7
+ * @returns {{ system: string, user: string }}
8
+ */
9
+ export function buildStagePrompt(stageIds, ctx, schema) {
10
+ return {
11
+ system: [
12
+ "You are an expert career framework author.",
13
+ "Output ONLY valid JSON. No markdown fences, no explanations.",
14
+ `The organization domain is: ${ctx.domain}.`,
15
+ `Industry: ${ctx.industry}.`,
16
+ ].join(" "),
17
+
18
+ user: [
19
+ "Generate engineering lifecycle stage definitions.",
20
+ "",
21
+ "## JSON Schema (you MUST conform to this exactly)",
22
+ "```json",
23
+ JSON.stringify(schema, null, 2),
24
+ "```",
25
+ "",
26
+ `## Stage IDs: ${stageIds.join(", ")}`,
27
+ "",
28
+ "## Instructions",
29
+ "- Output a JSON array of stage objects.",
30
+ "- For each stage ID, generate:",
31
+ " - id: The stage ID (must be one of: specify, plan, onboard, code, review, deploy).",
32
+ ' - name: Human-readable name (e.g., "Specify", "Plan").',
33
+ " - emojiIcon: A single emoji for this stage.",
34
+ ' - description: 2-3 sentences in second person ("You...").',
35
+ " - summary: 1 sentence in third person.",
36
+ " - handoffs: Array of transitions to other stages, each with:",
37
+ " targetStage, label (button text), prompt (instructions for next stage).",
38
+ " - constraints: 2-3 restrictions on behaviour in this stage.",
39
+ " - readChecklist: 2-4 Read-Then-Do steps.",
40
+ " - confirmChecklist: 2-4 Do-Then-Confirm items.",
41
+ "",
42
+ "Output a JSON array.",
43
+ ].join("\n"),
44
+ };
45
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Prompt template for a single track entity.
3
+ *
4
+ * @param {object} skeleton - Track skeleton { id, name }
5
+ * @param {object} ctx - Universe context (includes capabilityIds, behaviourIds)
6
+ * @param {object} schema - JSON schema for track
7
+ * @returns {{ system: string, user: string }}
8
+ */
9
+ export function buildTrackPrompt(skeleton, ctx, schema) {
10
+ return {
11
+ system: [
12
+ "You are an expert career framework author.",
13
+ "Output ONLY valid JSON. No markdown fences, no explanations.",
14
+ `The organization domain is: ${ctx.domain}.`,
15
+ `Industry: ${ctx.industry}.`,
16
+ ].join(" "),
17
+
18
+ user: [
19
+ "Generate a track definition for a career framework.",
20
+ "",
21
+ "## JSON Schema (you MUST conform to this exactly)",
22
+ "```json",
23
+ JSON.stringify(schema, null, 2),
24
+ "```",
25
+ "",
26
+ `## Skeleton`,
27
+ `Track ID: ${skeleton.id}`,
28
+ `Track name: ${skeleton.name}`,
29
+ "",
30
+ `## Available capability IDs: ${(ctx.capabilityIds || []).join(", ")}`,
31
+ `## Available behaviour IDs: ${(ctx.behaviourIds || []).join(", ")}`,
32
+ "",
33
+ "## Instructions",
34
+ "- name: Use the provided name exactly.",
35
+ "- description: 2-3 sentences describing this track's focus.",
36
+ "- roleContext: 1-2 sentences contextualizing the role for job listings.",
37
+ "- skillModifiers: Object mapping capability IDs to integer modifiers.",
38
+ " Use values from -1 to 1. Include modifiers for capabilities most",
39
+ " affected by this track specialization.",
40
+ "- behaviourModifiers: Object mapping behaviour IDs to integer modifiers.",
41
+ " Include 1-2 relevant modifiers.",
42
+ "- assessmentWeights: { skillWeight, behaviourWeight } summing to 1.",
43
+ "- agent.identity: 1 sentence identity override for the agent when working in this track.",
44
+ " May use {roleTitle} placeholder. Example: 'You specialize in platform infrastructure.'",
45
+ "- agent.priority: 1 sentence stating the track-specific priority.",
46
+ "- agent.constraints: 1-2 additional constraints specific to this track.",
47
+ "",
48
+ "Output a single JSON object for this track.",
49
+ ].join("\n"),
50
+ };
51
+ }
@@ -0,0 +1,2 @@
1
+ You are a technical writer for a pharmaceutical company. Generate concise,
2
+ realistic content. Output the text only, no explanations or markdown formatting.
@@ -0,0 +1,6 @@
1
+ Write {{length}} of {{tone}} prose about: {{topic}}. {{#domain}} Company domain:
2
+ {{domain}}. {{/domain}} {{#role}} Written from the perspective of: {{role}}.
3
+ {{/role}} {{#audience}} Target audience: {{audience}}. {{/audience}}
4
+ {{#scenario}} Context: during "{{scenario}}", the {{driver}} driver is
5
+ {{direction}} (magnitude: {{magnitude}}). {{/scenario}} Output the text only, no
6
+ explanations.