flex-md 4.6.0 → 4.7.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/README.md CHANGED
@@ -170,14 +170,72 @@ Compliance levels measure how much detail you provide in system parts:
170
170
  "Lines: ~50. Provide a complete working example."
171
171
  ```
172
172
 
173
+ ## Input vs Output Format Specs
174
+
175
+ Flex-MD supports both **Input Format Specs** and **Output Format Specs**, but they serve different purposes:
176
+
177
+ ### Input Format Specs (Planning & Design)
178
+
179
+ Input Format Specs (defined with `## Input format`) are **design-time tools** for planning and documentation:
180
+
181
+ - ✅ **Documentation**: Clearly specify what input structure your LLM expects
182
+ - ✅ **Planning**: Help design your prompt structure and data flow
183
+ - ✅ **Coding**: Guide developers on how to structure inputs
184
+ - ✅ **Validation**: Can be used to validate input data (optional)
185
+
186
+ **Input format specs are NOT used for runtime token estimation** — they're for human understanding and system design.
187
+
188
+ ### Output Format Specs (Runtime)
189
+
190
+ Output Format Specs (defined with `## Output format`) are **runtime tools**:
191
+
192
+ - ✅ **Token Estimation**: Used to calculate `max_tokens` for API calls
193
+ - ✅ **Validation**: Enforce structure on LLM responses
194
+ - ✅ **Extraction**: Parse structured data from responses
195
+ - ✅ **Guidance**: Tell the LLM exactly what format to return
196
+
197
+ **Output format specs ARE used for runtime token estimation** — they directly impact API call parameters.
198
+
199
+ ### Example: Using Both
200
+
201
+ ```typescript
202
+ const instructions = `
203
+ ## Input format
204
+ - User Query — text (required)
205
+ The question or request from the user.
206
+ - Context — list (optional)
207
+ Items: 1-5. Additional context items.
208
+
209
+ ## Output format
210
+ - Answer — text (required)
211
+ Length: 2-3 paragraphs. Provide a comprehensive answer.
212
+ - Sources — list (required)
213
+ Items: 3-7. List information sources used.
214
+ `;
215
+
216
+ // Parse both formats
217
+ import { parseFormatSpecs } from 'flex-md';
218
+ const { input, output } = parseFormatSpecs(instructions);
219
+
220
+ // Input format: Use for documentation/planning
221
+ console.log('Expected input structure:', input);
222
+
223
+ // Output format: Use for runtime token estimation
224
+ const maxTokens = getMaxTokens(output);
225
+ ```
226
+
173
227
  ## Smart Toolbox
174
228
 
175
229
  ### Token Estimation
176
230
 
231
+ #### Planning-Time Estimation
232
+
233
+ For estimating tokens from format specs directly (useful during development):
234
+
177
235
  ```typescript
178
236
  import { getMaxTokens, estimateSpecTokens } from 'flex-md';
179
237
 
180
- // Quick estimate
238
+ // Quick estimate from output format spec
181
239
  const maxTokens = getMaxTokens(spec);
182
240
 
183
241
  // Detailed estimate with options
@@ -195,6 +253,64 @@ console.log(estimate);
195
253
  // }
196
254
  ```
197
255
 
256
+ #### Runtime Estimation
257
+
258
+ For estimating tokens at runtime with actual prompt, context, and instructions:
259
+
260
+ ```typescript
261
+ import { runtimeEstimateTokens } from 'flex-md';
262
+
263
+ const estimate = runtimeEstimateTokens({
264
+ prompt: 'Analyze this code and explain what it does.',
265
+ context: 'Previous conversation about TypeScript...',
266
+ instructions: `
267
+ You are a helpful coding assistant.
268
+
269
+ ## Output format
270
+ - Analysis — text (required)
271
+ Length: 2-3 paragraphs. Explain the code functionality.
272
+ - Code Quality — list (required)
273
+ Items: 3-5. List quality observations.
274
+ `,
275
+ options: {
276
+ safetyMultiplier: 1.2,
277
+ strategy: 'average',
278
+ additionalOverhead: 50 // For system messages, formatting, etc.
279
+ }
280
+ });
281
+
282
+ console.log(estimate.maxTokens); // Recommended max_tokens for API call (output tokens only)
283
+ console.log(estimate.breakdown);
284
+ // {
285
+ // prompt: 12, // Input tokens (for budgeting)
286
+ // context: 8, // Input tokens (for budgeting)
287
+ // instructions: 45, // Input tokens (for budgeting)
288
+ // output: { total: { estimated: 450, ... }, ... }, // Output token estimate
289
+ // additionalOverhead: 50,
290
+ // total: 565 // Total tokens (input + output) for budgeting
291
+ // }
292
+
293
+ // Use in API call
294
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
295
+ method: 'POST',
296
+ body: JSON.stringify({
297
+ model: 'claude-sonnet-4-20250514',
298
+ max_tokens: estimate.maxTokens, // Output tokens only (extracted from output format spec)
299
+ messages: [{
300
+ role: 'user',
301
+ content: prompt + '\n\n' + instructions
302
+ }]
303
+ })
304
+ });
305
+ ```
306
+
307
+ The `runtimeEstimateTokens` function:
308
+ - **Extracts output format specs from instructions automatically** (input format specs are ignored)
309
+ - Estimates tokens for prompt, context, and instructions text (for budgeting/planning)
310
+ - **Estimates output tokens based on the output format spec** (this becomes `max_tokens`)
311
+ - Provides a breakdown showing input tokens (for budgeting) and output tokens (for API parameter)
312
+ - Returns `maxTokens` which is the value to use for the `max_tokens` API parameter (output only)
313
+
198
314
  ### Compliance Checking
