@forwardimpact/libsyntheticprose 0.1.2 → 0.1.4
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/engine/pathway.js +56 -26
- package/engine/prose.js +7 -6
- package/package.json +1 -1
- package/prompts/pathway/behaviour.js +28 -8
- package/prompts/pathway/capability.js +49 -16
- package/prompts/pathway/discipline.js +48 -8
- package/prompts/pathway/driver.js +11 -6
- package/prompts/pathway/framework.js +13 -7
- package/prompts/pathway/level.js +18 -9
- package/prompts/pathway/preamble.js +26 -0
- package/prompts/pathway/stage.js +14 -7
- package/prompts/pathway/track.js +47 -7
- package/prompts/prose-user.prompt.md +4 -2
- package/test/pathway-generator.test.js +176 -0
- package/test/prompt-builders.test.js +249 -0
- package/test/prose-engine.test.js +156 -0
package/engine/pathway.js
CHANGED
|
@@ -18,6 +18,10 @@ import { buildCapabilityPrompt } from "../prompts/pathway/capability.js";
|
|
|
18
18
|
import { buildDriverPrompt } from "../prompts/pathway/driver.js";
|
|
19
19
|
import { buildDisciplinePrompt } from "../prompts/pathway/discipline.js";
|
|
20
20
|
import { buildTrackPrompt } from "../prompts/pathway/track.js";
|
|
21
|
+
import {
|
|
22
|
+
PROFICIENCY_LEVELS,
|
|
23
|
+
MATURITY_LEVELS,
|
|
24
|
+
} from "@forwardimpact/libsyntheticgen/vocabulary.js";
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Load JSON schemas from the schema directory.
|
|
@@ -99,7 +103,10 @@ async function generatePathwayData({
|
|
|
99
103
|
schemas,
|
|
100
104
|
proseEngine,
|
|
101
105
|
}) {
|
|
102
|
-
const
|
|
106
|
+
const frameworkName = framework.name || domain;
|
|
107
|
+
const ctx = { domain, industry, frameworkName };
|
|
108
|
+
const BASE_TOKENS = 2000;
|
|
109
|
+
const PER_SKILL_TOKENS = 800;
|
|
103
110
|
|
|
104
111
|
// 1. Framework metadata
|
|
105
112
|
const fw = await generateEntity(
|
|
@@ -107,6 +114,7 @@ async function generatePathwayData({
|
|
|
107
114
|
"framework",
|
|
108
115
|
buildFrameworkPrompt(framework, ctx, schemas.framework),
|
|
109
116
|
proseEngine,
|
|
117
|
+
{ maxTokens: BASE_TOKENS },
|
|
110
118
|
);
|
|
111
119
|
|
|
112
120
|
// 2. Levels
|
|
@@ -123,15 +131,19 @@ async function generatePathwayData({
|
|
|
123
131
|
"stages",
|
|
124
132
|
buildStagePrompt(framework.stages, ctx, schemas.stages),
|
|
125
133
|
proseEngine,
|
|
134
|
+
{ maxTokens: BASE_TOKENS },
|
|
126
135
|
);
|
|
127
136
|
|
|
128
|
-
//
|
|
137
|
+
// Build prior output context for downstream prompts
|
|
138
|
+
const priorOutput = { levels };
|
|
139
|
+
|
|
140
|
+
// 4. Behaviours (parallel — receive level context)
|
|
129
141
|
const behaviours = await Promise.all(
|
|
130
142
|
framework.behaviours.map((b) =>
|
|
131
143
|
generateEntity(
|
|
132
144
|
"behaviour",
|
|
133
145
|
b.id,
|
|
134
|
-
buildBehaviourPrompt(b, ctx, schemas.behaviour),
|
|
146
|
+
buildBehaviourPrompt(b, ctx, schemas.behaviour, priorOutput),
|
|
135
147
|
proseEngine,
|
|
136
148
|
).then((data) => ({
|
|
137
149
|
...data,
|
|
@@ -140,7 +152,9 @@ async function generatePathwayData({
|
|
|
140
152
|
),
|
|
141
153
|
);
|
|
142
154
|
|
|
143
|
-
|
|
155
|
+
priorOutput.behaviours = behaviours;
|
|
156
|
+
|
|
157
|
+
// 5. Capabilities with skills (parallel — receive level + behaviour context)
|
|
144
158
|
const capabilities = await Promise.all(
|
|
145
159
|
framework.capabilities.map((c, i) =>
|
|
146
160
|
generateEntity(
|
|
@@ -150,8 +164,10 @@ async function generatePathwayData({
|
|
|
150
164
|
{ ...c, ordinalRank: i + 1 },
|
|
151
165
|
ctx,
|
|
152
166
|
schemas.capability,
|
|
167
|
+
priorOutput,
|
|
153
168
|
),
|
|
154
169
|
proseEngine,
|
|
170
|
+
{ maxTokens: BASE_TOKENS + (c.skills || []).length * PER_SKILL_TOKENS },
|
|
155
171
|
).then((data) => ({
|
|
156
172
|
...data,
|
|
157
173
|
_id: c.id,
|
|
@@ -159,6 +175,8 @@ async function generatePathwayData({
|
|
|
159
175
|
),
|
|
160
176
|
);
|
|
161
177
|
|
|
178
|
+
priorOutput.capabilities = capabilities;
|
|
179
|
+
|
|
162
180
|
// Collect all skill IDs and behaviour IDs from DSL declarations
|
|
163
181
|
// (not from LLM output — these must be available even in no-prose mode)
|
|
164
182
|
const skillIds = framework.capabilities.flatMap((c) => c.skills || []);
|
|
@@ -174,6 +192,7 @@ async function generatePathwayData({
|
|
|
174
192
|
schemas.drivers,
|
|
175
193
|
),
|
|
176
194
|
proseEngine,
|
|
195
|
+
{ maxTokens: BASE_TOKENS },
|
|
177
196
|
);
|
|
178
197
|
|
|
179
198
|
// 7. Disciplines (reference skills, behaviours, track IDs from DSL)
|
|
@@ -187,6 +206,7 @@ async function generatePathwayData({
|
|
|
187
206
|
d,
|
|
188
207
|
{ ...ctx, skillIds, behaviourIds, trackIds },
|
|
189
208
|
schemas.discipline,
|
|
209
|
+
priorOutput,
|
|
190
210
|
),
|
|
191
211
|
proseEngine,
|
|
192
212
|
).then((data) => ({
|
|
@@ -207,6 +227,7 @@ async function generatePathwayData({
|
|
|
207
227
|
t,
|
|
208
228
|
{ ...ctx, capabilityIds, skillIds, behaviourIds },
|
|
209
229
|
schemas.track,
|
|
230
|
+
priorOutput,
|
|
210
231
|
),
|
|
211
232
|
proseEngine,
|
|
212
233
|
).then((data) => ({
|
|
@@ -245,12 +266,22 @@ async function generatePathwayData({
|
|
|
245
266
|
* @param {import('./prose.js').ProseEngine} proseEngine - Prose engine
|
|
246
267
|
* @returns {Promise<object|null>} Parsed JSON data
|
|
247
268
|
*/
|
|
248
|
-
async function generateEntity(
|
|
269
|
+
async function generateEntity(
|
|
270
|
+
entityType,
|
|
271
|
+
entityId,
|
|
272
|
+
prompt,
|
|
273
|
+
proseEngine,
|
|
274
|
+
{ maxTokens } = {},
|
|
275
|
+
) {
|
|
249
276
|
const key = `pathway:${entityType}:${entityId}`;
|
|
250
|
-
const result = await proseEngine.generateJson(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
const result = await proseEngine.generateJson(
|
|
278
|
+
key,
|
|
279
|
+
[
|
|
280
|
+
{ role: "system", content: prompt.system },
|
|
281
|
+
{ role: "user", content: prompt.user },
|
|
282
|
+
],
|
|
283
|
+
maxTokens ? { maxTokens } : undefined,
|
|
284
|
+
);
|
|
254
285
|
return result;
|
|
255
286
|
}
|
|
256
287
|
|
|
@@ -302,20 +333,8 @@ function jitter(rng, base, max) {
|
|
|
302
333
|
* @returns {object[]}
|
|
303
334
|
*/
|
|
304
335
|
function generateSelfAssessments(framework, skillIds, behaviourIds) {
|
|
305
|
-
const proficiencies = framework.proficiencies ||
|
|
306
|
-
|
|
307
|
-
"foundational",
|
|
308
|
-
"working",
|
|
309
|
-
"practitioner",
|
|
310
|
-
"expert",
|
|
311
|
-
];
|
|
312
|
-
const maturities = framework.maturities || [
|
|
313
|
-
"emerging",
|
|
314
|
-
"developing",
|
|
315
|
-
"practicing",
|
|
316
|
-
"role_modeling",
|
|
317
|
-
"exemplifying",
|
|
318
|
-
];
|
|
336
|
+
const proficiencies = framework.proficiencies || PROFICIENCY_LEVELS;
|
|
337
|
+
const maturities = framework.maturities || MATURITY_LEVELS;
|
|
319
338
|
|
|
320
339
|
const seed = framework.seed || 1;
|
|
321
340
|
const rng = createRng(seed);
|
|
@@ -325,10 +344,18 @@ function generateSelfAssessments(framework, skillIds, behaviourIds) {
|
|
|
325
344
|
const assessments = [];
|
|
326
345
|
const levelNames = ["junior", "mid", "senior", "staff", "principal"];
|
|
327
346
|
|
|
347
|
+
// Track previous level's indices per skill/behaviour to enforce monotonicity
|
|
348
|
+
const prevSkillIdx = {};
|
|
349
|
+
const prevBehIdx = {};
|
|
350
|
+
|
|
328
351
|
for (let i = 0; i < Math.min(levelNames.length, proficiencies.length); i++) {
|
|
329
352
|
const skillProficiencies = {};
|
|
330
353
|
for (const skillId of skillIds) {
|
|
331
|
-
|
|
354
|
+
const raw = jitter(rng, i, maxP);
|
|
355
|
+
const floor = prevSkillIdx[skillId] ?? 0;
|
|
356
|
+
const idx = Math.max(floor, raw);
|
|
357
|
+
skillProficiencies[skillId] = proficiencies[idx];
|
|
358
|
+
prevSkillIdx[skillId] = idx;
|
|
332
359
|
}
|
|
333
360
|
|
|
334
361
|
const behaviourMaturities = {};
|
|
@@ -339,8 +366,11 @@ function generateSelfAssessments(framework, skillIds, behaviourIds) {
|
|
|
339
366
|
if (r < 0.55) offset = 0;
|
|
340
367
|
else if (r < 0.8) offset = 1;
|
|
341
368
|
else offset = -1;
|
|
342
|
-
|
|
343
|
-
|
|
369
|
+
const raw = Math.max(0, Math.min(maxM, i + offset));
|
|
370
|
+
const floor = prevBehIdx[behaviourId] ?? 0;
|
|
371
|
+
const idx = Math.max(floor, raw);
|
|
372
|
+
behaviourMaturities[behaviourId] = maturities[idx];
|
|
373
|
+
prevBehIdx[behaviourId] = idx;
|
|
344
374
|
}
|
|
345
375
|
|
|
346
376
|
assessments.push({
|
package/engine/prose.js
CHANGED
|
@@ -17,7 +17,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
17
17
|
export class ProseEngine {
|
|
18
18
|
/**
|
|
19
19
|
* @param {object} options
|
|
20
|
-
* @param {string} options.cachePath Path to
|
|
20
|
+
* @param {string} options.cachePath Path to prose cache JSON file
|
|
21
21
|
* @param {string} options.mode "cached" | "generate" | "no-prose"
|
|
22
22
|
* @param {boolean} [options.strict] Fail on cache miss
|
|
23
23
|
* @param {import('@forwardimpact/libllm').LlmApi} options.llmApi
|
|
@@ -108,7 +108,7 @@ export class ProseEngine {
|
|
|
108
108
|
* @param {object[]} messages - Pre-built messages array [{role, content}]
|
|
109
109
|
* @returns {Promise<string|null>}
|
|
110
110
|
*/
|
|
111
|
-
async generateStructured(key, messages) {
|
|
111
|
+
async generateStructured(key, messages, { maxTokens = 4000 } = {}) {
|
|
112
112
|
if (this.mode === "no-prose") return null;
|
|
113
113
|
|
|
114
114
|
const cacheKey = generateHash(key, JSON.stringify(messages));
|
|
@@ -127,7 +127,7 @@ export class ProseEngine {
|
|
|
127
127
|
|
|
128
128
|
const response = await this.llmApi.createCompletions({
|
|
129
129
|
messages,
|
|
130
|
-
max_tokens:
|
|
130
|
+
max_tokens: maxTokens,
|
|
131
131
|
});
|
|
132
132
|
const content = response.choices?.[0]?.message?.content?.trim() || null;
|
|
133
133
|
this.stats.generated++;
|
|
@@ -147,8 +147,8 @@ export class ProseEngine {
|
|
|
147
147
|
* @param {object[]} messages - Pre-built messages array
|
|
148
148
|
* @returns {Promise<object|null>}
|
|
149
149
|
*/
|
|
150
|
-
async generateJson(key, messages) {
|
|
151
|
-
const raw = await this.generateStructured(key, messages);
|
|
150
|
+
async generateJson(key, messages, options) {
|
|
151
|
+
const raw = await this.generateStructured(key, messages, options);
|
|
152
152
|
if (!raw) return null;
|
|
153
153
|
const cleaned = raw
|
|
154
154
|
.replace(/^```(?:json)?\s*\n?/m, "")
|
|
@@ -183,6 +183,7 @@ export class ProseEngine {
|
|
|
183
183
|
tone: context.tone || "technical",
|
|
184
184
|
length: context.length || "2-3 paragraphs",
|
|
185
185
|
domain: context.domain,
|
|
186
|
+
orgName: context.orgName,
|
|
186
187
|
role: context.role,
|
|
187
188
|
audience: context.audience,
|
|
188
189
|
scenario: context.scenario,
|
|
@@ -209,7 +210,7 @@ export class ProseEngine {
|
|
|
209
210
|
/**
|
|
210
211
|
* Creates a ProseEngine with real dependencies wired.
|
|
211
212
|
* @param {object} options
|
|
212
|
-
* @param {string} options.cachePath - Path to
|
|
213
|
+
* @param {string} options.cachePath - Path to prose cache JSON file
|
|
213
214
|
* @param {string} options.mode - "cached" | "generate" | "no-prose"
|
|
214
215
|
* @param {boolean} [options.strict] - Fail on cache miss
|
|
215
216
|
* @param {import('@forwardimpact/libllm').LlmApi} [options.llmApi] - LLM client
|
package/package.json
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { buildPreamble } from "./preamble.js";
|
|
2
|
+
import { MATURITY_LEVELS } from "@forwardimpact/libsyntheticgen/vocabulary.js";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Prompt template for a single behaviour entity.
|
|
3
6
|
*
|
|
@@ -6,14 +9,17 @@
|
|
|
6
9
|
* @param {object} schema - JSON schema for behaviour
|
|
7
10
|
* @returns {{ system: string, user: string }}
|
|
8
11
|
*/
|
|
9
|
-
export function buildBehaviourPrompt(skeleton, ctx, schema) {
|
|
12
|
+
export function buildBehaviourPrompt(skeleton, ctx, schema, priorOutput) {
|
|
10
13
|
return {
|
|
11
|
-
system:
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
system:
|
|
15
|
+
buildPreamble(ctx.frameworkName || ctx.domain) +
|
|
16
|
+
"\n\n" +
|
|
17
|
+
[
|
|
18
|
+
"You are an expert career framework author.",
|
|
19
|
+
"Output ONLY valid JSON. No markdown fences, no explanations.",
|
|
20
|
+
`The organization domain is: ${ctx.domain}.`,
|
|
21
|
+
`Industry: ${ctx.industry}.`,
|
|
22
|
+
].join(" "),
|
|
17
23
|
|
|
18
24
|
user: [
|
|
19
25
|
"Generate a behaviour definition for a career framework.",
|
|
@@ -29,13 +35,27 @@ export function buildBehaviourPrompt(skeleton, ctx, schema) {
|
|
|
29
35
|
"- name: Use the provided name exactly.",
|
|
30
36
|
"- human.description: 2-3 sentences describing this behaviour.",
|
|
31
37
|
"- human.maturityDescriptions: One paragraph per maturity level",
|
|
32
|
-
|
|
38
|
+
` (${MATURITY_LEVELS.join(", ")}).`,
|
|
33
39
|
' Use second-person ("You..."). Each level must show clear',
|
|
34
40
|
" progression in depth, consistency, and influence.",
|
|
35
41
|
"- agent.title: Short title (2-4 words) for how the agent applies this behaviour.",
|
|
36
42
|
"- agent.workingStyle: 1-2 sentences describing how the AI agent should embody",
|
|
37
43
|
" this behaviour in its work style and communication.",
|
|
38
44
|
"",
|
|
45
|
+
...(priorOutput?.levels
|
|
46
|
+
? [
|
|
47
|
+
"",
|
|
48
|
+
"## Previously generated context",
|
|
49
|
+
"Level titles and proficiency baselines:",
|
|
50
|
+
...(Array.isArray(priorOutput.levels)
|
|
51
|
+
? priorOutput.levels.map(
|
|
52
|
+
(l) =>
|
|
53
|
+
`- ${l.id}: ${l.professionalTitle || l.id} (primary: ${l.baseSkillProficiencies?.primary || "N/A"})`,
|
|
54
|
+
)
|
|
55
|
+
: []),
|
|
56
|
+
]
|
|
57
|
+
: []),
|
|
58
|
+
"",
|
|
39
59
|
"Output a single JSON object for this behaviour file.",
|
|
40
60
|
].join("\n"),
|
|
41
61
|
};
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { buildPreamble } from "./preamble.js";
|
|
2
|
+
import {
|
|
3
|
+
PROFICIENCY_LEVELS,
|
|
4
|
+
STAGE_NAMES,
|
|
5
|
+
} from "@forwardimpact/libsyntheticgen/vocabulary.js";
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Prompt template for a single capability entity (with skills).
|
|
3
9
|
*
|
|
@@ -6,14 +12,17 @@
|
|
|
6
12
|
* @param {object} schema - JSON schema for capability
|
|
7
13
|
* @returns {{ system: string, user: string }}
|
|
8
14
|
*/
|
|
9
|
-
export function buildCapabilityPrompt(skeleton, ctx, schema) {
|
|
15
|
+
export function buildCapabilityPrompt(skeleton, ctx, schema, priorOutput) {
|
|
10
16
|
return {
|
|
11
|
-
system:
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
system:
|
|
18
|
+
buildPreamble(ctx.frameworkName || ctx.domain) +
|
|
19
|
+
"\n\n" +
|
|
20
|
+
[
|
|
21
|
+
"You are an expert career framework author.",
|
|
22
|
+
"Output ONLY valid JSON. No markdown fences, no explanations.",
|
|
23
|
+
`The organization domain is: ${ctx.domain}.`,
|
|
24
|
+
`Industry: ${ctx.industry}.`,
|
|
25
|
+
].join(" "),
|
|
17
26
|
|
|
18
27
|
user: [
|
|
19
28
|
"Generate a capability definition for a career framework.",
|
|
@@ -36,14 +45,14 @@ export function buildCapabilityPrompt(skeleton, ctx, schema) {
|
|
|
36
45
|
`- ordinalRank: ${skeleton.ordinalRank}`,
|
|
37
46
|
"- description: 1-2 sentences describing this capability area.",
|
|
38
47
|
"- professionalResponsibilities: One sentence per proficiency level",
|
|
39
|
-
|
|
48
|
+
` (${PROFICIENCY_LEVELS.join(" through ")}) describing IC expectations.`,
|
|
40
49
|
"- managementResponsibilities: Same for management track.",
|
|
41
50
|
"- skills: For each skill ID listed above, generate:",
|
|
42
51
|
" - id: Use the provided skill ID exactly.",
|
|
43
52
|
" - name: Human-readable name (title case).",
|
|
44
53
|
" - human.description: 2-3 sentences.",
|
|
45
54
|
" - human.proficiencyDescriptions: One paragraph per level",
|
|
46
|
-
|
|
55
|
+
` (${PROFICIENCY_LEVELS.join(", ")}).`,
|
|
47
56
|
' Use second-person ("You..."). Each level must show clear',
|
|
48
57
|
" progression in scope, autonomy, and complexity.",
|
|
49
58
|
"- For each skill, also generate an agent section:",
|
|
@@ -51,19 +60,43 @@ export function buildCapabilityPrompt(skeleton, ctx, schema) {
|
|
|
51
60
|
" - agent.description: 1 sentence describing what this agent skill provides.",
|
|
52
61
|
" - agent.useWhen: 1 sentence describing when/why an agent should use this skill.",
|
|
53
62
|
" - agent.stages: Object with ONLY the stages where this skill is meaningfully relevant.",
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
` Not all skills need all ${STAGE_NAMES.length} stages. Use these criteria:`,
|
|
64
|
+
` - ${STAGE_NAMES[0]}: include if the skill informs what to build or constrains requirements`,
|
|
65
|
+
` - ${STAGE_NAMES[1]}: include if the skill drives architecture or design decisions`,
|
|
66
|
+
` - ${STAGE_NAMES[2]}: include if the skill requires tooling, dependencies, or env setup`,
|
|
67
|
+
` - ${STAGE_NAMES[3]}: include if the skill is directly exercised during implementation`,
|
|
68
|
+
` - ${STAGE_NAMES[4]}: include if the skill has quality criteria to verify`,
|
|
69
|
+
` - ${STAGE_NAMES[5]}: include if the skill has production or operational concerns`,
|
|
61
70
|
" Each skill must have at least 2 stages. Omit stages where the skill has no specific guidance.",
|
|
62
71
|
" Each stage has:",
|
|
63
72
|
" - focus: 1 sentence — the primary focus for this skill in this stage.",
|
|
64
73
|
" - readChecklist: Array of 2-3 items — steps to read/understand before acting.",
|
|
65
74
|
" - confirmChecklist: Array of 2-3 items — items to verify after completing work.",
|
|
66
75
|
"",
|
|
76
|
+
...(priorOutput?.levels || priorOutput?.behaviours
|
|
77
|
+
? [
|
|
78
|
+
"",
|
|
79
|
+
"## Previously generated context",
|
|
80
|
+
...(priorOutput.levels && Array.isArray(priorOutput.levels)
|
|
81
|
+
? [
|
|
82
|
+
"Level titles and proficiency baselines:",
|
|
83
|
+
...priorOutput.levels.map(
|
|
84
|
+
(l) =>
|
|
85
|
+
`- ${l.id}: ${l.professionalTitle || l.id} (primary: ${l.baseSkillProficiencies?.primary || "N/A"})`,
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
: []),
|
|
89
|
+
...(priorOutput.behaviours && Array.isArray(priorOutput.behaviours)
|
|
90
|
+
? [
|
|
91
|
+
"Behaviour names:",
|
|
92
|
+
...priorOutput.behaviours.map(
|
|
93
|
+
(b) => `- ${b._id || b.id}: ${b.name || b._id || b.id}`,
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
: []),
|
|
97
|
+
]
|
|
98
|
+
: []),
|
|
99
|
+
"",
|
|
67
100
|
"Output the JSON object for this single capability file.",
|
|
68
101
|
].join("\n"),
|
|
69
102
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildPreamble } from "./preamble.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Prompt template for a single discipline entity.
|
|
3
5
|
*
|
|
@@ -6,14 +8,17 @@
|
|
|
6
8
|
* @param {object} schema - JSON schema for discipline
|
|
7
9
|
* @returns {{ system: string, user: string }}
|
|
8
10
|
*/
|
|
9
|
-
export function buildDisciplinePrompt(skeleton, ctx, schema) {
|
|
11
|
+
export function buildDisciplinePrompt(skeleton, ctx, schema, priorOutput) {
|
|
10
12
|
return {
|
|
11
|
-
system:
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
system:
|
|
14
|
+
buildPreamble(ctx.frameworkName || ctx.domain) +
|
|
15
|
+
"\n\n" +
|
|
16
|
+
[
|
|
17
|
+
"You are an expert career framework author.",
|
|
18
|
+
"Output ONLY valid JSON. No markdown fences, no explanations.",
|
|
19
|
+
`The organization domain is: ${ctx.domain}.`,
|
|
20
|
+
`Industry: ${ctx.industry}.`,
|
|
21
|
+
].join(" "),
|
|
17
22
|
|
|
18
23
|
user: [
|
|
19
24
|
"Generate a discipline definition for a career framework.",
|
|
@@ -51,10 +56,45 @@ export function buildDisciplinePrompt(skeleton, ctx, schema) {
|
|
|
51
56
|
" Include 2-3 behaviour modifiers relevant to this discipline.",
|
|
52
57
|
"- human.roleSummary: 2-3 sentences describing this role. May use {roleTitle} or {specialization}.",
|
|
53
58
|
"- 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 {
|
|
59
|
+
" Frame as 'You are a {roleTitle} agent that...' May use {roleTitle} or {specialization} placeholder.",
|
|
55
60
|
"- agent.priority: 1 sentence stating the agent's top priority (e.g., code quality, system reliability).",
|
|
56
61
|
"- agent.constraints: 2-3 things the agent must avoid or never do.",
|
|
57
62
|
"",
|
|
63
|
+
...(priorOutput?.levels ||
|
|
64
|
+
priorOutput?.behaviours ||
|
|
65
|
+
priorOutput?.capabilities
|
|
66
|
+
? [
|
|
67
|
+
"",
|
|
68
|
+
"## Previously generated context",
|
|
69
|
+
...(priorOutput.levels && Array.isArray(priorOutput.levels)
|
|
70
|
+
? [
|
|
71
|
+
"Level titles:",
|
|
72
|
+
...priorOutput.levels.map(
|
|
73
|
+
(l) => `- ${l.id}: ${l.professionalTitle || l.id}`,
|
|
74
|
+
),
|
|
75
|
+
]
|
|
76
|
+
: []),
|
|
77
|
+
...(priorOutput.behaviours && Array.isArray(priorOutput.behaviours)
|
|
78
|
+
? [
|
|
79
|
+
"Behaviour names:",
|
|
80
|
+
...priorOutput.behaviours.map(
|
|
81
|
+
(b) => `- ${b._id || b.id}: ${b.name || b._id || b.id}`,
|
|
82
|
+
),
|
|
83
|
+
]
|
|
84
|
+
: []),
|
|
85
|
+
...(priorOutput.capabilities &&
|
|
86
|
+
Array.isArray(priorOutput.capabilities)
|
|
87
|
+
? [
|
|
88
|
+
"Capability names and skill IDs:",
|
|
89
|
+
...priorOutput.capabilities.map(
|
|
90
|
+
(c) =>
|
|
91
|
+
`- ${c._id || c.id}: ${c.name || c._id || c.id} (skills: ${(c.skills || []).map((s) => s.id || s).join(", ")})`,
|
|
92
|
+
),
|
|
93
|
+
]
|
|
94
|
+
: []),
|
|
95
|
+
]
|
|
96
|
+
: []),
|
|
97
|
+
"",
|
|
58
98
|
"Output a single JSON object for this discipline.",
|
|
59
99
|
].join("\n"),
|
|
60
100
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildPreamble } from "./preamble.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Prompt template for drivers.yaml — all drivers in a single call.
|
|
3
5
|
*
|
|
@@ -18,12 +20,15 @@ export function buildDriverPrompt(drivers, ctx, schema) {
|
|
|
18
20
|
.join("\n");
|
|
19
21
|
|
|
20
22
|
return {
|
|
21
|
-
system:
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
system:
|
|
24
|
+
buildPreamble(ctx.frameworkName || ctx.domain) +
|
|
25
|
+
"\n\n" +
|
|
26
|
+
[
|
|
27
|
+
"You are an expert career framework author.",
|
|
28
|
+
"Output ONLY valid JSON. No markdown fences, no explanations.",
|
|
29
|
+
`The organization domain is: ${ctx.domain}.`,
|
|
30
|
+
`Industry: ${ctx.industry}.`,
|
|
31
|
+
].join(" "),
|
|
27
32
|
|
|
28
33
|
user: [
|
|
29
34
|
"Generate organizational driver definitions for a career framework.",
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
+
import { buildPreamble } from "./preamble.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Prompt template for framework.yaml metadata.
|
|
3
5
|
*
|
|
4
6
|
* @param {object} skeleton - Framework skeleton from DSL
|
|
5
|
-
* @param {object} ctx - Universe context (domain, industry)
|
|
7
|
+
* @param {object} ctx - Universe context (domain, industry, frameworkName)
|
|
6
8
|
* @param {object} schema - JSON schema for framework entity
|
|
7
9
|
* @returns {{ system: string, user: string }}
|
|
8
10
|
*/
|
|
9
11
|
export function buildFrameworkPrompt(skeleton, ctx, schema) {
|
|
10
12
|
return {
|
|
11
|
-
system:
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
system:
|
|
14
|
+
buildPreamble(ctx.frameworkName || ctx.domain) +
|
|
15
|
+
"\n\n" +
|
|
16
|
+
[
|
|
17
|
+
"You are an expert career framework author.",
|
|
18
|
+
"Output ONLY valid JSON. No markdown fences, no explanations.",
|
|
19
|
+
`The organization domain is: ${ctx.domain}.`,
|
|
20
|
+
`Industry: ${ctx.industry}.`,
|
|
21
|
+
].join(" "),
|
|
17
22
|
|
|
18
23
|
user: [
|
|
19
24
|
"Generate a framework metadata file for an engineering career framework.",
|
|
@@ -32,6 +37,7 @@ export function buildFrameworkPrompt(skeleton, ctx, schema) {
|
|
|
32
37
|
"- entityDefinitions: Provide definitions for these entity types:",
|
|
33
38
|
" driver, skill, behaviour, discipline, level, track, job, agent, stage, tool.",
|
|
34
39
|
" Each needs: title, emojiIcon, description (1 sentence).",
|
|
40
|
+
' IMPORTANT: A "stage" is a lifecycle phase of work delivery (specify, plan, scaffold, code, review, deploy) — NOT a maturity or progression level.',
|
|
35
41
|
"",
|
|
36
42
|
"Output a single JSON object.",
|
|
37
43
|
].join("\n"),
|
package/prompts/pathway/level.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { buildPreamble } from "./preamble.js";
|
|
2
|
+
import {
|
|
3
|
+
PROFICIENCY_LEVELS,
|
|
4
|
+
MATURITY_LEVELS,
|
|
5
|
+
} from "@forwardimpact/libsyntheticgen/vocabulary.js";
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Prompt template for levels.yaml — all levels in a single call.
|
|
3
9
|
*
|
|
@@ -15,12 +21,15 @@ export function buildLevelPrompt(levels, ctx, schema) {
|
|
|
15
21
|
.join("\n");
|
|
16
22
|
|
|
17
23
|
return {
|
|
18
|
-
system:
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
system:
|
|
25
|
+
buildPreamble(ctx.frameworkName || ctx.domain) +
|
|
26
|
+
"\n\n" +
|
|
27
|
+
[
|
|
28
|
+
"You are an expert career framework author.",
|
|
29
|
+
"Output ONLY valid JSON. No markdown fences, no explanations.",
|
|
30
|
+
`The organization domain is: ${ctx.domain}.`,
|
|
31
|
+
`Industry: ${ctx.industry}.`,
|
|
32
|
+
].join(" "),
|
|
24
33
|
|
|
25
34
|
user: [
|
|
26
35
|
"Generate career level definitions for an engineering pathway.",
|
|
@@ -44,9 +53,9 @@ export function buildLevelPrompt(levels, ctx, schema) {
|
|
|
44
53
|
" - qualificationSummary: 2-3 sentences describing qualifications.",
|
|
45
54
|
" May use {typicalExperienceRange} placeholder.",
|
|
46
55
|
" - baseSkillProficiencies: { primary, secondary, broad } using",
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
` ${PROFICIENCY_LEVELS.join("/")}.`,
|
|
57
|
+
` Increase across levels (L1→${PROFICIENCY_LEVELS[0]}, L5→${PROFICIENCY_LEVELS.at(-1)} for primary).`,
|
|
58
|
+
` - baseBehaviourMaturity: ${MATURITY_LEVELS.join("/")}.`,
|
|
50
59
|
" Increase across levels.",
|
|
51
60
|
" - expectations: { impactScope, autonomyExpectation, influenceScope, complexityHandled }.",
|
|
52
61
|
" Each 1 sentence showing clear progression.",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared system preamble for all pathway prompt builders.
|
|
3
|
+
*
|
|
4
|
+
* Establishes consistent voice, terminology, and naming conventions
|
|
5
|
+
* across all 8 entity prompt builders.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
PROFICIENCY_LEVELS,
|
|
10
|
+
MATURITY_LEVELS,
|
|
11
|
+
} from "@forwardimpact/libsyntheticgen/vocabulary.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a shared system preamble for pathway prompt builders.
|
|
15
|
+
* @param {string} frameworkName - Name of the framework (or domain fallback)
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function buildPreamble(frameworkName) {
|
|
19
|
+
return [
|
|
20
|
+
`You are writing content for the "${frameworkName}" engineering career framework.`,
|
|
21
|
+
`Use these exact proficiency level names: ${PROFICIENCY_LEVELS.join(", ")}.`,
|
|
22
|
+
`Use these exact maturity level names: ${MATURITY_LEVELS.join(", ")}.`,
|
|
23
|
+
`Write in professional, concise, third-person voice.`,
|
|
24
|
+
`Use consistent terminology across all entities — prefer precise terms over synonyms.`,
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
package/prompts/pathway/stage.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildPreamble } from "./preamble.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Prompt template for stages.yaml — all stages in a single call.
|
|
3
5
|
*
|
|
@@ -8,12 +10,15 @@
|
|
|
8
10
|
*/
|
|
9
11
|
export function buildStagePrompt(stageIds, ctx, schema) {
|
|
10
12
|
return {
|
|
11
|
-
system:
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
system:
|
|
14
|
+
buildPreamble(ctx.frameworkName || ctx.domain) +
|
|
15
|
+
"\n\n" +
|
|
16
|
+
[
|
|
17
|
+
"You are an expert career framework author.",
|
|
18
|
+
"Output ONLY valid JSON. No markdown fences, no explanations.",
|
|
19
|
+
`The organization domain is: ${ctx.domain}.`,
|
|
20
|
+
`Industry: ${ctx.industry}.`,
|
|
21
|
+
].join(" "),
|
|
17
22
|
|
|
18
23
|
user: [
|
|
19
24
|
"Generate engineering lifecycle stage definitions.",
|
|
@@ -28,13 +33,15 @@ export function buildStagePrompt(stageIds, ctx, schema) {
|
|
|
28
33
|
"## Instructions",
|
|
29
34
|
"- Output a JSON array of stage objects.",
|
|
30
35
|
"- For each stage ID, generate:",
|
|
31
|
-
|
|
36
|
+
` - id: The stage ID (must be one of: ${stageIds.join(", ")}).`,
|
|
32
37
|
' - name: Human-readable name (e.g., "Specify", "Plan").',
|
|
33
38
|
" - emojiIcon: A single emoji for this stage.",
|
|
34
39
|
' - description: 2-3 sentences in second person ("You...").',
|
|
35
40
|
" - summary: 1 sentence in third person.",
|
|
36
41
|
" - handoffs: Array of transitions to other stages, each with:",
|
|
37
42
|
" targetStage, label (button text), prompt (instructions for next stage).",
|
|
43
|
+
` targetStage in each handoff MUST be one of the stage IDs listed above: ${stageIds.join(", ")}.`,
|
|
44
|
+
" Do not invent stage IDs that are not in the provided list.",
|
|
38
45
|
" - constraints: 2-3 restrictions on behaviour in this stage.",
|
|
39
46
|
" - readChecklist: 2-4 Read-Then-Do steps.",
|
|
40
47
|
" - confirmChecklist: 2-4 Do-Then-Confirm items.",
|
package/prompts/pathway/track.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildPreamble } from "./preamble.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Prompt template for a single track entity.
|
|
3
5
|
*
|
|
@@ -6,14 +8,17 @@
|
|
|
6
8
|
* @param {object} schema - JSON schema for track
|
|
7
9
|
* @returns {{ system: string, user: string }}
|
|
8
10
|
*/
|
|
9
|
-
export function buildTrackPrompt(skeleton, ctx, schema) {
|
|
11
|
+
export function buildTrackPrompt(skeleton, ctx, schema, priorOutput) {
|
|
10
12
|
return {
|
|
11
|
-
system:
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
system:
|
|
14
|
+
buildPreamble(ctx.frameworkName || ctx.domain) +
|
|
15
|
+
"\n\n" +
|
|
16
|
+
[
|
|
17
|
+
"You are an expert career framework author.",
|
|
18
|
+
"Output ONLY valid JSON. No markdown fences, no explanations.",
|
|
19
|
+
`The organization domain is: ${ctx.domain}.`,
|
|
20
|
+
`Industry: ${ctx.industry}.`,
|
|
21
|
+
].join(" "),
|
|
17
22
|
|
|
18
23
|
user: [
|
|
19
24
|
"Generate a track definition for a career framework.",
|
|
@@ -45,6 +50,41 @@ export function buildTrackPrompt(skeleton, ctx, schema) {
|
|
|
45
50
|
"- agent.priority: 1 sentence stating the track-specific priority.",
|
|
46
51
|
"- agent.constraints: 1-2 additional constraints specific to this track.",
|
|
47
52
|
"",
|
|
53
|
+
...(priorOutput?.levels ||
|
|
54
|
+
priorOutput?.behaviours ||
|
|
55
|
+
priorOutput?.capabilities
|
|
56
|
+
? [
|
|
57
|
+
"",
|
|
58
|
+
"## Previously generated context",
|
|
59
|
+
...(priorOutput.levels && Array.isArray(priorOutput.levels)
|
|
60
|
+
? [
|
|
61
|
+
"Level titles:",
|
|
62
|
+
...priorOutput.levels.map(
|
|
63
|
+
(l) => `- ${l.id}: ${l.professionalTitle || l.id}`,
|
|
64
|
+
),
|
|
65
|
+
]
|
|
66
|
+
: []),
|
|
67
|
+
...(priorOutput.behaviours && Array.isArray(priorOutput.behaviours)
|
|
68
|
+
? [
|
|
69
|
+
"Behaviour names:",
|
|
70
|
+
...priorOutput.behaviours.map(
|
|
71
|
+
(b) => `- ${b._id || b.id}: ${b.name || b._id || b.id}`,
|
|
72
|
+
),
|
|
73
|
+
]
|
|
74
|
+
: []),
|
|
75
|
+
...(priorOutput.capabilities &&
|
|
76
|
+
Array.isArray(priorOutput.capabilities)
|
|
77
|
+
? [
|
|
78
|
+
"Capability names and skill IDs:",
|
|
79
|
+
...priorOutput.capabilities.map(
|
|
80
|
+
(c) =>
|
|
81
|
+
`- ${c._id || c.id}: ${c.name || c._id || c.id} (skills: ${(c.skills || []).map((s) => s.id || s).join(", ")})`,
|
|
82
|
+
),
|
|
83
|
+
]
|
|
84
|
+
: []),
|
|
85
|
+
]
|
|
86
|
+
: []),
|
|
87
|
+
"",
|
|
48
88
|
"Output a single JSON object for this track.",
|
|
49
89
|
].join("\n"),
|
|
50
90
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
Write {{length}} of {{tone}} prose about: {{topic}}. {{#
|
|
2
|
-
{{
|
|
1
|
+
Write {{length}} of {{tone}} prose about: {{topic}}. {{#orgName}} Company name:
|
|
2
|
+
{{orgName}} (always use this exact capitalization). {{/orgName}} {{#domain}}
|
|
3
|
+
Company domain: {{domain}} (use only in URLs, never as the company name in
|
|
4
|
+
prose). {{/domain}} {{#role}} Written from the perspective of: {{role}}.
|
|
3
5
|
{{/role}} {{#audience}} Target audience: {{audience}}. {{/audience}}
|
|
4
6
|
{{#scenario}} Context: during "{{scenario}}", the {{driver}} driver is
|
|
5
7
|
{{direction}} (magnitude: {{magnitude}}). {{/scenario}} Output the text only, no
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { PathwayGenerator } from "../engine/pathway.js";
|
|
4
|
+
|
|
5
|
+
function makeLogger() {
|
|
6
|
+
return {
|
|
7
|
+
info: () => {},
|
|
8
|
+
debug: () => {},
|
|
9
|
+
warn: () => {},
|
|
10
|
+
error: () => {},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mock ProseEngine that returns fixture data for each entity type.
|
|
16
|
+
*/
|
|
17
|
+
function makeMockProseEngine() {
|
|
18
|
+
return {
|
|
19
|
+
generateJson: async (key) => {
|
|
20
|
+
if (key.includes("framework")) {
|
|
21
|
+
return { title: "Test", emojiIcon: "🏗️", tag: "#test" };
|
|
22
|
+
}
|
|
23
|
+
if (key.includes("levels")) {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
id: "J040",
|
|
27
|
+
professionalTitle: "Engineer",
|
|
28
|
+
baseSkillProficiencies: { primary: "awareness" },
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
if (key.includes("stages")) {
|
|
33
|
+
return [{ id: "specify", name: "Specify" }];
|
|
34
|
+
}
|
|
35
|
+
if (key.includes("behaviour")) {
|
|
36
|
+
return { name: "Collaboration" };
|
|
37
|
+
}
|
|
38
|
+
if (key.includes("capability")) {
|
|
39
|
+
return {
|
|
40
|
+
name: "Coding",
|
|
41
|
+
skills: [{ id: "python", name: "Python" }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (key.includes("drivers")) {
|
|
45
|
+
return [{ id: "clear_direction", name: "Clear Direction" }];
|
|
46
|
+
}
|
|
47
|
+
if (key.includes("discipline")) {
|
|
48
|
+
return { roleTitle: "Software Engineer" };
|
|
49
|
+
}
|
|
50
|
+
if (key.includes("track")) {
|
|
51
|
+
return { name: "Backend" };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("PathwayGenerator", () => {
|
|
59
|
+
test("requires proseEngine and logger", () => {
|
|
60
|
+
assert.throws(() => new PathwayGenerator(null, makeLogger()));
|
|
61
|
+
assert.throws(() => new PathwayGenerator(makeMockProseEngine(), null));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("generates all entity types", async () => {
|
|
65
|
+
const generator = new PathwayGenerator(makeMockProseEngine(), makeLogger());
|
|
66
|
+
|
|
67
|
+
const framework = {
|
|
68
|
+
name: "Test",
|
|
69
|
+
levels: [{ id: "J040", professionalTitle: "Engineer", rank: 1 }],
|
|
70
|
+
stages: ["specify", "plan"],
|
|
71
|
+
behaviours: [{ id: "collab", name: "Collaboration" }],
|
|
72
|
+
capabilities: [{ id: "coding", name: "Coding", skills: ["python"] }],
|
|
73
|
+
drivers: [
|
|
74
|
+
{ id: "clear_direction", name: "Clear Direction", skills: ["python"] },
|
|
75
|
+
],
|
|
76
|
+
disciplines: [
|
|
77
|
+
{
|
|
78
|
+
id: "se",
|
|
79
|
+
roleTitle: "Software Engineer",
|
|
80
|
+
core: ["python"],
|
|
81
|
+
supporting: [],
|
|
82
|
+
broad: [],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
tracks: [{ id: "backend", name: "Backend" }],
|
|
86
|
+
seed: 42,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const schemas = {
|
|
90
|
+
framework: {},
|
|
91
|
+
levels: {},
|
|
92
|
+
stages: {},
|
|
93
|
+
behaviour: {},
|
|
94
|
+
capability: {},
|
|
95
|
+
drivers: {},
|
|
96
|
+
discipline: {},
|
|
97
|
+
track: {},
|
|
98
|
+
"self-assessments": {},
|
|
99
|
+
defs: {},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = await generator.generate({
|
|
103
|
+
framework,
|
|
104
|
+
domain: "test.example",
|
|
105
|
+
industry: "pharma",
|
|
106
|
+
schemas,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
assert.ok(result.framework);
|
|
110
|
+
assert.ok(result.levels);
|
|
111
|
+
assert.ok(result.stages);
|
|
112
|
+
assert.ok(Array.isArray(result.behaviours));
|
|
113
|
+
assert.ok(Array.isArray(result.capabilities));
|
|
114
|
+
assert.ok(result.drivers);
|
|
115
|
+
assert.ok(Array.isArray(result.disciplines));
|
|
116
|
+
assert.ok(Array.isArray(result.tracks));
|
|
117
|
+
assert.ok(Array.isArray(result.selfAssessments));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("self-assessments use vocabulary constants", async () => {
|
|
121
|
+
const generator = new PathwayGenerator(makeMockProseEngine(), makeLogger());
|
|
122
|
+
|
|
123
|
+
const framework = {
|
|
124
|
+
name: "Test",
|
|
125
|
+
levels: [{ id: "J040", rank: 1 }],
|
|
126
|
+
stages: ["specify"],
|
|
127
|
+
behaviours: [{ id: "collab", name: "Collaboration" }],
|
|
128
|
+
capabilities: [{ id: "coding", name: "Coding", skills: ["python"] }],
|
|
129
|
+
drivers: [],
|
|
130
|
+
disciplines: [],
|
|
131
|
+
tracks: [],
|
|
132
|
+
seed: 42,
|
|
133
|
+
};
|
|
134
|
+
const schemas = {
|
|
135
|
+
framework: {},
|
|
136
|
+
levels: {},
|
|
137
|
+
stages: {},
|
|
138
|
+
behaviour: {},
|
|
139
|
+
capability: {},
|
|
140
|
+
drivers: {},
|
|
141
|
+
discipline: {},
|
|
142
|
+
track: {},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = await generator.generate({
|
|
146
|
+
framework,
|
|
147
|
+
domain: "test.example",
|
|
148
|
+
industry: "pharma",
|
|
149
|
+
schemas,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const validProficiencies = new Set([
|
|
153
|
+
"awareness",
|
|
154
|
+
"foundational",
|
|
155
|
+
"working",
|
|
156
|
+
"practitioner",
|
|
157
|
+
"expert",
|
|
158
|
+
]);
|
|
159
|
+
const validMaturities = new Set([
|
|
160
|
+
"emerging",
|
|
161
|
+
"developing",
|
|
162
|
+
"practicing",
|
|
163
|
+
"role_modeling",
|
|
164
|
+
"exemplifying",
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
for (const sa of result.selfAssessments) {
|
|
168
|
+
for (const val of Object.values(sa.skillProficiencies)) {
|
|
169
|
+
assert.ok(validProficiencies.has(val), `Invalid proficiency: ${val}`);
|
|
170
|
+
}
|
|
171
|
+
for (const val of Object.values(sa.behaviourMaturities)) {
|
|
172
|
+
assert.ok(validMaturities.has(val), `Invalid maturity: ${val}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { buildFrameworkPrompt } from "../prompts/pathway/framework.js";
|
|
4
|
+
import { buildLevelPrompt } from "../prompts/pathway/level.js";
|
|
5
|
+
import { buildStagePrompt } from "../prompts/pathway/stage.js";
|
|
6
|
+
import { buildBehaviourPrompt } from "../prompts/pathway/behaviour.js";
|
|
7
|
+
import { buildCapabilityPrompt } from "../prompts/pathway/capability.js";
|
|
8
|
+
import { buildDriverPrompt } from "../prompts/pathway/driver.js";
|
|
9
|
+
import { buildDisciplinePrompt } from "../prompts/pathway/discipline.js";
|
|
10
|
+
import { buildTrackPrompt } from "../prompts/pathway/track.js";
|
|
11
|
+
|
|
12
|
+
const CTX = {
|
|
13
|
+
domain: "test.example",
|
|
14
|
+
industry: "pharma",
|
|
15
|
+
frameworkName: "Test Framework",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SCHEMA = { type: "object", properties: {} };
|
|
19
|
+
|
|
20
|
+
describe("prompt builders", () => {
|
|
21
|
+
describe("buildFrameworkPrompt", () => {
|
|
22
|
+
test("returns system and user strings", () => {
|
|
23
|
+
const result = buildFrameworkPrompt({}, CTX, SCHEMA);
|
|
24
|
+
assert.ok(typeof result.system === "string");
|
|
25
|
+
assert.ok(typeof result.user === "string");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("includes preamble in system prompt", () => {
|
|
29
|
+
const result = buildFrameworkPrompt({}, CTX, SCHEMA);
|
|
30
|
+
assert.ok(result.system.includes("Test Framework"));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("includes JSON schema in user prompt", () => {
|
|
34
|
+
const result = buildFrameworkPrompt({}, CTX, SCHEMA);
|
|
35
|
+
assert.ok(result.user.includes('"type"'));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("buildLevelPrompt", () => {
|
|
40
|
+
const levels = [
|
|
41
|
+
{ id: "J040", professionalTitle: "Engineer", rank: 1, experience: "0-2" },
|
|
42
|
+
{
|
|
43
|
+
id: "J050",
|
|
44
|
+
professionalTitle: "Senior",
|
|
45
|
+
rank: 2,
|
|
46
|
+
experience: "2-5",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
test("returns system and user strings", () => {
|
|
51
|
+
const result = buildLevelPrompt(levels, CTX, SCHEMA);
|
|
52
|
+
assert.ok(typeof result.system === "string");
|
|
53
|
+
assert.ok(typeof result.user === "string");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("includes level IDs in user prompt", () => {
|
|
57
|
+
const result = buildLevelPrompt(levels, CTX, SCHEMA);
|
|
58
|
+
assert.ok(result.user.includes("J040"));
|
|
59
|
+
assert.ok(result.user.includes("J050"));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("uses vocabulary imports for proficiency names", () => {
|
|
63
|
+
const result = buildLevelPrompt(levels, CTX, SCHEMA);
|
|
64
|
+
assert.ok(result.user.includes("awareness"));
|
|
65
|
+
assert.ok(result.user.includes("expert"));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("buildStagePrompt", () => {
|
|
70
|
+
const stageIds = ["specify", "plan", "code"];
|
|
71
|
+
|
|
72
|
+
test("returns system and user strings", () => {
|
|
73
|
+
const result = buildStagePrompt(stageIds, CTX, SCHEMA);
|
|
74
|
+
assert.ok(typeof result.system === "string");
|
|
75
|
+
assert.ok(typeof result.user === "string");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("includes stage IDs in user prompt", () => {
|
|
79
|
+
const result = buildStagePrompt(stageIds, CTX, SCHEMA);
|
|
80
|
+
for (const id of stageIds) {
|
|
81
|
+
assert.ok(result.user.includes(id));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("includes handoff constraint", () => {
|
|
86
|
+
const result = buildStagePrompt(stageIds, CTX, SCHEMA);
|
|
87
|
+
assert.ok(result.user.includes("MUST be one of the stage IDs"));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("buildBehaviourPrompt", () => {
|
|
92
|
+
const skeleton = { id: "collaboration", name: "Collaboration" };
|
|
93
|
+
|
|
94
|
+
test("returns system and user strings", () => {
|
|
95
|
+
const result = buildBehaviourPrompt(skeleton, CTX, SCHEMA);
|
|
96
|
+
assert.ok(typeof result.system === "string");
|
|
97
|
+
assert.ok(typeof result.user === "string");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("includes behaviour ID", () => {
|
|
101
|
+
const result = buildBehaviourPrompt(skeleton, CTX, SCHEMA);
|
|
102
|
+
assert.ok(result.user.includes("collaboration"));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("includes prior output when provided", () => {
|
|
106
|
+
const priorOutput = {
|
|
107
|
+
levels: [
|
|
108
|
+
{
|
|
109
|
+
id: "L1",
|
|
110
|
+
professionalTitle: "Engineer",
|
|
111
|
+
baseSkillProficiencies: { primary: "awareness" },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
const result = buildBehaviourPrompt(skeleton, CTX, SCHEMA, priorOutput);
|
|
116
|
+
assert.ok(result.user.includes("Previously generated context"));
|
|
117
|
+
assert.ok(result.user.includes("Engineer"));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("buildCapabilityPrompt", () => {
|
|
122
|
+
const skeleton = {
|
|
123
|
+
id: "coding",
|
|
124
|
+
name: "Coding",
|
|
125
|
+
skills: ["python", "java"],
|
|
126
|
+
ordinalRank: 1,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
test("returns system and user strings", () => {
|
|
130
|
+
const result = buildCapabilityPrompt(skeleton, CTX, SCHEMA);
|
|
131
|
+
assert.ok(typeof result.system === "string");
|
|
132
|
+
assert.ok(typeof result.user === "string");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("includes skill IDs", () => {
|
|
136
|
+
const result = buildCapabilityPrompt(skeleton, CTX, SCHEMA);
|
|
137
|
+
assert.ok(result.user.includes("python"));
|
|
138
|
+
assert.ok(result.user.includes("java"));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("includes prior output when provided", () => {
|
|
142
|
+
const priorOutput = {
|
|
143
|
+
levels: [{ id: "L1", professionalTitle: "Junior" }],
|
|
144
|
+
behaviours: [{ _id: "collab", name: "Collaboration" }],
|
|
145
|
+
};
|
|
146
|
+
const result = buildCapabilityPrompt(skeleton, CTX, SCHEMA, priorOutput);
|
|
147
|
+
assert.ok(result.user.includes("Previously generated context"));
|
|
148
|
+
assert.ok(result.user.includes("Junior"));
|
|
149
|
+
assert.ok(result.user.includes("Collaboration"));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("buildDriverPrompt", () => {
|
|
154
|
+
const drivers = [
|
|
155
|
+
{ id: "clear_direction", name: "Clear Direction", skills: ["python"] },
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
test("returns system and user strings", () => {
|
|
159
|
+
const result = buildDriverPrompt(
|
|
160
|
+
drivers,
|
|
161
|
+
{ ...CTX, skillIds: ["python"], behaviourIds: ["collab"] },
|
|
162
|
+
SCHEMA,
|
|
163
|
+
);
|
|
164
|
+
assert.ok(typeof result.system === "string");
|
|
165
|
+
assert.ok(typeof result.user === "string");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("includes driver IDs", () => {
|
|
169
|
+
const result = buildDriverPrompt(
|
|
170
|
+
drivers,
|
|
171
|
+
{ ...CTX, skillIds: ["python"], behaviourIds: [] },
|
|
172
|
+
SCHEMA,
|
|
173
|
+
);
|
|
174
|
+
assert.ok(result.user.includes("clear_direction"));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("buildDisciplinePrompt", () => {
|
|
179
|
+
const skeleton = {
|
|
180
|
+
id: "se",
|
|
181
|
+
roleTitle: "Software Engineer",
|
|
182
|
+
specialization: "Backend",
|
|
183
|
+
isProfessional: true,
|
|
184
|
+
core: ["python"],
|
|
185
|
+
supporting: [],
|
|
186
|
+
broad: [],
|
|
187
|
+
validTracks: [null],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
test("returns system and user strings", () => {
|
|
191
|
+
const result = buildDisciplinePrompt(
|
|
192
|
+
skeleton,
|
|
193
|
+
{ ...CTX, skillIds: ["python"], behaviourIds: [], trackIds: [] },
|
|
194
|
+
SCHEMA,
|
|
195
|
+
);
|
|
196
|
+
assert.ok(typeof result.system === "string");
|
|
197
|
+
assert.ok(typeof result.user === "string");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("does not reference {roleName} placeholder", () => {
|
|
201
|
+
const result = buildDisciplinePrompt(
|
|
202
|
+
skeleton,
|
|
203
|
+
{ ...CTX, skillIds: ["python"], behaviourIds: [], trackIds: [] },
|
|
204
|
+
SCHEMA,
|
|
205
|
+
);
|
|
206
|
+
assert.ok(!result.user.includes("{roleName}"));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("includes prior output when provided", () => {
|
|
210
|
+
const priorOutput = {
|
|
211
|
+
levels: [{ id: "L1", professionalTitle: "Junior" }],
|
|
212
|
+
behaviours: [{ _id: "collab", name: "Collaboration" }],
|
|
213
|
+
capabilities: [
|
|
214
|
+
{ _id: "coding", name: "Coding", skills: [{ id: "python" }] },
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
const result = buildDisciplinePrompt(
|
|
218
|
+
skeleton,
|
|
219
|
+
{ ...CTX, skillIds: ["python"], behaviourIds: [], trackIds: [] },
|
|
220
|
+
SCHEMA,
|
|
221
|
+
priorOutput,
|
|
222
|
+
);
|
|
223
|
+
assert.ok(result.user.includes("Previously generated context"));
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("buildTrackPrompt", () => {
|
|
228
|
+
const skeleton = { id: "backend", name: "Backend" };
|
|
229
|
+
|
|
230
|
+
test("returns system and user strings", () => {
|
|
231
|
+
const result = buildTrackPrompt(
|
|
232
|
+
skeleton,
|
|
233
|
+
{ ...CTX, capabilityIds: ["coding"], behaviourIds: ["collab"] },
|
|
234
|
+
SCHEMA,
|
|
235
|
+
);
|
|
236
|
+
assert.ok(typeof result.system === "string");
|
|
237
|
+
assert.ok(typeof result.user === "string");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("includes track ID", () => {
|
|
241
|
+
const result = buildTrackPrompt(
|
|
242
|
+
skeleton,
|
|
243
|
+
{ ...CTX, capabilityIds: [], behaviourIds: [] },
|
|
244
|
+
SCHEMA,
|
|
245
|
+
);
|
|
246
|
+
assert.ok(result.user.includes("backend"));
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { writeFileSync, mkdtempSync, rmSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
import { ProseEngine } from "../engine/prose.js";
|
|
7
|
+
|
|
8
|
+
function makeLogger() {
|
|
9
|
+
return {
|
|
10
|
+
info: () => {},
|
|
11
|
+
debug: () => {},
|
|
12
|
+
warn: () => {},
|
|
13
|
+
error: () => {},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makePromptLoader() {
|
|
18
|
+
return {
|
|
19
|
+
load: () => "system prompt",
|
|
20
|
+
render: () => "rendered prompt",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeLlmApi(response = "test response") {
|
|
25
|
+
return {
|
|
26
|
+
createCompletions: async () => ({
|
|
27
|
+
choices: [{ message: { content: response } }],
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("ProseEngine", () => {
|
|
33
|
+
test("returns null in no-prose mode", async () => {
|
|
34
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "prose-test-"));
|
|
35
|
+
try {
|
|
36
|
+
const engine = new ProseEngine({
|
|
37
|
+
cachePath: join(tmpDir, "cache.json"),
|
|
38
|
+
mode: "no-prose",
|
|
39
|
+
promptLoader: makePromptLoader(),
|
|
40
|
+
logger: makeLogger(),
|
|
41
|
+
});
|
|
42
|
+
const result = await engine.generateProse("test-key", { topic: "test" });
|
|
43
|
+
assert.strictEqual(result, null);
|
|
44
|
+
} finally {
|
|
45
|
+
rmSync(tmpDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("returns cached result on cache hit", async () => {
|
|
50
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "prose-test-"));
|
|
51
|
+
try {
|
|
52
|
+
const cacheData = {};
|
|
53
|
+
// Pre-populate cache with a known hash
|
|
54
|
+
const cachePath = join(tmpDir, "cache.json");
|
|
55
|
+
writeFileSync(cachePath, JSON.stringify(cacheData));
|
|
56
|
+
|
|
57
|
+
const engine = new ProseEngine({
|
|
58
|
+
cachePath,
|
|
59
|
+
mode: "generate",
|
|
60
|
+
llmApi: makeLlmApi("fresh response"),
|
|
61
|
+
promptLoader: makePromptLoader(),
|
|
62
|
+
logger: makeLogger(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Generate first
|
|
66
|
+
const r1 = await engine.generateProse("key1", { topic: "test" });
|
|
67
|
+
assert.strictEqual(r1, "fresh response");
|
|
68
|
+
assert.strictEqual(engine.stats.generated, 1);
|
|
69
|
+
|
|
70
|
+
// Save and reload
|
|
71
|
+
engine.saveCache();
|
|
72
|
+
|
|
73
|
+
const engine2 = new ProseEngine({
|
|
74
|
+
cachePath,
|
|
75
|
+
mode: "cached",
|
|
76
|
+
promptLoader: makePromptLoader(),
|
|
77
|
+
logger: makeLogger(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const r2 = await engine2.generateProse("key1", { topic: "test" });
|
|
81
|
+
assert.strictEqual(r2, "fresh response");
|
|
82
|
+
assert.strictEqual(engine2.stats.hits, 1);
|
|
83
|
+
} finally {
|
|
84
|
+
rmSync(tmpDir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("generateStructured respects maxTokens option", async () => {
|
|
89
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "prose-test-"));
|
|
90
|
+
let capturedMaxTokens = null;
|
|
91
|
+
try {
|
|
92
|
+
const engine = new ProseEngine({
|
|
93
|
+
cachePath: join(tmpDir, "cache.json"),
|
|
94
|
+
mode: "generate",
|
|
95
|
+
llmApi: {
|
|
96
|
+
createCompletions: async (opts) => {
|
|
97
|
+
capturedMaxTokens = opts.max_tokens;
|
|
98
|
+
return {
|
|
99
|
+
choices: [{ message: { content: '{"test": true}' } }],
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
promptLoader: makePromptLoader(),
|
|
104
|
+
logger: makeLogger(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await engine.generateStructured(
|
|
108
|
+
"test-key",
|
|
109
|
+
[{ role: "user", content: "test" }],
|
|
110
|
+
{ maxTokens: 2000 },
|
|
111
|
+
);
|
|
112
|
+
assert.strictEqual(capturedMaxTokens, 2000);
|
|
113
|
+
} finally {
|
|
114
|
+
rmSync(tmpDir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("generateJson strips markdown fences", async () => {
|
|
119
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "prose-test-"));
|
|
120
|
+
try {
|
|
121
|
+
const engine = new ProseEngine({
|
|
122
|
+
cachePath: join(tmpDir, "cache.json"),
|
|
123
|
+
mode: "generate",
|
|
124
|
+
llmApi: makeLlmApi('```json\n{"key": "value"}\n```'),
|
|
125
|
+
promptLoader: makePromptLoader(),
|
|
126
|
+
logger: makeLogger(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await engine.generateJson("test-key", [
|
|
130
|
+
{ role: "user", content: "test" },
|
|
131
|
+
]);
|
|
132
|
+
assert.deepStrictEqual(result, { key: "value" });
|
|
133
|
+
} finally {
|
|
134
|
+
rmSync(tmpDir, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("generateJson returns null in no-prose mode", async () => {
|
|
139
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "prose-test-"));
|
|
140
|
+
try {
|
|
141
|
+
const engine = new ProseEngine({
|
|
142
|
+
cachePath: join(tmpDir, "cache.json"),
|
|
143
|
+
mode: "no-prose",
|
|
144
|
+
promptLoader: makePromptLoader(),
|
|
145
|
+
logger: makeLogger(),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = await engine.generateJson("test-key", [
|
|
149
|
+
{ role: "user", content: "test" },
|
|
150
|
+
]);
|
|
151
|
+
assert.strictEqual(result, null);
|
|
152
|
+
} finally {
|
|
153
|
+
rmSync(tmpDir, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|