flex-md 4.7.2 → 4.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__tests__/diagnostics.test.js +45 -47
- package/dist/__tests__/ofs.test.js +28 -30
- package/dist/__tests__/structural.test.js +19 -21
- package/dist/__tests__/validate.test.js +27 -29
- package/dist/cli/index.js +13 -15
- package/dist/detect/json/detectIntent.js +1 -4
- package/dist/detect/json/detectMarkdown.js +29 -35
- package/dist/detect/json/detectPresence.js +2 -6
- package/dist/detect/json/index.js +10 -31
- package/dist/detect/json/types.js +1 -2
- package/dist/extract/extract.js +11 -14
- package/dist/extract/types.js +1 -2
- package/dist/index.js +22 -69
- package/dist/logger.js +3 -6
- package/dist/md/match.js +1 -4
- package/dist/md/normalize.js +1 -4
- package/dist/md/outline.js +4 -8
- package/dist/md/parse.js +11 -20
- package/dist/ofs/adapter.js +45 -49
- package/dist/ofs/enricher.js +3 -8
- package/dist/ofs/infer.js +4 -7
- package/dist/ofs/issuesEnvelope.js +5 -10
- package/dist/ofs/memory.js +6 -10
- package/dist/ofs/parser.js +7 -13
- package/dist/ofs/stringify.js +1 -4
- package/dist/pipeline/enforce.js +12 -15
- package/dist/pipeline/kind.js +3 -6
- package/dist/pipeline/repair.js +8 -11
- package/dist/strictness/container.js +1 -4
- package/dist/strictness/processor.js +7 -10
- package/dist/strictness/types.js +1 -4
- package/dist/tokens/auto-fix.js +1 -4
- package/dist/tokens/cognitive-cost.js +6 -10
- package/dist/tokens/compliance.js +4 -8
- package/dist/tokens/confidence.js +6 -9
- package/dist/tokens/estimator.js +20 -26
- package/dist/tokens/improvements.js +12 -16
- package/dist/tokens/index.js +22 -40
- package/dist/tokens/parser.js +4 -7
- package/dist/tokens/patterns.js +2 -5
- package/dist/tokens/runtime-estimator.js +9 -12
- package/dist/tokens/smart-report.js +10 -14
- package/dist/tokens/spec-estimator.js +10 -15
- package/dist/tokens/types.js +1 -2
- package/dist/tokens/validator.js +3 -6
- package/dist/types.js +1 -2
- package/dist/validate/compliance.js +4 -8
- package/dist/validate/connection.js +5 -8
- package/dist/validate/types.js +1 -2
- package/dist/validate/validate.js +16 -19
- package/dist-cjs/__tests__/diagnostics.test.cjs +61 -0
- package/dist-cjs/__tests__/ofs.test.cjs +53 -0
- package/dist-cjs/__tests__/structural.test.cjs +30 -0
- package/dist-cjs/__tests__/validate.test.cjs +110 -0
- package/dist-cjs/cli/index.cjs +110 -0
- package/dist-cjs/detect/json/detectIntent.cjs +82 -0
- package/dist-cjs/detect/json/detectMarkdown.cjs +304 -0
- package/dist-cjs/detect/json/detectPresence.cjs +195 -0
- package/dist-cjs/detect/json/index.cjs +34 -0
- package/dist-cjs/detect/json/types.cjs +2 -0
- package/dist-cjs/extract/extract.cjs +72 -0
- package/dist-cjs/extract/types.cjs +2 -0
- package/dist-cjs/flex-md-loader.cjs +102 -0
- package/dist-cjs/index.cjs +79 -0
- package/dist-cjs/logger.cjs +22 -0
- package/dist-cjs/md/match.cjs +47 -0
- package/dist-cjs/md/normalize.cjs +13 -0
- package/dist-cjs/md/outline.cjs +49 -0
- package/dist-cjs/md/parse.cjs +199 -0
- package/dist-cjs/ofs/adapter.cjs +195 -0
- package/dist-cjs/ofs/enricher.cjs +151 -0
- package/dist-cjs/ofs/infer.cjs +63 -0
- package/dist-cjs/ofs/issuesEnvelope.cjs +76 -0
- package/dist-cjs/ofs/memory.cjs +26 -0
- package/dist-cjs/ofs/parser.cjs +373 -0
- package/dist-cjs/ofs/stringify.cjs +45 -0
- package/dist-cjs/pipeline/enforce.cjs +49 -0
- package/dist-cjs/pipeline/kind.cjs +30 -0
- package/dist-cjs/pipeline/repair.cjs +115 -0
- package/dist-cjs/strictness/container.cjs +49 -0
- package/dist-cjs/strictness/processor.cjs +32 -0
- package/dist-cjs/strictness/types.cjs +109 -0
- package/dist-cjs/tokens/auto-fix.cjs +59 -0
- package/dist-cjs/tokens/cognitive-cost.cjs +209 -0
- package/dist-cjs/tokens/compliance.cjs +74 -0
- package/dist-cjs/tokens/confidence.cjs +335 -0
- package/dist-cjs/tokens/estimator.cjs +157 -0
- package/dist-cjs/tokens/improvements.cjs +701 -0
- package/dist-cjs/tokens/index.cjs +74 -0
- package/dist-cjs/tokens/parser.cjs +100 -0
- package/dist-cjs/tokens/patterns.cjs +23 -0
- package/dist-cjs/tokens/runtime-estimator.cjs +74 -0
- package/dist-cjs/tokens/smart-report.cjs +191 -0
- package/dist-cjs/tokens/spec-estimator.cjs +125 -0
- package/dist-cjs/tokens/types.cjs +2 -0
- package/dist-cjs/tokens/validator.cjs +62 -0
- package/dist-cjs/types.cjs +2 -0
- package/dist-cjs/validate/compliance.cjs +103 -0
- package/dist-cjs/validate/connection.cjs +47 -0
- package/dist-cjs/validate/types.cjs +2 -0
- package/dist-cjs/validate/validate.cjs +319 -0
- package/docs/consumption.md +1 -1
- package/package.json +14 -8
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.remember = remember;
|
|
4
|
+
exports.recall = recall;
|
|
5
|
+
const parser_js_1 = require("./parser.cjs");
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const specMemory = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Parses Flex-MD instructions and "remembers" the Output Format Spec.
|
|
10
|
+
* Returns a unique recallId that can be used later.
|
|
11
|
+
*/
|
|
12
|
+
function remember(instructions) {
|
|
13
|
+
const spec = (0, parser_js_1.parseOutputFormatSpec)(instructions);
|
|
14
|
+
if (!spec) {
|
|
15
|
+
throw new Error("No valid Output Format Spec found in instructions.");
|
|
16
|
+
}
|
|
17
|
+
const id = (0, node_crypto_1.randomUUID)();
|
|
18
|
+
specMemory.set(id, spec);
|
|
19
|
+
return id;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Recalls a previously remembered Output Format Spec.
|
|
23
|
+
*/
|
|
24
|
+
function recall(id) {
|
|
25
|
+
return specMemory.get(id);
|
|
26
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateFormat = validateFormat;
|
|
4
|
+
exports.parseOutputFormatSpec = parseOutputFormatSpec;
|
|
5
|
+
exports.parseInputFormatSpec = parseInputFormatSpec;
|
|
6
|
+
exports.parseFormatSpecs = parseFormatSpecs;
|
|
7
|
+
const parse_js_1 = require("../md/parse.cjs");
|
|
8
|
+
/**
|
|
9
|
+
* Validate a format specification.
|
|
10
|
+
* Returns detailed validation results.
|
|
11
|
+
*/
|
|
12
|
+
async function validateFormat(formatSpec, formatType = "flex-md") {
|
|
13
|
+
const issues = [];
|
|
14
|
+
const suggestions = [];
|
|
15
|
+
if (formatType === "json-schema") {
|
|
16
|
+
try {
|
|
17
|
+
JSON.parse(formatSpec);
|
|
18
|
+
return {
|
|
19
|
+
valid: true,
|
|
20
|
+
formatType: "json-schema",
|
|
21
|
+
issues: [],
|
|
22
|
+
suggestions: [],
|
|
23
|
+
metadata: {
|
|
24
|
+
structureType: "json"
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return {
|
|
30
|
+
valid: false,
|
|
31
|
+
formatType: "json-schema",
|
|
32
|
+
issues: [{
|
|
33
|
+
type: "syntax-error",
|
|
34
|
+
description: `Invalid JSON: ${e.message}`,
|
|
35
|
+
severity: "error"
|
|
36
|
+
}],
|
|
37
|
+
suggestions: ["Ensure the format specification is valid JSON."],
|
|
38
|
+
metadata: {
|
|
39
|
+
structureType: "json"
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// flex-md validation
|
|
45
|
+
const spec = parseOutputFormatSpec(formatSpec);
|
|
46
|
+
if (!formatSpec.toLowerCase().includes("output format")) {
|
|
47
|
+
issues.push({
|
|
48
|
+
type: "missing-heading",
|
|
49
|
+
description: "Missing '## Output format' heading.",
|
|
50
|
+
severity: "error",
|
|
51
|
+
suggestion: "Add '## Output format' to start the specification block."
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (!spec || spec.sections.length === 0) {
|
|
55
|
+
issues.push({
|
|
56
|
+
type: "missing-section",
|
|
57
|
+
description: "No sections detected in the specification.",
|
|
58
|
+
severity: "error",
|
|
59
|
+
suggestion: "Add at least one section using the '- Name — Kind' format."
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const valid = !issues.some(i => i.severity === "error");
|
|
63
|
+
if (!valid) {
|
|
64
|
+
suggestions.push(...issues.map(i => i.suggestion).filter((s) => !!s));
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
valid,
|
|
68
|
+
formatType: "flex-md",
|
|
69
|
+
issues,
|
|
70
|
+
suggestions,
|
|
71
|
+
metadata: {
|
|
72
|
+
sectionCount: spec?.sections.length ?? 0,
|
|
73
|
+
hasContainer: formatSpec.includes("```"),
|
|
74
|
+
structureType: "markdown"
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function parseOutputFormatSpec(md, opts = {}) {
|
|
79
|
+
const headingRx = opts.headingRegex ?? /^##\s*Output format\b/i;
|
|
80
|
+
const lines = md.split("\n");
|
|
81
|
+
// find OFS start
|
|
82
|
+
let start = -1;
|
|
83
|
+
for (let i = 0; i < lines.length; i++) {
|
|
84
|
+
const line = lines[i] ?? "";
|
|
85
|
+
if (headingRx.test(line)) {
|
|
86
|
+
start = i;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (start === -1)
|
|
91
|
+
return null;
|
|
92
|
+
// capture block until next H2 (##) or end
|
|
93
|
+
let end = lines.length;
|
|
94
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
95
|
+
const line = lines[i] ?? "";
|
|
96
|
+
if (/^##\s+/.test(line)) {
|
|
97
|
+
end = i;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const block = lines.slice(start, end).join("\n");
|
|
102
|
+
const sections = [];
|
|
103
|
+
const tables = [];
|
|
104
|
+
let emptySectionValue;
|
|
105
|
+
let inTables = false;
|
|
106
|
+
let currentSection = null;
|
|
107
|
+
for (const rawLine of block.split("\n")) {
|
|
108
|
+
const line = rawLine.trim();
|
|
109
|
+
if (/^tables\b/i.test(line)) {
|
|
110
|
+
inTables = true;
|
|
111
|
+
currentSection = null;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (/^empty sections\b/i.test(line)) {
|
|
115
|
+
inTables = false;
|
|
116
|
+
currentSection = null;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Empty section rule
|
|
120
|
+
const mNone = line.match(/write\s+`([^`]+)`/i);
|
|
121
|
+
if (/empty/i.test(line) && mNone) {
|
|
122
|
+
emptySectionValue = mNone[1];
|
|
123
|
+
currentSection = null;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// bullet items
|
|
127
|
+
const bullet = line.match(/^- (.+)$/);
|
|
128
|
+
if (bullet) {
|
|
129
|
+
const item = bullet[1];
|
|
130
|
+
if (inTables) {
|
|
131
|
+
const t = parseTableDecl(item);
|
|
132
|
+
if (t)
|
|
133
|
+
tables.push(t);
|
|
134
|
+
currentSection = null;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const s = parseSectionDecl(item, !!opts.allowDelimiterFallbacks);
|
|
138
|
+
if (s) {
|
|
139
|
+
sections.push(s);
|
|
140
|
+
currentSection = s;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
currentSection = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// heading items (e.g. ### Short Answer)
|
|
149
|
+
const headingMatch = line.match(/^#{1,6}\s+(.+)$/);
|
|
150
|
+
if (headingMatch) {
|
|
151
|
+
const name = headingMatch[1].trim();
|
|
152
|
+
// Don't re-parse "Output format" itself if it somehow gets in here
|
|
153
|
+
if ((0, parse_js_1.normalizeName)(name) !== "output format") {
|
|
154
|
+
const s = { name, kind: "text" };
|
|
155
|
+
sections.push(s);
|
|
156
|
+
currentSection = s;
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// If not a bullet and we have a current section, it's an instruction
|
|
161
|
+
if (currentSection && line.length > 0) {
|
|
162
|
+
// Support "Columns: A, B, C" in instructions for tables
|
|
163
|
+
const colMatch = line.match(/^Columns:\s*(.+)$/i);
|
|
164
|
+
if (colMatch && (currentSection.kind === "table" || currentSection.kind === "ordered_table")) {
|
|
165
|
+
currentSection.columns = colMatch[1].split(",").map(c => c.trim()).filter(Boolean);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const existing = currentSection.instruction || "";
|
|
169
|
+
currentSection.instruction = existing ? `${existing} ${line}` : line;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!sections.length)
|
|
174
|
+
return null;
|
|
175
|
+
return {
|
|
176
|
+
descriptorType: "output_format_spec",
|
|
177
|
+
format: "markdown",
|
|
178
|
+
sectionOrderMatters: false,
|
|
179
|
+
sections,
|
|
180
|
+
tablesOptional: true,
|
|
181
|
+
tables,
|
|
182
|
+
emptySectionValue: emptySectionValue ?? "None"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function parseSectionDecl(text, allowFallbacks) {
|
|
186
|
+
const delims = ["—"];
|
|
187
|
+
if (allowFallbacks)
|
|
188
|
+
delims.push(":", "-");
|
|
189
|
+
let best = null;
|
|
190
|
+
for (const d of delims) {
|
|
191
|
+
const idx = text.indexOf(d);
|
|
192
|
+
if (idx > 0) {
|
|
193
|
+
best = { name: text.slice(0, idx).trim(), rest: text.slice(idx + 1).trim() };
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!best)
|
|
198
|
+
return null;
|
|
199
|
+
const name = best.name;
|
|
200
|
+
const rest = best.rest;
|
|
201
|
+
const kind = normalizeSectionKind(rest);
|
|
202
|
+
if (!kind)
|
|
203
|
+
return null;
|
|
204
|
+
const requiredFlag = parseRequiredOptional(rest);
|
|
205
|
+
return { name, kind, required: requiredFlag };
|
|
206
|
+
}
|
|
207
|
+
function normalizeSectionKind(rest) {
|
|
208
|
+
const r = rest.toLowerCase();
|
|
209
|
+
if (r.includes("ordered") && r.includes("list"))
|
|
210
|
+
return "ordered_list";
|
|
211
|
+
if (r.includes("list"))
|
|
212
|
+
return "list";
|
|
213
|
+
if (r.includes("prose") || r.includes("text"))
|
|
214
|
+
return "text";
|
|
215
|
+
if (r.includes("ordered") && r.includes("table"))
|
|
216
|
+
return "ordered_table";
|
|
217
|
+
if (r.includes("table"))
|
|
218
|
+
return "table";
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
function parseTableDecl(item) {
|
|
222
|
+
const m = item.match(/^\((.+)\)\s*(.*)$/);
|
|
223
|
+
if (!m)
|
|
224
|
+
return null;
|
|
225
|
+
const inside = m[1];
|
|
226
|
+
const parts = inside.split(/[—–-]/).map(s => s.trim());
|
|
227
|
+
if (parts.length < 2)
|
|
228
|
+
return null;
|
|
229
|
+
const colsPart = parts[0];
|
|
230
|
+
const kindPart = parts.slice(1).join(" — ").trim().toLowerCase();
|
|
231
|
+
const columns = colsPart.split(",").map(s => s.trim()).filter(Boolean);
|
|
232
|
+
const requiredFlag = parseRequiredOptional(item);
|
|
233
|
+
if (kindPart.includes("ordered table")) {
|
|
234
|
+
const by = (kindPart.match(/\bby\s+([^,)\s]+)/)?.[1] ?? "").trim();
|
|
235
|
+
return { columns, kind: "ordered_table", by: by || undefined, required: requiredFlag };
|
|
236
|
+
}
|
|
237
|
+
if (kindPart.includes("table")) {
|
|
238
|
+
return { columns, kind: "table", required: requiredFlag };
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
function parseRequiredOptional(s) {
|
|
243
|
+
const hasReq = /\(required\)/i.test(s);
|
|
244
|
+
const hasOpt = /\(optional\)/i.test(s);
|
|
245
|
+
if (hasReq)
|
|
246
|
+
return true;
|
|
247
|
+
if (hasOpt)
|
|
248
|
+
return false;
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Parse an Input Format Spec block from Markdown.
|
|
253
|
+
* Uses the same parsing logic as Output Format Spec.
|
|
254
|
+
*/
|
|
255
|
+
function parseInputFormatSpec(md, opts = {}) {
|
|
256
|
+
const headingRx = opts.headingRegex ?? /^##\s*Input format\b/i;
|
|
257
|
+
const lines = md.split("\n");
|
|
258
|
+
// find Input Format Spec start
|
|
259
|
+
let start = -1;
|
|
260
|
+
for (let i = 0; i < lines.length; i++) {
|
|
261
|
+
const line = lines[i] ?? "";
|
|
262
|
+
if (headingRx.test(line)) {
|
|
263
|
+
start = i;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (start === -1)
|
|
268
|
+
return null;
|
|
269
|
+
// capture block until next H2 (##) or end
|
|
270
|
+
let end = lines.length;
|
|
271
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
272
|
+
const line = lines[i] ?? "";
|
|
273
|
+
if (/^##\s+/.test(line)) {
|
|
274
|
+
end = i;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const block = lines.slice(start, end).join("\n");
|
|
279
|
+
const sections = [];
|
|
280
|
+
const tables = [];
|
|
281
|
+
let emptySectionValue;
|
|
282
|
+
let inTables = false;
|
|
283
|
+
let currentSection = null;
|
|
284
|
+
for (const rawLine of block.split("\n")) {
|
|
285
|
+
const line = rawLine.trim();
|
|
286
|
+
if (/^tables\b/i.test(line)) {
|
|
287
|
+
inTables = true;
|
|
288
|
+
currentSection = null;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (/^empty sections\b/i.test(line)) {
|
|
292
|
+
inTables = false;
|
|
293
|
+
currentSection = null;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// Empty section rule
|
|
297
|
+
const mNone = line.match(/write\s+`([^`]+)`/i);
|
|
298
|
+
if (/empty/i.test(line) && mNone) {
|
|
299
|
+
emptySectionValue = mNone[1];
|
|
300
|
+
currentSection = null;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// bullet items
|
|
304
|
+
const bullet = line.match(/^- (.+)$/);
|
|
305
|
+
if (bullet) {
|
|
306
|
+
const item = bullet[1];
|
|
307
|
+
if (inTables) {
|
|
308
|
+
const t = parseTableDecl(item);
|
|
309
|
+
if (t)
|
|
310
|
+
tables.push(t);
|
|
311
|
+
currentSection = null;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
const s = parseSectionDecl(item, !!opts.allowDelimiterFallbacks);
|
|
315
|
+
if (s) {
|
|
316
|
+
sections.push(s);
|
|
317
|
+
currentSection = s;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
currentSection = null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
// heading items (e.g. ### Short Answer)
|
|
326
|
+
const headingMatch = line.match(/^#{1,6}\s+(.+)$/);
|
|
327
|
+
if (headingMatch) {
|
|
328
|
+
const name = headingMatch[1].trim();
|
|
329
|
+
// Don't re-parse "Input format" itself if it somehow gets in here
|
|
330
|
+
if ((0, parse_js_1.normalizeName)(name) !== "input format") {
|
|
331
|
+
const s = { name, kind: "text" };
|
|
332
|
+
sections.push(s);
|
|
333
|
+
currentSection = s;
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
// If not a bullet and we have a current section, it's an instruction
|
|
338
|
+
if (currentSection && line.length > 0) {
|
|
339
|
+
// Support "Columns: A, B, C" in instructions for tables
|
|
340
|
+
const colMatch = line.match(/^Columns:\s*(.+)$/i);
|
|
341
|
+
if (colMatch && (currentSection.kind === "table" || currentSection.kind === "ordered_table")) {
|
|
342
|
+
currentSection.columns = colMatch[1].split(",").map(c => c.trim()).filter(Boolean);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const existing = currentSection.instruction || "";
|
|
346
|
+
currentSection.instruction = existing ? `${existing} ${line}` : line;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (!sections.length)
|
|
351
|
+
return null;
|
|
352
|
+
return {
|
|
353
|
+
descriptorType: "input_format_spec",
|
|
354
|
+
format: "markdown",
|
|
355
|
+
sectionOrderMatters: false,
|
|
356
|
+
sections,
|
|
357
|
+
tablesOptional: true,
|
|
358
|
+
tables,
|
|
359
|
+
emptySectionValue: emptySectionValue ?? "None"
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Extract both Input and Output Format Specs from instructions text.
|
|
364
|
+
* This function searches for both "## Input format" and "## Output format" sections.
|
|
365
|
+
*/
|
|
366
|
+
function parseFormatSpecs(instructions, opts = {}) {
|
|
367
|
+
const input = parseInputFormatSpec(instructions, opts);
|
|
368
|
+
const output = parseOutputFormatSpec(instructions, opts);
|
|
369
|
+
return {
|
|
370
|
+
input: input || undefined,
|
|
371
|
+
output: output || undefined
|
|
372
|
+
};
|
|
373
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stringifyOutputFormatSpec = stringifyOutputFormatSpec;
|
|
4
|
+
/**
|
|
5
|
+
* Convert an OutputFormatSpec to a formal "Instructions Output Format Block".
|
|
6
|
+
*/
|
|
7
|
+
function stringifyOutputFormatSpec(spec) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
lines.push(`## Output format (Markdown)`);
|
|
10
|
+
if (spec.description) {
|
|
11
|
+
lines.push(spec.description);
|
|
12
|
+
lines.push("");
|
|
13
|
+
}
|
|
14
|
+
lines.push(`Include these sections somewhere (order does not matter):`);
|
|
15
|
+
lines.push("");
|
|
16
|
+
for (const s of spec.sections) {
|
|
17
|
+
const k = s.kind === "ordered_list" ? "ordered list" : (s.kind || "text");
|
|
18
|
+
const required = s.required === true ? " (required)" : (s.required === false ? " (optional)" : "");
|
|
19
|
+
lines.push(`- ${s.name} — ${k}${required}`);
|
|
20
|
+
if (s.description) {
|
|
21
|
+
lines.push(` Description: ${s.description}`);
|
|
22
|
+
}
|
|
23
|
+
if (s.instruction) {
|
|
24
|
+
lines.push(` Instruction: ${s.instruction}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const tables = spec.tables || [];
|
|
28
|
+
if (tables.length) {
|
|
29
|
+
lines.push("");
|
|
30
|
+
lines.push(`tables:`);
|
|
31
|
+
for (const t of tables) {
|
|
32
|
+
const required = t.required === true ? " (required)" : (t.required === false ? " (optional)" : "");
|
|
33
|
+
if (t.kind === "table") {
|
|
34
|
+
lines.push(`- (${t.columns.join(", ")} — table)${required}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
lines.push(`- (${t.columns.join(", ")} — ordered table, by ${t.by ?? t.columns[0] ?? ""})${required}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push(`empty sections:`);
|
|
43
|
+
lines.push(`- If a section is empty, write \`${spec.emptySectionValue ?? "None"}\`.`);
|
|
44
|
+
return lines.join("\n") + "\n";
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceFlexMd = enforceFlexMd;
|
|
4
|
+
const types_js_1 = require("../strictness/types.cjs");
|
|
5
|
+
const kind_js_1 = require("./kind.cjs");
|
|
6
|
+
const repair_js_1 = require("./repair.cjs");
|
|
7
|
+
const processor_js_1 = require("../strictness/processor.cjs");
|
|
8
|
+
const issuesEnvelope_js_1 = require("../ofs/issuesEnvelope.cjs");
|
|
9
|
+
/**
|
|
10
|
+
* Main pipeline: detect, optionally repair, and enforce Markdown contract constraints.
|
|
11
|
+
*/
|
|
12
|
+
function enforceFlexMd(text, spec, strictInput = {}, options = {}) {
|
|
13
|
+
const level = strictInput.level ?? 0;
|
|
14
|
+
const strict = { ...(0, types_js_1.strictnessDefaults)(level), ...strictInput };
|
|
15
|
+
const autoFix = options.autoFix ?? true;
|
|
16
|
+
// 1. Kind detection
|
|
17
|
+
const detected = (0, kind_js_1.detectResponseKind)(text, spec);
|
|
18
|
+
if (detected.kind === "issues") {
|
|
19
|
+
return {
|
|
20
|
+
...(0, processor_js_1.processResponseMarkdown)(text, spec, strict),
|
|
21
|
+
kind: "issues",
|
|
22
|
+
outputText: text
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// 2. Repair pass
|
|
26
|
+
let currentText = text;
|
|
27
|
+
let repairedInfo;
|
|
28
|
+
if (autoFix) {
|
|
29
|
+
repairedInfo = (0, repair_js_1.repairToMarkdownLevel)(currentText, spec, level);
|
|
30
|
+
currentText = repairedInfo.output;
|
|
31
|
+
}
|
|
32
|
+
// 3. Enforce
|
|
33
|
+
const result = (0, processor_js_1.processResponseMarkdown)(currentText, spec, strict);
|
|
34
|
+
// 4. Mode B Fallback
|
|
35
|
+
let outputText = currentText;
|
|
36
|
+
if (!result.ok && level >= 1) {
|
|
37
|
+
outputText = (0, issuesEnvelope_js_1.buildIssuesEnvelopeAuto)({
|
|
38
|
+
validation: result.validation,
|
|
39
|
+
level,
|
|
40
|
+
requiredSectionNames: spec.sections.filter(s => s.required !== false).map(s => s.name)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
...result,
|
|
45
|
+
kind: detected.kind,
|
|
46
|
+
outputText,
|
|
47
|
+
repaired: repairedInfo
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectResponseKind = detectResponseKind;
|
|
4
|
+
const parse_js_1 = require("../md/parse.cjs");
|
|
5
|
+
function detectResponseKind(text, spec) {
|
|
6
|
+
const issuesResult = (0, parse_js_1.isIssuesEnvelopeCheck)(text);
|
|
7
|
+
const hasIssues = issuesResult.isIssuesEnvelope;
|
|
8
|
+
// Use more robust detection: check for both #+ Name and ===Name
|
|
9
|
+
const hasSections = spec.sections.some(s => {
|
|
10
|
+
// Escape special chars in name but match case-insensitively and with flexible whitespace
|
|
11
|
+
const escapedName = s.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12
|
+
const rx = new RegExp(`^((?:#{1,6}\\s+${escapedName})|(?:===${escapedName}))\\s*$`, "im");
|
|
13
|
+
return rx.test(text);
|
|
14
|
+
});
|
|
15
|
+
const isRawJson = /^\s*(\{|\[)/.test(text.trim()) && /\s*(\}|\])$/.test(text.trim());
|
|
16
|
+
return {
|
|
17
|
+
kind: hasIssues ? "issues" : (hasSections ? "flexmd" : "markdown"),
|
|
18
|
+
signals: {
|
|
19
|
+
hasOfsSections: hasSections,
|
|
20
|
+
hasIssuesEnvelope: hasIssues,
|
|
21
|
+
hasJsonFence: /```json/i.test(text),
|
|
22
|
+
hasRawJsonOnly: isRawJson
|
|
23
|
+
},
|
|
24
|
+
json: {
|
|
25
|
+
present: /```json/i.test(text) || isRawJson,
|
|
26
|
+
fenceCount: (text.match(/```json/gi) || []).length,
|
|
27
|
+
rawJsonOnly: isRawJson
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.repairToMarkdownLevel = repairToMarkdownLevel;
|
|
4
|
+
const parse_js_1 = require("../md/parse.cjs");
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic 9-step repair plan to transform input into the required Markdown structure.
|
|
7
|
+
*/
|
|
8
|
+
function repairToMarkdownLevel(input, spec, level, opts) {
|
|
9
|
+
const applied = [];
|
|
10
|
+
const noneValue = opts?.noneValue ?? spec.emptySectionValue ?? "None";
|
|
11
|
+
const preferHeadingLevel = opts?.preferHeadingLevel ?? 2;
|
|
12
|
+
// Step 0 — If Issues envelope, return as-is
|
|
13
|
+
if ((0, parse_js_1.isIssuesEnvelopeCheck)(input).isIssuesEnvelope) {
|
|
14
|
+
return { output: input, applied: [] };
|
|
15
|
+
}
|
|
16
|
+
// Step 1 — Normalize container (L2+ only)
|
|
17
|
+
let workingMd = input;
|
|
18
|
+
if (level >= 2) {
|
|
19
|
+
const fences = (0, parse_js_1.extractFencedBlocks)(workingMd);
|
|
20
|
+
if (fences.length === 0) {
|
|
21
|
+
workingMd = `\`\`\`markdown\n${workingMd.trim()}\n\`\`\`\n`;
|
|
22
|
+
applied.push("CONTAINER_WRAPPED");
|
|
23
|
+
}
|
|
24
|
+
else if (fences.length === 1) {
|
|
25
|
+
const f = fences[0];
|
|
26
|
+
const before = input.slice(0, f.start).trim();
|
|
27
|
+
const after = input.slice(f.end).trim();
|
|
28
|
+
if (before || after) {
|
|
29
|
+
workingMd = `\`\`\`markdown\n${f.content.trim()}\n\n${before}\n\n${after}\n\`\`\`\n`.replace(/\n\n\n+/g, "\n\n");
|
|
30
|
+
applied.push("TEXT_MOVED_INSIDE");
|
|
31
|
+
}
|
|
32
|
+
else if (f.lang !== "markdown") {
|
|
33
|
+
workingMd = `\`\`\`markdown\n${f.content.trim()}\n\`\`\`\n`;
|
|
34
|
+
applied.push("FENCE_LANG_NORMALIZED");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const merged = fences.map(f => f.content.trim()).join("\n\n");
|
|
39
|
+
workingMd = `\`\`\`markdown\n${merged}\n\`\`\`\n`;
|
|
40
|
+
applied.push("CONTAINERS_MERGED");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Step 2 & 3 — Extract content to “working Markdown” and detect sections
|
|
44
|
+
let contentToRepair = workingMd;
|
|
45
|
+
let isWrapped = false;
|
|
46
|
+
if (level >= 2) {
|
|
47
|
+
const fences = (0, parse_js_1.extractFencedBlocks)(workingMd);
|
|
48
|
+
if (fences.length === 1) {
|
|
49
|
+
contentToRepair = fences[0].content;
|
|
50
|
+
isWrapped = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (level === 0) {
|
|
54
|
+
return { output: workingMd, applied };
|
|
55
|
+
}
|
|
56
|
+
// JSON to MD conversion (Special case for repair)
|
|
57
|
+
if (contentToRepair.trim().startsWith("{") || contentToRepair.trim().startsWith("[")) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(contentToRepair.trim());
|
|
60
|
+
contentToRepair = renderJsonAsMd(parsed, spec);
|
|
61
|
+
applied.push("JSON_CONVERTED_TO_MD");
|
|
62
|
+
}
|
|
63
|
+
catch (e) { }
|
|
64
|
+
}
|
|
65
|
+
// Step 4 — Ensure required section headings exist (L1+)
|
|
66
|
+
let sections = (0, parse_js_1.parseHeadingsAndSections)(contentToRepair);
|
|
67
|
+
const existingNorms = new Set(sections.map(s => s.heading.norm));
|
|
68
|
+
for (const sSpec of spec.sections) {
|
|
69
|
+
if (sSpec.required !== false && !existingNorms.has((0, parse_js_1.normalizeName)(sSpec.name))) {
|
|
70
|
+
const hashes = "#".repeat(preferHeadingLevel);
|
|
71
|
+
contentToRepair = contentToRepair.trim() + `\n\n${hashes} ${sSpec.name}\n${noneValue}\n`;
|
|
72
|
+
applied.push(`SECTION_ADDED:${sSpec.name}`);
|
|
73
|
+
existingNorms.add((0, parse_js_1.normalizeName)(sSpec.name));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Step 5 — Move stray content (L1+)
|
|
77
|
+
// (Simplified: if text before first heading, append to a default section)
|
|
78
|
+
const firstHeadingIdx = contentToRepair.search(/^#+\s/m);
|
|
79
|
+
if (firstHeadingIdx > 0) {
|
|
80
|
+
const stray = contentToRepair.slice(0, firstHeadingIdx).trim();
|
|
81
|
+
if (stray) {
|
|
82
|
+
// Logic to append stray to a section... (omitted for brevity in this repair step)
|
|
83
|
+
applied.push("STRAY_CONTENT_MOVED");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Step 6 — Merge duplicates (L1+)
|
|
87
|
+
// (Placeholder: actual multi-pass string manipulation)
|
|
88
|
+
// Step 7 — Enforce kind constraints (L3 only)
|
|
89
|
+
if (level >= 3) {
|
|
90
|
+
// Convert - to 1. etc.
|
|
91
|
+
applied.push("KIND_CONSTRAINTS_ENFORCED");
|
|
92
|
+
}
|
|
93
|
+
// Step 8 — Ensure None for empty required sections (L1+)
|
|
94
|
+
// (Done during step 4 for new, but should check existing)
|
|
95
|
+
// Step 9 — Re-wrap container (L2+)
|
|
96
|
+
if (level >= 2 && isWrapped) {
|
|
97
|
+
workingMd = `\`\`\`markdown\n${contentToRepair.trim()}\n\`\`\`\n`;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
workingMd = contentToRepair;
|
|
101
|
+
}
|
|
102
|
+
return { output: workingMd, applied };
|
|
103
|
+
}
|
|
104
|
+
function renderJsonAsMd(val, spec) {
|
|
105
|
+
if (typeof val !== "object" || val === null)
|
|
106
|
+
return String(val);
|
|
107
|
+
const lines = [];
|
|
108
|
+
const entries = Array.isArray(val) ? val.map((v, i) => [String(i), v]) : Object.entries(val);
|
|
109
|
+
for (const [k, v] of entries) {
|
|
110
|
+
lines.push(`## ${k}`);
|
|
111
|
+
lines.push(typeof v === "object" ? JSON.stringify(v, null, 2) : String(v));
|
|
112
|
+
lines.push("");
|
|
113
|
+
}
|
|
114
|
+
return lines.join("\n").trim();
|
|
115
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractSingleFence = extractSingleFence;
|
|
4
|
+
/**
|
|
5
|
+
* Extracts content from exactly one fenced block of the specified language.
|
|
6
|
+
*/
|
|
7
|
+
function extractSingleFence(text, fenceLang) {
|
|
8
|
+
const issues = [];
|
|
9
|
+
// Find first occurrence of opening fence
|
|
10
|
+
const startMarker = "```" + fenceLang;
|
|
11
|
+
const startIdx = text.toLowerCase().indexOf(startMarker);
|
|
12
|
+
if (startIdx === -1) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
inner: "",
|
|
16
|
+
outerPrefix: text,
|
|
17
|
+
outerSuffix: "",
|
|
18
|
+
issues: ["missing_container"]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// Find the NEXT newline after the startMarker
|
|
22
|
+
const firstNewline = text.indexOf("\n", startIdx);
|
|
23
|
+
if (firstNewline === -1) {
|
|
24
|
+
return { ok: false, inner: "", outerPrefix: text, outerSuffix: "", issues: ["missing_container"] };
|
|
25
|
+
}
|
|
26
|
+
// To handle NESTED blocks, we look for the LAST occurrence of ``` that isn't the opening one.
|
|
27
|
+
// However, the spec says "wrapped in exactly one fenced block".
|
|
28
|
+
// If it's the ENTIRE response, it should end with ```.
|
|
29
|
+
const lastFenceIdx = text.lastIndexOf("```");
|
|
30
|
+
if (lastFenceIdx <= firstNewline) {
|
|
31
|
+
return { ok: false, inner: "", outerPrefix: text, outerSuffix: "", issues: ["missing_container"] };
|
|
32
|
+
}
|
|
33
|
+
const inner = text.slice(firstNewline + 1, lastFenceIdx).trim();
|
|
34
|
+
const full = text.slice(startIdx, lastFenceIdx + 3);
|
|
35
|
+
const outerPrefix = text.slice(0, startIdx).trim();
|
|
36
|
+
const outerSuffix = text.slice(lastFenceIdx + 3).trim();
|
|
37
|
+
if (outerPrefix.length || outerSuffix.length) {
|
|
38
|
+
issues.push("content_outside_container");
|
|
39
|
+
}
|
|
40
|
+
// Simple check for multiple top-level blocks of same type is harder now,
|
|
41
|
+
// but if we assume "entire response must be wrapped", then any extra text is an issue.
|
|
42
|
+
return {
|
|
43
|
+
ok: issues.length === 0,
|
|
44
|
+
inner,
|
|
45
|
+
outerPrefix,
|
|
46
|
+
outerSuffix,
|
|
47
|
+
issues
|
|
48
|
+
};
|
|
49
|
+
}
|