199
315
 
200
316
  ```typescript
@@ -410,14 +526,18 @@ const v3 = `
410
526
  ## API Reference
411
527
 
412
528
  ### Core Functions
413
- - `parseOutputFormatSpec(markdown)` - Parse spec from markdown
529
+ - `parseOutputFormatSpec(markdown)` - Parse output format spec from markdown
530
+ - `parseInputFormatSpec(markdown)` - Parse input format spec from markdown
531
+ - `parseFormatSpecs(instructions)` - Extract both input and output format specs from instructions
414
532
  - `stringifyOutputFormatSpec(spec)` - Convert spec to markdown
415
533
  - `buildMarkdownGuidance(spec, options)` - Generate LLM instructions
416
534
  - `enforceFlexMd(text, spec, options)` - Validate and repair LLM output
417
535
 
418
536
  ### Token Estimation (v4.0)
419
- - `getMaxTokens(spec, options?)` - Get estimated max_tokens
420
- - `estimateSpecTokens(spec, options?)` - Detailed token estimate
537
+ - `getMaxTokens(spec, options?)` - Get estimated max_tokens from spec
538
+ - `getMaxTokensFromInstructions(instructions, options?)` - Extract format specs and estimate tokens
539
+ - `estimateSpecTokens(spec, options?)` - Detailed token estimate from spec
540
+ - `runtimeEstimateTokens(params)` - Runtime estimation with prompt, context, and instructions
421
541
  - `parseSystemPart(instruction, kind)` - Parse system part from instruction
422
542
  - `estimateTokens(systemPart)` - Estimate tokens for system part
423
543
 
package/dist/index.cjs CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.jsonToMarkdown = exports.Schema = exports.MarkdownParser = exports.JSONTransformer = exports.enforceFlexMd = exports.repairToMarkdownLevel = exports.detectResponseKind = exports.buildIssuesEnvelopeAuto = exports.buildIssuesEnvelope = exports.parseIssuesEnvelope = exports.processResponseMarkdown = exports.extractFromMarkdown = exports.checkConnection = exports.hasFlexMdContract = exports.checkCompliance = exports.validateMarkdownAgainstOfs = exports.enrichInstructionsWithFlexMd = exports.enrichInstructions = exports.buildMarkdownGuidance = exports.stringifyOutputFormatSpec = exports.recall = exports.remember = exports.transformWithOfs = exports.ofsToSchema = exports.validateFormat = exports.parseOutputFormatSpec = exports.buildOutline = exports.logger = void 0;
17
+ exports.jsonToMarkdown = exports.Schema = exports.MarkdownParser = exports.JSONTransformer = exports.enforceFlexMd = exports.repairToMarkdownLevel = exports.detectResponseKind = exports.buildIssuesEnvelopeAuto = exports.buildIssuesEnvelope = exports.parseIssuesEnvelope = exports.processResponseMarkdown = exports.extractFromMarkdown = exports.checkConnection = exports.hasFlexMdContract = exports.checkCompliance = exports.validateMarkdownAgainstOfs = exports.enrichInstructionsWithFlexMd = exports.enrichInstructions = exports.buildMarkdownGuidance = exports.stringifyOutputFormatSpec = exports.recall = exports.remember = exports.transformWithOfs = exports.ofsToSchema = exports.validateFormat = exports.parseFormatSpecs = exports.parseInputFormatSpec = exports.parseOutputFormatSpec = exports.buildOutline = exports.logger = void 0;
18
18
  // Core SFMD Types
19
19
  __exportStar(require("./types.js"), exports);
20
20
  __exportStar(require("./strictness/types.js"), exports);
