flex-md 4.4.8 → 4.5.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/detect/json/detectMarkdown.d.ts +77 -0
- package/dist/detect/json/detectMarkdown.js +298 -0
- package/dist/detect/json/index.d.ts +1 -0
- package/dist/detect/json/index.js +1 -0
- package/dist/index.cjs +4 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.js +19 -0
- package/dist/ofs/adapter.js +57 -0
- package/package.json +2 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for "framed vs frameless" Markdown handling before Flex-MD.
|
|
3
|
+
*
|
|
4
|
+
* Goals:
|
|
5
|
+
* 1) Detect framed markdown (single fenced block) and extract its inner content.
|
|
6
|
+
* 2) If NOT markdown-ish, force-wrap into a minimal markdown shape so Flex-MD can still parse.
|
|
7
|
+
*
|
|
8
|
+
* Notes:
|
|
9
|
+
* - This is heuristic by design; tune thresholds as you learn your data.
|
|
10
|
+
*/
|
|
11
|
+
export type MarkdownDetection = {
|
|
12
|
+
isMarkdownLikely: boolean;
|
|
13
|
+
isFramed: boolean;
|
|
14
|
+
frameLanguage?: string | null;
|
|
15
|
+
reasons: string[];
|
|
16
|
+
stats: {
|
|
17
|
+
headings: number;
|
|
18
|
+
atxHeadings: number;
|
|
19
|
+
setextHeadings: number;
|
|
20
|
+
unorderedListLines: number;
|
|
21
|
+
orderedListLines: number;
|
|
22
|
+
tableRows: number;
|
|
23
|
+
codeFences: number;
|
|
24
|
+
inlineCodeSpans: number;
|
|
25
|
+
mdLinks: number;
|
|
26
|
+
emphasisTokens: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export declare function detectMarkdown(text: unknown): MarkdownDetection;
|
|
30
|
+
/**
|
|
31
|
+
* If the entire payload is a single fenced block, return its inner content.
|
|
32
|
+
* Otherwise return the original text.
|
|
33
|
+
*
|
|
34
|
+
* - Also handles ```md / ```markdown / ```json etc.
|
|
35
|
+
*/
|
|
36
|
+
export declare function stripSingleFence(input: string): {
|
|
37
|
+
stripped: string;
|
|
38
|
+
wasFramed: boolean;
|
|
39
|
+
language: string | null;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Force-wrap non-markdown text into a minimal heading-based markdown document.
|
|
43
|
+
* This helps Flex-MD / nx-md-parser operate even when the model returns plain text.
|
|
44
|
+
*
|
|
45
|
+
* Choose a heading that exists in your OFS to maximize alignment.
|
|
46
|
+
* Default: "Full Answer" (common sink section).
|
|
47
|
+
*/
|
|
48
|
+
export declare function forceWrapAsMarkdown(plainText: string, opts?: {
|
|
49
|
+
heading?: string;
|
|
50
|
+
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
51
|
+
preserveLeadingWhitespace?: boolean;
|
|
52
|
+
}): string;
|
|
53
|
+
/**
|
|
54
|
+
* End-to-end "normalize input for Flex-MD" helper:
|
|
55
|
+
* - If framed: strip the fence (so Flex-MD sees pure markdown)
|
|
56
|
+
* - Else: if markdown-likely: keep as-is
|
|
57
|
+
* - Else: wrap as markdown under a chosen heading
|
|
58
|
+
*/
|
|
59
|
+
export declare function normalizeForFlexMd(input: unknown, opts?: {
|
|
60
|
+
fallbackHeading?: string;
|
|
61
|
+
fallbackHeadingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
62
|
+
}): {
|
|
63
|
+
normalizedText: string;
|
|
64
|
+
detection: MarkdownDetection;
|
|
65
|
+
wasStripped: boolean;
|
|
66
|
+
stripLanguage: string | null;
|
|
67
|
+
wasWrapped: boolean;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Example integration with Flex-MD:
|
|
71
|
+
*
|
|
72
|
+
* import { parseOutputFormatSpec, transformWithOfs } from 'flex-md';
|
|
73
|
+
*
|
|
74
|
+
* const spec = parseOutputFormatSpec(ofsMarkdown);
|
|
75
|
+
* const prep = normalizeForFlexMd(llmText, { fallbackHeading: "Full Answer" });
|
|
76
|
+
* const out = transformWithOfs(prep.normalizedText, spec);
|
|
77
|
+
*/
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for "framed vs frameless" Markdown handling before Flex-MD.
|
|
3
|
+
*
|
|
4
|
+
* Goals:
|
|
5
|
+
* 1) Detect framed markdown (single fenced block) and extract its inner content.
|
|
6
|
+
* 2) If NOT markdown-ish, force-wrap into a minimal markdown shape so Flex-MD can still parse.
|
|
7
|
+
*
|
|
8
|
+
* Notes:
|
|
9
|
+
* - This is heuristic by design; tune thresholds as you learn your data.
|
|
10
|
+
*/
|
|
11
|
+
import { logger } from "../../logger.js";
|
|
12
|
+
const SINGLE_FENCE_BLOCK_RE = /^```([a-zA-Z0-9_-]+)?([^\n]*)\n([\s\S]*?)\n```$/;
|
|
13
|
+
const FENCE_OPEN_RE = /(^|\n)```/g;
|
|
14
|
+
export function detectMarkdown(text) {
|
|
15
|
+
logger.debug("Starting markdown detection", {
|
|
16
|
+
inputType: typeof text,
|
|
17
|
+
inputLength: typeof text === "string" ? text.length : JSON.stringify(text ?? "").length
|
|
18
|
+
});
|
|
19
|
+
const reasons = [];
|
|
20
|
+
const raw = typeof text === "string" ? text : JSON.stringify(text ?? "");
|
|
21
|
+
const s = raw.replace(/\r\n/g, "\n");
|
|
22
|
+
logger.verbose("Input normalized for processing", {
|
|
23
|
+
originalType: typeof text,
|
|
24
|
+
normalizedLength: s.length,
|
|
25
|
+
hasNewlines: s.includes('\n')
|
|
26
|
+
});
|
|
27
|
+
const codeFences = [...s.matchAll(FENCE_OPEN_RE)].length;
|
|
28
|
+
logger.debug("Code fence analysis", {
|
|
29
|
+
totalFences: codeFences,
|
|
30
|
+
fenceRegex: FENCE_OPEN_RE.source
|
|
31
|
+
});
|
|
32
|
+
// Framed detection (single fenced block)
|
|
33
|
+
const m = s.match(SINGLE_FENCE_BLOCK_RE);
|
|
34
|
+
const isFramed = !!m && codeFences === 2;
|
|
35
|
+
const frameLanguage = isFramed ? (m?.[1] ?? null) : null;
|
|
36
|
+
logger.debug("Framed markdown detection", {
|
|
37
|
+
regexMatch: !!m,
|
|
38
|
+
expectedFences: 2,
|
|
39
|
+
actualFences: codeFences,
|
|
40
|
+
isFramed,
|
|
41
|
+
frameLanguage,
|
|
42
|
+
regexGroups: m ? {
|
|
43
|
+
fullMatch: m[0],
|
|
44
|
+
language: m[1],
|
|
45
|
+
content: m[3]?.substring(0, 100) + (m[3]?.length > 100 ? '...' : '')
|
|
46
|
+
} : null
|
|
47
|
+
});
|
|
48
|
+
if (isFramed) {
|
|
49
|
+
reasons.push("Single fenced code block detected (framed payload).");
|
|
50
|
+
logger.info("Detected framed markdown", { language: frameLanguage });
|
|
51
|
+
}
|
|
52
|
+
else if (codeFences > 2) {
|
|
53
|
+
reasons.push("Multiple fenced code blocks detected.");
|
|
54
|
+
logger.debug("Multiple fences detected, not treating as single framed block", { fenceCount: codeFences });
|
|
55
|
+
}
|
|
56
|
+
else if (codeFences === 1) {
|
|
57
|
+
reasons.push("Single fence marker found but not properly framed.");
|
|
58
|
+
logger.debug("Single fence found but regex didn't match", {
|
|
59
|
+
regex: SINGLE_FENCE_BLOCK_RE.source,
|
|
60
|
+
hasMatch: !!m
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
logger.debug("No fence markers detected");
|
|
65
|
+
}
|
|
66
|
+
const lines = s.split("\n");
|
|
67
|
+
logger.verbose("Line-by-line analysis started", { totalLines: lines.length });
|
|
68
|
+
const atxHeadings = lines.filter((l) => /^#{1,6}\s+\S/.test(l.trim())).length;
|
|
69
|
+
let setextHeadings = 0;
|
|
70
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
71
|
+
const cur = lines[i].trim();
|
|
72
|
+
const nxt = lines[i + 1].trim();
|
|
73
|
+
if (cur.length > 0 && (/^={2,}$/.test(nxt) || /^-{2,}$/.test(nxt)))
|
|
74
|
+
setextHeadings++;
|
|
75
|
+
}
|
|
76
|
+
const headings = atxHeadings + setextHeadings;
|
|
77
|
+
const unorderedListLines = lines.filter((l) => /^\s*[-*+]\s+\S/.test(l)).length;
|
|
78
|
+
const orderedListLines = lines.filter((l) => /^\s*\d{1,3}([.)])\s+\S/.test(l)).length;
|
|
79
|
+
const tableRows = lines.filter((l) => {
|
|
80
|
+
const t = l.trim();
|
|
81
|
+
if (!t.startsWith("|") || t.length < 3)
|
|
82
|
+
return false;
|
|
83
|
+
const pipeCount = (t.match(/\|/g) ?? []).length;
|
|
84
|
+
return pipeCount >= 2;
|
|
85
|
+
}).length;
|
|
86
|
+
const inlineCodeSpans = (s.match(/`[^`\n]+`/g) ?? []).length;
|
|
87
|
+
const mdLinks = (s.match(/\[[^\]]+\]\([^)]+\)/g) ?? []).length;
|
|
88
|
+
const emphasisTokens = (s.match(/\*\*[^*\n]+\*\*/g) ?? []).length +
|
|
89
|
+
(s.match(/__[^_\n]+__/g) ?? []).length +
|
|
90
|
+
(s.match(/(^|[^*])\*[^*\n]+\*([^*]|$)/g) ?? []).length +
|
|
91
|
+
(s.match(/(^|[^_])_[^_\n]+_([^_]|$)/g) ?? []).length;
|
|
92
|
+
logger.debug("Markdown structure analysis", {
|
|
93
|
+
atxHeadings,
|
|
94
|
+
setextHeadings,
|
|
95
|
+
totalHeadings: headings,
|
|
96
|
+
unorderedListLines,
|
|
97
|
+
orderedListLines,
|
|
98
|
+
tableRows,
|
|
99
|
+
inlineCodeSpans,
|
|
100
|
+
mdLinks,
|
|
101
|
+
emphasisTokens
|
|
102
|
+
});
|
|
103
|
+
// Heuristic decision rules
|
|
104
|
+
const hasList = unorderedListLines + orderedListLines >= 2;
|
|
105
|
+
const hasTable = tableRows >= 2;
|
|
106
|
+
const hasOtherSignals = inlineCodeSpans + mdLinks + emphasisTokens >= 2;
|
|
107
|
+
logger.debug("Heuristic analysis", {
|
|
108
|
+
hasList,
|
|
109
|
+
hasTable,
|
|
110
|
+
hasOtherSignals,
|
|
111
|
+
listThreshold: 2,
|
|
112
|
+
tableThreshold: 2,
|
|
113
|
+
otherSignalsThreshold: 2
|
|
114
|
+
});
|
|
115
|
+
let isMarkdownLikely = false;
|
|
116
|
+
if (isFramed) {
|
|
117
|
+
isMarkdownLikely = true;
|
|
118
|
+
logger.debug("Markdown likelihood determined: framed content is always considered markdown");
|
|
119
|
+
}
|
|
120
|
+
else if (headings >= 2) {
|
|
121
|
+
isMarkdownLikely = true;
|
|
122
|
+
reasons.push(`Detected ${headings} markdown heading(s) (>=2).`);
|
|
123
|
+
logger.debug("Markdown likelihood: sufficient headings detected", { headingCount: headings });
|
|
124
|
+
}
|
|
125
|
+
else if (headings >= 1 && (hasList || hasTable)) {
|
|
126
|
+
isMarkdownLikely = true;
|
|
127
|
+
reasons.push(`Detected heading(s) plus ${hasList ? "list" : "table"} structure.`);
|
|
128
|
+
logger.debug("Markdown likelihood: heading plus structural element", {
|
|
129
|
+
hasHeading: headings >= 1,
|
|
130
|
+
hasList,
|
|
131
|
+
hasTable
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if ((hasList && hasTable) || (hasTable && hasOtherSignals) || (hasList && hasOtherSignals)) {
|
|
135
|
+
isMarkdownLikely = true;
|
|
136
|
+
reasons.push("Detected multiple markdown structural signals (lists/tables/links/code/emphasis).");
|
|
137
|
+
logger.debug("Markdown likelihood: multiple structural signals", {
|
|
138
|
+
listAndTable: hasList && hasTable,
|
|
139
|
+
tableAndOther: hasTable && hasOtherSignals,
|
|
140
|
+
listAndOther: hasList && hasOtherSignals
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
reasons.push("Insufficient markdown structure signals; treating as plain text.");
|
|
145
|
+
logger.debug("Markdown likelihood: insufficient signals, treating as plain text", {
|
|
146
|
+
headings,
|
|
147
|
+
hasList,
|
|
148
|
+
hasTable,
|
|
149
|
+
hasOtherSignals
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (/^\s*#{1,6}\s+\S/.test(lines[0] ?? "")) {
|
|
153
|
+
reasons.push("Text starts with an ATX heading (#...).");
|
|
154
|
+
isMarkdownLikely = true;
|
|
155
|
+
logger.debug("Additional check: starts with heading, upgrading to markdown");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
isMarkdownLikely,
|
|
159
|
+
isFramed,
|
|
160
|
+
frameLanguage,
|
|
161
|
+
reasons,
|
|
162
|
+
stats: {
|
|
163
|
+
headings,
|
|
164
|
+
atxHeadings,
|
|
165
|
+
setextHeadings,
|
|
166
|
+
unorderedListLines,
|
|
167
|
+
orderedListLines,
|
|
168
|
+
tableRows,
|
|
169
|
+
codeFences,
|
|
170
|
+
inlineCodeSpans,
|
|
171
|
+
mdLinks,
|
|
172
|
+
emphasisTokens,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* If the entire payload is a single fenced block, return its inner content.
|
|
178
|
+
* Otherwise return the original text.
|
|
179
|
+
*
|
|
180
|
+
* - Also handles ```md / ```markdown / ```json etc.
|
|
181
|
+
*/
|
|
182
|
+
export function stripSingleFence(input) {
|
|
183
|
+
const s = input.replace(/\r\n/g, "\n").trim();
|
|
184
|
+
const fenceCount = [...s.matchAll(FENCE_OPEN_RE)].length;
|
|
185
|
+
if (fenceCount !== 2) {
|
|
186
|
+
return { stripped: input, wasFramed: false, language: null };
|
|
187
|
+
}
|
|
188
|
+
const m = s.match(SINGLE_FENCE_BLOCK_RE);
|
|
189
|
+
if (!m)
|
|
190
|
+
return { stripped: input, wasFramed: false, language: null };
|
|
191
|
+
const lang = (m[1] ?? null);
|
|
192
|
+
const inner = (m[3] ?? "").trim();
|
|
193
|
+
return { stripped: inner, wasFramed: true, language: lang };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Force-wrap non-markdown text into a minimal heading-based markdown document.
|
|
197
|
+
* This helps Flex-MD / nx-md-parser operate even when the model returns plain text.
|
|
198
|
+
*
|
|
199
|
+
* Choose a heading that exists in your OFS to maximize alignment.
|
|
200
|
+
* Default: "Full Answer" (common sink section).
|
|
201
|
+
*/
|
|
202
|
+
export function forceWrapAsMarkdown(plainText, opts) {
|
|
203
|
+
const heading = opts?.heading ?? "Full Answer";
|
|
204
|
+
const level = opts?.level ?? 3;
|
|
205
|
+
const hashes = "#".repeat(level);
|
|
206
|
+
const raw = plainText.replace(/\r\n/g, "\n");
|
|
207
|
+
const body = opts?.preserveLeadingWhitespace ? raw : raw.trim();
|
|
208
|
+
// If empty, keep it explicit.
|
|
209
|
+
const safeBody = body.length ? body : "None";
|
|
210
|
+
return `${hashes} ${heading}\n${safeBody}\n`;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* End-to-end "normalize input for Flex-MD" helper:
|
|
214
|
+
* - If framed: strip the fence (so Flex-MD sees pure markdown)
|
|
215
|
+
* - Else: if markdown-likely: keep as-is
|
|
216
|
+
* - Else: wrap as markdown under a chosen heading
|
|
217
|
+
*/
|
|
218
|
+
export function normalizeForFlexMd(input, opts) {
|
|
219
|
+
logger.info("Starting Flex-MD normalization", {
|
|
220
|
+
inputType: typeof input,
|
|
221
|
+
inputLength: typeof input === "string" ? input.length : JSON.stringify(input ?? "").length,
|
|
222
|
+
options: opts
|
|
223
|
+
});
|
|
224
|
+
const raw = typeof input === "string" ? input : JSON.stringify(input ?? "");
|
|
225
|
+
const detection = detectMarkdown(raw);
|
|
226
|
+
logger.debug("Detection completed", {
|
|
227
|
+
isFramed: detection.isFramed,
|
|
228
|
+
isMarkdownLikely: detection.isMarkdownLikely,
|
|
229
|
+
frameLanguage: detection.frameLanguage,
|
|
230
|
+
reasons: detection.reasons
|
|
231
|
+
});
|
|
232
|
+
// 1) Strip if framed
|
|
233
|
+
const { stripped, wasFramed, language } = stripSingleFence(raw);
|
|
234
|
+
logger.debug("Strip fence check", {
|
|
235
|
+
wasFramed,
|
|
236
|
+
language,
|
|
237
|
+
strippedLength: stripped.length,
|
|
238
|
+
originalLength: raw.length
|
|
239
|
+
});
|
|
240
|
+
if (wasFramed) {
|
|
241
|
+
logger.info("Normalization: stripped framed markdown", {
|
|
242
|
+
language,
|
|
243
|
+
contentLength: stripped.length
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
normalizedText: stripped,
|
|
247
|
+
detection,
|
|
248
|
+
wasStripped: true,
|
|
249
|
+
stripLanguage: language,
|
|
250
|
+
wasWrapped: false,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// 2) Keep if markdown-likely
|
|
254
|
+
if (detection.isMarkdownLikely) {
|
|
255
|
+
logger.info("Normalization: keeping as-is (markdown-likely)", {
|
|
256
|
+
reasons: detection.reasons
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
normalizedText: raw,
|
|
260
|
+
detection,
|
|
261
|
+
wasStripped: false,
|
|
262
|
+
stripLanguage: null,
|
|
263
|
+
wasWrapped: false,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// 3) Wrap if not markdown-likely
|
|
267
|
+
const fallbackHeading = opts?.fallbackHeading ?? "Full Answer";
|
|
268
|
+
const fallbackLevel = opts?.fallbackHeadingLevel ?? 3;
|
|
269
|
+
logger.info("Normalization: wrapping as markdown", {
|
|
270
|
+
fallbackHeading,
|
|
271
|
+
fallbackLevel,
|
|
272
|
+
reasons: detection.reasons
|
|
273
|
+
});
|
|
274
|
+
const wrapped = forceWrapAsMarkdown(raw, {
|
|
275
|
+
heading: fallbackHeading,
|
|
276
|
+
level: fallbackLevel,
|
|
277
|
+
});
|
|
278
|
+
logger.debug("Wrapping completed", {
|
|
279
|
+
wrappedLength: wrapped.length,
|
|
280
|
+
originalLength: raw.length
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
normalizedText: wrapped,
|
|
284
|
+
detection,
|
|
285
|
+
wasStripped: false,
|
|
286
|
+
stripLanguage: null,
|
|
287
|
+
wasWrapped: true,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Example integration with Flex-MD:
|
|
292
|
+
*
|
|
293
|
+
* import { parseOutputFormatSpec, transformWithOfs } from 'flex-md';
|
|
294
|
+
*
|
|
295
|
+
* const spec = parseOutputFormatSpec(ofsMarkdown);
|
|
296
|
+
* const prep = normalizeForFlexMd(llmText, { fallbackHeading: "Full Answer" });
|
|
297
|
+
* const out = transformWithOfs(prep.normalizedText, spec);
|
|
298
|
+
*/
|
|
@@ -2,6 +2,7 @@ import { DetectJsonAllResult } from "./types.js";
|
|
|
2
2
|
export * from "./types.js";
|
|
3
3
|
export { detectJsonIntent } from "./detectIntent.js";
|
|
4
4
|
export { detectJsonContainers, detectJsonPresence } from "./detectPresence.js";
|
|
5
|
+
export * from "./detectMarkdown.js";
|
|
5
6
|
export declare function detectJsonAll(textOrMd: string, opts?: {
|
|
6
7
|
parseJson?: boolean;
|
|
7
8
|
}): DetectJsonAllResult;
|
|
@@ -3,6 +3,7 @@ import { detectJsonContainers, detectJsonPresence } from "./detectPresence.js";
|
|
|
3
3
|
export * from "./types.js";
|
|
4
4
|
export { detectJsonIntent } from "./detectIntent.js";
|
|
5
5
|
export { detectJsonContainers, detectJsonPresence } from "./detectPresence.js";
|
|
6
|
+
export * from "./detectMarkdown.js";
|
|
6
7
|
export function detectJsonAll(textOrMd, opts) {
|
|
7
8
|
return {
|
|
8
9
|
intent: detectJsonIntent(textOrMd),
|
package/dist/index.cjs
CHANGED
|
@@ -14,10 +14,13 @@ 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 = 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.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);
|
|
21
|
+
// Logging
|
|
22
|
+
var logger_js_1 = require("./logger.js");
|
|
23
|
+
Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return logger_js_1.logger; } });
|
|
21
24
|
// Shared MD Parsing
|
|
22
25
|
__exportStar(require("./md/parse.js"), exports);
|
|
23
26
|
var outline_js_1 = require("./md/outline.js");
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Logger } from 'micro-logs';
|
|
2
|
+
// Get DEBUG_LEVEL from environment, default to 'info'
|
|
3
|
+
const debugLevel = process.env.DEBUG_LEVEL || 'info';
|
|
4
|
+
// Map string levels to micro-logs levels
|
|
5
|
+
const levelMapping = {
|
|
6
|
+
'verbose': 'verbose',
|
|
7
|
+
'debug': 'debug',
|
|
8
|
+
'info': 'info',
|
|
9
|
+
'warn': 'warn',
|
|
10
|
+
'error': 'error'
|
|
11
|
+
};
|
|
12
|
+
// Default to 'info' if invalid level provided
|
|
13
|
+
const threshold = levelMapping[debugLevel.toLowerCase()] || 'info';
|
|
14
|
+
export const logger = new Logger({
|
|
15
|
+
packageName: 'flex-md',
|
|
16
|
+
envPrefix: 'FLEX_MD',
|
|
17
|
+
debugNamespace: 'flex-md:*'
|
|
18
|
+
});
|
|
19
|
+
export { threshold };
|
package/dist/ofs/adapter.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parseHeadingsAndSections, extractBullets, parseMarkdownTable, normalize
|
|
|
4
4
|
import { normalizeMarkdownInput } from "../md/normalize.js";
|
|
5
5
|
import { toCamelCase } from "nx-helpers";
|
|
6
6
|
import { markdownToJson as autoMarkdownToJson } from "nx-json-parser";
|
|
7
|
+
import { logger } from "../logger.js";
|
|
7
8
|
/**
|
|
8
9
|
* Converts a Flex-MD OutputFormatSpec to an nx-md-parser Schema.
|
|
9
10
|
*/
|
|
@@ -53,11 +54,27 @@ export function ofsToSchema(spec) {
|
|
|
53
54
|
* If no spec is provided, it attempts to infer it from the markdown (autospecs).
|
|
54
55
|
*/
|
|
55
56
|
export function transformWithOfs(md, specOrRecallId) {
|
|
57
|
+
logger.info("Starting Flex-MD transformation", {
|
|
58
|
+
inputLength: md.length,
|
|
59
|
+
hasSpec: !!specOrRecallId,
|
|
60
|
+
specType: specOrRecallId ? typeof specOrRecallId : 'none'
|
|
61
|
+
});
|
|
56
62
|
// 0. Normalize input (handle literal \n common in LLM outputs)
|
|
57
63
|
const normalizedMd = normalizeMarkdownInput(md);
|
|
64
|
+
logger.debug("Input normalization", {
|
|
65
|
+
originalLength: md.length,
|
|
66
|
+
normalizedLength: normalizedMd.length,
|
|
67
|
+
changed: md !== normalizedMd
|
|
68
|
+
});
|
|
58
69
|
// 1. Automatic parsing (Dual-Response) using nx-json-parser
|
|
70
|
+
logger.debug("Starting automatic parsing with nx-json-parser");
|
|
59
71
|
const parsedOutput = autoMarkdownToJson(normalizedMd);
|
|
72
|
+
logger.debug("Automatic parsing completed", {
|
|
73
|
+
parsedKeys: Object.keys(parsedOutput),
|
|
74
|
+
parsedKeyCount: Object.keys(parsedOutput).length
|
|
75
|
+
});
|
|
60
76
|
if (!specOrRecallId) {
|
|
77
|
+
logger.info("No spec provided, returning parsed output only");
|
|
61
78
|
return {
|
|
62
79
|
parsedOutput,
|
|
63
80
|
contractOutput: null,
|
|
@@ -68,8 +85,10 @@ export function transformWithOfs(md, specOrRecallId) {
|
|
|
68
85
|
}
|
|
69
86
|
let spec;
|
|
70
87
|
if (typeof specOrRecallId === "string") {
|
|
88
|
+
logger.debug("Recalling spec from memory", { recallId: specOrRecallId });
|
|
71
89
|
const recalled = recall(specOrRecallId);
|
|
72
90
|
if (!recalled) {
|
|
91
|
+
logger.warn("Recall ID not found", { recallId: specOrRecallId });
|
|
73
92
|
return {
|
|
74
93
|
parsedOutput,
|
|
75
94
|
contractOutput: null,
|
|
@@ -79,41 +98,79 @@ export function transformWithOfs(md, specOrRecallId) {
|
|
|
79
98
|
};
|
|
80
99
|
}
|
|
81
100
|
spec = recalled;
|
|
101
|
+
logger.debug("Spec recalled successfully", {
|
|
102
|
+
sectionCount: spec.sections.length,
|
|
103
|
+
sectionNames: spec.sections.map(s => s.name)
|
|
104
|
+
});
|
|
82
105
|
}
|
|
83
106
|
else {
|
|
84
107
|
spec = specOrRecallId;
|
|
108
|
+
logger.debug("Using provided spec directly", {
|
|
109
|
+
sectionCount: spec.sections.length,
|
|
110
|
+
sectionNames: spec.sections.map(s => s.name)
|
|
111
|
+
});
|
|
85
112
|
}
|
|
86
113
|
// 2. Parse sections using Flex-MD parser for the contract mapping
|
|
87
114
|
const bulletNames = spec.sections.map(s => s.name);
|
|
115
|
+
logger.debug("Starting section parsing", {
|
|
116
|
+
expectedSections: bulletNames,
|
|
117
|
+
sectionCount: bulletNames.length
|
|
118
|
+
});
|
|
88
119
|
// Note: We use the local headings parser to find the specific sections defined in the spec
|
|
89
120
|
const parsedSections = parseHeadingsAndSections(normalizedMd, { bulletNames });
|
|
121
|
+
logger.debug("Section parsing completed", {
|
|
122
|
+
foundSections: parsedSections.map(s => s.heading.name),
|
|
123
|
+
foundCount: parsedSections.length
|
|
124
|
+
});
|
|
90
125
|
const parsedObj = {};
|
|
91
126
|
for (const sectionSpec of spec.sections) {
|
|
92
127
|
const normName = normalizeName(sectionSpec.name);
|
|
93
128
|
const found = parsedSections.find(s => normalizeName(s.heading.name) === normName);
|
|
129
|
+
logger.verbose(`Processing section "${sectionSpec.name}"`, {
|
|
130
|
+
normalizedName: normName,
|
|
131
|
+
found: !!found,
|
|
132
|
+
kind: sectionSpec.kind,
|
|
133
|
+
hasColumns: !!sectionSpec.columns
|
|
134
|
+
});
|
|
94
135
|
if (found) {
|
|
95
136
|
let value;
|
|
96
137
|
switch (sectionSpec.kind) {
|
|
97
138
|
case "list":
|
|
98
139
|
case "ordered_list":
|
|
99
140
|
value = extractBullets(found.body);
|
|
141
|
+
logger.debug(`Extracted bullets for "${sectionSpec.name}"`, {
|
|
142
|
+
bulletCount: Array.isArray(value) ? value.length : 'unknown'
|
|
143
|
+
});
|
|
100
144
|
break;
|
|
101
145
|
case "table":
|
|
102
146
|
case "ordered_table":
|
|
103
147
|
if (sectionSpec.columns) {
|
|
104
148
|
value = parseMarkdownTable(found.body, sectionSpec.columns);
|
|
149
|
+
logger.debug(`Parsed table for "${sectionSpec.name}"`, {
|
|
150
|
+
columns: sectionSpec.columns,
|
|
151
|
+
rowCount: Array.isArray(value) ? value.length : 'unknown'
|
|
152
|
+
});
|
|
105
153
|
}
|
|
106
154
|
else {
|
|
107
155
|
value = found.body.trim();
|
|
156
|
+
logger.debug(`Using raw body for table "${sectionSpec.name}" (no columns specified)`);
|
|
108
157
|
}
|
|
109
158
|
break;
|
|
110
159
|
default:
|
|
111
160
|
value = found.body.trim();
|
|
161
|
+
logger.debug(`Using trimmed body for "${sectionSpec.name}"`);
|
|
112
162
|
break;
|
|
113
163
|
}
|
|
114
164
|
parsedObj[sectionSpec.name] = value;
|
|
115
165
|
}
|
|
166
|
+
else {
|
|
167
|
+
logger.warn(`Section "${sectionSpec.name}" not found in markdown`);
|
|
168
|
+
}
|
|
116
169
|
}
|
|
170
|
+
logger.debug("Contract parsing completed", {
|
|
171
|
+
parsedKeys: Object.keys(parsedObj),
|
|
172
|
+
parsedKeyCount: Object.keys(parsedObj).length
|
|
173
|
+
});
|
|
117
174
|
// 3. Transform using nx-md-parser (latest v2.2.0) for schema validation and fixing
|
|
118
175
|
const schema = ofsToSchema(spec);
|
|
119
176
|
const transformer = new JSONTransformer(schema);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flex-md",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.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": "",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"vitest": "^4.0.16"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
+
"micro-logs": "^1.0.0",
|
|
55
56
|
"nd": "^1.2.0",
|
|
56
57
|
"nx-helpers": "^1.5.0",
|
|
57
58
|
"nx-json-parser": "^1.2.1",
|