flex-md 4.4.9 → 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.
@@ -8,22 +8,63 @@
8
8
  * Notes:
9
9
  * - This is heuristic by design; tune thresholds as you learn your data.
10
10
  */
11
+ import { logger } from "../../logger.js";
11
12
  const SINGLE_FENCE_BLOCK_RE = /^```([a-zA-Z0-9_-]+)?([^\n]*)\n([\s\S]*?)\n```$/;
12
13
  const FENCE_OPEN_RE = /(^|\n)```/g;
13
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
+ });
14
19
  const reasons = [];
15
20
  const raw = typeof text === "string" ? text : JSON.stringify(text ?? "");
16
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
+ });
17
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
+ });
18
32
  // Framed detection (single fenced block)
19
33
  const m = s.match(SINGLE_FENCE_BLOCK_RE);
20
34
  const isFramed = !!m && codeFences === 2;
21
35
  const frameLanguage = isFramed ? (m?.[1] ?? null) : null;
22
- if (isFramed)
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) {
23
49
  reasons.push("Single fenced code block detected (framed payload).");
24
- else if (codeFences > 1)
50
+ logger.info("Detected framed markdown", { language: frameLanguage });
51
+ }
52
+ else if (codeFences > 2) {
25
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
+ }
26
66
  const lines = s.split("\n");
67
+ logger.verbose("Line-by-line analysis started", { totalLines: lines.length });
27
68
  const atxHeadings = lines.filter((l) => /^#{1,6}\s+\S/.test(l.trim())).length;
28
69
  let setextHeadings = 0;
29
70
  for (let i = 0; i < lines.length - 1; i++) {
@@ -48,32 +89,70 @@ export function detectMarkdown(text) {
48
89
  (s.match(/__[^_\n]+__/g) ?? []).length +
49
90
  (s.match(/(^|[^*])\*[^*\n]+\*([^*]|$)/g) ?? []).length +
50
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
+ });
51
103
  // Heuristic decision rules
52
104
  const hasList = unorderedListLines + orderedListLines >= 2;
53
105
  const hasTable = tableRows >= 2;
54
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
+ });
55
115
  let isMarkdownLikely = false;
56
116
  if (isFramed) {
57
117
  isMarkdownLikely = true;
118
+ logger.debug("Markdown likelihood determined: framed content is always considered markdown");
58
119
  }
59
120
  else if (headings >= 2) {
60
121
  isMarkdownLikely = true;
61
122
  reasons.push(`Detected ${headings} markdown heading(s) (>=2).`);
123
+ logger.debug("Markdown likelihood: sufficient headings detected", { headingCount: headings });
62
124
  }
63
125
  else if (headings >= 1 && (hasList || hasTable)) {
64
126
  isMarkdownLikely = true;
65
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
+ });
66
133
  }
67
134
  else if ((hasList && hasTable) || (hasTable && hasOtherSignals) || (hasList && hasOtherSignals)) {
68
135
  isMarkdownLikely = true;
69
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
+ });
70
142
  }
71
143
  else {
72
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
+ });
73
151
  }
74
152
  if (/^\s*#{1,6}\s+\S/.test(lines[0] ?? "")) {
75
153
  reasons.push("Text starts with an ATX heading (#...).");
76
154
  isMarkdownLikely = true;
155
+ logger.debug("Additional check: starts with heading, upgrading to markdown");
77
156
  }
78
157
  return {
79
158
  isMarkdownLikely,
@@ -137,11 +216,32 @@ export function forceWrapAsMarkdown(plainText, opts) {
137
216
  * - Else: wrap as markdown under a chosen heading
138
217
  */
139
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
+ });
140
224
  const raw = typeof input === "string" ? input : JSON.stringify(input ?? "");
141
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
+ });
142
232
  // 1) Strip if framed
143
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
+ });
144
240
  if (wasFramed) {
241
+ logger.info("Normalization: stripped framed markdown", {
242
+ language,
243
+ contentLength: stripped.length
244
+ });
145
245
  return {
146
246
  normalizedText: stripped,
147
247
  detection,
@@ -152,6 +252,9 @@ export function normalizeForFlexMd(input, opts) {
152
252
  }
153
253
  // 2) Keep if markdown-likely
154
254
  if (detection.isMarkdownLikely) {
255
+ logger.info("Normalization: keeping as-is (markdown-likely)", {
256
+ reasons: detection.reasons
257
+ });
155
258
  return {
156
259
  normalizedText: raw,
157
260
  detection,
@@ -161,9 +264,20 @@ export function normalizeForFlexMd(input, opts) {
161
264
  };
162
265
  }
163
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
+ });
164
274
  const wrapped = forceWrapAsMarkdown(raw, {
165
- heading: opts?.fallbackHeading ?? "Full Answer",
166
- level: opts?.fallbackHeadingLevel ?? 3,
275
+ heading: fallbackHeading,
276
+ level: fallbackLevel,
277
+ });
278
+ logger.debug("Wrapping completed", {
279
+ wrappedLength: wrapped.length,
280
+ originalLength: raw.length
167
281
  });
168
282
  return {
169
283
  normalizedText: wrapped,
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.9",
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",