pict-section-prompteditor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ const libPictTemplate = require('pict-template');
4
+
5
+ /**
6
+ * The {~WordListEntry:Name~} template expression (short form {~WLE:Name~}).
7
+ *
8
+ * Resolves to one entry drawn from the named word list by weighted random:
9
+ * an entry's chance is weight / sum(weights). With Dinosaurs holding
10
+ * [["Tyrannosaurus", 3], ["Diplodocus", 1]], the expression renders
11
+ * Tyrannosaurus 75% of the time and Diplodocus 25%.
12
+ *
13
+ * Word lists are found two ways, in order:
14
+ *
15
+ * 1. The data record: a generation run passes the lists in the parse data as
16
+ * __PromptEditorWordLists ({ lowercased name -> Entries }) along with an
17
+ * optional __PromptEditorRandom function. This is what the prompt editor
18
+ * view does, and it keeps multiple editor instances exact: each
19
+ * generation resolves against precisely the lists it was given.
20
+ *
21
+ * 2. The pict-level registry: any prompt editor view (or a host) can push a
22
+ * resolver function onto pict.__PictSectionPromptEditorResolvers, so the
23
+ * expression also works in ordinary application templates once a section
24
+ * is mounted. Resolvers take the lowercased list name and return Entries
25
+ * or null.
26
+ *
27
+ * A second optional parameter is the default for a miss:
28
+ * {~WordListEntry:Dinosaurs:a large lizard~} renders the default when the list
29
+ * does not resolve (or has nothing drawable). The split is on the first colon,
30
+ * so defaults may contain colons; an empty default ({~WLE:Name:~}) renders
31
+ * nothing on a miss. With no default, a miss renders the expression back out
32
+ * literally, so a typo is visible in the generated prompt instead of silently
33
+ * vanishing. Each occurrence draws independently; two references to the same
34
+ * list in one prompt can (and should be able to) land on different words.
35
+ */
36
+
37
+ /**
38
+ * Draw one word from [[word, weight], ...] by weighted random. Entries with a
39
+ * non-positive weight never draw. Returns null for an empty or all-zero list.
40
+ * @param {Array<Array>} pEntries
41
+ * @param {function} [pRandom] - returns [0, 1); defaults to Math.random
42
+ * @returns {string|null}
43
+ */
44
+ function weightedPick(pEntries, pRandom)
45
+ {
46
+ if (!Array.isArray(pEntries) || !pEntries.length) { return null; }
47
+ let tmpRandom = (typeof pRandom === 'function') ? pRandom : Math.random;
48
+ let tmpTotal = 0;
49
+ for (let i = 0; i < pEntries.length; i++)
50
+ {
51
+ let tmpWeight = Number(pEntries[i][1]);
52
+ if (isFinite(tmpWeight) && tmpWeight > 0) { tmpTotal += tmpWeight; }
53
+ }
54
+ if (tmpTotal <= 0) { return null; }
55
+ let tmpRoll = tmpRandom() * tmpTotal;
56
+ for (let i = 0; i < pEntries.length; i++)
57
+ {
58
+ let tmpWeight = Number(pEntries[i][1]);
59
+ if (!isFinite(tmpWeight) || tmpWeight <= 0) { continue; }
60
+ tmpRoll -= tmpWeight;
61
+ if (tmpRoll < 0) { return String(pEntries[i][0]); }
62
+ }
63
+ // Floating point edge: the roll landed exactly on the total.
64
+ for (let i = pEntries.length - 1; i >= 0; i--)
65
+ {
66
+ if (Number(pEntries[i][1]) > 0) { return String(pEntries[i][0]); }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ class PictTemplateWordListEntry extends libPictTemplate
72
+ {
73
+ constructor(pFable, pOptions, pServiceHash)
74
+ {
75
+ super(pFable, pOptions, pServiceHash);
76
+
77
+ this.addPattern('{~WordListEntry:', '~}');
78
+ this.addPattern('{~WLE:', '~}');
79
+ }
80
+
81
+ render(pTemplateHash, pRecord)
82
+ {
83
+ let tmpBody = String(pTemplateHash || '').trim();
84
+ if (!tmpBody) { return ''; }
85
+
86
+ // {~WordListEntry:ListName:Default~} -- everything after the FIRST colon
87
+ // is the default rendered when the list does not resolve (or resolves
88
+ // with nothing drawable). The default may contain colons, and may be
89
+ // empty: {~WLE:Name:~} renders nothing on a miss. With no default at
90
+ // all, a miss echoes the expression so a typo stays findable.
91
+ let tmpListName = tmpBody;
92
+ let tmpDefault = null;
93
+ let tmpColon = tmpBody.indexOf(':');
94
+ if (tmpColon > -1)
95
+ {
96
+ tmpListName = tmpBody.slice(0, tmpColon).trim();
97
+ tmpDefault = tmpBody.slice(tmpColon + 1).trim();
98
+ }
99
+ if (!tmpListName) { return (tmpDefault === null) ? '' : tmpDefault; }
100
+ let tmpWanted = tmpListName.toLowerCase();
101
+ let tmpMiss = (tmpDefault === null) ? ('{~WordListEntry:' + tmpListName + '~}') : tmpDefault;
102
+
103
+ let tmpEntries = null;
104
+ let tmpRandom = null;
105
+
106
+ // 1. Lists riding in the parse data (a generation run).
107
+ if (pRecord && pRecord.__PromptEditorWordLists && pRecord.__PromptEditorWordLists[tmpWanted])
108
+ {
109
+ tmpEntries = pRecord.__PromptEditorWordLists[tmpWanted];
110
+ }
111
+ if (pRecord && typeof pRecord.__PromptEditorRandom === 'function')
112
+ {
113
+ tmpRandom = pRecord.__PromptEditorRandom;
114
+ }
115
+
116
+ // 2. The pict-level resolver registry (mounted sections, host overrides).
117
+ if (!tmpEntries && this.pict && Array.isArray(this.pict.__PictSectionPromptEditorResolvers))
118
+ {
119
+ for (let i = 0; i < this.pict.__PictSectionPromptEditorResolvers.length; i++)
120
+ {
121
+ let tmpResolved = null;
122
+ try { tmpResolved = this.pict.__PictSectionPromptEditorResolvers[i](tmpWanted); }
123
+ catch (pError) { tmpResolved = null; }
124
+ if (tmpResolved && tmpResolved.length) { tmpEntries = tmpResolved; break; }
125
+ }
126
+ }
127
+
128
+ if (!tmpEntries || !tmpEntries.length)
129
+ {
130
+ return tmpMiss;
131
+ }
132
+
133
+ let tmpWord = weightedPick(tmpEntries, tmpRandom);
134
+ return (tmpWord === null) ? tmpMiss : tmpWord;
135
+ }
136
+ }
137
+
138
+ module.exports = PictTemplateWordListEntry;
139
+ module.exports.template_hash = 'WordListEntry';
140
+ module.exports.weightedPick = weightedPick;
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Built-in prompt types.
5
+ *
6
+ * A prompt type is a named arrangement of segments. Each segment:
7
+ *
8
+ * {
9
+ * Key: stable identifier; a prompt's Segments object is keyed by this
10
+ * Name: the heading the compiler writes (## Name)
11
+ * Guidance: one line shown to the author about what belongs here
12
+ * Fixed: true for locked text (a team preamble); Body below is used and
13
+ * the section renders it read-only instead of an editor
14
+ * Body: the fixed text (only read when Fixed is true)
15
+ * Optional: empty optional segments are skipped at compile time; empty
16
+ * required segments compile with a placeholder note
17
+ * }
18
+ *
19
+ * These cover the common shapes of working with an AI pair. They are defaults,
20
+ * not a registry: pass your own array as the PromptTypes option and it replaces
21
+ * this set entirely. To extend instead of replace, spread DefaultPromptTypes
22
+ * into your own array.
23
+ */
24
+
25
+ const DefaultPromptTypes =
26
+ [
27
+ {
28
+ Key: 'feature-request',
29
+ Name: 'Feature Request',
30
+ Description: 'Ask for a new capability: what exists, what you want, and how you will know it worked.',
31
+ Segments:
32
+ [
33
+ { Key: 'context', Name: 'Context', Guidance: 'What exists today, where it lives, and anything the reader needs to know cold.' },
34
+ { Key: 'request', Name: 'Request', Guidance: 'The capability you want, stated plainly.' },
35
+ { Key: 'success-criteria', Name: 'Success Criteria', Guidance: 'Observable outcomes that prove it works.' }
36
+ ]
37
+ },
38
+ {
39
+ Key: 'bug-report',
40
+ Name: 'Bug Report',
41
+ Description: 'Report something broken with enough signal to reproduce and fix it.',
42
+ Segments:
43
+ [
44
+ { Key: 'context', Name: 'Context', Guidance: 'The system, version, and environment where the problem shows.' },
45
+ { Key: 'observed', Name: 'Observed Behavior', Guidance: 'What actually happens, including exact errors.' },
46
+ { Key: 'expected', Name: 'Expected Behavior', Guidance: 'What should happen instead.' },
47
+ { Key: 'reproduction', Name: 'Reproduction', Guidance: 'Steps that make it happen, numbered.', Optional: true }
48
+ ]
49
+ },
50
+ {
51
+ Key: 'research',
52
+ Name: 'Research',
53
+ Description: 'Send the AI to learn something and come back with a usable answer.',
54
+ Segments:
55
+ [
56
+ { Key: 'context', Name: 'Context', Guidance: 'Why you are asking and what you already know.' },
57
+ { Key: 'question', Name: 'Question', Guidance: 'The question, sharp enough to answer.' },
58
+ { Key: 'constraints', Name: 'Constraints', Guidance: 'Boundaries: scope, sources, time period, format.', Optional: true },
59
+ { Key: 'deliverable', Name: 'Deliverable', Guidance: 'The shape of the answer you want back.' }
60
+ ]
61
+ },
62
+ {
63
+ Key: 'code-review',
64
+ Name: 'Code Review',
65
+ Description: 'Ask for a review with the focus stated up front.',
66
+ Segments:
67
+ [
68
+ { Key: 'context', Name: 'Context', Guidance: 'What the change does and why it exists.' },
69
+ { Key: 'focus', Name: 'Focus', Guidance: 'What kind of problems matter most here.' },
70
+ { Key: 'out-of-scope', Name: 'Out of Scope', Guidance: 'What not to spend attention on.', Optional: true }
71
+ ]
72
+ },
73
+ {
74
+ Key: 'freeform',
75
+ Name: 'Freeform',
76
+ Description: 'One open segment, no structure imposed.',
77
+ Segments:
78
+ [
79
+ { Key: 'body', Name: 'Prompt', Guidance: 'Anything goes.' }
80
+ ]
81
+ }
82
+ ];
83
+
84
+ /**
85
+ * Resolve the effective prompt type set from the view options: a provided
86
+ * array wins outright; null/undefined means the defaults.
87
+ * @param {Array|null} pConfigured
88
+ * @returns {Array}
89
+ */
90
+ function resolvePromptTypes(pConfigured)
91
+ {
92
+ if (Array.isArray(pConfigured) && pConfigured.length) { return pConfigured; }
93
+ return DefaultPromptTypes;
94
+ }
95
+
96
+ /**
97
+ * Find a type by Key within a resolved set, with a freeform-style fallback so
98
+ * a prompt whose type was removed by the host still renders and compiles.
99
+ * @param {Array} pTypes
100
+ * @param {string} pTypeKey
101
+ * @returns {object}
102
+ */
103
+ function getPromptType(pTypes, pTypeKey)
104
+ {
105
+ let tmpFound = (pTypes || []).find((pType) => pType.Key === pTypeKey);
106
+ if (tmpFound) { return tmpFound; }
107
+ return (
108
+ {
109
+ Key: pTypeKey || 'unknown',
110
+ Name: (pTypeKey ? pTypeKey : 'Unknown type'),
111
+ Description: 'This prompt type is not in the configured set; editing the raw segments.',
112
+ Segments: [{ Key: 'body', Name: 'Prompt', Guidance: 'Anything goes.' }]
113
+ });
114
+ }
115
+
116
+ module.exports = { DefaultPromptTypes, resolvePromptTypes, getPromptType };