@trazum/core 1.60.3 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/write.js ADDED
@@ -0,0 +1,243 @@
1
+ import { optimize } from './optimize.js';
2
+ import { BUNDLED_CATALOGUE } from './pricing.js';
3
+ /**
4
+ * The interview behind `trazum write`.
5
+ *
6
+ * Every other command in this product reads a prompt somebody already wrote.
7
+ * This one starts from nothing and asks. What it asks is the product: a
8
+ * question whose answer cannot change the output is waste, and waste is this
9
+ * tool's entire subject.
10
+ *
11
+ * **Deterministic, and deliberately so.** No model decides what to ask. The
12
+ * catalogue below is fixed, the gates are predicates over the answers so far,
13
+ * and the same answers produce the same interview on any machine — which is
14
+ * what lets the offline rule hold without a footnote.
15
+ *
16
+ * **Ids here, words in the CLI.** Same split as the rules catalogue: this file
17
+ * knows a slot exists and what opens it; `packages/cli/src/i18n` knows how to
18
+ * ask it in a locale. A locale changes the question, never which questions.
19
+ */
20
+ /**
21
+ * The sections of an assembled prompt, in the order they are written.
22
+ *
23
+ * The order is fixed, and for a reason this tool can price: prompt caching is
24
+ * a byte-for-byte prefix match, so everything stable goes first and everything
25
+ * that varies per call goes last, which makes the cacheable prefix as long as
26
+ * the prompt allows.
27
+ */
28
+ export const SECTIONS = [
29
+ 'role',
30
+ 'task',
31
+ 'inputs',
32
+ 'output',
33
+ 'constraints',
34
+ 'examples',
35
+ 'failure-modes',
36
+ ];
37
+ /**
38
+ * The closed vocabulary `output-shape` accepts.
39
+ *
40
+ * Named `OutputFormat` rather than `OutputShape` because that name is already
41
+ * taken by the usage report's finding about where output spend concentrates,
42
+ * and two unrelated things under one name in one package is how a consumer
43
+ * imports the wrong one.
44
+ */
45
+ export const OUTPUT_FORMATS = ['prose', 'json', 'list', 'table'];
46
+ const answered = (answers, id) => Object.prototype.hasOwnProperty.call(answers, id) && answers[id] !== null;
47
+ const shapeIs = (...formats) => (answers) => {
48
+ const shape = answers['output-shape'];
49
+ return typeof shape === 'string' && formats.includes(shape);
50
+ };
51
+ /**
52
+ * The catalogue, in the order the interview walks it.
53
+ *
54
+ * Required first is not an accident: somebody who abandons the interview
55
+ * halfway should have answered the things without which there is no prompt at
56
+ * all, rather than having spent their attention on the optional half.
57
+ */
58
+ export const SLOTS = [
59
+ { id: 'task', section: 'task', required: true },
60
+ { id: 'role', section: 'role', required: true },
61
+ { id: 'inputs', section: 'inputs', required: true },
62
+ { id: 'output-shape', section: 'output', required: true },
63
+ { id: 'output-schema', section: 'output', required: true, opensWhen: shapeIs('json', 'table') },
64
+ { id: 'output-length', section: 'output', required: false, opensWhen: shapeIs('prose', 'list') },
65
+ { id: 'audience', section: 'role', required: false },
66
+ { id: 'constraints', section: 'constraints', required: false },
67
+ { id: 'refusal', section: 'constraints', required: false },
68
+ { id: 'examples', section: 'examples', required: false },
69
+ {
70
+ id: 'example-inputs',
71
+ section: 'examples',
72
+ required: false,
73
+ opensWhen: (answers) => answered(answers, 'examples'),
74
+ },
75
+ { id: 'failure-modes', section: 'failure-modes', required: false },
76
+ { id: 'model', section: null, required: false },
77
+ { id: 'budget', section: null, required: false },
78
+ ];
79
+ export const SLOT_IDS = SLOTS.map((entry) => entry.id);
80
+ export function slot(id) {
81
+ return SLOTS.find((entry) => entry.id === id);
82
+ }
83
+ /** Whether a slot is worth asking, given what is known so far. */
84
+ export function isOpen(entry, answers) {
85
+ return entry.opensWhen === undefined || entry.opensWhen(answers);
86
+ }
87
+ export function interview(answers) {
88
+ const open = SLOTS.filter((entry) => isOpen(entry, answers));
89
+ const has = (entry) => Object.prototype.hasOwnProperty.call(answers, entry.id);
90
+ const unasked = open.filter((entry) => !has(entry));
91
+ return {
92
+ next: unasked[0]?.id ?? null,
93
+ done: unasked.length === 0,
94
+ open: open.map((entry) => entry.id),
95
+ answered: open.filter((entry) => answered(answers, entry.id)).map((entry) => entry.id),
96
+ declined: open
97
+ .filter((entry) => has(entry) && answers[entry.id] === null)
98
+ .map((entry) => entry.id),
99
+ missing: unasked.filter((entry) => entry.required).map((entry) => entry.id),
100
+ };
101
+ }
102
+ /**
103
+ * How each slot appears in the assembled prompt.
104
+ *
105
+ * A slot either supplies the section's opening line on its own, or arrives
106
+ * under a label. Nothing here paraphrases an answer: the words are the
107
+ * author's, and a writer that rewrote them would be answering a question
108
+ * nobody asked it.
109
+ */
110
+ const LABEL = {
111
+ role: null,
112
+ audience: 'Audience:',
113
+ task: null,
114
+ inputs: null,
115
+ 'output-shape': 'Format:',
116
+ 'output-schema': 'Fields:',
117
+ 'output-length': 'At most:',
118
+ constraints: null,
119
+ refusal: 'When you cannot answer:',
120
+ examples: null,
121
+ 'example-inputs': 'Input:',
122
+ 'failure-modes': null,
123
+ };
124
+ /**
125
+ * The heading each section is written under.
126
+ *
127
+ * **English, in every locale, on purpose.** These are structure rather than
128
+ * prose — a contract with the model, not words for a human reader — and the
129
+ * arc promises the same answers produce the same prompt byte for byte on any
130
+ * machine *and in any locale*. A heading that moved with `TRAZUM_LOCALE` would
131
+ * make the assembled prompt a function of the machine that ran the interview,
132
+ * which is the one thing [the locale rule](../../../ROADMAP.md) forbids.
133
+ */
134
+ const HEADING = {
135
+ role: 'Role',
136
+ task: 'Task',
137
+ inputs: 'Inputs',
138
+ output: 'Output',
139
+ constraints: 'Constraints',
140
+ examples: 'Examples',
141
+ 'failure-modes': 'Failure modes',
142
+ };
143
+ const line = (id, answer) => {
144
+ const label = LABEL[id] ?? null;
145
+ return label === null ? answer.trim() : `${label} ${answer.trim()}`;
146
+ };
147
+ /**
148
+ * Assemble the prompt, or refuse and say what is missing.
149
+ *
150
+ * Deterministic in both senses that matter: the same answers produce the same
151
+ * bytes, and nothing here consults the network, the clock or the locale.
152
+ */
153
+ export function assemble(answers, options = {}) {
154
+ const state = interview(answers);
155
+ const sections = [];
156
+ for (const section of SECTIONS) {
157
+ const filled = SLOTS.filter((entry) => entry.section === section &&
158
+ isOpen(entry, answers) &&
159
+ typeof answers[entry.id] === 'string' &&
160
+ answers[entry.id].trim().length > 0);
161
+ if (filled.length === 0)
162
+ continue;
163
+ const body = filled.map((entry) => line(entry.id, answers[entry.id])).join('\n');
164
+ sections.push({
165
+ section,
166
+ text: `${HEADING[section]}\n${body}`,
167
+ from: filled.map((entry) => entry.id),
168
+ });
169
+ }
170
+ const prompt = state.missing.length > 0 ? null : sections.map((entry) => entry.text).join('\n\n');
171
+ return {
172
+ schemaVersion: 1,
173
+ prompt,
174
+ sections,
175
+ answered: state.answered,
176
+ declined: state.declined,
177
+ missing: state.missing,
178
+ measured: prompt === null ? null : measure(prompt, answers, state, options),
179
+ };
180
+ }
181
+ const measure = (prompt, answers, state, options) => {
182
+ const model = typeof answers['model'] === 'string' ? answers['model'].trim() : null;
183
+ const budgetAnswer = typeof answers['budget'] === 'string' ? Number(answers['budget']) : NaN;
184
+ const budgetUsd = Number.isFinite(budgetAnswer) ? budgetAnswer : null;
185
+ /*
186
+ Priced only when the catalogue knows the model.
187
+
188
+ `optimize` throws on a model it cannot price, which is right for a command
189
+ somebody typed a model into and wrong here: an unpriced model is one of the
190
+ three answers this measurement gives, not a crash. So the tokens and the
191
+ rules — neither of which needs a price — are measured first, and the money
192
+ is asked for separately and only when there is something to ask.
193
+ */
194
+ const known = model !== null && (options.pricing ?? BUNDLED_CATALOGUE).byId.has(model);
195
+ const usage = {};
196
+ if (known)
197
+ usage.model = model;
198
+ if (options.callsPerMonth !== undefined)
199
+ usage.callsPerMonth = options.callsPerMonth;
200
+ if (options.avgOutputTokens !== undefined)
201
+ usage.avgOutputTokens = options.avgOutputTokens;
202
+ const report = optimize(prompt, {
203
+ level: options.level ?? 'safe',
204
+ pricing: options.pricing,
205
+ usage,
206
+ });
207
+ const monthlyUsd = known && report.savings !== null ? report.savings.perMonth.before.totalUsd : null;
208
+ let verdict = 'cannot-tell';
209
+ let reason = null;
210
+ if (budgetUsd === null)
211
+ reason = 'no-budget';
212
+ else if (model === null)
213
+ reason = 'no-model';
214
+ else if (monthlyUsd === null)
215
+ reason = 'model-unpriced';
216
+ else
217
+ verdict = monthlyUsd > budgetUsd ? 'over' : 'within';
218
+ return {
219
+ complete: {
220
+ required: SLOTS.filter((entry) => entry.required && isOpen(entry, answers)).length,
221
+ answered: state.answered.length,
222
+ declined: state.declined,
223
+ missing: state.missing,
224
+ },
225
+ cheap: {
226
+ tokens: report.tokensBefore,
227
+ tokenSource: report.tokenSource,
228
+ model,
229
+ monthlyUsd,
230
+ provenance: 'estimated',
231
+ budgetUsd,
232
+ verdict,
233
+ reason,
234
+ },
235
+ clean: {
236
+ rules: report.rules
237
+ .filter((entry) => entry.hits > 0)
238
+ .map((entry) => ({ id: entry.id, hits: entry.hits })),
239
+ tokensRecoverable: report.tokensSaved,
240
+ },
241
+ };
242
+ };
243
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../src/write.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trazum/core",
3
- "version": "1.60.3",
3
+ "version": "1.61.0",
4
4
  "description": "Trazum core: priced advisories for LLM prompts (caching, model tier, batching, schemas), plus deterministic trimming, token counting and pricing.",
