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.
@@ -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
@@ -1,5 +1,6 @@
1
1
  export * from "./types.js";
2
2
  export * from "./strictness/types.js";
3
+ export { logger } from "./logger.js";
3
4
  export * from "./md/parse.js";
4
5
  export { buildOutline } from "./md/outline.js";
5
6
  export { parseOutputFormatSpec, validateFormat } from "./ofs/parser.js";
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // Core SFMD Types
2
2
  export * from "./types.js";
3
3
  export * from "./strictness/types.js";
4
+ // Logging
5
+ export { logger } from "./logger.js";
4
6
  // Shared MD Parsing
5
7
  export * from "./md/parse.js";
6
8
  export { buildOutline } from "./md/outline.js";
@@ -0,0 +1,4 @@
1
+ import { Logger } from 'micro-logs';
2
+ declare const threshold: "error" | "warn" | "info" | "verbose" | "debug";
3
+ export declare const logger: Logger;
4
+ export { threshold };
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 };
@@ -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.4.8",
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",