flex-md 4.7.2 → 4.7.3

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.
Files changed (104) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/diagnostics.test.js +45 -47
  3. package/dist/__tests__/ofs.test.js +28 -30
  4. package/dist/__tests__/structural.test.js +19 -21
  5. package/dist/__tests__/validate.test.js +27 -29
  6. package/dist/cli/index.js +13 -15
  7. package/dist/detect/json/detectIntent.js +1 -4
  8. package/dist/detect/json/detectMarkdown.js +29 -35
  9. package/dist/detect/json/detectPresence.js +2 -6
  10. package/dist/detect/json/index.js +10 -31
  11. package/dist/detect/json/types.js +1 -2
  12. package/dist/extract/extract.js +11 -14
  13. package/dist/extract/types.js +1 -2
  14. package/dist/index.js +22 -69
  15. package/dist/logger.js +3 -6
  16. package/dist/md/match.js +1 -4
  17. package/dist/md/normalize.js +1 -4
  18. package/dist/md/outline.js +4 -8
  19. package/dist/md/parse.js +11 -20
  20. package/dist/ofs/adapter.js +45 -49
  21. package/dist/ofs/enricher.js +3 -8
  22. package/dist/ofs/infer.js +4 -7
  23. package/dist/ofs/issuesEnvelope.js +5 -10
  24. package/dist/ofs/memory.js +6 -10
  25. package/dist/ofs/parser.js +7 -13
  26. package/dist/ofs/stringify.js +1 -4
  27. package/dist/pipeline/enforce.js +12 -15
  28. package/dist/pipeline/kind.js +3 -6
  29. package/dist/pipeline/repair.js +8 -11
  30. package/dist/strictness/container.js +1 -4
  31. package/dist/strictness/processor.js +7 -10
  32. package/dist/strictness/types.js +1 -4
  33. package/dist/tokens/auto-fix.js +1 -4
  34. package/dist/tokens/cognitive-cost.js +6 -10
  35. package/dist/tokens/compliance.js +4 -8
  36. package/dist/tokens/confidence.js +6 -9
  37. package/dist/tokens/estimator.js +20 -26
  38. package/dist/tokens/improvements.js +12 -16
  39. package/dist/tokens/index.js +22 -40
  40. package/dist/tokens/parser.js +4 -7
  41. package/dist/tokens/patterns.js +2 -5
  42. package/dist/tokens/runtime-estimator.js +9 -12
  43. package/dist/tokens/smart-report.js +10 -14
  44. package/dist/tokens/spec-estimator.js +10 -15
  45. package/dist/tokens/types.js +1 -2
  46. package/dist/tokens/validator.js +3 -6
  47. package/dist/types.js +1 -2
  48. package/dist/validate/compliance.js +4 -8
  49. package/dist/validate/connection.js +5 -8
  50. package/dist/validate/types.js +1 -2
  51. package/dist/validate/validate.js +16 -19
  52. package/dist-cjs/__tests__/diagnostics.test.cjs +61 -0
  53. package/dist-cjs/__tests__/ofs.test.cjs +53 -0
  54. package/dist-cjs/__tests__/structural.test.cjs +30 -0
  55. package/dist-cjs/__tests__/validate.test.cjs +110 -0
  56. package/dist-cjs/cli/index.cjs +110 -0
  57. package/dist-cjs/detect/json/detectIntent.cjs +82 -0
  58. package/dist-cjs/detect/json/detectMarkdown.cjs +304 -0
  59. package/dist-cjs/detect/json/detectPresence.cjs +195 -0
  60. package/dist-cjs/detect/json/index.cjs +34 -0
  61. package/dist-cjs/detect/json/types.cjs +2 -0
  62. package/dist-cjs/extract/extract.cjs +72 -0
  63. package/dist-cjs/extract/types.cjs +2 -0
  64. package/dist-cjs/flex-md-loader.cjs +102 -0
  65. package/dist-cjs/index.cjs +79 -0
  66. package/dist-cjs/logger.cjs +22 -0
  67. package/dist-cjs/md/match.cjs +47 -0
  68. package/dist-cjs/md/normalize.cjs +13 -0
  69. package/dist-cjs/md/outline.cjs +49 -0
  70. package/dist-cjs/md/parse.cjs +199 -0
  71. package/dist-cjs/ofs/adapter.cjs +195 -0
  72. package/dist-cjs/ofs/enricher.cjs +151 -0
  73. package/dist-cjs/ofs/infer.cjs +63 -0
  74. package/dist-cjs/ofs/issuesEnvelope.cjs +76 -0
  75. package/dist-cjs/ofs/memory.cjs +26 -0
  76. package/dist-cjs/ofs/parser.cjs +373 -0
  77. package/dist-cjs/ofs/stringify.cjs +45 -0
  78. package/dist-cjs/pipeline/enforce.cjs +49 -0
  79. package/dist-cjs/pipeline/kind.cjs +30 -0
  80. package/dist-cjs/pipeline/repair.cjs +115 -0
  81. package/dist-cjs/strictness/container.cjs +49 -0
  82. package/dist-cjs/strictness/processor.cjs +32 -0
  83. package/dist-cjs/strictness/types.cjs +109 -0
  84. package/dist-cjs/tokens/auto-fix.cjs +59 -0
  85. package/dist-cjs/tokens/cognitive-cost.cjs +209 -0
  86. package/dist-cjs/tokens/compliance.cjs +74 -0
  87. package/dist-cjs/tokens/confidence.cjs +335 -0
  88. package/dist-cjs/tokens/estimator.cjs +157 -0
  89. package/dist-cjs/tokens/improvements.cjs +701 -0
  90. package/dist-cjs/tokens/index.cjs +74 -0
  91. package/dist-cjs/tokens/parser.cjs +100 -0
  92. package/dist-cjs/tokens/patterns.cjs +23 -0
  93. package/dist-cjs/tokens/runtime-estimator.cjs +74 -0
  94. package/dist-cjs/tokens/smart-report.cjs +191 -0
  95. package/dist-cjs/tokens/spec-estimator.cjs +125 -0
  96. package/dist-cjs/tokens/types.cjs +2 -0
  97. package/dist-cjs/tokens/validator.cjs +62 -0
  98. package/dist-cjs/types.cjs +2 -0
  99. package/dist-cjs/validate/compliance.cjs +103 -0
  100. package/dist-cjs/validate/connection.cjs +47 -0
  101. package/dist-cjs/validate/types.cjs +2 -0
  102. package/dist-cjs/validate/validate.cjs +319 -0
  103. package/docs/consumption.md +1 -1
  104. package/package.json +14 -8
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const stringify_js_1 = require("../ofs/stringify.cjs");
5
+ const enricher_js_1 = require("../ofs/enricher.cjs");
6
+ (0, vitest_1.describe)("Instructions Output Format Block Generation", () => {
7
+ const spec = {
8
+ description: "Standard report format for technical analysis.",
9
+ sections: [
10
+ {
11
+ name: "Summary",
12
+ kind: "text",
13
+ required: true,
14
+ description: "A brief summary of the topic.",
15
+ instruction: "Keep it under 3 sentences."
16
+ },
17
+ {
18
+ name: "Key Points",
19
+ kind: "list",
20
+ required: false,
21
+ description: "Important takeaways."
22
+ }
23
+ ],
24
+ emptySectionValue: "N/A"
25
+ };
26
+ (0, vitest_1.it)("should stringify an OFS object correctly", () => {
27
+ const md = (0, stringify_js_1.stringifyOutputFormatSpec)(spec);
28
+ (0, vitest_1.expect)(md).toContain("## Output format (Markdown)");
29
+ (0, vitest_1.expect)(md).toContain("Standard report format for technical analysis.");
30
+ (0, vitest_1.expect)(md).toContain("- Summary — text (required)");
31
+ (0, vitest_1.expect)(md).toContain("Description: A brief summary of the topic.");
32
+ (0, vitest_1.expect)(md).toContain("Instruction: Keep it under 3 sentences.");
33
+ (0, vitest_1.expect)(md).toContain("- Key Points — list (optional)");
34
+ (0, vitest_1.expect)(md).toContain("Description: Important takeaways.");
35
+ (0, vitest_1.expect)(md).toContain("If a section is empty, write `N/A`.");
36
+ });
37
+ (0, vitest_1.it)("should build markdown guidance (L1) correctly", () => {
38
+ const guidance = (0, enricher_js_1.buildMarkdownGuidance)(spec, { level: 1 });
39
+ (0, vitest_1.expect)(guidance).toContain("Include these section headings somewhere");
40
+ (0, vitest_1.expect)(guidance).toContain("- Summary");
41
+ (0, vitest_1.expect)(guidance).toContain("Description: A brief summary of the topic.");
42
+ (0, vitest_1.expect)(guidance).toContain("Instruction: Keep it under 3 sentences.");
43
+ (0, vitest_1.expect)(guidance).toContain("- Key Points");
44
+ (0, vitest_1.expect)(guidance).toContain("Description: Important takeaways.");
45
+ });
46
+ (0, vitest_1.it)("should build markdown guidance (L3) correctly", () => {
47
+ const guidance = (0, enricher_js_1.buildMarkdownGuidance)(spec, { level: 3 });
48
+ (0, vitest_1.expect)(guidance).toContain("Return your entire answer inside a single ```markdown fenced block");
49
+ (0, vitest_1.expect)(guidance).toContain("- Summary");
50
+ (0, vitest_1.expect)(guidance).toContain("- Key Points (list)");
51
+ (0, vitest_1.expect)(guidance).toContain("Do not return JSON");
52
+ });
53
+ });
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_js_1 = require("../index.cjs");
5
+ (0, vitest_1.describe)("Structural Diagnostics", () => {
6
+ (0, vitest_1.describe)("===key support", () => {
7
+ (0, vitest_1.it)("should parse ===key as a top-level section", () => {
8
+ const md = `===skills\nThis is the skills section.\n\n## Subskill\nDetails.`;
9
+ const outline = (0, index_js_1.buildOutline)(md);
10
+ (0, vitest_1.expect)(outline.nodes).toHaveLength(1);
11
+ (0, vitest_1.expect)(outline.nodes[0].title).toBe("skills");
12
+ (0, vitest_1.expect)(outline.nodes[0].children).toHaveLength(1);
13
+ (0, vitest_1.expect)(outline.nodes[0].children[0].title).toBe("Subskill");
14
+ });
15
+ });
16
+ (0, vitest_1.describe)("checkConnection", () => {
17
+ (0, vitest_1.it)("should find a section by name", () => {
18
+ const md = `# Overview\n## Details\n===skills\nContent.`;
19
+ const res = (0, index_js_1.checkConnection)(md, "skills");
20
+ (0, vitest_1.expect)(res.connected).toBe(true);
21
+ (0, vitest_1.expect)(res.path).toContain("skills");
22
+ });
23
+ (0, vitest_1.it)("should fail gracefully for missing sections", () => {
24
+ const md = `# Overview`;
25
+ const res = (0, index_js_1.checkConnection)(md, "missing");
26
+ (0, vitest_1.expect)(res.connected).toBe(false);
27
+ (0, vitest_1.expect)(res.suggestion).toContain("Overview");
28
+ });
29
+ });
30
+ });
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const validate_js_1 = require("../validate/validate.cjs");
5
+ const issuesEnvelope_js_1 = require("../ofs/issuesEnvelope.cjs");
6
+ const SPEC = {
7
+ emptySectionValue: "None",
8
+ sections: [
9
+ { name: "Short answer", kind: "text", required: true },
10
+ { name: "Long answer", kind: "text", required: true },
11
+ { name: "Reasoning", kind: "ordered_list", required: true },
12
+ { name: "Assumptions", kind: "list", required: true },
13
+ { name: "Unknowns", kind: "list", required: true },
14
+ ],
15
+ };
16
+ function mdL1Good() {
17
+ return [
18
+ "## Short answer",
19
+ "Yes.",
20
+ "",
21
+ "## Long answer",
22
+ "More details.",
23
+ "",
24
+ "## Reasoning",
25
+ "1. First",
26
+ "2. Second",
27
+ "",
28
+ "## Assumptions",
29
+ "- A",
30
+ "",
31
+ "## Unknowns",
32
+ "- U",
33
+ ].join("\n");
34
+ }
35
+ (0, vitest_1.describe)("parseIssuesEnvelope()", () => {
36
+ (0, vitest_1.it)("detects envelope and extracts bullets", () => {
37
+ const env = [
38
+ "## Status",
39
+ "Non-compliant output (cannot be repaired to the required format).",
40
+ "",
41
+ "## Issues",
42
+ "- Missing required section: \"Short answer\"",
43
+ "",
44
+ "## Expected",
45
+ "- Headings ...",
46
+ "",
47
+ "## Found",
48
+ "- Something ...",
49
+ "",
50
+ "## How to fix",
51
+ "- Do X",
52
+ ].join("\n");
53
+ const parsed = (0, issuesEnvelope_js_1.parseIssuesEnvelope)(env);
54
+ (0, vitest_1.expect)(parsed.isIssuesEnvelope).toBe(true);
55
+ (0, vitest_1.expect)(parsed.sections["issues"].bullets[0]).toContain("Missing required section");
56
+ });
57
+ });
58
+ (0, vitest_1.describe)("validateMarkdownAgainstOfs()", () => {
59
+ (0, vitest_1.it)("L0 accepts anything", () => {
60
+ const r = (0, validate_js_1.validateMarkdownAgainstOfs)("whatever", SPEC, 0);
61
+ (0, vitest_1.expect)(r.ok).toBe(true);
62
+ });
63
+ (0, vitest_1.it)("L1 fails when required section missing", () => {
64
+ const md = [
65
+ "## Short answer",
66
+ "Yes.",
67
+ "",
68
+ "## Reasoning",
69
+ "1. A",
70
+ ].join("\n");
71
+ const r = (0, validate_js_1.validateMarkdownAgainstOfs)(md, SPEC, 1);
72
+ (0, vitest_1.expect)(r.ok).toBe(false);
73
+ (0, vitest_1.expect)(r.issues.some(i => i.code === "MISSING_SECTION")).toBe(true);
74
+ });
75
+ (0, vitest_1.it)("L2 requires a single fenced markdown block", () => {
76
+ const r = (0, validate_js_1.validateMarkdownAgainstOfs)(mdL1Good(), SPEC, 2);
77
+ (0, vitest_1.expect)(r.ok).toBe(false);
78
+ (0, vitest_1.expect)(r.issues.some(i => i.code === "CONTAINER_MISSING")).toBe(true);
79
+ });
80
+ (0, vitest_1.it)("L2 passes with one fence containing valid L1", () => {
81
+ const md = ["```markdown", mdL1Good(), "```"].join("\n");
82
+ const r = (0, validate_js_1.validateMarkdownAgainstOfs)(md, SPEC, 2);
83
+ (0, vitest_1.expect)(r.ok).toBe(true);
84
+ });
85
+ (0, vitest_1.it)("L3 enforces section kinds (Reasoning must be ordered list)", () => {
86
+ const bad = [
87
+ "```markdown",
88
+ [
89
+ "## Short answer",
90
+ "Yes",
91
+ "",
92
+ "## Long answer",
93
+ "Details",
94
+ "",
95
+ "## Reasoning",
96
+ "- bullet but should be ordered",
97
+ "",
98
+ "## Assumptions",
99
+ "- A",
100
+ "",
101
+ "## Unknowns",
102
+ "- U",
103
+ ].join("\n"),
104
+ "```",
105
+ ].join("\n");
106
+ const r = (0, validate_js_1.validateMarkdownAgainstOfs)(bad, SPEC, 3);
107
+ (0, vitest_1.expect)(r.ok).toBe(false);
108
+ (0, vitest_1.expect)(r.issues.some(i => i.code === "WRONG_SECTION_KIND")).toBe(true);
109
+ });
110
+ });
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const index_js_1 = require("../index.cjs");
7
+ const [, , command, ...args] = process.argv;
8
+ async function main() {
9
+ if (!command || command === "--help" || command === "-h") {
10
+ printHelp();
11
+ return;
12
+ }
13
+ try {
14
+ switch (command) {
15
+ case "check":
16
+ await handleCheck(args);
17
+ break;
18
+ case "diagnose":
19
+ await handleDiagnose(args);
20
+ break;
21
+ case "create":
22
+ await handleCreate(args);
23
+ break;
24
+ case "sync":
25
+ await handleSync(args);
26
+ break;
27
+ default:
28
+ console.error(`Unknown command: ${command}`);
29
+ printHelp();
30
+ process.exit(1);
31
+ }
32
+ }
33
+ catch (error) {
34
+ console.error(`Error: ${error.message}`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ function printHelp() {
39
+ console.log(`
40
+ Flex-MD CLI v3.2.0
41
+
42
+ Usage:
43
+ flex-md <command> [options]
44
+
45
+ Commands:
46
+ check <file> [--level L2] Check compliance of a markdown file.
47
+ diagnose <file> <key> Diagnose if a specific key/section is connected.
48
+ create <file> <key> Create a new section (folder/file simulation).
49
+ sync <source> <target> Sync content from source to target.
50
+
51
+ Options:
52
+ --help, -h Show this help message.
53
+ `);
54
+ }
55
+ async function handleCheck(args) {
56
+ const file = args[0];
57
+ if (!file)
58
+ throw new Error("Missing file argument.");
59
+ const level = (args.includes("--level") ? args[args.indexOf("--level") + 1] : "L2");
60
+ const content = (0, fs_1.readFileSync)((0, path_1.resolve)(file), "utf-8");
61
+ const result = await (0, index_js_1.checkCompliance)(content, level);
62
+ if (result.meetsCompliance) {
63
+ console.log("✅ Content meets compliance.");
64
+ }
65
+ else {
66
+ console.log("❌ Content does not meet compliance.");
67
+ result.issues.forEach(i => console.log(` - [${i.severity}] ${i.description}`));
68
+ console.log("\nSuggestions:");
69
+ result.suggestions.forEach(s => console.log(` - ${s}`));
70
+ }
71
+ }
72
+ async function handleDiagnose(args) {
73
+ const file = args[0];
74
+ const key = args[1];
75
+ if (!file || !key)
76
+ throw new Error("Usage: flex-md diagnose <file> <key>");
77
+ if (!(0, fs_1.existsSync)(file))
78
+ throw new Error(`File not found: ${file}`);
79
+ const content = (0, fs_1.readFileSync)((0, path_1.resolve)(file), "utf-8");
80
+ const result = (0, index_js_1.checkConnection)(content, key);
81
+ if (result.connected) {
82
+ console.log(`✅ Key "${key}" found at path: ${result.path?.join(" -> ")}`);
83
+ }
84
+ else {
85
+ console.log(`❌ Key "${key}" not connected.`);
86
+ console.log(`Suggestion: ${result.suggestion}`);
87
+ }
88
+ }
89
+ async function handleCreate(args) {
90
+ const file = args[0];
91
+ const key = args[1];
92
+ if (!file || !key)
93
+ throw new Error("Usage: flex-md create <file> <key>");
94
+ const content = (0, fs_1.existsSync)(file) ? (0, fs_1.readFileSync)(file, "utf-8") : "";
95
+ const newContent = content + `\n\n## ${key}\n\n[Content for ${key}]\n`;
96
+ (0, fs_1.writeFileSync)(file, newContent);
97
+ console.log(`✅ Section "${key}" created in ${file}`);
98
+ }
99
+ async function handleSync(args) {
100
+ const source = args[0];
101
+ const target = args[1];
102
+ if (!source || !target)
103
+ throw new Error("Usage: flex-md sync <source> <target>");
104
+ if (!(0, fs_1.existsSync)(source))
105
+ throw new Error(`Source file not found: ${source}`);
106
+ const content = (0, fs_1.readFileSync)(source, "utf-8");
107
+ (0, fs_1.writeFileSync)(target, content);
108
+ console.log(`✅ Content synced from ${source} to ${target}`);
109
+ }
110
+ main();
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectJsonIntent = detectJsonIntent;
4
+ const HARD_PATTERNS = [
5
+ /\breturn\s+only\s+json\b/i,
6
+ /\boutput\s+only\s+json\b/i,
7
+ /\bvalid\s+json\s+(object|array)\b/i,
8
+ /\bno\s+(prose|text|markdown)\b/i,
9
+ /\bdo\s+not\s+output\s+anything\s+else\b/i,
10
+ ];
11
+ const SOFT_PATTERNS = [
12
+ /\binclude\s+json\b/i,
13
+ /\bjson\s+preferred\b/i,
14
+ /\badd\s+a\s+json\b/i,
15
+ /\bjson\s+format\b/i,
16
+ ];
17
+ const SCHEMA_PATTERNS = [
18
+ /\bjson\s+schema\b/i,
19
+ /\bschema\b.*\bjson\b/i,
20
+ /\bajv\b/i,
21
+ /\bzod\b/i,
22
+ /\bopenapi\b/i,
23
+ ];
24
+ const TOOLING_PATTERNS = [
25
+ /\bfunction\s+calling\b/i,
26
+ /\btools?\b/i,
27
+ /\bresponse_format\b/i,
28
+ /\btool\s+call\b/i,
29
+ /\barguments\b.*\bjson\b/i,
30
+ ];
31
+ function detectJsonIntent(text) {
32
+ const signals = [];
33
+ const add = (rx, strength, idx, len) => {
34
+ signals.push({
35
+ type: "pattern",
36
+ value: rx.source,
37
+ strength,
38
+ start: idx,
39
+ end: idx != null && len != null ? idx + len : undefined,
40
+ });
41
+ };
42
+ const scan = (patterns, strength) => {
43
+ for (const base of patterns) {
44
+ const rx = new RegExp(base.source, base.flags.includes("g") ? base.flags : base.flags + "g");
45
+ rx.lastIndex = 0;
46
+ let m;
47
+ while ((m = rx.exec(text)) !== null) {
48
+ add(base, strength, m.index, m[0]?.length ?? 0);
49
+ if (m[0]?.length === 0)
50
+ rx.lastIndex++;
51
+ }
52
+ }
53
+ };
54
+ scan(HARD_PATTERNS, "hard");
55
+ scan(TOOLING_PATTERNS, "soft");
56
+ scan(SCHEMA_PATTERNS, "soft");
57
+ scan(SOFT_PATTERNS, "soft");
58
+ const hasHard = signals.some(s => s.strength === "hard");
59
+ const hasSchema = signals.some(s => SCHEMA_PATTERNS.some(rx => rx.source === s.value));
60
+ const hasTooling = signals.some(s => TOOLING_PATTERNS.some(rx => rx.source === s.value));
61
+ const hasSoft = signals.some(s => s.strength === "soft");
62
+ let intent = "none";
63
+ if (hasHard)
64
+ intent = "hard";
65
+ else if (hasSchema)
66
+ intent = "schema";
67
+ else if (hasTooling)
68
+ intent = "tooling";
69
+ else if (hasSoft)
70
+ intent = "soft";
71
+ const confidence = scoreConfidence(intent, signals);
72
+ return { intent, signals, confidence };
73
+ }
74
+ function scoreConfidence(intent, signals) {
75
+ if (intent === "none")
76
+ return signals.length ? 0.2 : 0.0;
77
+ const hard = signals.filter(s => s.strength === "hard").length;
78
+ const soft = signals.filter(s => s.strength === "soft").length;
79
+ if (intent === "hard")
80
+ return Math.min(1, 0.7 + hard * 0.15);
81
+ return Math.min(1, 0.4 + soft * 0.1);
82
+ }
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ /**
3
+ * Helpers for "framed vs frameless" Markdown handling before Flex-MD.
4
+ *
5
+ * Goals:
6
+ * 1) Detect framed markdown (single fenced block) and extract its inner content.
7
+ * 2) If NOT markdown-ish, force-wrap into a minimal markdown shape so Flex-MD can still parse.
8
+ *
9
+ * Notes:
10
+ * - This is heuristic by design; tune thresholds as you learn your data.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.detectMarkdown = detectMarkdown;
14
+ exports.stripSingleFence = stripSingleFence;
15
+ exports.forceWrapAsMarkdown = forceWrapAsMarkdown;
16
+ exports.normalizeForFlexMd = normalizeForFlexMd;
17
+ const logger_js_1 = require("../../logger.cjs");
18
+ const SINGLE_FENCE_BLOCK_RE = /^```([a-zA-Z0-9_-]+)?([^\n]*)\n([\s\S]*?)\n```$/;
19
+ const FENCE_OPEN_RE = /(^|\n)```/g;
20
+ function detectMarkdown(text) {
21
+ logger_js_1.logger.debug("Starting markdown detection", {
22
+ inputType: typeof text,
23
+ inputLength: typeof text === "string" ? text.length : JSON.stringify(text ?? "").length
24
+ });
25
+ const reasons = [];
26
+ const raw = typeof text === "string" ? text : JSON.stringify(text ?? "");
27
+ const s = raw.replace(/\r\n/g, "\n");
28
+ logger_js_1.logger.verbose("Input normalized for processing", {
29
+ originalType: typeof text,
30
+ normalizedLength: s.length,
31
+ hasNewlines: s.includes('\n')
32
+ });
33
+ const codeFences = [...s.matchAll(FENCE_OPEN_RE)].length;
34
+ logger_js_1.logger.debug("Code fence analysis", {
35
+ totalFences: codeFences,
36
+ fenceRegex: FENCE_OPEN_RE.source
37
+ });
38
+ // Framed detection (single fenced block)
39
+ const m = s.match(SINGLE_FENCE_BLOCK_RE);
40
+ const isFramed = !!m && codeFences === 2;
41
+ const frameLanguage = isFramed ? (m?.[1] ?? null) : null;
42
+ logger_js_1.logger.debug("Framed markdown detection", {
43
+ regexMatch: !!m,
44
+ expectedFences: 2,
45
+ actualFences: codeFences,
46
+ isFramed,
47
+ frameLanguage,
48
+ regexGroups: m ? {
49
+ fullMatch: m[0],
50
+ language: m[1],
51
+ content: m[3]?.substring(0, 100) + (m[3]?.length > 100 ? '...' : '')
52
+ } : null
53
+ });
54
+ if (isFramed) {
55
+ reasons.push("Single fenced code block detected (framed payload).");
56
+ logger_js_1.logger.info("Detected framed markdown", { language: frameLanguage });
57
+ }
58
+ else if (codeFences > 2) {
59
+ reasons.push("Multiple fenced code blocks detected.");
60
+ logger_js_1.logger.debug("Multiple fences detected, not treating as single framed block", { fenceCount: codeFences });
61
+ }
62
+ else if (codeFences === 1) {
63
+ reasons.push("Single fence marker found but not properly framed.");
64
+ logger_js_1.logger.debug("Single fence found but regex didn't match", {
65
+ regex: SINGLE_FENCE_BLOCK_RE.source,
66
+ hasMatch: !!m
67
+ });
68
+ }
69
+ else {
70
+ logger_js_1.logger.debug("No fence markers detected");
71
+ }
72
+ const lines = s.split("\n");
73
+ logger_js_1.logger.verbose("Line-by-line analysis started", { totalLines: lines.length });
74
+ const atxHeadings = lines.filter((l) => /^#{1,6}\s+\S/.test(l.trim())).length;
75
+ let setextHeadings = 0;
76
+ for (let i = 0; i < lines.length - 1; i++) {
77
+ const cur = lines[i].trim();
78
+ const nxt = lines[i + 1].trim();
79
+ if (cur.length > 0 && (/^={2,}$/.test(nxt) || /^-{2,}$/.test(nxt)))
80
+ setextHeadings++;
81
+ }
82
+ const headings = atxHeadings + setextHeadings;
83
+ const unorderedListLines = lines.filter((l) => /^\s*[-*+]\s+\S/.test(l)).length;
84
+ const orderedListLines = lines.filter((l) => /^\s*\d{1,3}([.)])\s+\S/.test(l)).length;
85
+ const tableRows = lines.filter((l) => {
86
+ const t = l.trim();
87
+ if (!t.startsWith("|") || t.length < 3)
88
+ return false;
89
+ const pipeCount = (t.match(/\|/g) ?? []).length;
90
+ return pipeCount >= 2;
91
+ }).length;
92
+ const inlineCodeSpans = (s.match(/`[^`\n]+`/g) ?? []).length;
93
+ const mdLinks = (s.match(/\[[^\]]+\]\([^)]+\)/g) ?? []).length;
94
+ const emphasisTokens = (s.match(/\*\*[^*\n]+\*\*/g) ?? []).length +
95
+ (s.match(/__[^_\n]+__/g) ?? []).length +
96
+ (s.match(/(^|[^*])\*[^*\n]+\*([^*]|$)/g) ?? []).length +
97
+ (s.match(/(^|[^_])_[^_\n]+_([^_]|$)/g) ?? []).length;
98
+ logger_js_1.logger.debug("Markdown structure analysis", {
99
+ atxHeadings,
100
+ setextHeadings,
101
+ totalHeadings: headings,
102
+ unorderedListLines,
103
+ orderedListLines,
104
+ tableRows,
105
+ inlineCodeSpans,
106
+ mdLinks,
107
+ emphasisTokens
108
+ });
109
+ // Heuristic decision rules
110
+ const hasList = unorderedListLines + orderedListLines >= 2;
111
+ const hasTable = tableRows >= 2;
112
+ const hasOtherSignals = inlineCodeSpans + mdLinks + emphasisTokens >= 2;
113
+ logger_js_1.logger.debug("Heuristic analysis", {
114
+ hasList,
115
+ hasTable,
116
+ hasOtherSignals,
117
+ listThreshold: 2,
118
+ tableThreshold: 2,
119
+ otherSignalsThreshold: 2
120
+ });
121
+ let isMarkdownLikely = false;
122
+ if (isFramed) {
123
+ isMarkdownLikely = true;
124
+ logger_js_1.logger.debug("Markdown likelihood determined: framed content is always considered markdown");
125
+ }
126
+ else if (headings >= 2) {
127
+ isMarkdownLikely = true;
128
+ reasons.push(`Detected ${headings} markdown heading(s) (>=2).`);
129
+ logger_js_1.logger.debug("Markdown likelihood: sufficient headings detected", { headingCount: headings });
130
+ }
131
+ else if (headings >= 1 && (hasList || hasTable)) {
132
+ isMarkdownLikely = true;
133
+ reasons.push(`Detected heading(s) plus ${hasList ? "list" : "table"} structure.`);
134
+ logger_js_1.logger.debug("Markdown likelihood: heading plus structural element", {
135
+ hasHeading: headings >= 1,
136
+ hasList,
137
+ hasTable
138
+ });
139
+ }
140
+ else if ((hasList && hasTable) || (hasTable && hasOtherSignals) || (hasList && hasOtherSignals)) {
141
+ isMarkdownLikely = true;
142
+ reasons.push("Detected multiple markdown structural signals (lists/tables/links/code/emphasis).");
143
+ logger_js_1.logger.debug("Markdown likelihood: multiple structural signals", {
144
+ listAndTable: hasList && hasTable,
145
+ tableAndOther: hasTable && hasOtherSignals,
146
+ listAndOther: hasList && hasOtherSignals
147
+ });
148
+ }
149
+ else {
150
+ reasons.push("Insufficient markdown structure signals; treating as plain text.");
151
+ logger_js_1.logger.debug("Markdown likelihood: insufficient signals, treating as plain text", {
152
+ headings,
153
+ hasList,
154
+ hasTable,
155
+ hasOtherSignals
156
+ });
157
+ }
158
+ if (/^\s*#{1,6}\s+\S/.test(lines[0] ?? "")) {
159
+ reasons.push("Text starts with an ATX heading (#...).");
160
+ isMarkdownLikely = true;
161
+ logger_js_1.logger.debug("Additional check: starts with heading, upgrading to markdown");
162
+ }
163
+ return {
164
+ isMarkdownLikely,
165
+ isFramed,
166
+ frameLanguage,
167
+ reasons,
168
+ stats: {
169
+ headings,
170
+ atxHeadings,
171
+ setextHeadings,
172
+ unorderedListLines,
173
+ orderedListLines,
174
+ tableRows,
175
+ codeFences,
176
+ inlineCodeSpans,
177
+ mdLinks,
178
+ emphasisTokens,
179
+ },
180
+ };
181
+ }
182
+ /**
183
+ * If the entire payload is a single fenced block, return its inner content.
184
+ * Otherwise return the original text.
185
+ *
186
+ * - Also handles ```md / ```markdown / ```json etc.
187
+ */
188
+ function stripSingleFence(input) {
189
+ const s = input.replace(/\r\n/g, "\n").trim();
190
+ const fenceCount = [...s.matchAll(FENCE_OPEN_RE)].length;
191
+ if (fenceCount !== 2) {
192
+ return { stripped: input, wasFramed: false, language: null };
193
+ }
194
+ const m = s.match(SINGLE_FENCE_BLOCK_RE);
195
+ if (!m)
196
+ return { stripped: input, wasFramed: false, language: null };
197
+ const lang = (m[1] ?? null);
198
+ const inner = (m[3] ?? "").trim();
199
+ return { stripped: inner, wasFramed: true, language: lang };
200
+ }
201
+ /**
202
+ * Force-wrap non-markdown text into a minimal heading-based markdown document.
203
+ * This helps Flex-MD / nx-md-parser operate even when the model returns plain text.
204
+ *
205
+ * Choose a heading that exists in your OFS to maximize alignment.
206
+ * Default: "Full Answer" (common sink section).
207
+ */
208
+ function forceWrapAsMarkdown(plainText, opts) {
209
+ const heading = opts?.heading ?? "Full Answer";
210
+ const level = opts?.level ?? 3;
211
+ const hashes = "#".repeat(level);
212
+ const raw = plainText.replace(/\r\n/g, "\n");
213
+ const body = opts?.preserveLeadingWhitespace ? raw : raw.trim();
214
+ // If empty, keep it explicit.
215
+ const safeBody = body.length ? body : "None";
216
+ return `${hashes} ${heading}\n${safeBody}\n`;
217
+ }
218
+ /**
219
+ * End-to-end "normalize input for Flex-MD" helper:
220
+ * - If framed: strip the fence (so Flex-MD sees pure markdown)
221
+ * - Else: if markdown-likely: keep as-is
222
+ * - Else: wrap as markdown under a chosen heading
223
+ */
224
+ function normalizeForFlexMd(input, opts) {
225
+ logger_js_1.logger.info("Starting Flex-MD normalization", {
226
+ inputType: typeof input,
227
+ inputLength: typeof input === "string" ? input.length : JSON.stringify(input ?? "").length,
228
+ options: opts
229
+ });
230
+ const raw = typeof input === "string" ? input : JSON.stringify(input ?? "");
231
+ const detection = detectMarkdown(raw);
232
+ logger_js_1.logger.debug("Detection completed", {
233
+ isFramed: detection.isFramed,
234
+ isMarkdownLikely: detection.isMarkdownLikely,
235
+ frameLanguage: detection.frameLanguage,
236
+ reasons: detection.reasons
237
+ });
238
+ // 1) Strip if framed
239
+ const { stripped, wasFramed, language } = stripSingleFence(raw);
240
+ logger_js_1.logger.debug("Strip fence check", {
241
+ wasFramed,
242
+ language,
243
+ strippedLength: stripped.length,
244
+ originalLength: raw.length
245
+ });
246
+ if (wasFramed) {
247
+ logger_js_1.logger.info("Normalization: stripped framed markdown", {
248
+ language,
249
+ contentLength: stripped.length
250
+ });
251
+ return {
252
+ normalizedText: stripped,
253
+ detection,
254
+ wasStripped: true,
255
+ stripLanguage: language,
256
+ wasWrapped: false,
257
+ };
258
+ }
259
+ // 2) Keep if markdown-likely
260
+ if (detection.isMarkdownLikely) {
261
+ logger_js_1.logger.info("Normalization: keeping as-is (markdown-likely)", {
262
+ reasons: detection.reasons
263
+ });
264
+ return {
265
+ normalizedText: raw,
266
+ detection,
267
+ wasStripped: false,
268
+ stripLanguage: null,
269
+ wasWrapped: false,
270
+ };
271
+ }
272
+ // 3) Wrap if not markdown-likely
273
+ const fallbackHeading = opts?.fallbackHeading ?? "Full Answer";
274
+ const fallbackLevel = opts?.fallbackHeadingLevel ?? 3;
275
+ logger_js_1.logger.info("Normalization: wrapping as markdown", {
276
+ fallbackHeading,
277
+ fallbackLevel,
278
+ reasons: detection.reasons
279
+ });
280
+ const wrapped = forceWrapAsMarkdown(raw, {
281
+ heading: fallbackHeading,
282
+ level: fallbackLevel,
283
+ });
284
+ logger_js_1.logger.debug("Wrapping completed", {
285
+ wrappedLength: wrapped.length,
286
+ originalLength: raw.length
287
+ });
288
+ return {
289
+ normalizedText: wrapped,
290
+ detection,
291
+ wasStripped: false,
292
+ stripLanguage: null,
293
+ wasWrapped: true,
294
+ };
295
+ }
296
+ /**
297
+ * Example integration with Flex-MD:
298
+ *
299
+ * import { parseOutputFormatSpec, transformWithOfs } from 'flex-md';
300
+ *
301
+ * const spec = parseOutputFormatSpec(ofsMarkdown);
302
+ * const prep = normalizeForFlexMd(llmText, { fallbackHeading: "Full Answer" });
303
+ * const out = transformWithOfs(prep.normalizedText, spec);
304
+ */