5
5
  "license": "MIT",
6
6
  "author": "David Mu\u00f1oz Rey",
package/src/conform.ts CHANGED
@@ -45,6 +45,7 @@ export const CONTRACT_NAMES = [
45
45
  'outcome-report',
46
46
  'annual-record',
47
47
  'roll-up',
48
+ 'prompt-draft',
48
49
  ] as const;
49
50
 
50
51
  export type ContractName = (typeof CONTRACT_NAMES)[number];
@@ -326,6 +327,35 @@ const DOCUMENT_RULES: Record<Exclude<ContractName, 'usage-log'>, FieldRule[]> =
326
327
  rule('call', 'an object, or null when nothing was described', (v) => v === null || isObject(v)),
327
328
  rule('budget', 'an object, or null when there is no budget', (v) => v === null || isObject(v)),
328
329
  ],
330
+ /**
331
+ * A prompt somebody was interviewed into, and what the interview could not
332
+ * get out of them.
333
+ *
334
+ * `prompt` is **null and never an empty string** when required answers are
335
+ * missing: an empty string reads as a prompt that came out blank, and the
336
+ * difference between "not built" and "built and empty" is the one this
337
+ * format refuses to lose everywhere else.
338
+ *
339
+ * `declined` and `missing` are separate arrays for the same reason. Somebody
340
+ * who was asked and said no is not somebody who was never asked, and folding
341
+ * the two would turn a decision into a gap.
342
+ */
343
+ 'prompt-draft': [
344
+ rule(
345
+ 'prompt',
346
+ 'the assembled prompt, or **null** when required answers are missing — never an empty string',
347
+ (v) => v === null || typeof v === 'string',
348
+ ),
349
+ rule('sections', 'an array of the sections that got words, in the order they are written', isArray),
350
+ rule('answered', 'an array of the slots that were answered', isArray),
351
+ rule('declined', 'an array of the slots somebody was asked and declined — never folded into missing', isArray),
352
+ rule('missing', 'an array of required slots still unanswered; empty exactly when prompt is a string', isArray),
353
+ rule(
354
+ 'measured',
355
+ 'an object of the three claims, or **null** when there is no prompt to measure — never an object of zeros',
356
+ (v) => v === null || isObject(v),
357
+ ),
358
+ ],
329
359
  };
