flex-md 3.1.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/README.md CHANGED
@@ -9,8 +9,8 @@ Version 3.0 introduces the **Detect-Repair-Enforce** pipeline, ensuring LLM resp
9
9
  - **Standard Markdown**: No proprietary tags. Pure headings, lists, and tables.
10
10
  - **Strictness Levels (L0–L3)**: From loose guidance to rigid structural enforcement.
11
11
  - **Deterministic Repair**: Auto-fixes misformatted LLM output (merged fences, missing headings, format conversion).
12
+ - **Instructions Output Format Guidance**: Generate formal "Instructions Blocks" for LLM prompts directly from spec objects.
12
13
  - **Issues Envelope**: A structured failure format for when repairs fail, allowing safe fallbacks.
13
- - **Tax-Aware Prompts**: Generates minimal, relevant instructions to save tokens.
14
14
 
15
15
  ## Installation
16
16
 
@@ -61,6 +61,35 @@ if (result.ok) {
61
61
  }
62
62
  ```
63
63
 
64
+ ### 4. Dynamic Instructions Output Format Blocks
65
+
66
+ You can also define your spec as a plain JavaScript object and generate the formal "Instructions Block" for your LLM prompts.
67
+
68
+ ```typescript
69
+ import { stringifyOutputFormatSpec } from 'flex-md';
70
+
71
+ const spec = {
72
+ description: "Standard report format for technical analysis.",
73
+ sections: [
74
+ {
75
+ name: "Summary",
76
+ kind: "text",
77
+ required: true,
78
+ description: "A brief summary of the topic."
79
+ },
80
+ {
81
+ name: "Key Points",
82
+ kind: "list",
83
+ required: false,
84
+ instruction: "Include at least 3 bullet points."
85
+ }
86
+ ]
87
+ };
88
+
89
+ const instructionsBlock = stringifyOutputFormatSpec(spec);
90
+ // Output will include the ## Output format (Markdown) header, descriptions, and instructions.
91
+ ```
92
+
64
93
  ## Strictness Levels
65
94
 
66
95
  | Level | Goal | Guidance | Enforcement |
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { checkCompliance, hasFlexMdContract, enrichInstructionsWithFlexMd, validateFormat } from "../index.js";
3
+ describe("Diagnostics", () => {
4
+ describe("checkCompliance", () => {
5
+ it("should return meetsCompliance: true for L0 if Markdown is mentioned", async () => {
6
+ const res = await checkCompliance("Reply in Markdown.", "L0");
7
+ expect(res.meetsCompliance).toBe(true);
8
+ expect(res.issues).toHaveLength(0);
9
+ });
10
+ it("should return issues for L2 if container is missing", async () => {
11
+ const res = await checkCompliance("Reply in Markdown. Include headings.", "L2");
12
+ expect(res.meetsCompliance).toBe(false);
13
+ expect(res.issues.some(i => i.type === "missing-container")).toBe(true);
14
+ });
15
+ it("should return false for hasFlexMdContract if requirements not met", async () => {
16
+ const ok = await hasFlexMdContract("Hello", "L0");
17
+ expect(ok).toBe(false);
18
+ });
19
+ });
20
+ describe("enrichInstructionsWithFlexMd", () => {
21
+ it("should append guidance to existing instructions", async () => {
22
+ const res = await enrichInstructionsWithFlexMd("Do a good job.", "L0");
23
+ expect(res.enriched).toContain("Do a good job.");
24
+ expect(res.enriched).toContain("Reply in Markdown.");
25
+ expect(res.changes).toHaveLength(1);
26
+ expect(res.metadata?.guidanceAdded).toBe(true);
27
+ });
28
+ it("should track container addition for L2", async () => {
29
+ const res = await enrichInstructionsWithFlexMd("Do a good job.", "L2");
30
+ expect(res.changes.some(c => c.type === "added-container")).toBe(true);
31
+ expect(res.metadata?.containerAdded).toBe(true);
32
+ });
33
+ });
34
+ describe("validateFormat", () => {
35
+ it("should validate a correct flex-md OFS", async () => {
36
+ const ofs = `## Output format\n- Summary — prose\n- Actions — list`;
37
+ const res = await validateFormat(ofs);
38
+ expect(res.valid).toBe(true);
39
+ expect(res.metadata?.sectionCount).toBe(2);
40
+ });
41
+ it("should return errors for missing heading", async () => {
42
+ const ofs = `- Summary — prose`;
43
+ const res = await validateFormat(ofs);
44
+ expect(res.valid).toBe(false);
45
+ expect(res.issues.some(i => i.type === "missing-heading")).toBe(true);
46
+ });
47
+ it("should validate JSON schema", async () => {
48
+ const schema = `{"type": "object"}`;
49
+ const res = await validateFormat(schema, "json-schema");
50
+ expect(res.valid).toBe(true);
51
+ });
52
+ it("should fail on invalid JSON schema", async () => {
53
+ const schema = `{"type": "object"`;
54
+ const res = await validateFormat(schema, "json-schema");
55
+ expect(res.valid).toBe(false);
56
+ expect(res.issues[0].type).toBe("syntax-error");
57
+ });
58
+ });
59
+ });
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { stringifyOutputFormatSpec } from "../ofs/stringify.js";
3
3
  import { buildMarkdownGuidance } from "../ofs/enricher.js";
