flex-md 2.0.0 → 3.1.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__/ofs.test.d.ts +1 -0
- package/dist/__tests__/ofs.test.js +51 -0
- 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 +76 -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 +33 -21
- 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 +82 -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
package/dist/ofs/parser.js
CHANGED
|
@@ -1,57 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
export function parseOutputFormatSpec(md, opts = {}) {
|
|
2
|
+
const headingRx = opts.headingRegex ?? /^##\s*Output format\b/i;
|
|
3
|
+
const lines = md.split("\n");
|
|
4
|
+
// find OFS start
|
|
5
|
+
let start = -1;
|
|
6
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7
|
+
const line = lines[i] ?? "";
|
|
8
|
+
if (headingRx.test(line)) {
|
|
9
|
+
start = i;
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (start === -1)
|
|
14
|
+
return null;
|
|
15
|
+
// capture block until next H2 (##) or end
|
|
16
|
+
let end = lines.length;
|
|
17
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
18
|
+
const line = lines[i] ?? "";
|
|
19
|
+
if (/^##\s+/.test(line)) {
|
|
20
|
+
end = i;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const block = lines.slice(start, end).join("\n");
|
|
21
25
|
const sections = [];
|
|
22
26
|
const tables = [];
|
|
23
27
|
let emptySectionValue;
|
|
24
|
-
|
|
25
|
-
for (const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const hint = sectionMatch[3]?.trim();
|
|
34
|
-
sections.push({ name, kind, hint: hint || undefined });
|
|
28
|
+
let inTables = false;
|
|
29
|
+
for (const rawLine of block.split("\n")) {
|
|
30
|
+
const line = rawLine.trim();
|
|
31
|
+
if (/^tables\b/i.test(line)) {
|
|
32
|
+
inTables = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (/^empty sections\b/i.test(line)) {
|
|
36
|
+
inTables = false;
|
|
35
37
|
continue;
|
|
36
38
|
}
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
const columns = columnsStr.split(",").map(c => c.trim());
|
|
42
|
-
const kindStr = tableMatch[2].trim().toLowerCase().replace(/\s+/g, "_");
|
|
43
|
-
const kind = kindStr;
|
|
44
|
-
const by = tableMatch[3]?.trim();
|
|
45
|
-
tables.push({ columns, kind, by });
|
|
39
|
+
// Empty section rule
|
|
40
|
+
const mNone = line.match(/write\s+`([^`]+)`/i);
|
|
41
|
+
if (/empty/i.test(line) && mNone) {
|
|
42
|
+
emptySectionValue = mNone[1];
|
|
46
43
|
continue;
|
|
47
44
|
}
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
|
|
45
|
+
// bullet items
|
|
46
|
+
const bullet = line.match(/^- (.+)$/);
|
|
47
|
+
if (!bullet)
|
|
48
|
+
continue;
|
|
49
|
+
const item = bullet[1];
|
|
50
|
+
if (inTables) {
|
|
51
|
+
const t = parseTableDecl(item);
|
|
52
|
+
if (t)
|
|
53
|
+
tables.push(t);
|
|
52
54
|
continue;
|
|
53
55
|
}
|
|
56
|
+
const s = parseSectionDecl(item, !!opts.allowDelimiterFallbacks);
|
|
57
|
+
if (s)
|
|
58
|
+
sections.push(s);
|
|
54
59
|
}
|
|
60
|
+
if (!sections.length)
|
|
61
|
+
return null;
|
|
55
62
|
return {
|
|
56
63
|
descriptorType: "output_format_spec",
|
|
57
64
|
format: "markdown",
|
|
@@ -59,6 +66,68 @@ export function parseOutputFormatSpec(md) {
|
|
|
59
66
|
sections,
|
|
60
67
|
tablesOptional: true,
|
|
61
68
|
tables,
|
|
62
|
-
emptySectionValue
|
|
69
|
+
emptySectionValue: emptySectionValue ?? "None"
|
|
63
70
|
};
|
|
64
71
|
}
|
|
72
|
+
function parseSectionDecl(text, allowFallbacks) {
|
|
73
|
+
const delims = ["—"];
|
|
74
|
+
if (allowFallbacks)
|
|
75
|
+
delims.push(":", "-");
|
|
76
|
+
let best = null;
|
|
77
|
+
for (const d of delims) {
|
|
78
|
+
const idx = text.indexOf(d);
|
|
79
|
+
if (idx > 0) {
|
|
80
|
+
best = { name: text.slice(0, idx).trim(), rest: text.slice(idx + 1).trim() };
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!best)
|
|
85
|
+
return null;
|
|
86
|
+
const name = best.name;
|
|
87
|
+
const rest = best.rest;
|
|
88
|
+
const kind = normalizeSectionKind(rest);
|
|
89
|
+
if (!kind)
|
|
90
|
+
return null;
|
|
91
|
+
const requiredFlag = parseRequiredOptional(rest);
|
|
92
|
+
return { name, kind, required: requiredFlag };
|
|
93
|
+
}
|
|
94
|
+
function normalizeSectionKind(rest) {
|
|
95
|
+
const r = rest.toLowerCase();
|
|
96
|
+
if (r.includes("ordered") && r.includes("list"))
|
|
97
|
+
return "ordered_list";
|
|
98
|
+
if (r.includes("list"))
|
|
99
|
+
return "list";
|
|
100
|
+
if (r.includes("prose") || r.includes("text"))
|
|
101
|
+
return "text";
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function parseTableDecl(item) {
|
|
105
|
+
const m = item.match(/^\((.+)\)\s*(.*)$/);
|
|
106
|
+
if (!m)
|
|
107
|
+
return null;
|
|
108
|
+
const inside = m[1];
|
|
109
|
+
const parts = inside.split(/[—–-]/).map(s => s.trim());
|
|
110
|
+
if (parts.length < 2)
|
|
111
|
+
return null;
|
|
112
|
+
const colsPart = parts[0];
|
|
113
|
+
const kindPart = parts.slice(1).join(" — ").trim().toLowerCase();
|
|
114
|
+
const columns = colsPart.split(",").map(s => s.trim()).filter(Boolean);
|
|
115
|
+
const requiredFlag = parseRequiredOptional(item);
|
|
116
|
+
if (kindPart.includes("ordered table")) {
|
|
117
|
+
const by = (kindPart.match(/\bby\s+([^,)\s]+)/)?.[1] ?? "").trim();
|
|
118
|
+
return { columns, kind: "ordered_table", by: by || undefined, required: requiredFlag };
|
|
119
|
+
}
|
|
120
|
+
if (kindPart.includes("table")) {
|
|
121
|
+
return { columns, kind: "table", required: requiredFlag };
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function parseRequiredOptional(s) {
|
|
126
|
+
const hasReq = /\(required\)/i.test(s);
|
|
127
|
+
const hasOpt = /\(optional\)/i.test(s);
|
|
128
|
+
if (hasReq)
|
|
129
|
+
return true;
|
|
130
|
+
if (hasOpt)
|
|
131
|
+
return false;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
package/dist/ofs/stringify.js
CHANGED
|
@@ -2,29 +2,41 @@
|
|
|
2
2
|
* Convert an OutputFormatSpec to canonical Markdown format.
|
|
3
3
|
*/
|
|
4
4
|
export function stringifyOutputFormatSpec(spec) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const kindStr = section.kind.replace(/_/g, " ");
|
|
11
|
-
const hint = section.hint ? ` ${section.hint}` : "";
|
|
12
|
-
parts.push(`- ${section.name} — ${kindStr}${hint}\n`);
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push(`## Output format (Markdown)`);
|
|
7
|
+
if (spec.description) {
|
|
8
|
+
lines.push(spec.description);
|
|
9
|
+
lines.push("");
|
|
13
10
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
lines.push(`Include these sections somewhere (order does not matter):`);
|
|
12
|
+
lines.push("");
|
|
13
|
+
for (const s of spec.sections) {
|
|
14
|
+
const k = s.kind === "ordered_list" ? "ordered list" : (s.kind || "text");
|
|
15
|
+
const required = s.required === true ? " (required)" : (s.required === false ? " (optional)" : "");
|
|
16
|
+
lines.push(`- ${s.name} — ${k}${required}`);
|
|
17
|
+
if (s.description) {
|
|
18
|
+
lines.push(` Description: ${s.description}`);
|
|
19
|
+
}
|
|
20
|
+
if (s.instruction) {
|
|
21
|
+
lines.push(` Instruction: ${s.instruction}`);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const tables = spec.tables || [];
|
|
25
|
+
if (tables.length) {
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push(`tables:`);
|
|
28
|
+
for (const t of tables) {
|
|
29
|
+
const required = t.required === true ? " (required)" : (t.required === false ? " (optional)" : "");
|
|
30
|
+
if (t.kind === "table") {
|
|
31
|
+
lines.push(`- (${t.columns.join(", ")} — table)${required}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
lines.push(`- (${t.columns.join(", ")} — ordered table, by ${t.by ?? t.columns[0] ?? ""})${required}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
}
|
|
29
|
-
|
|
38
|
+
lines.push("");
|
|
39
|
+
lines.push(`empty sections:`);
|
|
40
|
+
lines.push(`- If a section is empty, write \`${spec.emptySectionValue ?? "None"}\`.`);
|
|
41
|
+
return lines.join("\n") + "\n";
|
|
30
42
|
}
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type StrictnessLevel = 0 | 1 | 2 | 3;
|
|
2
|
+
export type ContainerMode = "none" | "markdown_fence" | "flexmd_fence";
|
|
3
|
+
import { Enforcement } from "../types.js";
|
|
4
|
+
export type { Enforcement };
|
|
5
|
+
/**
|
|
6
|
+
* Semi-strict enforcement policy.
|
|
7
|
+
* Lets you be strict about some aspects (e.g. container) and lenient about others (e.g. optional sections).
|
|
8
|
+
*/
|
|
9
|
+
export interface SemiStrictPolicy {
|
|
10
|
+
outsideContainer?: Enforcement;
|
|
11
|
+
missingContainer?: Enforcement;
|
|
12
|
+
multipleContainers?: Enforcement;
|
|
13
|
+
missingOfs?: Enforcement;
|
|
14
|
+
missingRequiredSection?: Enforcement;
|
|
15
|
+
missingOptionalSection?: Enforcement;
|
|
16
|
+
wrongSectionKindRequired?: Enforcement;
|
|
17
|
+
wrongSectionKindOptional?: Enforcement;
|
|
18
|
+
emptySectionWithoutNoneRequired?: Enforcement;
|
|
19
|
+
emptySectionWithoutNoneOptional?: Enforcement;
|
|
20
|
+
missingRequiredTable?: Enforcement;
|
|
21
|
+
missingOptionalTable?: Enforcement;
|
|
22
|
+
orderedTableViolationsRequired?: Enforcement;
|
|
23
|
+
orderedTableViolationsOptional?: Enforcement;
|
|
24
|
+
jsonPayloadMissing?: Enforcement;
|
|
25
|
+
}
|
|
26
|
+
export type GuidanceMode = "auto" | "always" | "never";
|
|
27
|
+
export interface StrictnessOptions {
|
|
28
|
+
level: StrictnessLevel;
|
|
29
|
+
container?: "none" | "markdown_fence" | "flexmd_fence";
|
|
30
|
+
requireSingleContainer?: boolean;
|
|
31
|
+
requireOfs?: boolean;
|
|
32
|
+
requireAllSections?: boolean;
|
|
33
|
+
enforceEmptyNone?: boolean;
|
|
34
|
+
acceptAnyHeadingLevel?: boolean;
|
|
35
|
+
duplicateSectionStrategy?: "merge" | "first";
|
|
36
|
+
requireTypedJson?: boolean;
|
|
37
|
+
jsonSchema?: unknown;
|
|
38
|
+
jsonPayloadHeading?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Controls whether prompt enrichment should mention table rules.
|
|
41
|
+
* - auto: only when required/likely
|
|
42
|
+
* - always: always include table guidance if spec declares tables
|
|
43
|
+
* - never: never include table guidance
|
|
44
|
+
*/
|
|
45
|
+
tablesGuidance?: GuidanceMode;
|
|
46
|
+
/**
|
|
47
|
+
* Controls whether to mention list formatting rules in the prompt.
|
|
48
|
+
* - auto: mention only if the spec includes list/ordered-list sections (and L1+)
|
|
49
|
+
* - always: mention whenever such sections exist, regardless of strictness level
|
|
50
|
+
* - never: never mention list rules
|
|
51
|
+
*/
|
|
52
|
+
listsGuidance?: GuidanceMode;
|
|
53
|
+
/**
|
|
54
|
+
* Controls whether to mention the "empty sections => None" rule.
|
|
55
|
+
* - auto: mention only when spec.emptySectionValue exists and L1+
|
|
56
|
+
* - always: always mention if emptySectionValue exists
|
|
57
|
+
* - never: never mention it
|
|
58
|
+
*/
|
|
59
|
+
emptyNoneGuidance?: GuidanceMode;
|
|
60
|
+
/**
|
|
61
|
+
* Controls whether to mention the container rule ("single fenced block").
|
|
62
|
+
* - auto: mention only at L2+
|
|
63
|
+
* - always: always mention (even at L1)
|
|
64
|
+
* - never: never mention (not recommended if L2+)
|
|
65
|
+
*/
|
|
66
|
+
containerGuidance?: GuidanceMode;
|
|
67
|
+
/**
|
|
68
|
+
* Controls whether to mention the typed JSON rule for L3.
|
|
69
|
+
* - auto: mention only at L3 when requireTypedJson=true
|
|
70
|
+
* - always: always mention at L3
|
|
71
|
+
* - never: never mention (not recommended if you require it)
|
|
72
|
+
*/
|
|
73
|
+
typedJsonGuidance?: GuidanceMode;
|
|
74
|
+
/** Optional semi-strict policy overrides. */
|
|
75
|
+
policy?: SemiStrictPolicy;
|
|
76
|
+
}
|
|
77
|
+
export declare function strictnessDefaults(level: StrictnessLevel): StrictnessOptions;
|