flex-md 3.2.0 → 3.5.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/dist/__tests__/structural.test.d.ts +1 -0
- package/dist/__tests__/structural.test.js +28 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +108 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/md/outline.d.ts +6 -3
- package/dist/md/outline.js +28 -50
- package/dist/md/parse.js +15 -4
- package/dist/validate/connection.d.ts +12 -0
- package/dist/validate/connection.js +44 -0
- package/package.json +6 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildOutline, checkConnection } from "../index.js";
|
|
3
|
+
describe("Structural Diagnostics", () => {
|
|
4
|
+
describe("===key support", () => {
|
|
5
|
+
it("should parse ===key as a top-level section", () => {
|
|
6
|
+
const md = `===skills\nThis is the skills section.\n\n## Subskill\nDetails.`;
|
|
7
|
+
const outline = buildOutline(md);
|
|
8
|
+
expect(outline.nodes).toHaveLength(1);
|
|
9
|
+
expect(outline.nodes[0].title).toBe("skills");
|
|
10
|
+
expect(outline.nodes[0].children).toHaveLength(1);
|
|
11
|
+
expect(outline.nodes[0].children[0].title).toBe("Subskill");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe("checkConnection", () => {
|
|
15
|
+
it("should find a section by name", () => {
|
|
16
|
+
const md = `# Overview\n## Details\n===skills\nContent.`;
|
|
17
|
+
const res = checkConnection(md, "skills");
|
|
18
|
+
expect(res.connected).toBe(true);
|
|
19
|
+
expect(res.path).toContain("skills");
|
|
20
|
+
});
|
|
21
|
+
it("should fail gracefully for missing sections", () => {
|
|
22
|
+
const md = `# Overview`;
|
|
23
|
+
const res = checkConnection(md, "missing");
|
|
24
|
+
expect(res.connected).toBe(false);
|
|
25
|
+
expect(res.suggestion).toContain("Overview");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { checkCompliance, checkConnection } from "../index.js";
|
|
5
|
+
const [, , command, ...args] = process.argv;
|
|
6
|
+
async function main() {
|
|
7
|
+
if (!command || command === "--help" || command === "-h") {
|
|
8
|
+
printHelp();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
switch (command) {
|
|
13
|
+
case "check":
|
|
14
|
+
await handleCheck(args);
|
|
15
|
+
break;
|
|
16
|
+
case "diagnose":
|
|
17
|
+
await handleDiagnose(args);
|
|
18
|
+
break;
|
|
19
|
+
case "create":
|
|
20
|
+
await handleCreate(args);
|
|
21
|
+
break;
|
|
22
|
+
case "sync":
|
|
23
|
+
await handleSync(args);
|
|
24
|
+
break;
|
|
25
|
+
default:
|
|
26
|
+
console.error(`Unknown command: ${command}`);
|
|
27
|
+
printHelp();
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error(`Error: ${error.message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function printHelp() {
|
|
37
|
+
console.log(`
|
|
38
|
+
Flex-MD CLI v3.2.0
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
flex-md <command> [options]
|
|
42
|
+
|
|
43
|
+
Commands:
|
|
44
|
+
check <file> [--level L2] Check compliance of a markdown file.
|
|
45
|
+
diagnose <file> <key> Diagnose if a specific key/section is connected.
|
|
46
|
+
create <file> <key> Create a new section (folder/file simulation).
|
|
47
|
+
sync <source> <target> Sync content from source to target.
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--help, -h Show this help message.
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
async function handleCheck(args) {
|
|
54
|
+
const file = args[0];
|
|
55
|
+
if (!file)
|
|
56
|
+
throw new Error("Missing file argument.");
|
|
57
|
+
const level = (args.includes("--level") ? args[args.indexOf("--level") + 1] : "L2");
|
|
58
|
+
const content = readFileSync(resolve(file), "utf-8");
|
|
59
|
+
const result = await checkCompliance(content, level);
|
|
60
|
+
if (result.meetsCompliance) {
|
|
61
|
+
console.log("✅ Content meets compliance.");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log("❌ Content does not meet compliance.");
|
|
65
|
+
result.issues.forEach(i => console.log(` - [${i.severity}] ${i.description}`));
|
|
66
|
+
console.log("\nSuggestions:");
|
|
67
|
+
result.suggestions.forEach(s => console.log(` - ${s}`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function handleDiagnose(args) {
|
|
71
|
+
const file = args[0];
|
|
72
|
+
const key = args[1];
|
|
73
|
+
if (!file || !key)
|
|
74
|
+
throw new Error("Usage: flex-md diagnose <file> <key>");
|
|
75
|
+
if (!existsSync(file))
|
|
76
|
+
throw new Error(`File not found: ${file}`);
|
|
77
|
+
const content = readFileSync(resolve(file), "utf-8");
|
|
78
|
+
const result = checkConnection(content, key);
|
|
79
|
+
if (result.connected) {
|
|
80
|
+
console.log(`✅ Key "${key}" found at path: ${result.path?.join(" -> ")}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(`❌ Key "${key}" not connected.`);
|
|
84
|
+
console.log(`Suggestion: ${result.suggestion}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function handleCreate(args) {
|
|
88
|
+
const file = args[0];
|
|
89
|
+
const key = args[1];
|
|
90
|
+
if (!file || !key)
|
|
91
|
+
throw new Error("Usage: flex-md create <file> <key>");
|
|
92
|
+
const content = existsSync(file) ? readFileSync(file, "utf-8") : "";
|
|
93
|
+
const newContent = content + `\n\n## ${key}\n\n[Content for ${key}]\n`;
|
|
94
|
+
writeFileSync(file, newContent);
|
|
95
|
+
console.log(`✅ Section "${key}" created in ${file}`);
|
|
96
|
+
}
|
|
97
|
+
async function handleSync(args) {
|
|
98
|
+
const source = args[0];
|
|
99
|
+
const target = args[1];
|
|
100
|
+
if (!source || !target)
|
|
101
|
+
throw new Error("Usage: flex-md sync <source> <target>");
|
|
102
|
+
if (!existsSync(source))
|
|
103
|
+
throw new Error(`Source file not found: ${source}`);
|
|
104
|
+
const content = readFileSync(source, "utf-8");
|
|
105
|
+
writeFileSync(target, content);
|
|
106
|
+
console.log(`✅ Content synced from ${source} to ${target}`);
|
|
107
|
+
}
|
|
108
|
+
main();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
2
|
export * from "./strictness/types.js";
|
|
3
3
|
export * from "./md/parse.js";
|
|
4
|
+
export { buildOutline } from "./md/outline.js";
|
|
4
5
|
export { parseOutputFormatSpec, validateFormat } from "./ofs/parser.js";
|
|
5
6
|
export { stringifyOutputFormatSpec } from "./ofs/stringify.js";
|
|
6
7
|
export { buildMarkdownGuidance, enrichInstructions, enrichInstructionsWithFlexMd } from "./ofs/enricher.js";
|
|
7
8
|
export { validateMarkdownAgainstOfs } from "./validate/validate.js";
|
|
8
9
|
export { checkCompliance, hasFlexMdContract } from "./validate/compliance.js";
|
|
10
|
+
export { checkConnection } from "./validate/connection.js";
|
|
9
11
|
export { extractFromMarkdown } from "./extract/extract.js";
|
|
10
12
|
export { processResponseMarkdown } from "./strictness/processor.js";
|
|
11
13
|
export { parseIssuesEnvelope, buildIssuesEnvelope, buildIssuesEnvelopeAuto } from "./ofs/issuesEnvelope.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./types.js";
|
|
|
3
3
|
export * from "./strictness/types.js";
|
|
4
4
|
// Shared MD Parsing
|
|
5
5
|
export * from "./md/parse.js";
|
|
6
|
+
export { buildOutline } from "./md/outline.js";
|
|
6
7
|
// Output Format Spec (OFS)
|
|
7
8
|
export { parseOutputFormatSpec, validateFormat } from "./ofs/parser.js";
|
|
8
9
|
export { stringifyOutputFormatSpec } from "./ofs/stringify.js";
|
|
@@ -10,6 +11,7 @@ export { buildMarkdownGuidance, enrichInstructions, enrichInstructionsWithFlexMd
|
|
|
10
11
|
// Validation & Extraction
|
|
11
12
|
export { validateMarkdownAgainstOfs } from "./validate/validate.js";
|
|
12
13
|
export { checkCompliance, hasFlexMdContract } from "./validate/compliance.js";
|
|
14
|
+
export { checkConnection } from "./validate/connection.js";
|
|
13
15
|
export { extractFromMarkdown } from "./extract/extract.js";
|
|
14
16
|
// Processor & Fallback
|
|
15
17
|
export { processResponseMarkdown } from "./strictness/processor.js";
|
package/dist/md/outline.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { MdOutline } from "../types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Builds a nested outline
|
|
4
|
-
* Supports any heading level and captures content between headings.
|
|
3
|
+
* Builds a nested tree of headings (outline) from Markdown text.
|
|
5
4
|
*/
|
|
6
5
|
export declare function buildOutline(md: string): MdOutline;
|
|
6
|
+
/**
|
|
7
|
+
* Slugifies a string into an internal key.
|
|
8
|
+
*/
|
|
9
|
+
export declare function slugify(text: string): string;
|
package/dist/md/outline.js
CHANGED
|
@@ -1,67 +1,45 @@
|
|
|
1
|
+
import { parseHeadingsAndSections } from "./parse.js";
|
|
1
2
|
/**
|
|
2
|
-
* Builds a nested outline
|
|
3
|
-
* Supports any heading level and captures content between headings.
|
|
3
|
+
* Builds a nested tree of headings (outline) from Markdown text.
|
|
4
4
|
*/
|
|
5
5
|
export function buildOutline(md) {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
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 = [];
|
|
6
|
+
const sections = parseHeadingsAndSections(md);
|
|
7
|
+
const roots = [];
|
|
18
8
|
const stack = [];
|
|
19
|
-
for (
|
|
20
|
-
const
|
|
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";
|
|
9
|
+
for (const s of sections) {
|
|
10
|
+
const h = s.heading;
|
|
25
11
|
const node = {
|
|
26
|
-
title:
|
|
27
|
-
level:
|
|
28
|
-
key:
|
|
29
|
-
content_md,
|
|
12
|
+
title: h.name,
|
|
13
|
+
level: h.level,
|
|
14
|
+
key: h.norm,
|
|
15
|
+
content_md: s.body,
|
|
30
16
|
children: []
|
|
31
17
|
};
|
|
32
|
-
|
|
18
|
+
// Pop from stack until we find a parent (level < node.level)
|
|
19
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= node.level) {
|
|
33
20
|
stack.pop();
|
|
34
21
|
}
|
|
35
|
-
if (
|
|
36
|
-
|
|
22
|
+
if (stack.length === 0) {
|
|
23
|
+
roots.push(node);
|
|
37
24
|
}
|
|
38
25
|
else {
|
|
39
26
|
stack[stack.length - 1].children.push(node);
|
|
40
27
|
}
|
|
41
28
|
stack.push(node);
|
|
42
29
|
}
|
|
43
|
-
|
|
44
|
-
|
|
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);
|
|
30
|
+
return {
|
|
31
|
+
type: "md_outline",
|
|
32
|
+
nodes: roots
|
|
65
33
|
};
|
|
66
|
-
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Slugifies a string into an internal key.
|
|
37
|
+
*/
|
|
38
|
+
export function slugify(text) {
|
|
39
|
+
return text
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.trim()
|
|
42
|
+
.replace(/[^\w\s-]/g, "")
|
|
43
|
+
.replace(/[\s_-]+/g, "-")
|
|
44
|
+
.replace(/^-+|-+$/g, "");
|
|
67
45
|
}
|
package/dist/md/parse.js
CHANGED
|
@@ -28,17 +28,28 @@ export function extractFencedBlocks(text) {
|
|
|
28
28
|
return blocks;
|
|
29
29
|
}
|
|
30
30
|
export function parseHeadingsAndSections(md) {
|
|
31
|
-
|
|
31
|
+
// Standard headings #... and alternative ===key
|
|
32
|
+
const rx = /^((?:#{1,6})\s+(.+?)\s*|===(.+?)\s*)$/gm;
|
|
32
33
|
const headings = [];
|
|
33
34
|
let m;
|
|
34
35
|
while ((m = rx.exec(md)) !== null) {
|
|
35
|
-
const
|
|
36
|
-
|
|
36
|
+
const full = m[1] ?? "";
|
|
37
|
+
let level;
|
|
38
|
+
let name;
|
|
39
|
+
if (full.startsWith("===")) {
|
|
40
|
+
level = 1; // Treat ===key as a top-level heading
|
|
41
|
+
name = (m[3] ?? "").trim();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const hashes = (full.match(/^#+/) ?? [""])[0];
|
|
45
|
+
level = hashes.length;
|
|
46
|
+
name = (m[2] ?? "").trim();
|
|
47
|
+
}
|
|
37
48
|
const raw = m[0] ?? "";
|
|
38
49
|
const start = m.index;
|
|
39
50
|
const end = start + raw.length;
|
|
40
51
|
headings.push({
|
|
41
|
-
level
|
|
52
|
+
level,
|
|
42
53
|
raw,
|
|
43
54
|
name,
|
|
44
55
|
norm: normalizeName(name),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ConnectionResult {
|
|
2
|
+
connected: boolean;
|
|
3
|
+
key: string;
|
|
4
|
+
normalizedKey: string;
|
|
5
|
+
path?: string[];
|
|
6
|
+
alternativeMatches?: string[];
|
|
7
|
+
suggestion?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a specific key (section) is "connected" in the given markdown.
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkConnection(md: string, key: string): ConnectionResult;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { buildOutline } from "../md/outline.js";
|
|
2
|
+
import { normalizeName } from "../md/parse.js";
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a specific key (section) is "connected" in the given markdown.
|
|
5
|
+
*/
|
|
6
|
+
export function checkConnection(md, key) {
|
|
7
|
+
const outline = buildOutline(md);
|
|
8
|
+
const normKey = normalizeName(key);
|
|
9
|
+
const path = [];
|
|
10
|
+
// Breadth-first search for the key
|
|
11
|
+
const queue = outline.nodes.map(n => ({ node: n, currentPath: [n.title] }));
|
|
12
|
+
const alternatives = [];
|
|
13
|
+
while (queue.length > 0) {
|
|
14
|
+
const { node, currentPath } = queue.shift();
|
|
15
|
+
if (node.key === normKey) {
|
|
16
|
+
return {
|
|
17
|
+
connected: true,
|
|
18
|
+
key,
|
|
19
|
+
normalizedKey: normKey,
|
|
20
|
+
path: currentPath
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Collect alternatives (any heading that exists)
|
|
24
|
+
alternatives.push(node.title);
|
|
25
|
+
for (const child of node.children) {
|
|
26
|
+
queue.push({ node: child, currentPath: [...currentPath, child.title] });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Not found, look for fuzzy matches or common issues
|
|
30
|
+
const suggestions = [];
|
|
31
|
+
if (alternatives.length === 0) {
|
|
32
|
+
suggestions.push("The document appears to have no headings.");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
suggestions.push(`Available sections: ${alternatives.join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
connected: false,
|
|
39
|
+
key,
|
|
40
|
+
normalizedKey: normKey,
|
|
41
|
+
alternativeMatches: alternatives,
|
|
42
|
+
suggestion: suggestions.join(" ")
|
|
43
|
+
};
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flex-md",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
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": "",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"main": "./dist/index.cjs",
|
|
21
21
|
"module": "./dist/index.js",
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
|
+
"bin": {
|
|
24
|
+
"flex-md": "./dist/cli/index.js"
|
|
25
|
+
},
|
|
23
26
|
"exports": {
|
|
24
27
|
".": {
|
|
25
28
|
"types": "./dist/index.d.ts",
|
|
@@ -39,7 +42,8 @@
|
|
|
39
42
|
"test:watch": "vitest"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.0.3",
|
|
42
46
|
"typescript": "^5.6.3",
|
|
43
47
|
"vitest": "^4.0.16"
|
|
44
48
|
}
|
|
45
|
-
}
|
|
49
|
+
}
|