@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 +201 -0
- package/engine/pathway.js +342 -0
- package/engine/prose.js +222 -0
- package/index.js +2 -0
- package/package.json +32 -0
- package/prompts/pathway/behaviour.js +42 -0
- package/prompts/pathway/capability.js +70 -0
- package/prompts/pathway/discipline.js +61 -0
- package/prompts/pathway/driver.js +56 -0
- package/prompts/pathway/framework.js +39 -0
- package/prompts/pathway/level.js +58 -0
- package/prompts/pathway/stage.js +45 -0
- package/prompts/pathway/track.js +51 -0
- package/prompts/prose-system.prompt.md +2 -0
- package/prompts/prose-user.prompt.md +6 -0
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
|
+
}
|
package/engine/prose.js
ADDED
|
@@ -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
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,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.
|