330
360
 
331
361
  /**
package/src/index.ts CHANGED
@@ -547,3 +547,27 @@ export {
547
547
  } from './config-schema.js';
548
548
  export type { ResolvedBudget, SpendConfig, TrazumConfig, WaiveEntry } from './config-schema.js';
549
549
  export { WAIVABLE_GATES } from './config-schema.js';
550
+
551
+ export { RULE_LEVELS } from './types.js';
552
+ export {
553
+ assemble,
554
+ interview,
555
+ isOpen,
556
+ OUTPUT_FORMATS,
557
+ SECTIONS,
558
+ slot,
559
+ SLOT_IDS,
560
+ SLOTS,
561
+ } from './write.js';
562
+ export type {
563
+ Answer,
564
+ Answers,
565
+ AssembleOptions,
566
+ DraftMeasurement,
567
+ DraftSection,
568
+ Interview,
569
+ OutputFormat,
570
+ PromptDraft,
571
+ Section,
572
+ Slot,
573
+ } from './write.js';
package/src/optimize.ts CHANGED
@@ -9,6 +9,7 @@ import { computeSavings } from './savings.js';
9
9
  import { join, segment } from './segment.js';
10
10
  import { estimateTokens } from './tokenizer.js';
11
11
  import type { AsyncTokenCounter } from './tokenizer.js';
12
+ import { RULE_LEVELS } from './types.js';
12
13
  import type {
13
14
  OptimizationResult,
14
15
  OptimizeOptions,
@@ -92,6 +93,22 @@ function distinctProtected(segments: Segment[]): string[] {
92
93
  */