4
- describe("OFS Object-to-Prompt Flow", () => {
4
+ describe("Instructions Output Format Block Generation", () => {
5
5
  const spec = {
6
6
  description: "Standard report format for technical analysis.",
7
7
  sections: [
@@ -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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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,10 +1,13 @@
1
1
  export * from "./types.js";
2
2
  export * from "./strictness/types.js";
3
3
  export * from "./md/parse.js";
4
- export { parseOutputFormatSpec } from "./ofs/parser.js";
4
+ export { buildOutline } from "./md/outline.js";
5
+ export { parseOutputFormatSpec, validateFormat } from "./ofs/parser.js";
5
6
  export { stringifyOutputFormatSpec } from "./ofs/stringify.js";
6
- export { buildMarkdownGuidance, enrichInstructions } from "./ofs/enricher.js";
7
+ export { buildMarkdownGuidance, enrichInstructions, enrichInstructionsWithFlexMd } from "./ofs/enricher.js";
7
8
  export { validateMarkdownAgainstOfs } from "./validate/validate.js";
9
+ export { checkCompliance, hasFlexMdContract } from "./validate/compliance.js";
10
+ export { checkConnection } from "./validate/connection.js";
8
11
  export { extractFromMarkdown } from "./extract/extract.js";
9
12
  export { processResponseMarkdown } from "./strictness/processor.js";
10
13
  export { parseIssuesEnvelope, buildIssuesEnvelope, buildIssuesEnvelopeAuto } from "./ofs/issuesEnvelope.js";
package/dist/index.js CHANGED
@@ -3,12 +3,15 @@ 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
- export { parseOutputFormatSpec } from "./ofs/parser.js";
8
+ export { parseOutputFormatSpec, validateFormat } from "./ofs/parser.js";
8
9
  export { stringifyOutputFormatSpec } from "./ofs/stringify.js";
9
- export { buildMarkdownGuidance, enrichInstructions } from "./ofs/enricher.js";
10
+ export { buildMarkdownGuidance, enrichInstructions, enrichInstructionsWithFlexMd } from "./ofs/enricher.js";
10
11
  // Validation & Extraction
11
12
  export { validateMarkdownAgainstOfs } from "./validate/validate.js";
13
+ export { checkCompliance, hasFlexMdContract } from "./validate/compliance.js";
14
+ export { checkConnection } from "./validate/connection.js";
12
15
  export { extractFromMarkdown } from "./extract/extract.js";
13
16
  // Processor & Fallback
14
17
  export { processResponseMarkdown } from "./strictness/processor.js";
@@ -1,6 +1,9 @@
1
- import type { MdOutline } from "../types.js";
1
+ import { MdOutline } from "../types.js";
2
2
  /**
3
- * Builds a nested outline tree from Markdown headings.
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;
@@ -1,67 +1,45 @@
1
+ import { parseHeadingsAndSections } from "./parse.js";
1
2
  /**
2
- * Builds a nested outline tree from Markdown headings.
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 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 = [];
6
+ const sections = parseHeadingsAndSections(md);
7
+ const roots = [];
18
8
  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";
9
+ for (const s of sections) {
10
+ const h = s.heading;
25
11
  const node = {
26
- title: cur.title,
27
- level: cur.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
- while (stack.length && stack[stack.length - 1].level >= node.level) {
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 (!stack.length) {
36
- nodes.push(node);
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
- 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);
30
+ return {
31
+ type: "md_outline",
32
+ nodes: roots
65
33
  };
66
- nodes.forEach(visit);
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
- const rx = /^(#{1,6})\s+(.+?)\s*$/gm;
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 hashes = m[1] ?? "";
36
- const name = (m[2] ?? "").trim();
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: hashes.length,
52
+ level,
42
53
  raw,
43
54
  name,
44
55
  norm: normalizeName(name),
@@ -1,4 +1,4 @@
1
- import { OutputFormatSpec } from "../types.js";
1
+ import { OutputFormatSpec, EnhancementResult } from "../types.js";
2
2
  import { StrictnessOptions } from "../strictness/types.js";
3
3
  /**
4
4
  * Generates Markdown guidance instructions for the LLM based on the OFS and contract level.
@@ -14,3 +14,8 @@ export declare function buildMarkdownGuidance(spec: OutputFormatSpec, strict: St
14
14
  * @deprecated Use buildMarkdownGuidance
15
15
  */
16
16
  export declare function enrichInstructions(spec: OutputFormatSpec, strict: StrictnessOptions): string;
17
+ /**
18
+ * Enrich instructions with flex-md compliance guidance.
19
+ * Returns detailed information about what was changed.
20
+ */
21
+ export declare function enrichInstructionsWithFlexMd(instructions: string, strictnessLevel: "L0" | "L1" | "L2" | "L3", spec?: OutputFormatSpec): Promise<EnhancementResult>;
@@ -83,3 +83,64 @@ export function buildMarkdownGuidance(spec, strict, opts) {
83
83
  export function enrichInstructions(spec, strict) {
84
84
  return buildMarkdownGuidance(spec, strict);
85
85
  }
86
+ /**
87
+ * Enrich instructions with flex-md compliance guidance.
88
+ * Returns detailed information about what was changed.
89
+ */
90
+ export async function enrichInstructionsWithFlexMd(instructions, strictnessLevel, spec // Optional spec, if provided will include section guidance
91
+ ) {
92
+ const originalLength = instructions.length;
93
+ const level = parseInt(strictnessLevel.slice(1));
94
+ // Default spec if none provided (minimal)
95
+ const effectiveSpec = spec ?? { sections: [] };
96
+ const guidance = buildMarkdownGuidance(effectiveSpec, { level });
97
+ // Check if guidance is already present (simple check)
98
+ if (instructions.includes(guidance)) {
99
+ return {
100
+ enriched: instructions,
101
+ changes: [],
102
+ originalLength,
103
+ enhancedLength: originalLength,
104
+ metadata: {
105
+ sectionsAdded: 0,
106
+ containerAdded: false,
107
+ guidanceAdded: false
108
+ }
109
+ };
110
+ }
111
+ const separator = instructions.trim().length > 0 ? "\n\n" : "";
112
+ const enriched = instructions.trim() + separator + guidance;
113
+ const enhancedLength = enriched.length;
114
+ const changes = [
115
+ {
116
+ type: "added-guidance",
117
+ description: `Added Flex-MD ${strictnessLevel} compliance guidance.`,
118
+ location: {
119
+ start: instructions.trim().length + separator.length,
120
+ end: enhancedLength
121
+ },
122
+ content: guidance
123
+ }
124
+ ];
125
+ if (level >= 2) {
126
+ changes.push({
127
+ type: "added-container",
128
+ description: "Added requirement for a fenced container block.",
129
+ location: {
130
+ start: instructions.trim().length + separator.length,
131
+ end: enhancedLength
132
+ }
133
+ });
134
+ }
135
+ return {
136
+ enriched,
137
+ changes,
138
+ originalLength,
139
+ enhancedLength,
140
+ metadata: {
141
+ sectionsAdded: effectiveSpec.sections.length,
142
+ containerAdded: level >= 2,
143
+ guidanceAdded: true
144
+ }
145
+ };
146
+ }
@@ -1,4 +1,9 @@
1
- import type { OutputFormatSpec } from "../types.js";
1
+ import type { OutputFormatSpec, FormatValidationResult } from "../types.js";
2
+ /**
3
+ * Validate a format specification.
4
+ * Returns detailed validation results.
5
+ */
6
+ export declare function validateFormat(formatSpec: string, formatType?: "flex-md" | "json-schema"): Promise<FormatValidationResult>;
2
7
  /**
3
8
  * Parse an Output Format Spec block from Markdown.
4
9
  */
@@ -1,3 +1,73 @@
1
+ /**
2
+ * Validate a format specification.
3
+ * Returns detailed validation results.
4
+ */
5
+ export async function validateFormat(formatSpec, formatType = "flex-md") {
6
+ const issues = [];
7
+ const suggestions = [];
8
+ if (formatType === "json-schema") {
9
+ try {
10
+ JSON.parse(formatSpec);
11
+ return {
12
+ valid: true,
13
+ formatType: "json-schema",
14
+ issues: [],
15
+ suggestions: [],
16
+ metadata: {
17
+ structureType: "json"
18
+ }
19
+ };
20
+ }
21
+ catch (e) {
22
+ return {
23
+ valid: false,
24
+ formatType: "json-schema",
25
+ issues: [{
26
+ type: "syntax-error",
27
+ description: `Invalid JSON: ${e.message}`,
28
+ severity: "error"
29
+ }],
30
+ suggestions: ["Ensure the format specification is valid JSON."],
31
+ metadata: {
32
+ structureType: "json"
33
+ }
34
+ };
35
+ }
36
+ }
37
+ // flex-md validation
38
+ const spec = parseOutputFormatSpec(formatSpec);
39
+ if (!formatSpec.toLowerCase().includes("output format")) {
40
+ issues.push({
41
+ type: "missing-heading",
42
+ description: "Missing '## Output format' heading.",
43
+ severity: "error",
44
+ suggestion: "Add '## Output format' to start the specification block."
45
+ });
46
+ }
47
+ if (!spec || spec.sections.length === 0) {
48
+ issues.push({
49
+ type: "missing-section",
50
+ description: "No sections detected in the specification.",
51
+ severity: "error",
52
+ suggestion: "Add at least one section using the '- Name — Kind' format."
53
+ });
54
+ }
55
+ const valid = !issues.some(i => i.severity === "error");
56
+ if (!valid) {
57
+ suggestions.push(...issues.map(i => i.suggestion).filter((s) => !!s));
58
+ }
59
+ return {
60
+ valid,
61
+ formatType: "flex-md",
62
+ issues,
63
+ suggestions,
64
+ metadata: {
65
+ sectionCount: spec?.sections.length ?? 0,
66
+ hasContainer: formatSpec.includes("```"),
67
+ structureType: "markdown"
68
+ }
69
+ };
70
+ }
1
71
  export function parseOutputFormatSpec(md, opts = {}) {
2
72
  const headingRx = opts.headingRegex ?? /^##\s*Output format\b/i;
3
73
  const lines = md.split("\n");
@@ -1,5 +1,5 @@
1
1
  import type { OutputFormatSpec } from "../types.js";
2
2
  /**
3
- * Convert an OutputFormatSpec to canonical Markdown format.
3
+ * Convert an OutputFormatSpec to a formal "Instructions Output Format Block".
4
4
  */
5
5
  export declare function stringifyOutputFormatSpec(spec: OutputFormatSpec): string;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Convert an OutputFormatSpec to canonical Markdown format.
2
+ * Convert an OutputFormatSpec to a formal "Instructions Output Format Block".
3
3
  */
4
4
  export function stringifyOutputFormatSpec(spec) {
5
5
  const lines = [];
package/dist/types.d.ts CHANGED
@@ -177,3 +177,69 @@ export interface IssuesEnvelope {
177
177
  bullets: string[];
178
178
  }>;
179
179
  }
180
+ export interface ComplianceIssue {
181
+ type: "missing-section" | "missing-container" | "invalid-format" | "missing-heading" | "other";
182
+ description: string;
183
+ severity: "error" | "warning";
184
+ location?: {
185
+ start: number;
186
+ end: number;
187
+ line?: number;
188
+ column?: number;
189
+ };
190
+ suggestion?: string;
191
+ }
192
+ export interface ComplianceCheckResult {
193
+ meetsCompliance: boolean;
194
+ complianceLevel: "L0" | "L1" | "L2" | "L3";
195
+ issues: ComplianceIssue[];
196
+ suggestions: string[];
197
+ metadata?: {
198
+ hasContainer?: boolean;
199
+ hasRequiredSections?: boolean;
200
+ sectionCount?: number;
201
+ containerType?: "fenced-block" | "none";
202
+ };
203
+ }
204
+ export interface EnhancementChange {
205
+ type: "added-section" | "added-container" | "modified-section" | "added-guidance" | "other";
206
+ description: string;
207
+ location: {
208
+ start: number;
209
+ end: number;
210
+ };
211
+ content?: string;
212
+ }
213
+ export interface EnhancementResult {
214
+ enriched: string;
215
+ changes: EnhancementChange[];
216
+ originalLength: number;
217
+ enhancedLength: number;
218
+ metadata?: {
219
+ sectionsAdded?: number;
220
+ containerAdded?: boolean;
221
+ guidanceAdded?: boolean;
222
+ };
223
+ }
224
+ export interface FormatValidationIssue {
225
+ type: "syntax-error" | "missing-section" | "invalid-structure" | "invalid-markdown" | "missing-heading" | "other";
226
+ description: string;
227
+ severity: "error" | "warning";
228
+ location?: {
229
+ start: number;
230
+ end: number;
231
+ line?: number;
232
+ };
233
+ suggestion?: string;
234
+ }
235
+ export interface FormatValidationResult {
236
+ valid: boolean;
237
+ formatType: "flex-md" | "json-schema" | "other" | "unknown";
238
+ issues: FormatValidationIssue[];
239
+ suggestions: string[];
240
+ metadata?: {
241
+ sectionCount?: number;
242
+ hasContainer?: boolean;
243
+ structureType?: string;
244
+ };
245
+ }
@@ -0,0 +1,11 @@
1
+ import { ComplianceCheckResult } from "../types.js";
2
+ /**
3
+ * Check if instructions meet the required flex-md compliance level.
4
+ * Returns detailed results including what's missing or wrong.
5
+ */
6
+ export declare function checkCompliance(instructions: string, complianceLevel: "L0" | "L1" | "L2" | "L3"): Promise<ComplianceCheckResult>;
7
+ /**
8
+ * Check if instructions meet the required flex-md compliance level.
9
+ * @deprecated Use checkCompliance for detailed results.
10
+ */
11
+ export declare function hasFlexMdContract(instructions: string, complianceLevel: "L0" | "L1" | "L2" | "L3"): Promise<boolean>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Check if instructions meet the required flex-md compliance level.
3
+ * Returns detailed results including what's missing or wrong.
4
+ */
5
+ export async function checkCompliance(instructions, complianceLevel) {
6
+ const issues = [];
7
+ const suggestions = [];
8
+ const lower = instructions.toLowerCase();
9
+ const hasMarkdownMention = lower.includes("markdown");
10
+ const hasSectionMention = lower.includes("section") || lower.includes("heading");
11
+ const hasContainerMention = (lower.includes("fenced block") || lower.includes("```")) && (lower.includes("inside") || lower.includes("wrapped"));
12
+ const hasNoneRule = lower.includes("none") && lower.includes("empty");
13
+ const hasListRules = lower.includes("list rules") || (lower.includes("ordered list") && lower.includes("unordered list"));
14
+ const hasTableRules = lower.includes("table") && lower.includes("pipe");
15
+ const levelNum = parseInt(complianceLevel.slice(1));
16
+ // L0 requirements
17
+ if (levelNum >= 0) {
18
+ if (!hasMarkdownMention) {
19
+ issues.push({
20
+ type: "other",
21
+ description: "Instructions do not mention 'Markdown'.",
22
+ severity: "error",
23
+ suggestion: "Add 'Reply in Markdown.' to your instructions."
24
+ });
25
+ }
26
+ }
27
+ // L1 requirements
28
+ if (levelNum >= 1) {
29
+ if (!hasSectionMention) {
30
+ issues.push({
31
+ type: "missing-section",
32
+ description: "Instructions do not specify required sections or headings.",
33
+ severity: "error",
34
+ suggestion: "Include a list of required section headings."
35
+ });
36
+ }
37
+ }
38
+ // L2 requirements
39
+ if (levelNum >= 2) {
40
+ if (!hasContainerMention) {
41
+ issues.push({
42
+ type: "missing-container",
43
+ description: "Instructions do not require a fenced container block (L2+ requirement).",
44
+ severity: "error",
45
+ suggestion: "Add 'Return your entire answer inside a single ```markdown fenced block and nothing else.' to your instructions."
46
+ });
47
+ }
48
+ }
49
+ // L3 requirements
50
+ if (levelNum >= 3) {
51
+ if (!hasNoneRule) {
52
+ issues.push({
53
+ type: "other",
54
+ description: "Instructions are missing the 'None' rule for empty sections (L3 requirement).",
55
+ severity: "warning",
56
+ suggestion: "Add 'If a section is empty, write `None`.' to your instructions."
57
+ });
58
+ }
59
+ if (!hasListRules) {
60
+ issues.push({
61
+ type: "invalid-format",
62
+ description: "Instructions are missing specific list formatting rules (L3 requirement).",
63
+ severity: "warning",
64
+ suggestion: "Add bullet and numbered list formatting instructions."
65
+ });
66
+ }
67
+ }
68
+ const meetsCompliance = !issues.some(i => i.severity === "error");
69
+ if (!meetsCompliance) {
70
+ suggestions.push(...issues.map(i => i.suggestion).filter((s) => !!s));
71
+ }
72
+ return {
73
+ meetsCompliance,
74
+ complianceLevel,
75
+ issues,
76
+ suggestions,
77
+ metadata: {
78
+ hasContainer: hasContainerMention,
79
+ hasRequiredSections: hasSectionMention,
80
+ containerType: hasContainerMention ? "fenced-block" : "none"
81
+ }
82
+ };
83
+ }
84
+ /**
85
+ * Check if instructions meet the required flex-md compliance level.
86
+ * @deprecated Use checkCompliance for detailed results.
87
+ */
88
+ export async function hasFlexMdContract(instructions, complianceLevel) {
89
+ const result = await checkCompliance(instructions, complianceLevel);
90
+ return result.meetsCompliance;
91
+ }
@@ -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.1.0",
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
+ }