flex-md 2.0.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.
- package/README.md +57 -309
- package/dist/__tests__/validate.test.d.ts +1 -0
- package/dist/__tests__/validate.test.js +108 -0
- package/dist/detect/json/detectIntent.d.ts +2 -0
- package/dist/detect/json/detectIntent.js +79 -0
- package/dist/detect/json/detectPresence.d.ts +6 -0
- package/dist/detect/json/detectPresence.js +191 -0
- package/dist/detect/json/index.d.ts +7 -0
- package/dist/detect/json/index.js +12 -0
- package/dist/detect/json/types.d.ts +43 -0
- package/dist/detect/json/types.js +1 -0
- package/dist/extract/extract.d.ts +5 -0
- package/dist/extract/extract.js +50 -0
- package/dist/extract/types.d.ts +11 -0
- package/dist/extract/types.js +1 -0
- package/dist/index.d.ts +11 -15
- package/dist/index.js +18 -17
- package/dist/issues/build.d.ts +26 -0
- package/dist/issues/build.js +62 -0
- package/dist/md/lists.d.ts +14 -0
- package/dist/md/lists.js +33 -0
- package/dist/md/match.d.ts +12 -0
- package/dist/md/match.js +44 -0
- package/dist/md/outline.d.ts +6 -0
- package/dist/md/outline.js +67 -0
- package/dist/md/parse.d.ts +29 -0
- package/dist/md/parse.js +105 -0
- package/dist/md/tables.d.ts +25 -0
- package/dist/md/tables.js +72 -0
- package/dist/ofs/enricher.d.ts +14 -4
- package/dist/ofs/enricher.js +68 -20
- package/dist/ofs/issues.d.ts +14 -0
- package/dist/ofs/issues.js +92 -0
- package/dist/ofs/issuesEnvelope.d.ts +15 -0
- package/dist/ofs/issuesEnvelope.js +71 -0
- package/dist/ofs/parser.d.ts +5 -17
- package/dist/ofs/parser.js +114 -45
- package/dist/ofs/stringify.js +24 -22
- package/dist/pipeline/enforce.d.ts +10 -0
- package/dist/pipeline/enforce.js +46 -0
- package/dist/pipeline/kind.d.ts +16 -0
- package/dist/pipeline/kind.js +24 -0
- package/dist/pipeline/repair.d.ts +14 -0
- package/dist/pipeline/repair.js +112 -0
- package/dist/strictness/container.d.ts +14 -0
- package/dist/strictness/container.js +46 -0
- package/dist/strictness/processor.d.ts +5 -0
- package/dist/strictness/processor.js +29 -0
- package/dist/strictness/types.d.ts +77 -0
- package/dist/strictness/types.js +106 -0
- package/dist/test-pipeline.d.ts +1 -0
- package/dist/test-pipeline.js +53 -0
- package/dist/test-runner.js +10 -7
- package/dist/test-strictness.d.ts +1 -0
- package/dist/test-strictness.js +213 -0
- package/dist/types.d.ts +79 -43
- package/dist/validate/policy.d.ts +10 -0
- package/dist/validate/policy.js +17 -0
- package/dist/validate/types.d.ts +11 -0
- package/dist/validate/types.js +1 -0
- package/dist/validate/validate.d.ts +2 -0
- package/dist/validate/validate.js +308 -0
- package/docs/mdflex-compliance.md +216 -0
- package/package.json +7 -3
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a nested outline tree from Markdown headings.
|
|
3
|
+
* Supports any heading level and captures content between headings.
|
|
4
|
+
*/
|
|
5
|
+
export function buildOutline(md) {
|
|
6
|
+
const lines = md.split("\n");
|
|
7
|
+
const headings = [];
|
|
8
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9
|
+
const m = (lines[i] ?? "").match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
10
|
+
if (m) {
|
|
11
|
+
headings.push({ idx: i, level: m[1].length, title: cleanTitle(m[2]) });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (!headings.length) {
|
|
15
|
+
return { type: "md_outline", nodes: [] };
|
|
16
|
+
}
|
|
17
|
+
const nodes = [];
|
|
18
|
+
const stack = [];
|
|
19
|
+
for (let h = 0; h < headings.length; h++) {
|
|
20
|
+
const cur = headings[h];
|
|
21
|
+
const next = headings[h + 1];
|
|
22
|
+
const contentStart = cur.idx + 1;
|
|
23
|
+
const contentEnd = next ? next.idx : lines.length;
|
|
24
|
+
const content_md = lines.slice(contentStart, contentEnd).join("\n").trimEnd() + "\n";
|
|
25
|
+
const node = {
|
|
26
|
+
title: cur.title,
|
|
27
|
+
level: cur.level,
|
|
28
|
+
key: "",
|
|
29
|
+
content_md,
|
|
30
|
+
children: []
|
|
31
|
+
};
|
|
32
|
+
while (stack.length && stack[stack.length - 1].level >= node.level) {
|
|
33
|
+
stack.pop();
|
|
34
|
+
}
|
|
35
|
+
if (!stack.length) {
|
|
36
|
+
nodes.push(node);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
stack[stack.length - 1].children.push(node);
|
|
40
|
+
}
|
|
41
|
+
stack.push(node);
|
|
42
|
+
}
|
|
43
|
+
assignKeys(nodes);
|
|
44
|
+
return { type: "md_outline", nodes };
|
|
45
|
+
}
|
|
46
|
+
function cleanTitle(t) {
|
|
47
|
+
return t.trim().replace(/[:\-–—]\s*$/, "").trim();
|
|
48
|
+
}
|
|
49
|
+
function slugify(t) {
|
|
50
|
+
return t.toLowerCase()
|
|
51
|
+
.replace(/[:\-–—]+$/g, "")
|
|
52
|
+
.replace(/\s+/g, "_")
|
|
53
|
+
.replace(/[^a-z0-9_]/g, "")
|
|
54
|
+
.replace(/_+/g, "_")
|
|
55
|
+
.replace(/^_+|_+$/g, "");
|
|
56
|
+
}
|
|
57
|
+
function assignKeys(nodes) {
|
|
58
|
+
const seen = new Map();
|
|
59
|
+
const visit = (n) => {
|
|
60
|
+
const base = slugify(n.title) || "section";
|
|
61
|
+
const count = (seen.get(base) ?? 0) + 1;
|
|
62
|
+
seen.set(base, count);
|
|
63
|
+
n.key = count === 1 ? base : `${base}__${count}`;
|
|
64
|
+
n.children.forEach(visit);
|
|
65
|
+
};
|
|
66
|
+
nodes.forEach(visit);
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { IssuesEnvelope } from "../types.js";
|
|
2
|
+
export interface FenceBlock {
|
|
3
|
+
lang: string;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
contentStart: number;
|
|
7
|
+
contentEnd: number;
|
|
8
|
+
content: string;
|
|
9
|
+
full: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function normalizeName(s: string): string;
|
|
12
|
+
export declare function extractFencedBlocks(text: string): FenceBlock[];
|
|
13
|
+
export interface ParsedHeading {
|
|
14
|
+
level: number;
|
|
15
|
+
raw: string;
|
|
16
|
+
name: string;
|
|
17
|
+
norm: string;
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ParsedSection {
|
|
22
|
+
heading: ParsedHeading;
|
|
23
|
+
bodyStart: number;
|
|
24
|
+
bodyEnd: number;
|
|
25
|
+
body: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function parseHeadingsAndSections(md: string): ParsedSection[];
|
|
28
|
+
export declare function extractBullets(body: string): string[];
|
|
29
|
+
export declare function isIssuesEnvelopeCheck(md: string): IssuesEnvelope;
|
package/dist/md/parse.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export function normalizeName(s) {
|
|
2
|
+
return s.trim().replace(/\s+/g, " ").toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
export function extractFencedBlocks(text) {
|
|
5
|
+
const rx = /```(\w+)?\s*\n([\s\S]*?)\n```/g;
|
|
6
|
+
const blocks = [];
|
|
7
|
+
let m;
|
|
8
|
+
while ((m = rx.exec(text)) !== null) {
|
|
9
|
+
const full = m[0];
|
|
10
|
+
const lang = String(m[1] ?? "").trim().toLowerCase();
|
|
11
|
+
const content = m[2] ?? "";
|
|
12
|
+
const start = m.index;
|
|
13
|
+
const end = start + full.length;
|
|
14
|
+
// compute content start/end (best-effort)
|
|
15
|
+
const headerLen = full.indexOf("\n") + 1; // after first newline
|
|
16
|
+
const contentEnd = end - 3; // "```" at end
|
|
17
|
+
const contentStart = start + headerLen;
|
|
18
|
+
blocks.push({
|
|
19
|
+
lang,
|
|
20
|
+
start,
|
|
21
|
+
end,
|
|
22
|
+
contentStart,
|
|
23
|
+
contentEnd,
|
|
24
|
+
content,
|
|
25
|
+
full,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return blocks;
|
|
29
|
+
}
|
|
30
|
+
export function parseHeadingsAndSections(md) {
|
|
31
|
+
const rx = /^(#{1,6})\s+(.+?)\s*$/gm;
|
|
32
|
+
const headings = [];
|
|
33
|
+
let m;
|
|
34
|
+
while ((m = rx.exec(md)) !== null) {
|
|
35
|
+
const hashes = m[1] ?? "";
|
|
36
|
+
const name = (m[2] ?? "").trim();
|
|
37
|
+
const raw = m[0] ?? "";
|
|
38
|
+
const start = m.index;
|
|
39
|
+
const end = start + raw.length;
|
|
40
|
+
headings.push({
|
|
41
|
+
level: hashes.length,
|
|
42
|
+
raw,
|
|
43
|
+
name,
|
|
44
|
+
norm: normalizeName(name),
|
|
45
|
+
start,
|
|
46
|
+
end,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// compute section bodies
|
|
50
|
+
const sections = [];
|
|
51
|
+
for (let i = 0; i < headings.length; i++) {
|
|
52
|
+
const h = headings[i];
|
|
53
|
+
const bodyStart = nextLineIndex(md, h.end);
|
|
54
|
+
let bodyEnd = md.length;
|
|
55
|
+
for (let j = i + 1; j < headings.length; j++) {
|
|
56
|
+
const nxt = headings[j];
|
|
57
|
+
bodyEnd = nxt.start;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
sections.push({
|
|
61
|
+
heading: h,
|
|
62
|
+
bodyStart,
|
|
63
|
+
bodyEnd,
|
|
64
|
+
body: md.slice(bodyStart, bodyEnd).trimEnd(),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return sections;
|
|
68
|
+
}
|
|
69
|
+
function nextLineIndex(text, idx) {
|
|
70
|
+
if (idx >= text.length)
|
|
71
|
+
return text.length;
|
|
72
|
+
if (text[idx] === "\n")
|
|
73
|
+
return idx + 1;
|
|
74
|
+
const n = text.indexOf("\n", idx);
|
|
75
|
+
return n === -1 ? text.length : n + 1;
|
|
76
|
+
}
|
|
77
|
+
export function extractBullets(body) {
|
|
78
|
+
const lines = body.split(/\r?\n/);
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const m = line.match(/^\s*[-*]\s+(.*)\s*$/);
|
|
82
|
+
if (m)
|
|
83
|
+
out.push(m[1] ?? "");
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
export function isIssuesEnvelopeCheck(md) {
|
|
88
|
+
const parsed = parseHeadingsAndSections(md);
|
|
89
|
+
const want = ["status", "issues", "expected", "found", "how to fix"].map(normalizeName);
|
|
90
|
+
const got = new Set(parsed.map(s => s.heading.norm));
|
|
91
|
+
const ok = want.every(w => got.has(w));
|
|
92
|
+
const sections = {};
|
|
93
|
+
if (ok) {
|
|
94
|
+
for (const sec of parsed) {
|
|
95
|
+
if (want.includes(sec.heading.norm)) {
|
|
96
|
+
sections[sec.heading.norm] = {
|
|
97
|
+
heading: sec.heading.name,
|
|
98
|
+
body: sec.body.trim(),
|
|
99
|
+
bullets: extractBullets(sec.body),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { isIssuesEnvelope: ok, sections };
|
|
105
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ParsedTable {
|
|
2
|
+
kind: "table" | "ordered_table";
|
|
3
|
+
by?: string;
|
|
4
|
+
columns: string[];
|
|
5
|
+
rows: string[][];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Extracts all pipe table blocks from a markdown string.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractPipeTables(md: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Parses a single pipe table block into columns and rows.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parsePipeTable(block: string): {
|
|
15
|
+
columns: string[];
|
|
16
|
+
rows: string[][];
|
|
17
|
+
} | null;
|
|
18
|
+
export declare function normalizeHeader(s: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Enforces ordered table constraints (must have '#' column numbered 1..N).
|
|
21
|
+
*/
|
|
22
|
+
export declare function enforceOrderedTable(table: {
|
|
23
|
+
columns: string[];
|
|
24
|
+
rows: string[][];
|
|
25
|
+
}): string[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts all pipe table blocks from a markdown string.
|
|
3
|
+
*/
|
|
4
|
+
export function extractPipeTables(md) {
|
|
5
|
+
const lines = md.split("\n");
|
|
6
|
+
const blocks = [];
|
|
7
|
+
let i = 0;
|
|
8
|
+
while (i < lines.length) {
|
|
9
|
+
const h = lines[i] ?? "";
|
|
10
|
+
const s = lines[i + 1] ?? "";
|
|
11
|
+
const looksHeader = h.includes("|");
|
|
12
|
+
const looksSep = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(s);
|
|
13
|
+
if (looksHeader && looksSep) {
|
|
14
|
+
const start = i;
|
|
15
|
+
i += 2;
|
|
16
|
+
while (i < lines.length && (lines[i] ?? "").includes("|") && (lines[i] ?? "").trim() !== "") {
|
|
17
|
+
i++;
|
|
18
|
+
}
|
|
19
|
+
const block = lines.slice(start, i).join("\n");
|
|
20
|
+
blocks.push(block);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
i++;
|
|
24
|
+
}
|
|
25
|
+
return blocks;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parses a single pipe table block into columns and rows.
|
|
29
|
+
*/
|
|
30
|
+
export function parsePipeTable(block) {
|
|
31
|
+
const lines = block.split("\n").map(l => l.trim()).filter(Boolean);
|
|
32
|
+
if (lines.length < 2)
|
|
33
|
+
return null;
|
|
34
|
+
const parseRow = (row) => {
|
|
35
|
+
let content = row;
|
|
36
|
+
if (content.startsWith("|"))
|
|
37
|
+
content = content.slice(1);
|
|
38
|
+
if (content.endsWith("|"))
|
|
39
|
+
content = content.slice(0, -1);
|
|
40
|
+
return content.split("|").map(c => c.trim());
|
|
41
|
+
};
|
|
42
|
+
const columns = parseRow(lines[0]);
|
|
43
|
+
const rows = lines.slice(2).map(parseRow);
|
|
44
|
+
for (const r of rows) {
|
|
45
|
+
while (r.length < columns.length)
|
|
46
|
+
r.push("");
|
|
47
|
+
}
|
|
48
|
+
return { columns, rows };
|
|
49
|
+
}
|
|
50
|
+
export function normalizeHeader(s) {
|
|
51
|
+
return s.trim().toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Enforces ordered table constraints (must have '#' column numbered 1..N).
|
|
55
|
+
*/
|
|
56
|
+
export function enforceOrderedTable(table) {
|
|
57
|
+
const issues = [];
|
|
58
|
+
if (!table.columns.length || normalizeHeader(table.columns[0]) !== "#") {
|
|
59
|
+
issues.push("ordered_table_missing_hash_column");
|
|
60
|
+
return issues;
|
|
61
|
+
}
|
|
62
|
+
// enforce 1..N
|
|
63
|
+
for (let i = 0; i < table.rows.length; i++) {
|
|
64
|
+
const cell = (table.rows[i]?.[0] ?? "").trim();
|
|
65
|
+
const n = Number(cell);
|
|
66
|
+
if (!Number.isInteger(n) || n !== i + 1) {
|
|
67
|
+
issues.push("ordered_table_bad_index_sequence");
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return issues;
|
|
72
|
+
}
|
package/dist/ofs/enricher.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { OutputFormatSpec } from "../types.js";
|
|
2
|
+
import { StrictnessOptions } from "../strictness/types.js";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
4
|
+
* Generates Markdown guidance instructions for the LLM based on the OFS and contract level.
|
|
5
|
+
* Strictly avoids the word "flex-md" and remains "tax-aware" by only including relevant rules.
|
|
5
6
|
*/
|
|
6
|
-
export declare function
|
|
7
|
+
export declare function buildMarkdownGuidance(spec: OutputFormatSpec, strict: StrictnessOptions, opts?: {
|
|
8
|
+
includeNoneRule?: "auto" | "always" | "never";
|
|
9
|
+
includeListRules?: "auto" | "always" | "never";
|
|
10
|
+
includeTableRules?: "auto" | "always" | "never";
|
|
11
|
+
containerFence?: "markdown" | "flexmd";
|
|
12
|
+
}): string;
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Use buildMarkdownGuidance
|
|
15
|
+
*/
|
|
16
|
+
export declare function enrichInstructions(spec: OutputFormatSpec, strict: StrictnessOptions): string;
|
package/dist/ofs/enricher.js
CHANGED
|
@@ -1,29 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Generates Markdown guidance instructions for the LLM based on the OFS and contract level.
|
|
3
|
+
* Strictly avoids the word "flex-md" and remains "tax-aware" by only including relevant rules.
|
|
4
4
|
*/
|
|
5
|
-
export function
|
|
5
|
+
export function buildMarkdownGuidance(spec, strict, opts) {
|
|
6
|
+
const level = strict.level ?? 0;
|
|
7
|
+
const fence = opts?.containerFence === "flexmd" ? "```flexmd" : "```markdown";
|
|
8
|
+
// L0 - Minimal Markdown
|
|
9
|
+
if (level === 0) {
|
|
10
|
+
return "Reply in Markdown.";
|
|
11
|
+
}
|
|
6
12
|
const lines = [];
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
// L2+ Container Rule
|
|
14
|
+
if (level >= 2) {
|
|
15
|
+
lines.push(`Return your entire answer inside a single ${fence} fenced block and nothing else.`);
|
|
16
|
+
lines.push("");
|
|
17
|
+
lines.push("Inside the block, include these section headings somewhere (order does not matter):");
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
lines.push("Reply in Markdown.");
|
|
21
|
+
lines.push("");
|
|
22
|
+
lines.push("Include these section headings somewhere (order does not matter):");
|
|
23
|
+
}
|
|
24
|
+
// Heading List
|
|
25
|
+
for (const s of spec.sections) {
|
|
26
|
+
let suffix = "";
|
|
27
|
+
if (level >= 3) {
|
|
28
|
+
if (s.kind === "list")
|
|
29
|
+
suffix = " (list)";
|
|
30
|
+
else if (s.kind === "ordered_list")
|
|
31
|
+
suffix = " (ordered list)";
|
|
32
|
+
}
|
|
33
|
+
lines.push(`- ${s.name}${suffix}`);
|
|
9
34
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
35
|
+
lines.push("");
|
|
36
|
+
// None Rule
|
|
37
|
+
const noneRule = opts?.includeNoneRule ?? "auto";
|
|
38
|
+
const hasRequired = spec.sections.some(s => s.required !== false);
|
|
39
|
+
const includeNone = noneRule === "always" || (noneRule === "auto" && (hasRequired || level >= 3));
|
|
40
|
+
if (includeNone) {
|
|
41
|
+
lines.push(`If a section is empty, write \`None\`.`);
|
|
42
|
+
lines.push("");
|
|
16
43
|
}
|
|
17
|
-
|
|
18
|
-
|
|
44
|
+
// List Rules
|
|
45
|
+
const listRule = opts?.includeListRules ?? "auto";
|
|
46
|
+
const hasListDecls = spec.sections.some(s => s.kind === "list" || s.kind === "ordered_list");
|
|
47
|
+
const includeList = listRule === "always" || (listRule === "auto" && (hasListDecls || level >= 3));
|
|
48
|
+
if (includeList) {
|
|
49
|
+
lines.push("List rules:");
|
|
50
|
+
lines.push("- Use numbered lists (`1.`, `2.`, …) for ordered lists.");
|
|
51
|
+
lines.push("- Use `-` bullets for unordered lists.");
|
|
52
|
+
lines.push("");
|
|
19
53
|
}
|
|
20
|
-
|
|
21
|
-
|
|
54
|
+
// Table Rules
|
|
55
|
+
const tableRule = opts?.includeTableRules ?? "auto";
|
|
56
|
+
const hasTableDecls = (spec.tables && spec.tables.length > 0) || spec.sections.some(s => s.kind === "table" || s.kind === "ordered_table");
|
|
57
|
+
const includeTable = tableRule === "always" || (tableRule === "auto" && (hasTableDecls || level >= 3));
|
|
58
|
+
if (includeTable) {
|
|
59
|
+
const hasOrderedTable = spec.tables?.some(t => t.kind === "ordered_table") || spec.sections.some(s => s.kind === "ordered_table");
|
|
60
|
+
lines.push("Tables:");
|
|
61
|
+
lines.push("- If you include a table, use a Markdown pipe table.");
|
|
62
|
+
if (hasOrderedTable || level >= 3) {
|
|
63
|
+
lines.push("- For ordered tables, add a first column named `#` and number rows 1..N.");
|
|
64
|
+
}
|
|
65
|
+
lines.push("");
|
|
22
66
|
}
|
|
23
|
-
if (
|
|
24
|
-
lines.push("
|
|
67
|
+
if (level >= 3) {
|
|
68
|
+
lines.push("Do not return JSON as the response format.");
|
|
25
69
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
70
|
+
return lines.join("\n").trim();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* @deprecated Use buildMarkdownGuidance
|
|
74
|
+
*/
|
|
75
|
+
export function enrichInstructions(spec, strict) {
|
|
76
|
+
return buildMarkdownGuidance(spec, strict);
|
|
29
77
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IssuesEnvelope, FlexMdResponseMode } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detects if the response is an Issues envelope (Mode B).
|
|
4
|
+
*/
|
|
5
|
+
export declare function detectIssuesEnvelope(md: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Historical alias for processResponseMarkdown.
|
|
8
|
+
* @deprecated Use detectIssuesEnvelope
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectResponseMode(md: string): FlexMdResponseMode;
|
|
11
|
+
/**
|
|
12
|
+
* Parses a Markdown Issues envelope into a structured JSON object.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseIssuesEnvelope(md: string): IssuesEnvelope | null;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects if the response is an Issues envelope (Mode B).
|
|
3
|
+
*/
|
|
4
|
+
export function detectIssuesEnvelope(md) {
|
|
5
|
+
const hasStatus = /(^|\n)##\s+Status\s*$/im.test(md);
|
|
6
|
+
const hasIssuesHeading = /(^|\n)##\s+Issues\s*$/im.test(md);
|
|
7
|
+
const hasStatusLine = /(^|\n)-\s*status:\s*(error|partial)\s*$/im.test(md);
|
|
8
|
+
return hasStatus && hasIssuesHeading && hasStatusLine;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Historical alias for processResponseMarkdown.
|
|
12
|
+
* @deprecated Use detectIssuesEnvelope
|
|
13
|
+
*/
|
|
14
|
+
export function detectResponseMode(md) {
|
|
15
|
+
return detectIssuesEnvelope(md) ? "issues" : "answer";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parses a Markdown Issues envelope into a structured JSON object.
|
|
19
|
+
*/
|
|
20
|
+
export function parseIssuesEnvelope(md) {
|
|
21
|
+
if (detectResponseMode(md) !== "issues")
|
|
22
|
+
return null;
|
|
23
|
+
const statusBlock = sectionBody(md, "Status");
|
|
24
|
+
const issuesBlock = sectionBody(md, "Issues");
|
|
25
|
+
if (!statusBlock || !issuesBlock)
|
|
26
|
+
return null;
|
|
27
|
+
const statusKv = parseTopLevelKvBullets(statusBlock);
|
|
28
|
+
const status = {
|
|
29
|
+
status: statusKv["status"] ?? "error",
|
|
30
|
+
code: statusKv["code"] ?? "invalid_format",
|
|
31
|
+
message: statusKv["message"] ?? "An error occurred."
|
|
32
|
+
};
|
|
33
|
+
const missingInputs = parseSimpleList(sectionBody(md, "Missing inputs"));
|
|
34
|
+
const clarificationsNeeded = parseSimpleList(sectionBody(md, "Clarifications needed"));
|
|
35
|
+
const issues = parseIssueItems(issuesBlock);
|
|
36
|
+
return {
|
|
37
|
+
mode: "issues",
|
|
38
|
+
status,
|
|
39
|
+
missingInputs: missingInputs.length ? missingInputs : undefined,
|
|
40
|
+
clarificationsNeeded: clarificationsNeeded.length ? clarificationsNeeded : undefined,
|
|
41
|
+
issues
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function sectionBody(md, heading) {
|
|
45
|
+
const rx = new RegExp(`(^|\\n)##\\s+${escapeRx(heading)}\\s*\\n([\\s\\S]*?)(\\n##\\s+|$)`, "i");
|
|
46
|
+
const m = md.match(rx);
|
|
47
|
+
return m ? (m[2] ?? "").trim() : null;
|
|
48
|
+
}
|
|
49
|
+
function parseTopLevelKvBullets(block) {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const line of block.split("\n")) {
|
|
52
|
+
const m = line.match(/^\s*-\s*([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*$/);
|
|
53
|
+
if (m)
|
|
54
|
+
out[m[1]] = m[2];
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function parseSimpleList(block) {
|
|
59
|
+
if (!block)
|
|
60
|
+
return [];
|
|
61
|
+
const items = [];
|
|
62
|
+
for (const line of block.split("\n")) {
|
|
63
|
+
const m = line.match(/^\s*-\s+(.+?)\s*$/);
|
|
64
|
+
if (m)
|
|
65
|
+
items.push(m[1].trim());
|
|
66
|
+
}
|
|
67
|
+
return items;
|
|
68
|
+
}
|
|
69
|
+
function parseIssueItems(block) {
|
|
70
|
+
const lines = block.split("\n");
|
|
71
|
+
const items = [];
|
|
72
|
+
let current = null;
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
const top = line.match(/^\s*-\s*issue:\s*(.+?)\s*$/i);
|
|
75
|
+
if (top) {
|
|
76
|
+
if (current)
|
|
77
|
+
items.push(current);
|
|
78
|
+
current = { issue: top[1].trim() };
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const sub = line.match(/^\s{2,}([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*$/);
|
|
82
|
+
if (sub && current) {
|
|
83
|
+
current[sub[1]] = sub[2].trim();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (current)
|
|
87
|
+
items.push(current);
|
|
88
|
+
return items;
|
|
89
|
+
}
|
|
90
|
+
function escapeRx(s) {
|
|
91
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
92
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { IssuesEnvelope, ValidationResult } from "../types.js";
|
|
2
|
+
export declare function parseIssuesEnvelope(md: string): IssuesEnvelope;
|
|
3
|
+
export declare function buildIssuesEnvelope(args: {
|
|
4
|
+
validation: ValidationResult;
|
|
5
|
+
level: 0 | 1 | 2 | 3;
|
|
6
|
+
expectedSummary: string[];
|
|
7
|
+
foundSummary: string[];
|
|
8
|
+
howToFix: string[];
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function buildIssuesEnvelopeAuto(args: {
|
|
11
|
+
validation: ValidationResult;
|
|
12
|
+
level: 0 | 1 | 2 | 3;
|
|
13
|
+
requiredSectionNames?: string[];
|
|
14
|
+
kindRequirements?: string[];
|
|
15
|
+
}): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isIssuesEnvelopeCheck } from "../md/parse.js";
|
|
2
|
+
export function parseIssuesEnvelope(md) {
|
|
3
|
+
return isIssuesEnvelopeCheck(md);
|
|
4
|
+
}
|
|
5
|
+
export function buildIssuesEnvelope(args) {
|
|
6
|
+
const issues = args.validation.issues
|
|
7
|
+
.filter(i => i.severity === "warn" || i.severity === "error")
|
|
8
|
+
.map(i => `- ${i.message}`);
|
|
9
|
+
const expected = args.expectedSummary.map(x => `- ${x}`);
|
|
10
|
+
const found = args.foundSummary.map(x => `- ${x}`);
|
|
11
|
+
const how = args.howToFix.map(x => `- ${x}`);
|
|
12
|
+
return [
|
|
13
|
+
"## Status",
|
|
14
|
+
"Non-compliant output (cannot be repaired to the required format).",
|
|
15
|
+
"",
|
|
16
|
+
"## Issues",
|
|
17
|
+
issues.length ? issues.join("\n") : "- None",
|
|
18
|
+
"",
|
|
19
|
+
"## Expected",
|
|
20
|
+
expected.length ? expected.join("\n") : "- None",
|
|
21
|
+
"",
|
|
22
|
+
"## Found",
|
|
23
|
+
found.length ? found.join("\n") : "- None",
|
|
24
|
+
"",
|
|
25
|
+
"## How to fix",
|
|
26
|
+
how.length ? how.join("\n") : "- None",
|
|
27
|
+
"",
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
export function buildIssuesEnvelopeAuto(args) {
|
|
31
|
+
const v = args.validation;
|
|
32
|
+
const expected = [];
|
|
33
|
+
const found = [];
|
|
34
|
+
const how = [];
|
|
35
|
+
if (args.level >= 2) {
|
|
36
|
+
expected.push("One single ```markdown fenced block containing the entire answer (no text outside).");
|
|
37
|
+
}
|
|
38
|
+
if (args.level >= 1 && (args.requiredSectionNames?.length ?? 0) > 0) {
|
|
39
|
+
expected.push(`Section headings present (order does not matter): ${args.requiredSectionNames.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
if (args.level >= 3 && (args.kindRequirements?.length ?? 0) > 0) {
|
|
42
|
+
for (const k of args.kindRequirements)
|
|
43
|
+
expected.push(k);
|
|
44
|
+
}
|
|
45
|
+
if (v.stats) {
|
|
46
|
+
found.push(`Detected sections: ${v.stats.sectionCount}`);
|
|
47
|
+
if (v.stats.missingRequired.length)
|
|
48
|
+
found.push(`Missing: ${v.stats.missingRequired.join(", ")}`);
|
|
49
|
+
if (v.stats.duplicates.length)
|
|
50
|
+
found.push(`Duplicates: ${v.stats.duplicates.join(", ")}`);
|
|
51
|
+
found.push(`Fences: ${v.stats.container.fenceCount} (langs: ${v.stats.container.fenceLangs.join(", ") || "none"})`);
|
|
52
|
+
found.push(`Outside text: ${v.stats.container.hasOutsideText ? "yes" : "no"}`);
|
|
53
|
+
}
|
|
54
|
+
if (args.level >= 2)
|
|
55
|
+
how.push("Return a single ```markdown fenced block and nothing else.");
|
|
56
|
+
if (args.level >= 1)
|
|
57
|
+
how.push("Include the required headings and put content under each (order does not matter).");
|
|
58
|
+
how.push('If a section is empty, write "None".');
|
|
59
|
+
if (args.level >= 3) {
|
|
60
|
+
how.push('Use numbered lists ("1.", "2.", …) for ordered lists and "-" bullets for unordered lists.');
|
|
61
|
+
how.push("Use Markdown pipe tables for table sections; ordered tables include a first column '#'.");
|
|
62
|
+
how.push("Do not return JSON as the response format.");
|
|
63
|
+
}
|
|
64
|
+
return buildIssuesEnvelope({
|
|
65
|
+
validation: v,
|
|
66
|
+
level: args.level,
|
|
67
|
+
expectedSummary: expected,
|
|
68
|
+
foundSummary: found,
|
|
69
|
+
howToFix: how,
|
|
70
|
+
});
|
|
71
|
+
}
|
package/dist/ofs/parser.d.ts
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
import type { OutputFormatSpec } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Parse an Output Format Spec block from Markdown.
|
|
4
|
-
*
|
|
5
|
-
* Expected format:
|
|
6
|
-
* ## Output format (Markdown)
|
|
7
|
-
* Include these sections somewhere (order does not matter):
|
|
8
|
-
*
|
|
9
|
-
* - Short answer — prose
|
|
10
|
-
* - Long answer — prose
|
|
11
|
-
* - Reasoning — ordered list
|
|
12
|
-
* - Assumptions — list
|
|
13
|
-
*
|
|
14
|
-
* Tables (only if needed):
|
|
15
|
-
* - (property1, property2, property3 — table)
|
|
16
|
-
* - (property1, property2, property3 — ordered table, by property2)
|
|
17
|
-
*
|
|
18
|
-
* Empty sections:
|
|
19
|
-
* - If a section is empty, write `None`.
|
|
20
4
|
*/
|
|
21
|
-
export
|
|
5
|
+
export interface ParseOfsOptions {
|
|
6
|
+
headingRegex?: RegExp;
|
|
7
|
+
allowDelimiterFallbacks?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseOutputFormatSpec(md: string, opts?: ParseOfsOptions): OutputFormatSpec | null;
|