93
94
  export function optimize(prompt: string, options: OptimizeOptions = {}): OptimizationResult {
94
95
  const level = options.level ?? 'safe';
96
+ /*
97
+ An unknown level used to run `safe` and say nothing.
98
+
99
+ The rule loop skips aggressive rules unless the level *is* `aggressive`, so
100
+ every other string — a typo, a plausible-sounding `balanced` — silently
101
+ produced safe-level results. The CLI has always refused `--level balanced`
102
+ by name; a library caller got the quiet downgrade instead, and a report
103
+ that says `safe` work was done at the level somebody asked for is the
104
+ swallowed-flag defect one layer down. Nothing correct depends on the old
105
+ behaviour: no program means `safe` by writing something else.
106
+ */
107
+ if (!RULE_LEVELS.includes(level)) {
108
+ throw new Error(
109
+ `level must be one of ${RULE_LEVELS.join(', ')} (received: ${JSON.stringify(level)})`,
110
+ );
111
+ }
95
112
  const locale: Locale = options.locale ?? DEFAULT_LOCALE;
96
113
  const t = getMessages(locale);
97
114
  const disabled = new Set<RuleId>(options.disableRules ?? []);
package/src/types.ts CHANGED
@@ -5,7 +5,17 @@ import type { Locale, RuleId } from './i18n/types.js';
5
5
  import type { PricingCatalogue } from './pricing.js';
6
6
 
7
7
  /** How aggressive the deterministic rules are allowed to be. */
8
- export type RuleLevel = 'safe' | 'aggressive';
8
+ /**
9
+ * The levels an optimisation runs at, from the one place that defines them.
10
+ *
11
+ * Exported as a value, not only as a type. `@trazum/core` had the union and no
12
+ * list, so the valid set was not discoverable from the package and a caller
13
+ * reaching for a plausible third name — `balanced` — got no answer from the
14
+ * types at runtime and no error either.
15
+ */
16
+ export const RULE_LEVELS = ['safe', 'aggressive'] as const;
17
+
18
+ export type RuleLevel = (typeof RULE_LEVELS)[number];
9
19
 
10
20
  /**
11
21
  * A deterministic rule applied to the mutable text of a prompt.