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,50 @@
|
|
|
1
|
+
import { parseHeadingsAndSections, extractBullets, normalizeName } from "../md/parse.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts sections, lists, and tables from Markdown based on the OFS.
|
|
4
|
+
*/
|
|
5
|
+
export function extractFromMarkdown(md, spec) {
|
|
6
|
+
const parsed = parseHeadingsAndSections(md);
|
|
7
|
+
const sectionsByName = {};
|
|
8
|
+
const tables = [];
|
|
9
|
+
const specMap = new Map(spec.sections.map(s => [normalizeName(s.name), s]));
|
|
10
|
+
for (const p of parsed) {
|
|
11
|
+
const norm = normalizeName(p.heading.name);
|
|
12
|
+
const sSpec = specMap.get(norm);
|
|
13
|
+
if (sSpec) {
|
|
14
|
+
const body = p.body.trim();
|
|
15
|
+
let list;
|
|
16
|
+
if (sSpec.kind === "list" || sSpec.kind === "ordered_list") {
|
|
17
|
+
const bullets = extractBullets(body);
|
|
18
|
+
if (bullets.length > 0) {
|
|
19
|
+
list = {
|
|
20
|
+
kind: "list",
|
|
21
|
+
ordered: sSpec.kind === "ordered_list",
|
|
22
|
+
items: bullets.map(b => ({ text: b, children: [] }))
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Merge if duplicate heading (standard policy for extraction)
|
|
27
|
+
if (sectionsByName[sSpec.name]) {
|
|
28
|
+
sectionsByName[sSpec.name].md += "\n\n" + body;
|
|
29
|
+
if (list && sectionsByName[sSpec.name].list) {
|
|
30
|
+
sectionsByName[sSpec.name].list.items.push(...list.items);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
sectionsByName[sSpec.name] = {
|
|
35
|
+
nodeKey: norm,
|
|
36
|
+
nodeLevel: p.heading.level,
|
|
37
|
+
md: body,
|
|
38
|
+
list
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Table extraction logic (simplified for now)
|
|
44
|
+
// In a full impl, we'd search section bodies for tables.
|
|
45
|
+
return {
|
|
46
|
+
outline: { type: "md_outline", nodes: [] }, // Simplified outline
|
|
47
|
+
sectionsByName,
|
|
48
|
+
tables
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MdOutline, ParsedList, ParsedTable } from "../types.js";
|
|
2
|
+
export interface ExtractedResult {
|
|
3
|
+
outline: MdOutline;
|
|
4
|
+
sectionsByName: Record<string, {
|
|
5
|
+
nodeKey: string;
|
|
6
|
+
nodeLevel: number;
|
|
7
|
+
md: string;
|
|
8
|
+
list?: ParsedList;
|
|
9
|
+
}>;
|
|
10
|
+
tables: ParsedTable[];
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export {
|
|
2
|
+
export * from "./strictness/types.js";
|
|
3
|
+
export * from "./md/parse.js";
|
|
4
|
+
export { parseOutputFormatSpec } from "./ofs/parser.js";
|
|
5
|
+
export { stringifyOutputFormatSpec } from "./ofs/stringify.js";
|
|
6
|
+
export { buildMarkdownGuidance, enrichInstructions } from "./ofs/enricher.js";
|
|
7
|
+
export { validateMarkdownAgainstOfs } from "./validate/validate.js";
|
|
8
|
+
export { extractFromMarkdown } from "./extract/extract.js";
|
|
9
|
+
export { processResponseMarkdown } from "./strictness/processor.js";
|
|
10
|
+
export { parseIssuesEnvelope, buildIssuesEnvelope, buildIssuesEnvelopeAuto } from "./ofs/issuesEnvelope.js";
|
|
11
|
+
export { detectResponseKind } from "./pipeline/kind.js";
|
|
12
|
+
export { repairToMarkdownLevel } from "./pipeline/repair.js";
|
|
13
|
+
export { enforceFlexMd } from "./pipeline/enforce.js";
|
|
14
|
+
export * from "./detect/json/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
+
// Core SFMD Types
|
|
1
2
|
export * from "./types.js";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
export
|
|
3
|
+
export * from "./strictness/types.js";
|
|
4
|
+
// Shared MD Parsing
|
|
5
|
+
export * from "./md/parse.js";
|
|
6
|
+
// Output Format Spec (OFS)
|
|
7
|
+
export { parseOutputFormatSpec } from "./ofs/parser.js";
|
|
8
|
+
export { stringifyOutputFormatSpec } from "./ofs/stringify.js";
|
|
9
|
+
export { buildMarkdownGuidance, enrichInstructions } from "./ofs/enricher.js";
|
|
10
|
+
// Validation & Extraction
|
|
11
|
+
export { validateMarkdownAgainstOfs } from "./validate/validate.js";
|
|
12
|
+
export { extractFromMarkdown } from "./extract/extract.js";
|
|
13
|
+
// Processor & Fallback
|
|
14
|
+
export { processResponseMarkdown } from "./strictness/processor.js";
|
|
15
|
+
export { parseIssuesEnvelope, buildIssuesEnvelope, buildIssuesEnvelopeAuto } from "./ofs/issuesEnvelope.js";
|
|
16
|
+
// Pipeline
|
|
17
|
+
export { detectResponseKind } from "./pipeline/kind.js";
|
|
18
|
+
export { repairToMarkdownLevel } from "./pipeline/repair.js";
|
|
19
|
+
export { enforceFlexMd } from "./pipeline/enforce.js";
|
|
20
|
+
// JSON Detection
|
|
21
|
+
export * from "./detect/json/index.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StrictnessOptions } from "../strictness/types.js";
|
|
2
|
+
export type IssuesStatusCode = "missing_input" | "unclear_instructions" | "invalid_format" | "unsupported" | (string & {});
|
|
3
|
+
export interface IssueItem {
|
|
4
|
+
issue: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
expected?: string;
|
|
7
|
+
got?: string;
|
|
8
|
+
hint?: string;
|
|
9
|
+
[k: string]: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface BuildIssuesEnvelopeInput {
|
|
12
|
+
status: "error" | "partial";
|
|
13
|
+
code: IssuesStatusCode;
|
|
14
|
+
message: string;
|
|
15
|
+
issues: IssueItem[];
|
|
16
|
+
missingInputs?: string[];
|
|
17
|
+
clarificationsNeeded?: string[];
|
|
18
|
+
/** If provided, wraps output in a single container fence when strictness requires it. */
|
|
19
|
+
strictness?: StrictnessOptions;
|
|
20
|
+
/** Override fence language: "markdown" | "flexmd" (defaults from strictness.container) */
|
|
21
|
+
fence?: "markdown" | "flexmd";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build a Markdown-only issues envelope.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildIssuesEnvelope(input: BuildIssuesEnvelopeInput): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { strictnessDefaults } from "../strictness/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build a Markdown-only issues envelope.
|
|
4
|
+
*/
|
|
5
|
+
export function buildIssuesEnvelope(input) {
|
|
6
|
+
const strict = input.strictness ? { ...strictnessDefaults(input.strictness.level), ...input.strictness } : undefined;
|
|
7
|
+
const fence = input.fence ?? resolveFence(strict);
|
|
8
|
+
const lines = [];
|
|
9
|
+
lines.push(`## Status`);
|
|
10
|
+
lines.push(`- status: ${input.status}`);
|
|
11
|
+
lines.push(`- code: ${input.code}`);
|
|
12
|
+
lines.push(`- message: ${input.message}`);
|
|
13
|
+
lines.push(``);
|
|
14
|
+
if (input.missingInputs && input.missingInputs.length) {
|
|
15
|
+
lines.push(`## Missing inputs`);
|
|
16
|
+
for (const mi of input.missingInputs)
|
|
17
|
+
lines.push(`- ${mi}`);
|
|
18
|
+
lines.push(``);
|
|
19
|
+
}
|
|
20
|
+
if (input.clarificationsNeeded && input.clarificationsNeeded.length) {
|
|
21
|
+
lines.push(`## Clarifications needed`);
|
|
22
|
+
for (const q of input.clarificationsNeeded)
|
|
23
|
+
lines.push(`- ${q}`);
|
|
24
|
+
lines.push(``);
|
|
25
|
+
}
|
|
26
|
+
lines.push(`## Issues`);
|
|
27
|
+
if (!input.issues.length) {
|
|
28
|
+
lines.push(`- issue: none`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
for (const it of input.issues) {
|
|
32
|
+
lines.push(`- issue: ${it.issue}`);
|
|
33
|
+
emitField(lines, "path", it.path);
|
|
34
|
+
emitField(lines, "expected", it.expected);
|
|
35
|
+
emitField(lines, "got", it.got);
|
|
36
|
+
emitField(lines, "hint", it.hint);
|
|
37
|
+
for (const [k, v] of Object.entries(it)) {
|
|
38
|
+
if (k === "issue" || k === "path" || k === "expected" || k === "got" || k === "hint")
|
|
39
|
+
continue;
|
|
40
|
+
emitField(lines, k, v);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let md = lines.join("\n").trimEnd() + "\n";
|
|
45
|
+
if (strict && strict.level >= 2) {
|
|
46
|
+
const lang = (fence === "flexmd") ? "flexmd" : "markdown";
|
|
47
|
+
md = `\`\`\`${lang}\n${md}\`\`\`\n`;
|
|
48
|
+
}
|
|
49
|
+
return md;
|
|
50
|
+
}
|
|
51
|
+
function emitField(lines, key, value) {
|
|
52
|
+
if (value == null || value === "")
|
|
53
|
+
return;
|
|
54
|
+
lines.push(` ${key}: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
function resolveFence(strict) {
|
|
57
|
+
if (!strict)
|
|
58
|
+
return "markdown";
|
|
59
|
+
if (strict.container === "flexmd_fence")
|
|
60
|
+
return "flexmd";
|
|
61
|
+
return "markdown";
|
|
62
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ListItem {
|
|
2
|
+
text: string;
|
|
3
|
+
index?: number;
|
|
4
|
+
children: ListItem[];
|
|
5
|
+
}
|
|
6
|
+
export interface ParsedList {
|
|
7
|
+
kind: "list";
|
|
8
|
+
ordered: boolean;
|
|
9
|
+
items: ListItem[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parses a flat list segment from Markdown into a nested structure.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseNestedList(sectionMd: string): ParsedList | null;
|
package/dist/md/lists.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a flat list segment from Markdown into a nested structure.
|
|
3
|
+
*/
|
|
4
|
+
export function parseNestedList(sectionMd) {
|
|
5
|
+
const lines = sectionMd.split("\n");
|
|
6
|
+
const listLines = lines
|
|
7
|
+
.map(l => ({ raw: l, indent: (l.match(/^\s*/)?.[0].length ?? 0) }))
|
|
8
|
+
.filter(x => /^\s*(-\s+|\d+\.\s+)/.test(x.raw));
|
|
9
|
+
if (!listLines.length)
|
|
10
|
+
return null;
|
|
11
|
+
const ordered = listLines.some(x => /^\s*\d+\.\s+/.test(x.raw));
|
|
12
|
+
const root = [];
|
|
13
|
+
const stack = [];
|
|
14
|
+
for (const { raw, indent } of listLines) {
|
|
15
|
+
const mO = raw.match(/^\s*(\d+)\.\s+(.*)$/);
|
|
16
|
+
const mU = raw.match(/^\s*-\s+(.*)$/);
|
|
17
|
+
const text = (mO?.[2] ?? mU?.[1] ?? "").trim();
|
|
18
|
+
const item = { text, children: [] };
|
|
19
|
+
if (mO)
|
|
20
|
+
item.index = Number(mO[1]);
|
|
21
|
+
while (stack.length && stack[stack.length - 1].indent >= indent) {
|
|
22
|
+
stack.pop();
|
|
23
|
+
}
|
|
24
|
+
if (!stack.length) {
|
|
25
|
+
root.push(item);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
stack[stack.length - 1].item.children.push(item);
|
|
29
|
+
}
|
|
30
|
+
stack.push({ indent, item });
|
|
31
|
+
}
|
|
32
|
+
return { kind: "list", ordered, items: root };
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MdNode, MdOutline } from "../types.js";
|
|
2
|
+
export interface MatchedSection {
|
|
3
|
+
name: string;
|
|
4
|
+
nodes: MdNode[];
|
|
5
|
+
chosen: MdNode[];
|
|
6
|
+
mergedContent: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Matches sections from the outline against required section names.
|
|
10
|
+
* Supports merging multiple nodes of the same name and selecting highest-level matches.
|
|
11
|
+
*/
|
|
12
|
+
export declare function matchSections(outline: MdOutline, sectionNames: string[], strategy?: "merge" | "first"): Map<string, MatchedSection>;
|
package/dist/md/match.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matches sections from the outline against required section names.
|
|
3
|
+
* Supports merging multiple nodes of the same name and selecting highest-level matches.
|
|
4
|
+
*/
|
|
5
|
+
export function matchSections(outline, sectionNames, strategy = "merge") {
|
|
6
|
+
const normalizedTargets = new Map(sectionNames.map(n => [norm(n), n]));
|
|
7
|
+
const found = new Map();
|
|
8
|
+
walk(outline.nodes, (node) => {
|
|
9
|
+
const k = norm(node.title);
|
|
10
|
+
if (normalizedTargets.has(k)) {
|
|
11
|
+
if (!found.has(k))
|
|
12
|
+
found.set(k, []);
|
|
13
|
+
found.get(k).push(node);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const res = new Map();
|
|
17
|
+
for (const [nk, originalName] of normalizedTargets.entries()) {
|
|
18
|
+
const nodes = found.get(nk) ?? [];
|
|
19
|
+
if (!nodes.length)
|
|
20
|
+
continue;
|
|
21
|
+
// choose highest-level (smallest level number)
|
|
22
|
+
const minLevel = Math.min(...nodes.map(n => n.level));
|
|
23
|
+
const top = nodes.filter(n => n.level === minLevel);
|
|
24
|
+
let chosen;
|
|
25
|
+
if (strategy === "first") {
|
|
26
|
+
chosen = [top[0]];
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
chosen = top; // merge same-level matches
|
|
30
|
+
}
|
|
31
|
+
const merged = chosen.map(n => n.content_md).join("\n").trimEnd() + "\n";
|
|
32
|
+
res.set(originalName, { name: originalName, nodes, chosen, mergedContent: merged });
|
|
33
|
+
}
|
|
34
|
+
return res;
|
|
35
|
+
}
|
|
36
|
+
function walk(nodes, fn) {
|
|
37
|
+
for (const n of nodes) {
|
|
38
|
+
fn(n);
|
|
39
|
+
walk(n.children, fn);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function norm(s) {
|
|
43
|
+
return s.trim().replace(/[:\-–—]\s*$/, "").trim().toLowerCase();
|
|
44
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { OutputFormatSpec } from "../types.js";
|
|
2
|
+
import { StrictnessOptions } from "../strictness/types.js";
|
|
3
|
+
/**
|
|
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.
|
|
6
|
+
*/
|
|
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;
|