@@ -28,6 +28,8 @@ Object.defineProperty(exports, "buildOutline", { enumerable: true, get: function
28
28
  // Output Format Spec (OFS)
29
29
  var parser_js_1 = require("./ofs/parser.js");
30
30
  Object.defineProperty(exports, "parseOutputFormatSpec", { enumerable: true, get: function () { return parser_js_1.parseOutputFormatSpec; } });
31
+ Object.defineProperty(exports, "parseInputFormatSpec", { enumerable: true, get: function () { return parser_js_1.parseInputFormatSpec; } });
32
+ Object.defineProperty(exports, "parseFormatSpecs", { enumerable: true, get: function () { return parser_js_1.parseFormatSpecs; } });
31
33
  Object.defineProperty(exports, "validateFormat", { enumerable: true, get: function () { return parser_js_1.validateFormat; } });
32
34
  var adapter_js_1 = require("./ofs/adapter.js");
33
35
  Object.defineProperty(exports, "ofsToSchema", { enumerable: true, get: function () { return adapter_js_1.ofsToSchema; } });
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export * from "./strictness/types.js";
3
3
  export { logger } from "./logger.js";
4
4
  export * from "./md/parse.js";
5
5
  export { buildOutline } from "./md/outline.js";
6
- export { parseOutputFormatSpec, validateFormat } from "./ofs/parser.js";
6
+ export { parseOutputFormatSpec, parseInputFormatSpec, parseFormatSpecs, validateFormat } from "./ofs/parser.js";
7
7
  export { ofsToSchema, transformWithOfs } from "./ofs/adapter.js";
8
8
  export { remember, recall } from "./ofs/memory.js";
9
9
  export { stringifyOutputFormatSpec } from "./ofs/stringify.js";
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.jsonToMarkdown = exports.Schema = exports.MarkdownParser = exports.JSONTransformer = exports.enforceFlexMd = exports.repairToMarkdownLevel = exports.detectResponseKind = exports.buildIssuesEnvelopeAuto = exports.buildIssuesEnvelope = exports.parseIssuesEnvelope = exports.processResponseMarkdown = exports.extractFromMarkdown = exports.checkConnection = exports.hasFlexMdContract = exports.checkCompliance = exports.validateMarkdownAgainstOfs = exports.enrichInstructionsWithFlexMd = exports.enrichInstructions = exports.buildMarkdownGuidance = exports.stringifyOutputFormatSpec = exports.recall = exports.remember = exports.transformWithOfs = exports.ofsToSchema = exports.validateFormat = exports.parseOutputFormatSpec = exports.buildOutline = exports.logger = void 0;
17
+ exports.jsonToMarkdown = exports.Schema = exports.MarkdownParser = exports.JSONTransformer = exports.enforceFlexMd = exports.repairToMarkdownLevel = exports.detectResponseKind = exports.buildIssuesEnvelopeAuto = exports.buildIssuesEnvelope = exports.parseIssuesEnvelope = exports.processResponseMarkdown = exports.extractFromMarkdown = exports.checkConnection = exports.hasFlexMdContract = exports.checkCompliance = exports.validateMarkdownAgainstOfs = exports.enrichInstructionsWithFlexMd = exports.enrichInstructions = exports.buildMarkdownGuidance = exports.stringifyOutputFormatSpec = exports.recall = exports.remember = exports.transformWithOfs = exports.ofsToSchema = exports.validateFormat = exports.parseFormatSpecs = exports.parseInputFormatSpec = exports.parseOutputFormatSpec = exports.buildOutline = exports.logger = void 0;
18
18
  // Core SFMD Types
19
19
  __exportStar(require("./types.js"), exports);
20
20
  __exportStar(require("./strictness/types.js"), exports);
@@ -28,6 +28,8 @@ Object.defineProperty(exports, "buildOutline", { enumerable: true, get: function
28
28
  // Output Format Spec (OFS)
29
29
  var parser_js_1 = require("./ofs/parser.js");
30
30
  Object.defineProperty(exports, "parseOutputFormatSpec", { enumerable: true, get: function () { return parser_js_1.parseOutputFormatSpec; } });
31
+ Object.defineProperty(exports, "parseInputFormatSpec", { enumerable: true, get: function () { return parser_js_1.parseInputFormatSpec; } });
32
+ Object.defineProperty(exports, "parseFormatSpecs", { enumerable: true, get: function () { return parser_js_1.parseFormatSpecs; } });
31
33
  Object.defineProperty(exports, "validateFormat", { enumerable: true, get: function () { return parser_js_1.validateFormat; } });
32
34
  var adapter_js_1 = require("./ofs/adapter.js");
33
35
  Object.defineProperty(exports, "ofsToSchema", { enumerable: true, get: function () { return adapter_js_1.ofsToSchema; } });
@@ -1,4 +1,4 @@
1
- import type { OutputFormatSpec, FormatValidationResult } from "../types.js";
1
+ import type { OutputFormatSpec, InputFormatSpec, FormatSpecs, FormatValidationResult } from "../types.js";
2
2
  /**
3
3
  * Validate a format specification.
4
4
  * Returns detailed validation results.
@@ -12,3 +12,13 @@ export interface ParseOfsOptions {
12
12
  allowDelimiterFallbacks?: boolean;
13
13
  }
14
14
  export declare function parseOutputFormatSpec(md: string, opts?: ParseOfsOptions): OutputFormatSpec | null;
15
+ /**
16
+ * Parse an Input Format Spec block from Markdown.
17
+ * Uses the same parsing logic as Output Format Spec.
18
+ */
19
+ export declare function parseInputFormatSpec(md: string, opts?: ParseOfsOptions): InputFormatSpec | null;
20
+ /**
21
+ * Extract both Input and Output Format Specs from instructions text.
22
+ * This function searches for both "## Input format" and "## Output format" sections.
23
+ */
24
+ export declare function parseFormatSpecs(instructions: string, opts?: ParseOfsOptions): FormatSpecs;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateFormat = validateFormat;
4
4
  exports.parseOutputFormatSpec = parseOutputFormatSpec;
5
+ exports.parseInputFormatSpec = parseInputFormatSpec;
6
+ exports.parseFormatSpecs = parseFormatSpecs;
5
7
  const parse_js_1 = require("../md/parse.js");
6
8
  /**
7
9
  * Validate a format specification.
@@ -246,3 +248,126 @@ function parseRequiredOptional(s) {
246
248
  return false;
247
249
  return undefined;
248
250
  }
251
+ /**
252
+ * Parse an Input Format Spec block from Markdown.
253
+ * Uses the same parsing logic as Output Format Spec.
254
+ */
255
+ function parseInputFormatSpec(md, opts = {}) {
256
+ const headingRx = opts.headingRegex ?? /^##\s*Input format\b/i;
257
+ const lines = md.split("\n");
258
+ // find Input Format Spec start
259
+ let start = -1;
260
+ for (let i = 0; i < lines.length; i++) {
261
+ const line = lines[i] ?? "";
262
+ if (headingRx.test(line)) {
263
+ start = i;
264
+ break;
265
+ }
266
+ }
267
+ if (start === -1)
268
+ return null;
269
+ // capture block until next H2 (##) or end
270
+ let end = lines.length;
271
+ for (let i = start + 1; i < lines.length; i++) {
272
+ const line = lines[i] ?? "";
273
+ if (/^##\s+/.test(line)) {
274
+ end = i;
275
+ break;
276
+ }
277
+ }
278
+ const block = lines.slice(start, end).join("\n");
279
+ const sections = [];
280
+ const tables = [];
281
+ let emptySectionValue;
282
+ let inTables = false;
283
+ let currentSection = null;
284
+ for (const rawLine of block.split("\n")) {
285
+ const line = rawLine.trim();
286
+ if (/^tables\b/i.test(line)) {
287
+ inTables = true;
288
+ currentSection = null;
289
+ continue;
290
+ }
291
+ if (/^empty sections\b/i.test(line)) {
292
+ inTables = false;
293
+ currentSection = null;
294
+ continue;
295
+ }
296
+ // Empty section rule
297
+ const mNone = line.match(/write\s+`([^`]+)`/i);
298
+ if (/empty/i.test(line) && mNone) {
299
+ emptySectionValue = mNone[1];
300
+ currentSection = null;
301
+ continue;
302
+ }
303
+ // bullet items
304
+ const bullet = line.match(/^- (.+)$/);
305
+ if (bullet) {
306
+ const item = bullet[1];
307
+ if (inTables) {
308
+ const t = parseTableDecl(item);
309
+ if (t)
310
+ tables.push(t);
311
+ currentSection = null;
312
+ }
313
+ else {
314
+ const s = parseSectionDecl(item, !!opts.allowDelimiterFallbacks);
315
+ if (s) {
316
+ sections.push(s);
317
+ currentSection = s;
318
+ }
319
+ else {
320
+ currentSection = null;
321
+ }
322
+ }
323
+ continue;
324
+ }
325
+ // heading items (e.g. ### Short Answer)
326
+ const headingMatch = line.match(/^#{1,6}\s+(.+)$/);
327
+ if (headingMatch) {
328
+ const name = headingMatch[1].trim();
329
+ // Don't re-parse "Input format" itself if it somehow gets in here
330
+ if ((0, parse_js_1.normalizeName)(name) !== "input format") {
331
+ const s = { name, kind: "text" };
332
+ sections.push(s);
333
+ currentSection = s;
334
+ }
335
+ continue;
336
+ }
337
+ // If not a bullet and we have a current section, it's an instruction
338
+ if (currentSection && line.length > 0) {
339
+ // Support "Columns: A, B, C" in instructions for tables
340
+ const colMatch = line.match(/^Columns:\s*(.+)$/i);
341
+ if (colMatch && (currentSection.kind === "table" || currentSection.kind === "ordered_table")) {
342
+ currentSection.columns = colMatch[1].split(",").map(c => c.trim()).filter(Boolean);
343
+ }
344
+ else {
345
+ const existing = currentSection.instruction || "";
346
+ currentSection.instruction = existing ? `${existing} ${line}` : line;
347
+ }
348
+ }
349
+ }
350
+ if (!sections.length)
351
+ return null;
352
+ return {
353
+ descriptorType: "input_format_spec",
354
+ format: "markdown",
355
+ sectionOrderMatters: false,
356
+ sections,
357
+ tablesOptional: true,
358
+ tables,
359
+ emptySectionValue: emptySectionValue ?? "None"
360
+ };
361
+ }
362
+ /**
363
+ * Extract both Input and Output Format Specs from instructions text.
364
+ * This function searches for both "## Input format" and "## Output format" sections.
365
+ */
366
+ function parseFormatSpecs(instructions, opts = {}) {
367
+ const input = parseInputFormatSpec(instructions, opts);
368
+ const output = parseOutputFormatSpec(instructions, opts);
369
+ return {
370
+ input: input || undefined,
371
+ output: output || undefined
372
+ };
373
+ }
@@ -7,6 +7,12 @@ export declare const TOKEN_CONSTANTS: {
7
7
  readonly perCodeLine: 5;
8
8
  readonly headingOverhead: 10;
9
9
  readonly baseOverhead: 50;
10
+ readonly charsPerToken: 4;
10
11
  };
11
12
  export declare function estimateTokens(systemPart: SystemPart): TokenEstimate;
12
13
  export declare function getFallbackEstimate(kind: SectionKind, required: boolean): TokenEstimate;
14
+ /**
15
+ * Estimate tokens from plain text using character-based approximation.
16
+ * Uses a rule of thumb: ~4 characters per token (varies by model, but good approximation).
17
+ */
18
+ export declare function estimateTextTokens(text: string): number;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TOKEN_CONSTANTS = void 0;
4
4
  exports.estimateTokens = estimateTokens;
5
5
  exports.getFallbackEstimate = getFallbackEstimate;
6
+ exports.estimateTextTokens = estimateTextTokens;
6
7
  // Calibrated token constants
7
8
  exports.TOKEN_CONSTANTS = {
8
9
  // Text lengths (enumerated)
@@ -22,7 +23,9 @@ exports.TOKEN_CONSTANTS = {
22
23
  perCodeLine: 5,
23
24
  // Overhead
24
25
  headingOverhead: 10,
25
- baseOverhead: 50
26
+ baseOverhead: 50,
27
+ // Text token estimation (rough approximation: 1 token ≈ 4 characters)
28
+ charsPerToken: 4
26
29
  };
27
30
  function estimateTokens(systemPart) {
28
31
  const { parsed } = systemPart;
@@ -141,3 +144,14 @@ function getFallbackEstimate(kind, required) {
141
144
  confidence: 'low'
142
145
  };
143
146
  }
147
+ /**
148
+ * Estimate tokens from plain text using character-based approximation.
149
+ * Uses a rule of thumb: ~4 characters per token (varies by model, but good approximation).
150
+ */
151
+ function estimateTextTokens(text) {
152
+ if (!text || text.length === 0)
153
+ return 0;
154
+ // Rough approximation: 1 token ≈ 4 characters for English text
155
+ // This is a common rule of thumb and works reasonably well for most cases
156
+ return Math.ceil(text.length / exports.TOKEN_CONSTANTS.charsPerToken);
157
+ }
@@ -3,6 +3,7 @@ export * from './types.js';
3
3
  export * from './parser.js';
4
4
  export * from './estimator.js';
5
5
  export * from './spec-estimator.js';
6
+ export * from './runtime-estimator.js';
6
7
  export * from './validator.js';
7
8
  export * from './compliance.js';
8
9
  export * from './cognitive-cost.js';
@@ -13,7 +14,7 @@ export * from './auto-fix.js';
13
14
  /**
14
15
  * Convenience function: estimate max_tokens from spec
15
16
  *
16
- * @param spec - OutputFormatSpec object or markdown string
17
+ * @param spec - OutputFormatSpec object, markdown string, or full instructions text
17
18
  * @param options - Estimation options
18
19
  * @returns Estimated max_tokens value
19
20
  */
@@ -21,4 +22,23 @@ export declare function getMaxTokens(spec: OutputFormatSpec | string | null | un
21
22
  includeOptional?: boolean;
22
23
  safetyMultiplier?: number;
23
24
  strategy?: 'conservative' | 'average' | 'generous';
25
+ /**
26
+ * If true, treats the input as full instructions and extracts both
27
+ * input and output format specs. If false, only looks for output format.
28
+ * @default false (backwards compatible)
29
+ */
30
+ extractFromInstructions?: boolean;
31
+ }): number;
32
+ /**
33
+ * Estimate max tokens from entire instructions text.
34
+ * Automatically extracts both input and output format specs if present.
35
+ *
36
+ * @param instructions - Full instructions text that may contain format specs
37
+ * @param options - Estimation options
38
+ * @returns Estimated max_tokens value based on both input and output formats
39
+ */
40
+ export declare function getMaxTokensFromInstructions(instructions: string, options?: {
41
+ includeOptional?: boolean;
42
+ safetyMultiplier?: number;
43
+ strategy?: 'conservative' | 'average' | 'generous';
24
44
  }): number;
@@ -15,12 +15,14 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.getMaxTokens = getMaxTokens;
18
+ exports.getMaxTokensFromInstructions = getMaxTokensFromInstructions;
18
19
  const parser_js_1 = require("../ofs/parser.js");
19
20
  const spec_estimator_js_1 = require("./spec-estimator.js");
20
21
  __exportStar(require("./types.js"), exports);
21
22
  __exportStar(require("./parser.js"), exports);
22
23
  __exportStar(require("./estimator.js"), exports);
23
24
  __exportStar(require("./spec-estimator.js"), exports);
25
+ __exportStar(require("./runtime-estimator.js"), exports);
24
26
  __exportStar(require("./validator.js"), exports);
25
27
  __exportStar(require("./compliance.js"), exports);
26
28
  __exportStar(require("./cognitive-cost.js"), exports);
@@ -31,13 +33,24 @@ __exportStar(require("./auto-fix.js"), exports);
31
33
  /**
32
34
  * Convenience function: estimate max_tokens from spec
33
35
  *
34
- * @param spec - OutputFormatSpec object or markdown string
36
+ * @param spec - OutputFormatSpec object, markdown string, or full instructions text
35
37
  * @param options - Estimation options
36
38
  * @returns Estimated max_tokens value
37
39
  */
38
40
  function getMaxTokens(spec, options) {
39
41
  if (!spec)
40
42
  return 0;
43
+ const extractFromInstructions = options?.extractFromInstructions ?? false;
44
+ // If extractFromInstructions is true and spec is a string, try to extract both formats
45
+ if (extractFromInstructions && typeof spec === 'string') {
46
+ const formatSpecs = (0, parser_js_1.parseFormatSpecs)(spec);
47
+ if (formatSpecs.input || formatSpecs.output) {
48
+ const estimate = (0, spec_estimator_js_1.estimateFormatSpecsTokens)(formatSpecs.input, formatSpecs.output, options);
49
+ return estimate.total.estimated;
50
+ }
51
+ // Fall through to output-only parsing if no formats found
52
+ }
53
+ // Original behavior: parse as output format spec
41
54
  const parsedSpec = typeof spec === 'string'
42
55
  ? (0, parser_js_1.parseOutputFormatSpec)(spec)
43
56
  : spec;
@@ -46,3 +59,16 @@ function getMaxTokens(spec, options) {
46
59
  const estimate = (0, spec_estimator_js_1.estimateSpecTokens)(parsedSpec, options);
47
60
  return estimate.total.estimated;
48
61
  }
62
+ /**
63
+ * Estimate max tokens from entire instructions text.
64
+ * Automatically extracts both input and output format specs if present.
65
+ *
66
+ * @param instructions - Full instructions text that may contain format specs
67
+ * @param options - Estimation options
68
+ * @returns Estimated max_tokens value based on both input and output formats
69
+ */
70
+ function getMaxTokensFromInstructions(instructions, options) {
71
+ const formatSpecs = (0, parser_js_1.parseFormatSpecs)(instructions);
72
+ const estimate = (0, spec_estimator_js_1.estimateFormatSpecsTokens)(formatSpecs.input, formatSpecs.output, options);
73
+ return estimate.total.estimated;
74
+ }
@@ -0,0 +1,92 @@
1
+ import type { SpecTokenEstimate } from './types.js';
2
+ export interface RuntimeEstimateOptions {
3
+ /**
4
+ * Whether to include optional sections in output format estimation
5
+ * @default true
6
+ */
7
+ includeOptional?: boolean;
8
+ /**
9
+ * Safety multiplier to add headroom (e.g., 1.2 = 20% extra)
10
+ * @default 1.2
11
+ */
12
+ safetyMultiplier?: number;
13
+ /**
14
+ * Estimation strategy
15
+ * @default 'average'
16
+ */
17
+ strategy?: 'conservative' | 'average' | 'generous';
18
+ /**
19
+ * Whether to estimate tokens for the prompt text
20
+ * @default true
21
+ */
22
+ estimatePrompt?: boolean;
23
+ /**
24
+ * Whether to estimate tokens for the context text
25
+ * @default true
26
+ */
27
+ estimateContext?: boolean;
28
+ /**
29
+ * Whether to estimate tokens for the instructions text
30
+ * @default true
31
+ */
32
+ estimateInstructions?: boolean;
33
+ /**
34
+ * Additional overhead tokens to add (for system messages, formatting, etc.)
35
+ * @default 0
36
+ */
37
+ additionalOverhead?: number;
38
+ }
39
+ export interface RuntimeTokenEstimate {
40
+ /**
41
+ * Estimated max_tokens needed for the LLM response
42
+ */
43
+ maxTokens: number;
44
+ /**
45
+ * Breakdown of token estimates
46
+ */
47
+ breakdown: {
48
+ prompt: number;
49
+ context: number;
50
+ instructions: number;
51
+ output: SpecTokenEstimate;
52
+ additionalOverhead: number;
53
+ total: number;
54
+ };
55
+ /**
56
+ * Confidence level of the estimate
57
+ */
58
+ confidence: 'high' | 'medium' | 'low';
59
+ }
60
+ /**
61
+ * Estimate max_tokens for an LLM API call at runtime.
62
+ *
63
+ * This function estimates the total tokens needed by considering:
64
+ * - The prompt text (user input)
65
+ * - Context (previous messages, system messages, etc.)
66
+ * - Instructions (which may contain output format specs)
67
+ * - Expected output format (extracted from instructions)
68
+ *
69
+ * Note: Input format specs in instructions are ignored (they're for planning/design only).
70
+ * Only output format specs are used for runtime token estimation.
71
+ *
72
+ * @param params - Runtime estimation parameters
73
+ * @returns Token estimate with breakdown
74
+ */
75
+ export declare function runtimeEstimateTokens(params: {
76
+ /**
77
+ * The user prompt/message text
78
+ */
79
+ prompt?: string;
80
+ /**
81
+ * Context text (system messages, previous conversation, etc.)
82
+ */
83
+ context?: string;
84
+ /**
85
+ * Instructions text (may contain format specs)
86
+ */
87
+ instructions?: string;
88
+ /**
89
+ * Estimation options
90
+ */
91
+ options?: RuntimeEstimateOptions;
92
+ }): RuntimeTokenEstimate;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runtimeEstimateTokens = runtimeEstimateTokens;
4
+ const parser_js_1 = require("../ofs/parser.js");
5
+ const spec_estimator_js_1 = require("./spec-estimator.js");
6
+ const estimator_js_1 = require("./estimator.js");
7
+ /**
8
+ * Estimate max_tokens for an LLM API call at runtime.
9
+ *
10
+ * This function estimates the total tokens needed by considering:
11
+ * - The prompt text (user input)
12
+ * - Context (previous messages, system messages, etc.)
13
+ * - Instructions (which may contain output format specs)
14
+ * - Expected output format (extracted from instructions)
15
+ *
16
+ * Note: Input format specs in instructions are ignored (they're for planning/design only).
17
+ * Only output format specs are used for runtime token estimation.
18
+ *
19
+ * @param params - Runtime estimation parameters
20
+ * @returns Token estimate with breakdown
21
+ */
22
+ function runtimeEstimateTokens(params) {
23
+ const { prompt = '', context = '', instructions = '' } = params;
24
+ const opts = {
25
+ includeOptional: params.options?.includeOptional ?? true,
26
+ safetyMultiplier: params.options?.safetyMultiplier ?? 1.2,
27
+ strategy: params.options?.strategy ?? 'average',
28
+ estimatePrompt: params.options?.estimatePrompt ?? true,
29
+ estimateContext: params.options?.estimateContext ?? true,
30
+ estimateInstructions: params.options?.estimateInstructions ?? true,
31
+ additionalOverhead: params.options?.additionalOverhead ?? 0
32
+ };
33
+ // Extract format specs from instructions (only output format is used at runtime)
34
+ const formatSpecs = (0, parser_js_1.parseFormatSpecs)(instructions);
35
+ // Estimate tokens for each component
36
+ const promptTokens = opts.estimatePrompt ? (0, estimator_js_1.estimateTextTokens)(prompt) : 0;
37
+ const contextTokens = opts.estimateContext ? (0, estimator_js_1.estimateTextTokens)(context) : 0;
38
+ const instructionsTokens = opts.estimateInstructions ? (0, estimator_js_1.estimateTextTokens)(instructions) : 0;
39
+ // Estimate output tokens from output format spec (input format is for planning only)
40
+ const outputEstimate = formatSpecs.output
41
+ ? (0, spec_estimator_js_1.estimateFormatSpecsTokens)(undefined, formatSpecs.output, {
42
+ includeOptional: opts.includeOptional,
43
+ safetyMultiplier: 1.0, // Don't apply safety multiplier here, apply at the end
44
+ strategy: opts.strategy
45
+ })
46
+ : {
47
+ total: { estimated: 0, min: 0, max: 0, confidence: 'low' },
48
+ bySectionName: {},
49
+ overhead: 0
50
+ };
51
+ // Calculate total input tokens (prompt + context + instructions)
52
+ const inputTokens = promptTokens + contextTokens + instructionsTokens;
53
+ // Calculate total output tokens (from format spec)
54
+ // Note: max_tokens parameter in LLM APIs refers to OUTPUT tokens only
55
+ const outputTokens = outputEstimate.total.estimated;
56
+ // Apply safety multiplier to output tokens only (this is what max_tokens represents)
57
+ const maxTokens = Math.ceil(outputTokens * opts.safetyMultiplier) + opts.additionalOverhead;
58
+ // Total tokens for budgeting/planning (input + output)
59
+ const totalTokens = inputTokens + outputTokens + opts.additionalOverhead;
60
+ // Determine confidence based on output spec confidence
61
+ const confidence = outputEstimate.total.confidence;
62
+ return {
63
+ maxTokens, // This is the value to use for max_tokens API parameter (output only)
64
+ breakdown: {
65
+ prompt: promptTokens,
66
+ context: contextTokens,
67
+ instructions: instructionsTokens,
68
+ output: outputEstimate,
69
+ additionalOverhead: opts.additionalOverhead,
70
+ total: totalTokens // Total for budgeting (input + output)
71
+ },
72
+ confidence
73
+ };
74
+ }
@@ -1,7 +1,25 @@
1
1
  import type { SpecTokenEstimate } from './types.js';
2
- import type { OutputFormatSpec } from '../types.js';
2
+ import type { OutputFormatSpec, InputFormatSpec } from '../types.js';
3
3
  export declare function estimateSpecTokens(spec: OutputFormatSpec, options?: {
4
4
  includeOptional?: boolean;
5
5
  safetyMultiplier?: number;
6
6
  strategy?: 'conservative' | 'average' | 'generous';
7
7
  }): SpecTokenEstimate;
8
+ /**
9
+ * Estimate tokens for an Input Format Spec.
10
+ * Uses the same logic as output format estimation.
11
+ */
12
+ export declare function estimateInputSpecTokens(spec: InputFormatSpec, options?: {
13
+ includeOptional?: boolean;
14
+ safetyMultiplier?: number;
15
+ strategy?: 'conservative' | 'average' | 'generous';
16
+ }): SpecTokenEstimate;
17
+ /**
18
+ * Estimate tokens for both input and output format specs.
19
+ * Combines estimates from both if present.
20
+ */
21
+ export declare function estimateFormatSpecsTokens(inputSpec: InputFormatSpec | undefined, outputSpec: OutputFormatSpec | undefined, options?: {
22
+ includeOptional?: boolean;
23
+ safetyMultiplier?: number;
24
+ strategy?: 'conservative' | 'average' | 'generous';
25
+ }): SpecTokenEstimate;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.estimateSpecTokens = estimateSpecTokens;
4
+ exports.estimateInputSpecTokens = estimateInputSpecTokens;
5
+ exports.estimateFormatSpecsTokens = estimateFormatSpecsTokens;
4
6
  const parser_js_1 = require("./parser.js");
5
7
  const estimator_js_1 = require("./estimator.js");
6
8
  function estimateSpecTokens(spec, options = {}) {
@@ -69,3 +71,55 @@ function estimateSpecTokens(spec, options = {}) {
69
71
  overhead
70
72
  };
71
73
  }
74
+ /**
75
+ * Estimate tokens for an Input Format Spec.
76
+ * Uses the same logic as output format estimation.
77
+ */
78
+ function estimateInputSpecTokens(spec, options = {}) {
79
+ // Input format uses the same structure as output format, so reuse the same logic
80
+ const outputSpec = {
81
+ ...spec,
82
+ descriptorType: "output_format_spec"
83
+ };
84
+ return estimateSpecTokens(outputSpec, options);
85
+ }
86
+ /**
87
+ * Estimate tokens for both input and output format specs.
88
+ * Combines estimates from both if present.
89
+ */
90
+ function estimateFormatSpecsTokens(inputSpec, outputSpec, options = {}) {
91
+ const inputEstimate = inputSpec ? estimateInputSpecTokens(inputSpec, options) : null;
92
+ const outputEstimate = outputSpec ? estimateSpecTokens(outputSpec, options) : null;
93
+ // If only one is present, return it
94
+ if (!inputEstimate && outputEstimate)
95
+ return outputEstimate;
96
+ if (inputEstimate && !outputEstimate)
97
+ return inputEstimate;
98
+ if (!inputEstimate && !outputEstimate) {
99
+ // Return empty estimate
100
+ return {
101
+ total: { estimated: 0, min: 0, max: 0, confidence: 'low' },
102
+ bySectionName: {},
103
+ overhead: 0
104
+ };
105
+ }
106
+ // Combine both estimates
107
+ const combined = {
108
+ total: {
109
+ estimated: inputEstimate.total.estimated + outputEstimate.total.estimated,
110
+ min: inputEstimate.total.min + outputEstimate.total.min,
111
+ max: inputEstimate.total.max + outputEstimate.total.max,
112
+ confidence: inputEstimate.total.confidence === 'high' && outputEstimate.total.confidence === 'high'
113
+ ? 'high'
114
+ : inputEstimate.total.confidence === 'low' || outputEstimate.total.confidence === 'low'
115
+ ? 'low'
116
+ : 'medium'
117
+ },
118
+ bySectionName: {
119
+ ...Object.fromEntries(Object.entries(inputEstimate.bySectionName).map(([k, v]) => [`input:${k}`, v])),
120
+ ...Object.fromEntries(Object.entries(outputEstimate.bySectionName).map(([k, v]) => [`output:${k}`, v]))
121
+ },
122
+ overhead: inputEstimate.overhead + outputEstimate.overhead
123
+ };
124
+ return combined;
125
+ }
package/dist/types.d.ts CHANGED
@@ -112,6 +112,20 @@ export interface OutputFormatSpec {
112
112
  tablesOptional?: boolean;
113
113
  tables?: OfsTable[];
114
114
  }
115
+ export interface InputFormatSpec {
116
+ description?: string;
117
+ sections: OutputSectionSpec[];
118
+ emptySectionValue?: string;
119
+ descriptorType?: "input_format_spec";
120
+ format?: "markdown";
121
+ sectionOrderMatters?: boolean;
122
+ tablesOptional?: boolean;
123
+ tables?: OfsTable[];
124
+ }
125
+ export interface FormatSpecs {
126
+ input?: InputFormatSpec;
127
+ output?: OutputFormatSpec;
128
+ }
115
129
  export interface MdNode {
116
130
  title: string;
117
131
  level: number;
@@ -199,6 +213,8 @@ export interface ComplianceCheckResult {
199
213
  hasRequiredSections?: boolean;
200
214
  sectionCount?: number;
201
215
  containerType?: "fenced-block" | "none";
216
+ hasInputFormat?: boolean;
217
+ hasOutputFormat?: boolean;
202
218
  };
203
219
  }
204
220
  export interface EnhancementChange {
@@ -2,6 +2,7 @@ import { ComplianceCheckResult } from "../types.js";
2
2
  /**
3
3
  * Check if instructions meet the required flex-md compliance level.
4
4
  * Returns detailed results including what's missing or wrong.
5
+ * Now also checks for Input Format Spec if present.
5
6
  */
6
7
  export declare function checkCompliance(instructions: string, complianceLevel: "L0" | "L1" | "L2" | "L3"): Promise<ComplianceCheckResult>;
7
8
  /**
@@ -2,14 +2,20 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkCompliance = checkCompliance;
4
4
  exports.hasFlexMdContract = hasFlexMdContract;
5
+ const parser_js_1 = require("../ofs/parser.js");
5
6
  /**
6
7
  * Check if instructions meet the required flex-md compliance level.
7
8
  * Returns detailed results including what's missing or wrong.
9
+ * Now also checks for Input Format Spec if present.
8
10
  */
9
11
  async function checkCompliance(instructions, complianceLevel) {
10
12
  const issues = [];
11
13
  const suggestions = [];
12
14
  const lower = instructions.toLowerCase();
15
+ // Extract both input and output format specs
16
+ const formatSpecs = (0, parser_js_1.parseFormatSpecs)(instructions);
17
+ const hasInputFormat = !!formatSpecs.input;
18
+ const hasOutputFormat = !!formatSpecs.output;
13
19
  const hasMarkdownMention = lower.includes("markdown");
14
20
  const hasSectionMention = lower.includes("section") || lower.includes("heading");
15
21
  const hasContainerMention = (lower.includes("fenced block") || lower.includes("```")) && (lower.includes("inside") || lower.includes("wrapped"));
@@ -81,7 +87,9 @@ async function checkCompliance(instructions, complianceLevel) {
81
87
  metadata: {
82
88
  hasContainer: hasContainerMention,
83
89
  hasRequiredSections: hasSectionMention,
84
- containerType: hasContainerMention ? "fenced-block" : "none"
90
+ containerType: hasContainerMention ? "fenced-block" : "none",
91
+ hasInputFormat,
92
+ hasOutputFormat
85
93
  }
86
94
  };
87
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flex-md",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "description": "Parse and stringify FlexMD: semi-structured Markdown with three powerful layers - Frames, Output Format Spec (OFS), and Detection/Extraction.",
5
5
  "license": "MIT",
6
6
  "author": "",