flex-md 4.2.8 → 4.4.2
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 +23 -32
- package/dist/md/normalize.d.ts +5 -0
- package/dist/md/normalize.js +10 -0
- package/dist/md/parse.js +31 -5
- package/dist/ofs/adapter.d.ts +13 -2
- package/dist/ofs/adapter.js +41 -9
- package/dist/ofs/infer.d.ts +5 -0
- package/dist/ofs/infer.js +60 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -523,46 +523,37 @@ if (status === 'validated' || status === 'fixed') {
|
|
|
523
523
|
}
|
|
524
524
|
```
|
|
525
525
|
|
|
526
|
-
|
|
526
|
+
Flex-MD allows you to use its native **Output Format Spec (OFS)** as the source of truth for **NX-MD-Parser's** structured extraction.
|
|
527
527
|
|
|
528
|
-
|
|
528
|
+
### Modern "Smart" Transformation
|
|
529
529
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
import { parseOutputFormatSpec, transformWithOfs } from 'flex-md';
|
|
530
|
+
The `transformWithOfs` function is now smarter. It performs **dual parsing**:
|
|
531
|
+
1. **Automatic Parsing**: Even if you don't have a spec, it extracts all sections and camel-cases keys.
|
|
532
|
+
2. **Contract Enforcement**: If you provide a spec, it uses `nx-md-parser` to validate, type-cast (lists/tables), and repair the output.
|
|
534
533
|
|
|
535
|
-
|
|
536
|
-
const spec = parseOutputFormatSpec(`
|
|
537
|
-
## Output format
|
|
538
|
-
- Executive Summary — text (required)
|
|
539
|
-
- Key Findings — ordered list (required)
|
|
540
|
-
- Technical Specs — table (optional)
|
|
541
|
-
Columns: Component, Version, Notes
|
|
542
|
-
`);
|
|
534
|
+
#### Usage with LLM Outputs:
|
|
543
535
|
|
|
544
|
-
|
|
545
|
-
const md = `
|
|
546
|
-
### Summary
|
|
547
|
-
The system is fully operational with 99.9% uptime.
|
|
536
|
+
When working with LLMs, pass the **entire response text** directly. Flex-MD handles internal normalization (like escaped `\n` characters) automatically.
|
|
548
537
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
2. Memory leak in cache module fixed
|
|
552
|
-
`;
|
|
538
|
+
```typescript
|
|
539
|
+
import { transformWithOfs } from 'flex-md';
|
|
553
540
|
|
|
554
|
-
|
|
541
|
+
// Pass the RAW content string from your LLM provider
|
|
542
|
+
const {
|
|
543
|
+
parsedOutput, // Always populated (auto-extraction)
|
|
544
|
+
contractOutput, // Populated if spec was provided
|
|
545
|
+
contractStatus, // "ok" | "different" | "skipped"
|
|
546
|
+
status // "validated" | "fixed" | "failed"
|
|
547
|
+
} = transformWithOfs(llmResponseText, spec);
|
|
555
548
|
|
|
556
|
-
|
|
557
|
-
console.log(result['Executive Summary']); // "The system is fully operational..."
|
|
558
|
-
console.log(result['Key Findings']); // ["Scalability improved...", "Memory leak..."]
|
|
559
|
-
}
|
|
549
|
+
console.log(parsedOutput.shortAnswer);
|
|
560
550
|
```
|
|
561
551
|
|
|
562
552
|
### Why use this?
|
|
563
|
-
1. **
|
|
564
|
-
2. **
|
|
565
|
-
3. **
|
|
553
|
+
1. **Zero-Config Extraction**: Get structured data without writing a schema first.
|
|
554
|
+
2. **Dual-Safe**: Compare what the LLM *sent* (`parsedOutput`) with what the contract *required* (`contractOutput`).
|
|
555
|
+
3. **Internal Normalization**: Handles messy data (escaped newlines, merged code blocks) so you don't have to.
|
|
556
|
+
4. **Fuzzy Matching**: Even if the LLM slightly changes the heading (e.g., "Summary" vs "Executive Summary"), the contract will correctly map it.
|
|
566
557
|
|
|
567
558
|
## Advanced AI Features (via NX-MD-Parser 1.4.0)
|
|
568
559
|
|
|
@@ -622,8 +613,8 @@ const md = `
|
|
|
622
613
|
Everything looks correctly formatted based on initial evidence.
|
|
623
614
|
`;
|
|
624
615
|
|
|
625
|
-
const {
|
|
626
|
-
console.log(
|
|
616
|
+
const { contractOutput } = transformWithOfs(md, recallId);
|
|
617
|
+
console.log(contractOutput.Confidence); // 0.95
|
|
627
618
|
```
|
|
628
619
|
|
|
629
620
|
### Why use this?
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized normalization for Markdown input.
|
|
3
|
+
* Handles common LLM output artifacts like literal \n.
|
|
4
|
+
*/
|
|
5
|
+
export function normalizeMarkdownInput(md) {
|
|
6
|
+
if (!md)
|
|
7
|
+
return "";
|
|
8
|
+
// Handle literal \n common in LLM outputs delivered via JSON
|
|
9
|
+
return md.replace(/\\n/g, "\n");
|
|
10
|
+
}
|
package/dist/md/parse.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toCamelCase } from "nx-helpers";
|
|
2
|
+
import { normalizeMarkdownInput } from "./normalize.js";
|
|
2
3
|
export function normalizeName(s) {
|
|
3
4
|
return s.trim().replace(/\s+/g, " ").toLowerCase();
|
|
4
5
|
}
|
|
@@ -157,8 +158,33 @@ export function isIssuesEnvelopeCheck(md) {
|
|
|
157
158
|
}
|
|
158
159
|
export function markdownToJson(md) {
|
|
159
160
|
// Robustly handle both actual newlines and literal \n (common in LLM JSON outputs)
|
|
160
|
-
const normalizedMd = (md
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
const normalizedMd = normalizeMarkdownInput(md);
|
|
162
|
+
// Collect all bullet names that look like headers ("- Name")
|
|
163
|
+
// We look for patterns like "- Name\n" at the start of lines, ensuring it's not a sub-bullet.
|
|
164
|
+
const bulletNames = [];
|
|
165
|
+
const bulletLinesRx = /^[-*+]\s+([^—:\n\r]{2,50})$/gm;
|
|
166
|
+
let m;
|
|
167
|
+
while ((m = bulletLinesRx.exec(normalizedMd)) !== null) {
|
|
168
|
+
bulletNames.push(m[1].trim());
|
|
169
|
+
}
|
|
170
|
+
// Use Flex-MD's native parser (supports === headings and avoids colon-as-object bug)
|
|
171
|
+
const sections = parseHeadingsAndSections(normalizedMd, { bulletNames });
|
|
172
|
+
const result = {};
|
|
173
|
+
for (const sec of sections) {
|
|
174
|
+
const key = toCamelCase(sec.heading.name);
|
|
175
|
+
const body = sec.body.trim();
|
|
176
|
+
// 1. Try to detect list
|
|
177
|
+
const bullets = extractBullets(body);
|
|
178
|
+
if (bullets.length > 0) {
|
|
179
|
+
result[key] = bullets;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// 2. Try to detect table (basic check)
|
|
183
|
+
const lines = body.split("\n").map(l => l.trim()).filter(l => l);
|
|
184
|
+
if (lines.length >= 2 && lines[0].startsWith("|") && /^[|\s-:]+$/.test(lines[1])) {
|
|
185
|
+
// It looks like a table - we could use nx-md-parser's table logic here safely
|
|
186
|
+
}
|
|
187
|
+
result[key] = body;
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
164
190
|
}
|
package/dist/ofs/adapter.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { type SchemaType
|
|
1
|
+
import { type SchemaType } from "nx-md-parser";
|
|
2
2
|
import { type OutputFormatSpec } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Result of a Flex-MD transformation.
|
|
5
|
+
*/
|
|
6
|
+
export interface FlexTransformResult<T = any> {
|
|
7
|
+
parsedOutput: Record<string, any>;
|
|
8
|
+
contractOutput: T | null;
|
|
9
|
+
contractStatus: "ok" | "different" | "skipped";
|
|
10
|
+
status: "validated" | "fixed" | "failed";
|
|
11
|
+
errors: string[];
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Converts a Flex-MD OutputFormatSpec to an nx-md-parser Schema.
|
|
5
15
|
*/
|
|
6
16
|
export declare function ofsToSchema(spec: OutputFormatSpec): SchemaType;
|
|
7
17
|
/**
|
|
8
18
|
* Transforms markdown text using a Flex-MD OutputFormatSpec or a recallId.
|
|
19
|
+
* If no spec is provided, it attempts to infer it from the markdown (autospecs).
|
|
9
20
|
*/
|
|
10
|
-
export declare function transformWithOfs<T = any>(md: string, specOrRecallId
|
|
21
|
+
export declare function transformWithOfs<T = any>(md: string, specOrRecallId?: OutputFormatSpec | string): FlexTransformResult<T>;
|
package/dist/ofs/adapter.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { JSONTransformer, Schema } from "nx-md-parser";
|
|
2
2
|
import { recall } from "./memory.js";
|
|
3
3
|
import { parseHeadingsAndSections, extractBullets, parseMarkdownTable, normalizeName } from "../md/parse.js";
|
|
4
|
+
import { normalizeMarkdownInput } from "../md/normalize.js";
|
|
5
|
+
import { toCamelCase } from "nx-helpers";
|
|
6
|
+
import { markdownToJson as autoMarkdownToJson } from "nx-json-parser";
|
|
4
7
|
/**
|
|
5
8
|
* Converts a Flex-MD OutputFormatSpec to an nx-md-parser Schema.
|
|
6
9
|
*/
|
|
@@ -47,30 +50,46 @@ export function ofsToSchema(spec) {
|
|
|
47
50
|
}
|
|
48
51
|
/**
|
|
49
52
|
* Transforms markdown text using a Flex-MD OutputFormatSpec or a recallId.
|
|
53
|
+
* If no spec is provided, it attempts to infer it from the markdown (autospecs).
|
|
50
54
|
*/
|
|
51
55
|
export function transformWithOfs(md, specOrRecallId) {
|
|
56
|
+
// 0. Normalize input (handle literal \n common in LLM outputs)
|
|
57
|
+
const normalizedMd = normalizeMarkdownInput(md);
|
|
58
|
+
// 1. Automatic parsing (Dual-Response) using nx-json-parser
|
|
59
|
+
const parsedOutput = autoMarkdownToJson(normalizedMd);
|
|
60
|
+
if (!specOrRecallId) {
|
|
61
|
+
return {
|
|
62
|
+
parsedOutput,
|
|
63
|
+
contractOutput: null,
|
|
64
|
+
contractStatus: "skipped",
|
|
65
|
+
status: "validated",
|
|
66
|
+
errors: []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
52
69
|
let spec;
|
|
53
70
|
if (typeof specOrRecallId === "string") {
|
|
54
|
-
|
|
55
|
-
if (!
|
|
71
|
+
const recalled = recall(specOrRecallId);
|
|
72
|
+
if (!recalled) {
|
|
56
73
|
return {
|
|
74
|
+
parsedOutput,
|
|
75
|
+
contractOutput: null,
|
|
76
|
+
contractStatus: "skipped",
|
|
57
77
|
status: "failed",
|
|
58
|
-
result: null,
|
|
59
78
|
errors: [`Recall ID "${specOrRecallId}" not found in memory.`]
|
|
60
79
|
};
|
|
61
80
|
}
|
|
81
|
+
spec = recalled;
|
|
62
82
|
}
|
|
63
83
|
else {
|
|
64
84
|
spec = specOrRecallId;
|
|
65
85
|
}
|
|
66
|
-
//
|
|
86
|
+
// 2. Parse sections using Flex-MD parser for the contract mapping
|
|
67
87
|
const bulletNames = spec.sections.map(s => s.name);
|
|
68
|
-
|
|
88
|
+
// Note: We use the local headings parser to find the specific sections defined in the spec
|
|
89
|
+
const parsedSections = parseHeadingsAndSections(normalizedMd, { bulletNames });
|
|
69
90
|
const parsedObj = {};
|
|
70
|
-
// 2. Map sections to OFS and apply complex parsing (tables/lists)
|
|
71
91
|
for (const sectionSpec of spec.sections) {
|
|
72
92
|
const normName = normalizeName(sectionSpec.name);
|
|
73
|
-
// Find section with similar name
|
|
74
93
|
const found = parsedSections.find(s => normalizeName(s.heading.name) === normName);
|
|
75
94
|
if (found) {
|
|
76
95
|
let value;
|
|
@@ -95,8 +114,21 @@ export function transformWithOfs(md, specOrRecallId) {
|
|
|
95
114
|
parsedObj[sectionSpec.name] = value;
|
|
96
115
|
}
|
|
97
116
|
}
|
|
98
|
-
// 3. Transform using nx-md-parser for schema validation and
|
|
117
|
+
// 3. Transform using nx-md-parser (latest v2.2.0) for schema validation and fixing
|
|
99
118
|
const schema = ofsToSchema(spec);
|
|
100
119
|
const transformer = new JSONTransformer(schema);
|
|
101
|
-
|
|
120
|
+
const transformResult = transformer.transform(parsedObj);
|
|
121
|
+
// 4. Compare parsed results with contract results
|
|
122
|
+
const autoKeys = Object.keys(parsedOutput).sort();
|
|
123
|
+
const contractKeys = transformResult.result ? Object.keys(transformResult.result).map(k => toCamelCase(k)).sort() : [];
|
|
124
|
+
const isSame = autoKeys.length > 0 &&
|
|
125
|
+
autoKeys.every(k => contractKeys.includes(k)) &&
|
|
126
|
+
contractKeys.length === autoKeys.length;
|
|
127
|
+
return {
|
|
128
|
+
parsedOutput,
|
|
129
|
+
contractOutput: transformResult.result,
|
|
130
|
+
contractStatus: isSame ? "ok" : "different",
|
|
131
|
+
status: transformResult.status,
|
|
132
|
+
errors: transformResult.errors || []
|
|
133
|
+
};
|
|
102
134
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { parseHeadingsAndSections, extractBullets } from "../md/parse.js";
|
|
2
|
+
/**
|
|
3
|
+
* Infers an OutputFormatSpec from a Markdown string.
|
|
4
|
+
*/
|
|
5
|
+
export function inferOfsFromMarkdown(md) {
|
|
6
|
+
// Collect all bullet names that look like headers ("- Name")
|
|
7
|
+
const lines = md.split("\n");
|
|
8
|
+
const bulletNames = [];
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
// Match "- Name" or "- Name\n" or "- Name " but NOT "- Name: more text"
|
|
11
|
+
const m = line.match(/^[-*+]\s+([^—:\n]+)$/);
|
|
12
|
+
if (m) {
|
|
13
|
+
bulletNames.push(m[1].trim());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const sections = parseHeadingsAndSections(md, { bulletNames });
|
|
17
|
+
const specSections = [];
|
|
18
|
+
for (const sec of sections) {
|
|
19
|
+
const name = sec.heading.name;
|
|
20
|
+
const body = sec.body.trim();
|
|
21
|
+
// 1. Detect list
|
|
22
|
+
const bullets = extractBullets(body);
|
|
23
|
+
if (bullets.length > 0) {
|
|
24
|
+
specSections.push({
|
|
25
|
+
name,
|
|
26
|
+
kind: "list",
|
|
27
|
+
required: true
|
|
28
|
+
});
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// 2. Detect table (basic check)
|
|
32
|
+
const lines = body.split("\n").map(l => l.trim()).filter(Boolean);
|
|
33
|
+
if (lines.length >= 2 && lines[0].startsWith("|") && /^[|\s-:]+$/.test(lines[1])) {
|
|
34
|
+
// Extract columns
|
|
35
|
+
const cols = lines[0].split("|").map(c => c.trim()).filter(Boolean);
|
|
36
|
+
specSections.push({
|
|
37
|
+
name,
|
|
38
|
+
kind: "table",
|
|
39
|
+
columns: cols,
|
|
40
|
+
required: true
|
|
41
|
+
});
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Default to text
|
|
45
|
+
specSections.push({
|
|
46
|
+
name,
|
|
47
|
+
kind: "text",
|
|
48
|
+
required: true
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
descriptorType: "output_format_spec",
|
|
53
|
+
format: "markdown",
|
|
54
|
+
sectionOrderMatters: false,
|
|
55
|
+
sections: specSections,
|
|
56
|
+
tablesOptional: true,
|
|
57
|
+
tables: [],
|
|
58
|
+
emptySectionValue: "None"
|
|
59
|
+
};
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flex-md",
|
|
3
|
-
"version": "4.2
|
|
3
|
+
"version": "4.4.2",
|
|
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": "",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"detection",
|
|
17
17
|
"extraction"
|
|
18
18
|
],
|
|
19
|
+
"ts-node": {
|
|
20
|
+
"esm": true
|
|
21
|
+
},
|
|
19
22
|
"type": "module",
|
|
20
23
|
"main": "./dist/index.cjs",
|
|
21
24
|
"module": "./dist/index.js",
|
|
@@ -44,12 +47,14 @@
|
|
|
44
47
|
},
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"@types/node": "^25.0.3",
|
|
50
|
+
"tsx": "^4.21.0",
|
|
47
51
|
"typescript": "^5.6.3",
|
|
48
52
|
"vitest": "^4.0.16"
|
|
49
53
|
},
|
|
50
54
|
"dependencies": {
|
|
51
55
|
"nd": "^1.2.0",
|
|
52
56
|
"nx-helpers": "^1.5.0",
|
|
53
|
-
"nx-
|
|
57
|
+
"nx-json-parser": "^1.1.0",
|
|
58
|
+
"nx-md-parser": "^2.2.0"
|
|
54
59
|
}
|
|
55
60
|
}
|