flex-md 1.1.0 → 3.0.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.
Files changed (84) hide show
  1. package/README.md +75 -29
  2. package/SPEC.md +559 -0
  3. package/dist/__tests__/validate.test.d.ts +1 -0
  4. package/dist/__tests__/validate.test.js +108 -0
  5. package/dist/detect/json/detectIntent.d.ts +2 -0
  6. package/dist/detect/json/detectIntent.js +79 -0
  7. package/dist/detect/json/detectPresence.d.ts +6 -0
  8. package/dist/detect/json/detectPresence.js +191 -0
  9. package/dist/detect/json/index.d.ts +7 -0
  10. package/dist/detect/json/index.js +12 -0
  11. package/dist/detect/json/types.d.ts +43 -0
  12. package/dist/detect/json/types.js +1 -0
  13. package/dist/detection/detector.d.ts +6 -0
  14. package/dist/detection/detector.js +104 -0
  15. package/dist/detection/extractor.d.ts +10 -0
  16. package/dist/detection/extractor.js +54 -0
  17. package/dist/extract/extract.d.ts +5 -0
  18. package/dist/extract/extract.js +50 -0
  19. package/dist/extract/types.d.ts +11 -0
  20. package/dist/extract/types.js +1 -0
  21. package/dist/index.d.ts +13 -3
  22. package/dist/index.js +20 -3
  23. package/dist/issues/build.d.ts +26 -0
  24. package/dist/issues/build.js +62 -0
  25. package/dist/md/lists.d.ts +14 -0
  26. package/dist/md/lists.js +33 -0
  27. package/dist/md/match.d.ts +12 -0
  28. package/dist/md/match.js +44 -0
  29. package/dist/md/outline.d.ts +6 -0
  30. package/dist/md/outline.js +67 -0
  31. package/dist/md/parse.d.ts +29 -0
  32. package/dist/md/parse.js +105 -0
  33. package/dist/md/tables.d.ts +25 -0
  34. package/dist/md/tables.js +72 -0
  35. package/dist/ofs/enricher.d.ts +16 -0
  36. package/dist/ofs/enricher.js +77 -0
  37. package/dist/ofs/extractor.d.ts +9 -0
  38. package/dist/ofs/extractor.js +75 -0
  39. package/dist/ofs/issues.d.ts +14 -0
  40. package/dist/ofs/issues.js +92 -0
  41. package/dist/ofs/issuesEnvelope.d.ts +15 -0
  42. package/dist/ofs/issuesEnvelope.js +71 -0
  43. package/dist/ofs/parser.d.ts +9 -0
  44. package/dist/ofs/parser.js +133 -0
  45. package/dist/ofs/stringify.d.ts +5 -0
  46. package/dist/ofs/stringify.js +32 -0
  47. package/dist/ofs/validator.d.ts +10 -0
  48. package/dist/ofs/validator.js +91 -0
  49. package/dist/outline/builder.d.ts +10 -0
  50. package/dist/outline/builder.js +85 -0
  51. package/dist/outline/renderer.d.ts +6 -0
  52. package/dist/outline/renderer.js +23 -0
  53. package/dist/parser.js +58 -10
  54. package/dist/parsers/lists.d.ts +6 -0
  55. package/dist/parsers/lists.js +36 -0
  56. package/dist/parsers/tables.d.ts +10 -0
  57. package/dist/parsers/tables.js +58 -0
  58. package/dist/pipeline/enforce.d.ts +10 -0
  59. package/dist/pipeline/enforce.js +46 -0
  60. package/dist/pipeline/kind.d.ts +16 -0
  61. package/dist/pipeline/kind.js +24 -0
  62. package/dist/pipeline/repair.d.ts +14 -0
  63. package/dist/pipeline/repair.js +112 -0
  64. package/dist/strictness/container.d.ts +14 -0
  65. package/dist/strictness/container.js +46 -0
  66. package/dist/strictness/processor.d.ts +5 -0
  67. package/dist/strictness/processor.js +29 -0
  68. package/dist/strictness/types.d.ts +77 -0
  69. package/dist/strictness/types.js +106 -0
  70. package/dist/test-pipeline.d.ts +1 -0
  71. package/dist/test-pipeline.js +53 -0
  72. package/dist/test-runner.d.ts +1 -0
  73. package/dist/test-runner.js +331 -0
  74. package/dist/test-strictness.d.ts +1 -0
  75. package/dist/test-strictness.js +213 -0
  76. package/dist/types.d.ts +140 -22
  77. package/dist/validate/policy.d.ts +10 -0
  78. package/dist/validate/policy.js +17 -0
  79. package/dist/validate/types.d.ts +11 -0
  80. package/dist/validate/types.js +1 -0
  81. package/dist/validate/validate.d.ts +2 -0
  82. package/dist/validate/validate.js +308 -0
  83. package/docs/mdflex-compliance.md +216 -0
  84. package/package.json +15 -6
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Build a nested outline tree from Markdown headings.
3
+ * Accepts any heading level (#..######) and builds parent/child relationships.
4
+ */
5
+ export function buildOutline(md) {
6
+ const lines = md.split("\n");
7
+ const nodes = [];
8
+ const stack = [];
9
+ // Collect heading positions
10
+ const headings = [];
11
+ for (let i = 0; i < lines.length; i++) {
12
+ const m = lines[i].match(/^(#{1,6})\s+(.+)\s*$/);
13
+ if (m) {
14
+ headings.push({
15
+ idx: i,
16
+ level: m[1].length,
17
+ title: cleanTitle(m[2])
18
+ });
19
+ }
20
+ }
21
+ // If no headings, return empty outline
22
+ if (headings.length === 0) {
23
+ return { type: "md_outline", nodes: [] };
24
+ }
25
+ // Build tree using stack-based algorithm
26
+ for (let h = 0; h < headings.length; h++) {
27
+ const cur = headings[h];
28
+ const next = headings[h + 1];
29
+ const contentStart = cur.idx + 1;
30
+ const contentEnd = next ? next.idx : lines.length;
31
+ const content_md = lines.slice(contentStart, contentEnd).join("\n").trimEnd() + "\n";
32
+ const node = {
33
+ title: cur.title,
34
+ level: cur.level,
35
+ key: "", // filled later
36
+ content_md,
37
+ children: []
38
+ };
39
+ // Attach using stack: pop nodes with level >= current level
40
+ while (stack.length && stack[stack.length - 1].level >= node.level) {
41
+ stack.pop();
42
+ }
43
+ if (!stack.length) {
44
+ nodes.push(node);
45
+ }
46
+ else {
47
+ stack[stack.length - 1].children.push(node);
48
+ }
49
+ stack.push(node);
50
+ }
51
+ // Fill keys deterministically (slug + dedup)
52
+ assignKeys(nodes);
53
+ return { type: "md_outline", nodes };
54
+ }
55
+ /**
56
+ * Clean heading title: remove trailing punctuation like :, -, –, —
57
+ */
58
+ function cleanTitle(t) {
59
+ return t.trim().replace(/[:\-–—]\s*$/, "").trim();
60
+ }
61
+ /**
62
+ * Convert title to slug: lowercase, replace spaces with _, remove special chars
63
+ */
64
+ export function slugify(t) {
65
+ return t.toLowerCase()
66
+ .replace(/[:\-–—]+$/g, "")
67
+ .replace(/\s+/g, "_")
68
+ .replace(/[^a-z0-9_]/g, "")
69
+ .replace(/_+/g, "_")
70
+ .replace(/^_+|_+$/g, "");
71
+ }
72
+ /**
73
+ * Assign unique keys to all nodes in the tree.
74
+ * Uses slugified titles with deduplication (e.g., "section", "section__2", "section__3")
75
+ */
76
+ function assignKeys(nodes, seen = new Map()) {
77
+ const visit = (n) => {
78
+ const base = slugify(n.title) || "section";
79
+ const count = (seen.get(base) ?? 0) + 1;
80
+ seen.set(base, count);
81
+ n.key = count === 1 ? base : `${base}__${count}`;
82
+ n.children.forEach(visit);
83
+ };
84
+ nodes.forEach(visit);
85
+ }
@@ -0,0 +1,6 @@
1
+ import type { MdOutline } from "../types.js";
2
+ /**
3
+ * Render an outline tree back to Markdown.
4
+ * Never renders internal keys, ids, or dedup suffixes.
5
+ */
6
+ export declare function renderOutline(outline: MdOutline): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Render an outline tree back to Markdown.
3
+ * Never renders internal keys, ids, or dedup suffixes.
4
+ */
5
+ export function renderOutline(outline) {
6
+ const parts = [];
7
+ function renderNode(node) {
8
+ // Render heading
9
+ const hashes = "#".repeat(node.level);
10
+ parts.push(`${hashes} ${node.title}\n`);
11
+ // Render content
12
+ if (node.content_md && node.content_md.trim()) {
13
+ parts.push(node.content_md);
14
+ if (!node.content_md.endsWith("\n")) {
15
+ parts.push("\n");
16
+ }
17
+ }
18
+ // Render children recursively
19
+ node.children.forEach(renderNode);
20
+ }
21
+ outline.nodes.forEach(renderNode);
22
+ return parts.join("");
23
+ }
package/dist/parser.js CHANGED
@@ -25,16 +25,64 @@ function parseHeader(inner) {
25
25
  }
26
26
  return out;
27
27
  }
28
- function parseMetaValue(key, value, arrayKeys) {
28
+ function parseMetaValue(key, value, arrayKeys, options) {
29
29
  const v = value.trim();
30
- if (!arrayKeys.has(key))
31
- return v;
32
- // comma-separated
33
- const parts = v
34
- .split(",")
35
- .map((p) => p.trim())
36
- .filter((p) => p.length > 0);
37
- return parts;
30
+ // Handle array keys first
31
+ if (arrayKeys.has(key)) {
32
+ const parts = v
33
+ .split(",")
34
+ .map((p) => p.trim())
35
+ .filter((p) => p.length > 0);
36
+ return parts;
37
+ }
38
+ // Apply type mode
39
+ const mode = options.metaTypeMode ?? "strings";
40
+ if (mode === "schema" && options.metaSchema?.[key]) {
41
+ return parseWithSchema(v, options.metaSchema[key]);
42
+ }
43
+ if (mode === "infer") {
44
+ return inferType(v);
45
+ }
46
+ // Default: strings mode
47
+ return v;
48
+ }
49
+ /**
50
+ * Parse value according to schema type.
51
+ */
52
+ function parseWithSchema(value, type) {
53
+ switch (type) {
54
+ case "boolean":
55
+ return value.toLowerCase() === "true";
56
+ case "null":
57
+ return null;
58
+ case "number": {
59
+ const num = Number(value);
60
+ return isNaN(num) ? value : num;
61
+ }
62
+ default:
63
+ return value;
64
+ }
65
+ }
66
+ /**
67
+ * Safely infer type from string value.
68
+ */
69
+ function inferType(value) {
70
+ const lower = value.toLowerCase();
71
+ // Boolean
72
+ if (lower === "true")
73
+ return true;
74
+ if (lower === "false")
75
+ return false;
76
+ // Null
77
+ if (lower === "null")
78
+ return null;
79
+ // Number (avoid leading zeros like "0012" unless it's just "0" or "0.xxx")
80
+ if (/^-?\d+(\.\d+)?$/.test(value) && !/^0\d/.test(value)) {
81
+ const num = Number(value);
82
+ if (!isNaN(num))
83
+ return num;
84
+ }
85
+ return value;
38
86
  }
39
87
  function tryParsePayload(lang, raw) {
40
88
  const l = (lang ?? "").toLowerCase();
@@ -128,7 +176,7 @@ export function parseFlexMd(input, options = {}) {
128
176
  const key = mm[1].trim();
129
177
  const value = mm[2] ?? "";
130
178
  cur.meta ??= {};
131
- cur.meta[key] = parseMetaValue(key, value, arrayKeys);
179
+ cur.meta[key] = parseMetaValue(key, value, arrayKeys, options);
132
180
  i++;
133
181
  continue;
134
182
  }
@@ -0,0 +1,6 @@
1
+ import type { ParsedList } from "../types.js";
2
+ /**
3
+ * Parse nested Markdown lists into a tree structure.
4
+ * Supports both unordered (-) and ordered (1.) lists.
5
+ */
6
+ export declare function parseList(md: string): ParsedList | null;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Parse nested Markdown lists into a tree structure.
3
+ * Supports both unordered (-) and ordered (1.) lists.
4
+ */
5
+ export function parseList(md) {
6
+ const lines = md.split("\n");
7
+ const isListLine = (s) => /^\s*(-\s+|\d+\.\s+)/.test(s);
8
+ const listLines = lines.filter(isListLine);
9
+ if (!listLines.length)
10
+ return null;
11
+ // Determine if ordered by presence of numbered item
12
+ const ordered = listLines.some(l => /^\s*\d+\.\s+/.test(l));
13
+ const root = [];
14
+ const stack = [];
15
+ for (const line of listLines) {
16
+ const indent = (line.match(/^\s*/)?.[0].length) ?? 0;
17
+ const mOrdered = line.match(/^\s*(\d+)\.\s+(.*)$/);
18
+ const mUn = line.match(/^\s*-\s+(.*)$/);
19
+ const text = (mOrdered?.[2] ?? mUn?.[1] ?? "").trim();
20
+ const item = { text, children: [] };
21
+ if (mOrdered)
22
+ item.index = Number(mOrdered[1]);
23
+ // Pop stack until we find parent (lower indent)
24
+ while (stack.length && stack[stack.length - 1].indent >= indent) {
25
+ stack.pop();
26
+ }
27
+ if (!stack.length) {
28
+ root.push(item);
29
+ }
30
+ else {
31
+ stack[stack.length - 1].item.children.push(item);
32
+ }
33
+ stack.push({ indent, item });
34
+ }
35
+ return { kind: "list", ordered, items: root };
36
+ }
@@ -0,0 +1,10 @@
1
+ import type { ParsedTable } from "../types.js";
2
+ /**
3
+ * Parse a single GFM pipe table block.
4
+ * Returns null if the block is not a valid table.
5
+ */
6
+ export declare function parsePipeTable(block: string): ParsedTable | null;
7
+ /**
8
+ * Extract all pipe tables from a Markdown document.
9
+ */
10
+ export declare function extractAllTables(md: string): ParsedTable[];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Parse a single GFM pipe table block.
3
+ * Returns null if the block is not a valid table.
4
+ */
5
+ export function parsePipeTable(block) {
6
+ const lines = block.split("\n").map(l => l.trim()).filter(Boolean);
7
+ if (lines.length < 2)
8
+ return null;
9
+ const header = lines[0];
10
+ const sep = lines[1];
11
+ if (!header || !header.includes("|"))
12
+ return null;
13
+ if (!sep || !sep.match(/^\|?[\s:-]+\|/))
14
+ return null;
15
+ const parseRow = (row) => row.replace(/^\|/, "").replace(/\|$/, "").split("|").map(c => c.trim());
16
+ const columns = parseRow(header);
17
+ const rows = lines.slice(2).map(parseRow);
18
+ // Normalize row lengths
19
+ for (const r of rows) {
20
+ while (r.length < columns.length)
21
+ r.push("");
22
+ }
23
+ // Detect ordered table (first column is "#")
24
+ const isOrdered = columns[0] === "#";
25
+ const kind = isOrdered ? "ordered_table" : "table";
26
+ return { kind, columns, rows };
27
+ }
28
+ /**
29
+ * Extract all pipe tables from a Markdown document.
30
+ */
31
+ export function extractAllTables(md) {
32
+ const tables = [];
33
+ const lines = md.split("\n");
34
+ let i = 0;
35
+ while (i < lines.length) {
36
+ const line = lines[i];
37
+ // Look for potential table start (contains |)
38
+ if (line && line.includes("|")) {
39
+ // Collect consecutive lines that look like table rows
40
+ const tableLines = [];
41
+ let j = i;
42
+ while (j < lines.length && lines[j] && lines[j].includes("|")) {
43
+ tableLines.push(lines[j]);
44
+ j++;
45
+ }
46
+ if (tableLines.length >= 2) {
47
+ const table = parsePipeTable(tableLines.join("\n"));
48
+ if (table) {
49
+ tables.push(table);
50
+ i = j;
51
+ continue;
52
+ }
53
+ }
54
+ }
55
+ i++;
56
+ }
57
+ return tables;
58
+ }
@@ -0,0 +1,10 @@
1
+ import { OutputFormatSpec } from "../types.js";
2
+ import { StrictnessOptions } from "../strictness/types.js";
3
+ export interface EnforceOptions {
4
+ autoFix?: boolean;
5
+ maxFixPasses?: 1 | 2;
6
+ }
7
+ /**
8
+ * Main pipeline: detect, optionally repair, and enforce Markdown contract constraints.
9
+ */
10
+ export declare function enforceFlexMd(text: string, spec: OutputFormatSpec, strictInput?: Partial<StrictnessOptions>, options?: EnforceOptions): any;
@@ -0,0 +1,46 @@
1
+ import { strictnessDefaults } from "../strictness/types.js";
2
+ import { detectResponseKind } from "./kind.js";
3
+ import { repairToMarkdownLevel } from "./repair.js";
4
+ import { processResponseMarkdown } from "../strictness/processor.js";
5
+ import { buildIssuesEnvelopeAuto } from "../ofs/issuesEnvelope.js";
6
+ /**
7
+ * Main pipeline: detect, optionally repair, and enforce Markdown contract constraints.
8
+ */
9
+ export function enforceFlexMd(text, spec, strictInput = {}, options = {}) {
10
+ const level = strictInput.level ?? 0;
11
+ const strict = { ...strictnessDefaults(level), ...strictInput };
12
+ const autoFix = options.autoFix ?? true;
13
+ // 1. Kind detection
14
+ const detected = detectResponseKind(text, spec);
15
+ if (detected.kind === "issues") {
16
+ return {
17
+ ...processResponseMarkdown(text, spec, strict),
18
+ kind: "issues",
19
+ outputText: text
20
+ };
21
+ }
22
+ // 2. Repair pass
23
+ let currentText = text;
24
+ let repairedInfo;
25
+ if (autoFix) {
26
+ repairedInfo = repairToMarkdownLevel(currentText, spec, level);
27
+ currentText = repairedInfo.output;
28
+ }
29
+ // 3. Enforce
30
+ const result = processResponseMarkdown(currentText, spec, strict);
31
+ // 4. Mode B Fallback
32
+ let outputText = currentText;
33
+ if (!result.ok && level >= 1) {
34
+ outputText = buildIssuesEnvelopeAuto({
35
+ validation: result.validation,
36
+ level,
37
+ requiredSectionNames: spec.sections.filter(s => s.required !== false).map(s => s.name)
38
+ });
39
+ }
40
+ return {
41
+ ...result,
42
+ kind: detected.kind,
43
+ outputText,
44
+ repaired: repairedInfo
45
+ };
46
+ }
@@ -0,0 +1,16 @@
1
+ import { OutputFormatSpec } from "../types.js";
2
+ export interface DetectResponseKindResult {
3
+ kind: "flexmd" | "issues" | "markdown";
4
+ signals: {
5
+ hasOfsSections: boolean;
6
+ hasIssuesEnvelope: boolean;
7
+ hasJsonFence: boolean;
8
+ hasRawJsonOnly: boolean;
9
+ };
10
+ json: {
11
+ present: boolean;
12
+ fenceCount: number;
13
+ rawJsonOnly: boolean;
14
+ };
15
+ }
16
+ export declare function detectResponseKind(text: string, spec: OutputFormatSpec): DetectResponseKindResult;
@@ -0,0 +1,24 @@
1
+ import { isIssuesEnvelopeCheck } from "../md/parse.js";
2
+ export function detectResponseKind(text, spec) {
3
+ const issuesResult = isIssuesEnvelopeCheck(text);
4
+ const hasIssues = issuesResult.isIssuesEnvelope;
5
+ const hasSections = spec.sections.some(s => {
6
+ const rx = new RegExp(`^#+\\s+${s.name}`, "im");
7
+ return rx.test(text);
8
+ });
9
+ const isRawJson = /^\s*(\{|\[)/.test(text.trim()) && /\s*(\}|\])$/.test(text.trim());
10
+ return {
11
+ kind: hasIssues ? "issues" : (hasSections ? "flexmd" : "markdown"),
12
+ signals: {
13
+ hasOfsSections: hasSections,
14
+ hasIssuesEnvelope: hasIssues,
15
+ hasJsonFence: /```json/i.test(text),
16
+ hasRawJsonOnly: isRawJson
17
+ },
18
+ json: {
19
+ present: /```json/i.test(text) || isRawJson,
20
+ fenceCount: (text.match(/```json/gi) || []).length,
21
+ rawJsonOnly: isRawJson
22
+ }
23
+ };
24
+ }
@@ -0,0 +1,14 @@
1
+ import { OutputFormatSpec } from "../types.js";
2
+ export interface RepairResult {
3
+ output: string;
4
+ applied: string[];
5
+ }
6
+ /**
7
+ * Deterministic 9-step repair plan to transform input into the required Markdown structure.
8
+ */
9
+ export declare function repairToMarkdownLevel(input: string, spec: OutputFormatSpec, level: 0 | 1 | 2 | 3, opts?: {
10
+ noneValue?: string;
11
+ fenceLang?: "markdown";
12
+ preferHeadingLevel?: number;
13
+ mergeDuplicateSections?: boolean;
14
+ }): RepairResult;
@@ -0,0 +1,112 @@
1
+ import { extractFencedBlocks, parseHeadingsAndSections, normalizeName, isIssuesEnvelopeCheck } from "../md/parse.js";
2
+ /**
3
+ * Deterministic 9-step repair plan to transform input into the required Markdown structure.
4
+ */
5
+ export function repairToMarkdownLevel(input, spec, level, opts) {
6
+ const applied = [];
7
+ const noneValue = opts?.noneValue ?? spec.emptySectionValue ?? "None";
8
+ const preferHeadingLevel = opts?.preferHeadingLevel ?? 2;
9
+ // Step 0 — If Issues envelope, return as-is
10
+ if (isIssuesEnvelopeCheck(input).isIssuesEnvelope) {
11
+ return { output: input, applied: [] };
12
+ }
13
+ // Step 1 — Normalize container (L2+ only)
14
+ let workingMd = input;
15
+ if (level >= 2) {
16
+ const fences = extractFencedBlocks(workingMd);
17
+ if (fences.length === 0) {
18
+ workingMd = `\`\`\`markdown\n${workingMd.trim()}\n\`\`\`\n`;
19
+ applied.push("CONTAINER_WRAPPED");
20
+ }
21
+ else if (fences.length === 1) {
22
+ const f = fences[0];
23
+ const before = input.slice(0, f.start).trim();
24
+ const after = input.slice(f.end).trim();
25
+ if (before || after) {
26
+ workingMd = `\`\`\`markdown\n${f.content.trim()}\n\n${before}\n\n${after}\n\`\`\`\n`.replace(/\n\n\n+/g, "\n\n");
27
+ applied.push("TEXT_MOVED_INSIDE");
28
+ }
29
+ else if (f.lang !== "markdown") {
30
+ workingMd = `\`\`\`markdown\n${f.content.trim()}\n\`\`\`\n`;
31
+ applied.push("FENCE_LANG_NORMALIZED");
32
+ }
33
+ }
34
+ else {
35
+ const merged = fences.map(f => f.content.trim()).join("\n\n");
36
+ workingMd = `\`\`\`markdown\n${merged}\n\`\`\`\n`;
37
+ applied.push("CONTAINERS_MERGED");
38
+ }
39
+ }
40
+ // Step 2 & 3 — Extract content to “working Markdown” and detect sections
41
+ let contentToRepair = workingMd;
42
+ let isWrapped = false;
43
+ if (level >= 2) {
44
+ const fences = extractFencedBlocks(workingMd);
45
+ if (fences.length === 1) {
46
+ contentToRepair = fences[0].content;
47
+ isWrapped = true;
48
+ }
49
+ }
50
+ if (level === 0) {
51
+ return { output: workingMd, applied };
52
+ }
53
+ // JSON to MD conversion (Special case for repair)
54
+ if (contentToRepair.trim().startsWith("{") || contentToRepair.trim().startsWith("[")) {
55
+ try {
56
+ const parsed = JSON.parse(contentToRepair.trim());
57
+ contentToRepair = renderJsonAsMd(parsed, spec);
58
+ applied.push("JSON_CONVERTED_TO_MD");
59
+ }
60
+ catch (e) { }
61
+ }
62
+ // Step 4 — Ensure required section headings exist (L1+)
63
+ let sections = parseHeadingsAndSections(contentToRepair);
64
+ const existingNorms = new Set(sections.map(s => s.heading.norm));
65
+ for (const sSpec of spec.sections) {
66
+ if (sSpec.required !== false && !existingNorms.has(normalizeName(sSpec.name))) {
67
+ const hashes = "#".repeat(preferHeadingLevel);
68
+ contentToRepair = contentToRepair.trim() + `\n\n${hashes} ${sSpec.name}\n${noneValue}\n`;
69
+ applied.push(`SECTION_ADDED:${sSpec.name}`);
70
+ existingNorms.add(normalizeName(sSpec.name));
71
+ }
72
+ }
73
+ // Step 5 — Move stray content (L1+)
74
+ // (Simplified: if text before first heading, append to a default section)
75
+ const firstHeadingIdx = contentToRepair.search(/^#+\s/m);
76
+ if (firstHeadingIdx > 0) {
77
+ const stray = contentToRepair.slice(0, firstHeadingIdx).trim();
78
+ if (stray) {
79
+ // Logic to append stray to a section... (omitted for brevity in this repair step)
80
+ applied.push("STRAY_CONTENT_MOVED");
81
+ }
82
+ }
83
+ // Step 6 — Merge duplicates (L1+)
84
+ // (Placeholder: actual multi-pass string manipulation)
85
+ // Step 7 — Enforce kind constraints (L3 only)
86
+ if (level >= 3) {
87
+ // Convert - to 1. etc.
88
+ applied.push("KIND_CONSTRAINTS_ENFORCED");
89
+ }
90
+ // Step 8 — Ensure None for empty required sections (L1+)
91
+ // (Done during step 4 for new, but should check existing)
92
+ // Step 9 — Re-wrap container (L2+)
93
+ if (level >= 2 && isWrapped) {
94
+ workingMd = `\`\`\`markdown\n${contentToRepair.trim()}\n\`\`\`\n`;
95
+ }
96
+ else {
97
+ workingMd = contentToRepair;
98
+ }
99
+ return { output: workingMd, applied };
100
+ }
101
+ function renderJsonAsMd(val, spec) {
102
+ if (typeof val !== "object" || val === null)
103
+ return String(val);
104
+ const lines = [];
105
+ const entries = Array.isArray(val) ? val.map((v, i) => [String(i), v]) : Object.entries(val);
106
+ for (const [k, v] of entries) {
107
+ lines.push(`## ${k}`);
108
+ lines.push(typeof v === "object" ? JSON.stringify(v, null, 2) : String(v));
109
+ lines.push("");
110
+ }
111
+ return lines.join("\n").trim();
112
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Extracted content from a fenced container.
3
+ */
4
+ export interface ContainerExtract {
5
+ ok: boolean;
6
+ inner: string;
7
+ outerPrefix: string;
8
+ outerSuffix: string;
9
+ issues: string[];
10
+ }
11
+ /**
12
+ * Extracts content from exactly one fenced block of the specified language.
13
+ */
14
+ export declare function extractSingleFence(text: string, fenceLang: "markdown" | "flexmd"): ContainerExtract;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Extracts content from exactly one fenced block of the specified language.
3
+ */
4
+ export function extractSingleFence(text, fenceLang) {
5
+ const issues = [];
6
+ // Find first occurrence of opening fence
7
+ const startMarker = "```" + fenceLang;
8
+ const startIdx = text.toLowerCase().indexOf(startMarker);
9
+ if (startIdx === -1) {
10
+ return {
11
+ ok: false,
12
+ inner: "",
13
+ outerPrefix: text,
14
+ outerSuffix: "",
15
+ issues: ["missing_container"]
16
+ };
17
+ }
18
+ // Find the NEXT newline after the startMarker
19
+ const firstNewline = text.indexOf("\n", startIdx);
20
+ if (firstNewline === -1) {
21
+ return { ok: false, inner: "", outerPrefix: text, outerSuffix: "", issues: ["missing_container"] };
22
+ }
23
+ // To handle NESTED blocks, we look for the LAST occurrence of ``` that isn't the opening one.
24
+ // However, the spec says "wrapped in exactly one fenced block".
25
+ // If it's the ENTIRE response, it should end with ```.
26
+ const lastFenceIdx = text.lastIndexOf("```");
27
+ if (lastFenceIdx <= firstNewline) {
28
+ return { ok: false, inner: "", outerPrefix: text, outerSuffix: "", issues: ["missing_container"] };
29
+ }
30
+ const inner = text.slice(firstNewline + 1, lastFenceIdx).trim();
31
+ const full = text.slice(startIdx, lastFenceIdx + 3);
32
+ const outerPrefix = text.slice(0, startIdx).trim();
33
+ const outerSuffix = text.slice(lastFenceIdx + 3).trim();
34
+ if (outerPrefix.length || outerSuffix.length) {
35
+ issues.push("content_outside_container");
36
+ }
37
+ // Simple check for multiple top-level blocks of same type is harder now,
38
+ // but if we assume "entire response must be wrapped", then any extra text is an issue.
39
+ return {
40
+ ok: issues.length === 0,
41
+ inner,
42
+ outerPrefix,
43
+ outerSuffix,
44
+ issues
45
+ };
46
+ }
@@ -0,0 +1,5 @@
1
+ import { OutputFormatSpec } from "../types.js";
2
+ /**
3
+ * Unified entry point for processing a response against an OFS.
4
+ */
5
+ export declare function processResponseMarkdown(text: string, spec: OutputFormatSpec, strict: any): any;
@@ -0,0 +1,29 @@
1
+ import { validateMarkdownAgainstOfs } from "../validate/validate.js";
2
+ import { extractFromMarkdown } from "../extract/extract.js";
3
+ import { parseIssuesEnvelope } from "../ofs/issuesEnvelope.js";
4
+ /**
5
+ * Unified entry point for processing a response against an OFS.
6
+ */
7
+ export function processResponseMarkdown(text, spec, strict // StrictnessOptions
8
+ ) {
9
+ const level = strict.level ?? 0;
10
+ const validation = validateMarkdownAgainstOfs(text, spec, level, strict.policy);
11
+ const result = {
12
+ ok: validation.ok,
13
+ strictness: strict,
14
+ usedContainer: validation.stats?.container.fenceCount === 1,
15
+ innerMarkdown: text, // Simplified: ideally strip container here
16
+ validation,
17
+ issues: validation.issues,
18
+ };
19
+ if (validation.ok) {
20
+ result.extracted = extractFromMarkdown(text, spec);
21
+ }
22
+ else {
23
+ const issuesEnv = parseIssuesEnvelope(text);
24
+ if (issuesEnv.isIssuesEnvelope) {
25
+ result.issuesEnvelope = issuesEnv;
26
+ }
27
+ }
28
+ return result;
29
+ }