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.
- package/README.md +75 -29
- package/SPEC.md +559 -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/detection/detector.d.ts +6 -0
- package/dist/detection/detector.js +104 -0
- package/dist/detection/extractor.d.ts +10 -0
- package/dist/detection/extractor.js +54 -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 +13 -3
- package/dist/index.js +20 -3
- 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 +16 -0
- package/dist/ofs/enricher.js +77 -0
- package/dist/ofs/extractor.d.ts +9 -0
- package/dist/ofs/extractor.js +75 -0
- 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 +9 -0
- package/dist/ofs/parser.js +133 -0
- package/dist/ofs/stringify.d.ts +5 -0
- package/dist/ofs/stringify.js +32 -0
- package/dist/ofs/validator.d.ts +10 -0
- package/dist/ofs/validator.js +91 -0
- package/dist/outline/builder.d.ts +10 -0
- package/dist/outline/builder.js +85 -0
- package/dist/outline/renderer.d.ts +6 -0
- package/dist/outline/renderer.js +23 -0
- package/dist/parser.js +58 -10
- package/dist/parsers/lists.d.ts +6 -0
- package/dist/parsers/lists.js +36 -0
- package/dist/parsers/tables.d.ts +10 -0
- package/dist/parsers/tables.js +58 -0
- 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.d.ts +1 -0
- package/dist/test-runner.js +331 -0
- package/dist/test-strictness.d.ts +1 -0
- package/dist/test-strictness.js +213 -0
- package/dist/types.d.ts +140 -22
- 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 +15 -6
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { extractFencedBlocks, parseHeadingsAndSections, normalizeName, isIssuesEnvelopeCheck } from "../md/parse.js";
|
|
2
|
+
function enforcementToSeverity(e) {
|
|
3
|
+
if (e === "ignore")
|
|
4
|
+
return null;
|
|
5
|
+
return e === "warn" ? "warn" : "error";
|
|
6
|
+
}
|
|
7
|
+
function defaultPolicy(level) {
|
|
8
|
+
return {
|
|
9
|
+
missingSection: level >= 1 ? "error" : "ignore",
|
|
10
|
+
emptyRequiredSection: level >= 1 ? "error" : "ignore",
|
|
11
|
+
wrongSectionKind: level >= 3 ? "error" : "ignore",
|
|
12
|
+
containerMissing: level >= 2 ? "error" : "ignore",
|
|
13
|
+
containerMultiple: level >= 2 ? "error" : "ignore",
|
|
14
|
+
containerOutsideText: level >= 2 ? "error" : "ignore",
|
|
15
|
+
duplicateSection: level >= 1 ? "warn" : "ignore",
|
|
16
|
+
malformedTable: level >= 3 ? "error" : "ignore",
|
|
17
|
+
malformedOrderedTable: level >= 3 ? "error" : "ignore",
|
|
18
|
+
noneIsAcceptableContent: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function addIssue(issues, policyLevel, issue) {
|
|
22
|
+
const sev = enforcementToSeverity(policyLevel);
|
|
23
|
+
if (!sev)
|
|
24
|
+
return;
|
|
25
|
+
issues.push({ ...issue, severity: sev });
|
|
26
|
+
}
|
|
27
|
+
function isRequiredSection(spec, level) {
|
|
28
|
+
if (typeof spec.required === "boolean")
|
|
29
|
+
return spec.required;
|
|
30
|
+
return level >= 1;
|
|
31
|
+
}
|
|
32
|
+
function stripToWorkingMarkdown(input, level) {
|
|
33
|
+
if (level < 2)
|
|
34
|
+
return { working: input, container: null };
|
|
35
|
+
const fences = extractFencedBlocks(input);
|
|
36
|
+
if (fences.length === 1) {
|
|
37
|
+
const f = fences[0];
|
|
38
|
+
return { working: f.content, container: { fence: f } };
|
|
39
|
+
}
|
|
40
|
+
return { working: input, container: null };
|
|
41
|
+
}
|
|
42
|
+
export function validateMarkdownAgainstOfs(input, spec, level, policyOverride) {
|
|
43
|
+
const policy = { ...defaultPolicy(level), ...(policyOverride ?? {}) };
|
|
44
|
+
const issues = [];
|
|
45
|
+
const fencesAll = extractFencedBlocks(input);
|
|
46
|
+
if (level === 0) {
|
|
47
|
+
const env = isIssuesEnvelopeCheck(input);
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
level,
|
|
51
|
+
issues: [],
|
|
52
|
+
stats: {
|
|
53
|
+
detectedKind: env.isIssuesEnvelope ? "issues" : "markdown",
|
|
54
|
+
sectionCount: 0,
|
|
55
|
+
missingRequired: [],
|
|
56
|
+
duplicates: [],
|
|
57
|
+
container: { fenceCount: fencesAll.length, hasOutsideText: false, fenceLangs: fencesAll.map(f => f.lang) },
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const env = isIssuesEnvelopeCheck(input);
|
|
62
|
+
if (env.isIssuesEnvelope) {
|
|
63
|
+
issues.push({
|
|
64
|
+
code: "ISSUES_ENVELOPE_PRESENT",
|
|
65
|
+
severity: "error",
|
|
66
|
+
message: "Issues envelope present (model did not return a normal answer).",
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
level,
|
|
71
|
+
issues,
|
|
72
|
+
stats: {
|
|
73
|
+
detectedKind: "issues",
|
|
74
|
+
sectionCount: Object.keys(env.sections).length,
|
|
75
|
+
missingRequired: [],
|
|
76
|
+
duplicates: [],
|
|
77
|
+
container: { fenceCount: fencesAll.length, hasOutsideText: false, fenceLangs: fencesAll.map(f => f.lang) },
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const fenceLangs = fencesAll.map(f => f.lang || "(none)");
|
|
82
|
+
let hasOutsideText = false;
|
|
83
|
+
if (level >= 2) {
|
|
84
|
+
if (fencesAll.length === 0) {
|
|
85
|
+
addIssue(issues, policy.containerMissing, {
|
|
86
|
+
code: "CONTAINER_MISSING",
|
|
87
|
+
message: "L2+ requires a single fenced ```markdown block (none found).",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else if (fencesAll.length > 1) {
|
|
91
|
+
addIssue(issues, policy.containerMultiple, {
|
|
92
|
+
code: "CONTAINER_MULTIPLE",
|
|
93
|
+
message: `L2+ requires a single fenced block (found ${fencesAll.length}).`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const f = fencesAll[0];
|
|
98
|
+
const before = input.slice(0, f.start).trim();
|
|
99
|
+
const after = input.slice(f.end).trim();
|
|
100
|
+
hasOutsideText = before.length > 0 || after.length > 0;
|
|
101
|
+
if (hasOutsideText) {
|
|
102
|
+
addIssue(issues, policy.containerOutsideText, {
|
|
103
|
+
code: "CONTAINER_OUTSIDE_TEXT",
|
|
104
|
+
message: "Text exists outside the single fenced block (L2+ requires nothing outside).",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const { working } = stripToWorkingMarkdown(input, level);
|
|
110
|
+
const parsed = parseHeadingsAndSections(working);
|
|
111
|
+
const specByNorm = new Map();
|
|
112
|
+
for (const s of spec.sections)
|
|
113
|
+
specByNorm.set(normalizeName(s.name), s);
|
|
114
|
+
const occurrences = new Map();
|
|
115
|
+
const bodiesByNorm = {};
|
|
116
|
+
for (const sec of parsed) {
|
|
117
|
+
if (!specByNorm.has(sec.heading.norm))
|
|
118
|
+
continue;
|
|
119
|
+
occurrences.set(sec.heading.norm, (occurrences.get(sec.heading.norm) ?? 0) + 1);
|
|
120
|
+
bodiesByNorm[sec.heading.norm] = bodiesByNorm[sec.heading.norm] ?? [];
|
|
121
|
+
bodiesByNorm[sec.heading.norm].push(sec.body.trim());
|
|
122
|
+
}
|
|
123
|
+
const missingRequired = [];
|
|
124
|
+
for (const s of spec.sections) {
|
|
125
|
+
const req = isRequiredSection(s, level);
|
|
126
|
+
if (!req)
|
|
127
|
+
continue;
|
|
128
|
+
const norm = normalizeName(s.name);
|
|
129
|
+
const count = occurrences.get(norm) ?? 0;
|
|
130
|
+
if (count === 0) {
|
|
131
|
+
missingRequired.push(s.name);
|
|
132
|
+
addIssue(issues, policy.missingSection, {
|
|
133
|
+
code: "MISSING_SECTION",
|
|
134
|
+
message: `Missing required section: "${s.name}"`,
|
|
135
|
+
sectionName: s.name,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const noneValue = spec.emptySectionValue ?? "None";
|
|
140
|
+
for (const s of spec.sections) {
|
|
141
|
+
const req = isRequiredSection(s, level);
|
|
142
|
+
if (!req)
|
|
143
|
+
continue;
|
|
144
|
+
const norm = normalizeName(s.name);
|
|
145
|
+
const bodies = bodiesByNorm[norm] ?? [];
|
|
146
|
+
if (!bodies.length)
|
|
147
|
+
continue;
|
|
148
|
+
const allEmpty = bodies.every(b => b.trim().length === 0);
|
|
149
|
+
const allNone = bodies.every(b => normalizeName(b) === normalizeName(noneValue));
|
|
150
|
+
const emptyByPolicy = allEmpty || (!policy.noneIsAcceptableContent && allNone);
|
|
151
|
+
if (emptyByPolicy) {
|
|
152
|
+
addIssue(issues, policy.emptyRequiredSection, {
|
|
153
|
+
code: "EMPTY_REQUIRED_SECTION",
|
|
154
|
+
message: `Empty required section: "${s.name}"`,
|
|
155
|
+
sectionName: s.name,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const duplicates = [];
|
|
160
|
+
for (const [norm, count] of occurrences.entries()) {
|
|
161
|
+
if (count > 1) {
|
|
162
|
+
const name = specByNorm.get(norm).name;
|
|
163
|
+
duplicates.push(name);
|
|
164
|
+
addIssue(issues, policy.duplicateSection, {
|
|
165
|
+
code: "DUPLICATE_SECTION",
|
|
166
|
+
message: `Duplicate section heading: "${name}" (appears ${count} times)`,
|
|
167
|
+
sectionName: name,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (level >= 3) {
|
|
172
|
+
for (const s of spec.sections) {
|
|
173
|
+
const norm = normalizeName(s.name);
|
|
174
|
+
const bodies = bodiesByNorm[norm] ?? [];
|
|
175
|
+
if (!bodies.length)
|
|
176
|
+
continue;
|
|
177
|
+
const kind = s.kind ?? "text";
|
|
178
|
+
for (const body of bodies) {
|
|
179
|
+
const b = body.trim();
|
|
180
|
+
if (normalizeName(b) === normalizeName(noneValue))
|
|
181
|
+
continue;
|
|
182
|
+
if (kind === "text")
|
|
183
|
+
continue;
|
|
184
|
+
if (kind === "list") {
|
|
185
|
+
const ok = /^\s*[-*]\s+/m.test(b);
|
|
186
|
+
if (!ok) {
|
|
187
|
+
addIssue(issues, policy.wrongSectionKind, {
|
|
188
|
+
code: "WRONG_SECTION_KIND",
|
|
189
|
+
message: `Section "${s.name}" must be a bullet list (use "-" bullets).`,
|
|
190
|
+
sectionName: s.name,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (kind === "ordered_list") {
|
|
195
|
+
const ok = /^\s*\d+\.\s+/m.test(b);
|
|
196
|
+
if (!ok) {
|
|
197
|
+
addIssue(issues, policy.wrongSectionKind, {
|
|
198
|
+
code: "WRONG_SECTION_KIND",
|
|
199
|
+
message: `Section "${s.name}" must be an ordered list (use "1.", "2.", …).`,
|
|
200
|
+
sectionName: s.name,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (kind === "table" || kind === "ordered_table") {
|
|
205
|
+
const table = findPipeTable(b);
|
|
206
|
+
if (!table) {
|
|
207
|
+
addIssue(issues, policy.malformedTable, {
|
|
208
|
+
code: "MALFORMED_TABLE",
|
|
209
|
+
message: `Section "${s.name}" must be a Markdown pipe table.`,
|
|
210
|
+
sectionName: s.name,
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const cols = (s.columns ?? []).map(c => normalizeName(c));
|
|
215
|
+
if (cols.length) {
|
|
216
|
+
const headerNorm = table.header.map(c => normalizeName(c));
|
|
217
|
+
const missingCols = cols.filter(c => !headerNorm.includes(c));
|
|
218
|
+
if (missingCols.length) {
|
|
219
|
+
addIssue(issues, policy.malformedTable, {
|
|
220
|
+
code: "MALFORMED_TABLE",
|
|
221
|
+
message: `Table in section "${s.name}" is missing columns: ${missingCols.join(", ")}`,
|
|
222
|
+
sectionName: s.name,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (kind === "ordered_table") {
|
|
227
|
+
const header0 = normalizeName(table.header[0] ?? "");
|
|
228
|
+
if (header0 !== "#") {
|
|
229
|
+
addIssue(issues, policy.malformedOrderedTable, {
|
|
230
|
+
code: "MALFORMED_ORDERED_TABLE",
|
|
231
|
+
message: `Ordered table in section "${s.name}" must include a first column named "#".`,
|
|
232
|
+
sectionName: s.name,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const nums = table.rows.map(r => (r[0] ?? "").trim()).filter(Boolean);
|
|
237
|
+
const okSeq = nums.every((v, idx) => String(idx + 1) === v);
|
|
238
|
+
if (!okSeq && nums.length) {
|
|
239
|
+
addIssue(issues, policy.malformedOrderedTable, {
|
|
240
|
+
code: "MALFORMED_ORDERED_TABLE",
|
|
241
|
+
message: `Ordered table in section "${s.name}" should have sequential row numbers 1..N in column "#".`,
|
|
242
|
+
sectionName: s.name,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const ok = !issues.some(i => i.severity === "error");
|
|
252
|
+
return {
|
|
253
|
+
ok,
|
|
254
|
+
level,
|
|
255
|
+
issues,
|
|
256
|
+
stats: {
|
|
257
|
+
detectedKind: level >= 2 ? (fencesAll.length ? "fenced" : "markdown") : (parsed.length ? "sectioned" : "markdown"),
|
|
258
|
+
sectionCount: occurrences.size,
|
|
259
|
+
missingRequired,
|
|
260
|
+
duplicates,
|
|
261
|
+
container: {
|
|
262
|
+
fenceCount: fencesAll.length,
|
|
263
|
+
hasOutsideText,
|
|
264
|
+
fenceLangs,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function findPipeTable(body) {
|
|
270
|
+
const lines = body.split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
|
|
271
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
272
|
+
const h = lines[i];
|
|
273
|
+
const sep = lines[i + 1];
|
|
274
|
+
if (!looksLikePipeRow(h))
|
|
275
|
+
continue;
|
|
276
|
+
if (!looksLikeSeparatorRow(sep))
|
|
277
|
+
continue;
|
|
278
|
+
const header = splitPipeRow(h);
|
|
279
|
+
const rows = [];
|
|
280
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
281
|
+
const r = lines[j];
|
|
282
|
+
if (!looksLikePipeRow(r))
|
|
283
|
+
break;
|
|
284
|
+
rows.push(splitPipeRow(r));
|
|
285
|
+
}
|
|
286
|
+
return { header, rows };
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
function looksLikePipeRow(line) {
|
|
291
|
+
return line.includes("|") && /[^|]/.test(line.replace(/\|/g, ""));
|
|
292
|
+
}
|
|
293
|
+
function looksLikeSeparatorRow(line) {
|
|
294
|
+
if (!line.includes("|"))
|
|
295
|
+
return false;
|
|
296
|
+
const cells = splitPipeRow(line);
|
|
297
|
+
if (!cells.length)
|
|
298
|
+
return false;
|
|
299
|
+
return cells.every(c => /^:?-{3,}:?$/.test(c.trim()));
|
|
300
|
+
}
|
|
301
|
+
function splitPipeRow(line) {
|
|
302
|
+
let s = line.trim();
|
|
303
|
+
if (s.startsWith("|"))
|
|
304
|
+
s = s.slice(1);
|
|
305
|
+
if (s.endsWith("|"))
|
|
306
|
+
s = s.slice(0, -1);
|
|
307
|
+
return s.split("|").map(c => c.trim());
|
|
308
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Below is the **instruction spec** for the component that generates **LLM guidance** for Flex-MD compliance, by strictness level. This is written for *people writing prompts* or for your `buildPrompt(spec, strictness)` generator.
|
|
2
|
+
|
|
3
|
+
Flex-MD is **Markdown**. The “levels” only change **how much structure you require and enforce**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Flex-MD strictness levels
|
|
8
|
+
|
|
9
|
+
### L0 — Plain Markdown (minimum)
|
|
10
|
+
|
|
11
|
+
Goal: get readable content with low prompt-tax.
|
|
12
|
+
|
|
13
|
+
**What you tell the LLM**
|
|
14
|
+
|
|
15
|
+
* “Reply in Markdown.”
|
|
16
|
+
|
|
17
|
+
**What you enforce**
|
|
18
|
+
|
|
19
|
+
* Nothing beyond “it’s text/Markdown”.
|
|
20
|
+
* No required sections.
|
|
21
|
+
|
|
22
|
+
**When to use**
|
|
23
|
+
|
|
24
|
+
* Creative / exploratory answers.
|
|
25
|
+
* You don’t need predictable parsing.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
### L1 — Sectioned Flex-MD (human-readable + machine-extractable)
|
|
30
|
+
|
|
31
|
+
Goal: ensure predictable headings exist (order doesn’t matter).
|
|
32
|
+
|
|
33
|
+
**What you tell the LLM (minimal)**
|
|
34
|
+
|
|
35
|
+
* “Reply in Markdown.”
|
|
36
|
+
* “Include these section headings somewhere (order doesn’t matter):”
|
|
37
|
+
|
|
38
|
+
* `Short answer`
|
|
39
|
+
* `Long answer`
|
|
40
|
+
* `Reasoning`
|
|
41
|
+
* `Assumptions`
|
|
42
|
+
* `Unknowns`
|
|
43
|
+
* “If a section has nothing to say, write `None`.”
|
|
44
|
+
|
|
45
|
+
**What you enforce**
|
|
46
|
+
|
|
47
|
+
* Headings exist (case-insensitive match).
|
|
48
|
+
* Content can be any Markdown under each heading.
|
|
49
|
+
* Order is **never** required.
|
|
50
|
+
|
|
51
|
+
**Extra typing rules (only if requested by spec)**
|
|
52
|
+
|
|
53
|
+
* `Reasoning` is an **ordered list** (`1.`, `2.`, …) when you care about sequence.
|
|
54
|
+
* `Assumptions` and `Unknowns` are **bullets** (`- ...`) when unordered.
|
|
55
|
+
|
|
56
|
+
**When to use**
|
|
57
|
+
|
|
58
|
+
* Most production calls where you want stable extraction.
|
|
59
|
+
* Best “effort vs structure” tradeoff.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### L2 — Single-container Flex-MD (safer parsing)
|
|
64
|
+
|
|
65
|
+
Goal: eliminate “extra chatter” and make extraction reliable.
|
|
66
|
+
|
|
67
|
+
**What you tell the LLM**
|
|
68
|
+
Everything from L1, plus:
|
|
69
|
+
|
|
70
|
+
* “Return the entire answer inside one fenced Markdown block and nothing else.”
|
|
71
|
+
|
|
72
|
+
Example phrasing:
|
|
73
|
+
|
|
74
|
+
* “Put your full answer inside a single ` ```markdown ` fenced block. No text before or after.”
|
|
75
|
+
|
|
76
|
+
**What you enforce**
|
|
77
|
+
|
|
78
|
+
* Exactly one container fence, and all content is inside it.
|
|
79
|
+
* Still no ordering requirement for sections.
|
|
80
|
+
|
|
81
|
+
**When to use**
|
|
82
|
+
|
|
83
|
+
* Tool pipelines where stray tokens break parsing.
|
|
84
|
+
* You want consistent capture of the whole response.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### L3 — Fully-typed Flex-MD (max determinism, still Markdown)
|
|
89
|
+
|
|
90
|
+
Goal: strongest structural guarantees, still using Markdown (not JSON output).
|
|
91
|
+
|
|
92
|
+
**What you tell the LLM**
|
|
93
|
+
Everything from L2, plus **stricter formatting constraints** derived from the spec:
|
|
94
|
+
|
|
95
|
+
* Section presence is strict:
|
|
96
|
+
|
|
97
|
+
* all required sections must exist (and be non-empty unless allowed).
|
|
98
|
+
* Section type rules are strict:
|
|
99
|
+
|
|
100
|
+
* ordered list sections must be ordered lists
|
|
101
|
+
* list sections must be bullet lists
|
|
102
|
+
* tables must be Markdown pipe tables if requested
|
|
103
|
+
* ordered tables must include a `#` column with 1..N
|
|
104
|
+
|
|
105
|
+
And (important):
|
|
106
|
+
|
|
107
|
+
* “Do not output JSON as the response format.”
|
|
108
|
+
* “If you include JSON-like content, present it as Markdown (lists/tables), not a JSON block.”
|
|
109
|
+
|
|
110
|
+
**What you enforce**
|
|
111
|
+
|
|
112
|
+
* All required sections present
|
|
113
|
+
* All required section type constraints met
|
|
114
|
+
* All required table constraints met
|
|
115
|
+
* Container constraints met
|
|
116
|
+
|
|
117
|
+
**When to use**
|
|
118
|
+
|
|
119
|
+
* High reliability extraction and downstream automation.
|
|
120
|
+
* You can afford slightly more prompt-tax.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Minimal instruction templates for your generator
|
|
125
|
+
|
|
126
|
+
### L0 template (lowest tax)
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Reply in Markdown.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### L1 template (recommended default)
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
Reply in Markdown.
|
|
136
|
+
|
|
137
|
+
Include these section headings somewhere (order does not matter):
|
|
138
|
+
- Short answer
|
|
139
|
+
- Long answer
|
|
140
|
+
- Reasoning
|
|
141
|
+
- Assumptions
|
|
142
|
+
- Unknowns
|
|
143
|
+
|
|
144
|
+
If a section is empty, write `None`.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### L1 with explicit type rules (only if your spec needs them)
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Reply in Markdown.
|
|
151
|
+
|
|
152
|
+
Include these section headings somewhere (order does not matter):
|
|
153
|
+
- Short answer
|
|
154
|
+
- Long answer
|
|
155
|
+
- Reasoning (ordered list)
|
|
156
|
+
- Assumptions (list)
|
|
157
|
+
- Unknowns (list)
|
|
158
|
+
|
|
159
|
+
If a section is empty, write `None`.
|
|
160
|
+
|
|
161
|
+
List rules:
|
|
162
|
+
- Use numbered lists (1., 2., …) for ordered lists.
|
|
163
|
+
- Use '-' bullets for unordered lists.
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### L2 template (single container)
|
|
167
|
+
|
|
168
|
+
````
|
|
169
|
+
Return your entire answer inside a single ```markdown fenced block and nothing else.
|
|
170
|
+
|
|
171
|
+
Inside the block, include these section headings somewhere (order does not matter):
|
|
172
|
+
- Short answer
|
|
173
|
+
- Long answer
|
|
174
|
+
- Reasoning
|
|
175
|
+
- Assumptions
|
|
176
|
+
- Unknowns
|
|
177
|
+
|
|
178
|
+
If a section is empty, write `None`.
|
|
179
|
+
````
|
|
180
|
+
|
|
181
|
+
### L3 template (max strict)
|
|
182
|
+
|
|
183
|
+
````
|
|
184
|
+
Return your entire answer inside a single ```markdown fenced block and nothing else.
|
|
185
|
+
|
|
186
|
+
Include these section headings somewhere (order does not matter):
|
|
187
|
+
- Short answer
|
|
188
|
+
- Long answer
|
|
189
|
+
- Reasoning (ordered list)
|
|
190
|
+
- Assumptions (list)
|
|
191
|
+
- Unknowns (list)
|
|
192
|
+
|
|
193
|
+
If a section is empty, write `None`.
|
|
194
|
+
|
|
195
|
+
Formatting rules:
|
|
196
|
+
- Ordered lists use 1., 2., …
|
|
197
|
+
- Unordered lists use '-' bullets.
|
|
198
|
+
- If a table is used, use a Markdown pipe table with the declared columns.
|
|
199
|
+
- If an ordered table is required, add a first column named '#' and number rows 1..N.
|
|
200
|
+
- Do not return JSON as the response format.
|
|
201
|
+
````
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## How the “instructions builder” should decide what to include (tax-aware)
|
|
206
|
+
|
|
207
|
+
Your generator should only include guidance that is relevant to the current spec:
|
|
208
|
+
|
|
209
|
+
Include:
|
|
210
|
+
|
|
211
|
+
* Section list always at L1+
|
|
212
|
+
* `None` rule only if you want it (recommended at L1+ when sections are required)
|
|
213
|
+
* List rules only if any required section is a list / ordered list
|
|
214
|
+
* Table rules only if any required table exists (or strictness L3)
|
|
215
|
+
* Container rule only at L2+
|
|
216
|
+
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flex-md",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Parse and stringify FlexMD
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Parse and stringify FlexMD: semi-structured Markdown with three powerful layers - Frames, Output Format Spec (OFS), and Detection/Extraction.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "",
|
|
7
7
|
"keywords": [
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
"serialization",
|
|
11
11
|
"llm",
|
|
12
12
|
"prompting",
|
|
13
|
-
"semi-structured"
|
|
13
|
+
"semi-structured",
|
|
14
|
+
"ofs",
|
|
15
|
+
"output-format",
|
|
16
|
+
"detection",
|
|
17
|
+
"extraction"
|
|
14
18
|
],
|
|
15
19
|
"type": "module",
|
|
16
20
|
"main": "./dist/index.cjs",
|
|
@@ -24,13 +28,18 @@
|
|
|
24
28
|
}
|
|
25
29
|
},
|
|
26
30
|
"files": [
|
|
27
|
-
"dist"
|
|
31
|
+
"dist",
|
|
32
|
+
"docs",
|
|
33
|
+
"README.md",
|
|
34
|
+
"SPEC.md"
|
|
28
35
|
],
|
|
29
36
|
"scripts": {
|
|
30
37
|
"build": "tsc && node ./scripts/cjs-bridge.mjs",
|
|
31
|
-
"test": "
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest"
|
|
32
40
|
},
|
|
33
41
|
"devDependencies": {
|
|
34
|
-
"typescript": "^5.6.3"
|
|
42
|
+
"typescript": "^5.6.3",
|
|
43
|
+
"vitest": "^4.0.16"
|
|
35
44
|
}
|
|
36
45
|
}
|